@caltho/site-kit 0.2.10 → 0.3.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 +146 -16
- package/dist/behaviors/accordion.d.ts +4 -0
- package/dist/behaviors/accordion.js +56 -0
- package/dist/behaviors/carousel.d.ts +4 -0
- package/dist/behaviors/carousel.js +127 -0
- package/dist/behaviors/filter.d.ts +1 -0
- package/dist/behaviors/filter.js +25 -0
- package/dist/behaviors/index.d.ts +14 -0
- package/dist/behaviors/index.js +78 -0
- package/dist/behaviors/motion.d.ts +13 -0
- package/dist/behaviors/motion.js +88 -0
- package/dist/behaviors/tabs.d.ts +3 -0
- package/dist/behaviors/tabs.js +55 -0
- package/dist/built/css.d.ts +2 -0
- package/dist/built/css.js +26 -0
- package/dist/built/theme.d.ts +19 -0
- package/dist/built/theme.js +86 -0
- package/dist/built/tree.d.ts +24 -0
- package/dist/built/tree.js +96 -0
- package/dist/built.test.d.ts +1 -0
- package/dist/built.test.js +257 -0
- package/dist/cli/check.js +31 -1
- package/dist/consent.d.ts +33 -0
- package/dist/consent.js +36 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +61 -0
- package/dist/content/index.d.ts +450 -33
- package/dist/content/index.js +297 -15
- package/dist/content/lists.d.ts +69 -0
- package/dist/content/lists.js +77 -0
- package/dist/content/lists.test.d.ts +1 -0
- package/dist/content/lists.test.js +257 -0
- package/dist/content/look.d.ts +260 -9
- package/dist/content/look.js +386 -22
- package/dist/content-migrate.d.ts +51 -0
- package/dist/content-migrate.js +69 -0
- package/dist/finish.d.ts +1 -1
- package/dist/finish.js +27 -6
- package/dist/fonts/families/dm-sans/LICENSE +93 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/LICENSE +93 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/LICENSE +93 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/LICENSE +93 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/LICENSE +93 -0
- package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/LICENSE +93 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/LICENSE +93 -0
- package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/LICENSE +93 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/LICENSE +93 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/LICENSE +93 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/google.json +1 -0
- package/dist/fonts-google.d.ts +35 -0
- package/dist/fonts-google.js +70 -0
- package/dist/fonts.css +365 -0
- package/dist/frame.test.d.ts +1 -0
- package/dist/frame.test.js +58 -0
- package/dist/migrate.d.ts +45 -0
- package/dist/migrate.js +284 -0
- package/dist/next/block-mark.d.ts +7 -0
- package/dist/next/block-mark.js +16 -0
- package/dist/next/blocks.d.ts +11 -0
- package/dist/next/blocks.js +35 -0
- package/dist/next/built.d.ts +40 -0
- package/dist/next/built.js +318 -0
- package/dist/next/consent.d.ts +4 -0
- package/dist/next/consent.js +13 -0
- package/dist/next/data.d.ts +6 -2
- package/dist/next/data.js +10 -4
- package/dist/next/frame.d.ts +42 -0
- package/dist/next/frame.js +73 -0
- package/dist/next/index.d.ts +8 -2
- package/dist/next/index.js +9 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -0
- package/dist/next/metadata.js +3 -2
- package/dist/next/parts/Img.d.ts +6 -1
- package/dist/next/parts/Img.js +11 -3
- package/dist/next/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +511 -27
- package/dist/next/sections.d.ts +19 -10
- package/dist/next/sections.js +134 -17
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +15 -1
- package/dist/prepare.js +125 -3
- package/dist/preview-match.d.ts +29 -0
- package/dist/preview-match.js +58 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +29 -1
- package/dist/sections/icons.d.ts +49 -0
- package/dist/sections/icons.js +55 -0
- package/dist/sections/index.d.ts +188 -12
- package/dist/sections/index.js +136 -10
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1584 -41
- package/dist/sections.test.js +276 -14
- package/dist/seo.js +2 -1
- package/dist/tree/behaviors.d.ts +50 -0
- package/dist/tree/behaviors.js +67 -0
- package/dist/tree/classes.d.ts +24 -0
- package/dist/tree/classes.js +94 -0
- package/dist/tree/index.d.ts +145 -0
- package/dist/tree/index.js +196 -0
- package/dist/tree/messages.d.ts +45 -0
- package/dist/tree/messages.js +7 -0
- package/package.json +22 -3
package/README.md
CHANGED
|
@@ -45,7 +45,11 @@ export default function CalosPreview() { return <Preview origins={previewOrigins
|
|
|
45
45
|
|
|
46
46
|
Components read content only through the kit (`site()`, `image()`), so the same components draw the
|
|
47
47
|
built pages (from `.site/prepared.json`) and the draft (from CalOS). A click on the preview is matched
|
|
48
|
-
back to the field it meant with nothing added to the components.
|
|
48
|
+
back to the field it meant with nothing added to the components. Which block a click landed in is
|
|
49
|
+
exact where the page puts `<BlockMark at={i} />` (from `@caltho/site-kit/next`) before each block it
|
|
50
|
+
draws: a block may then draw nothing (a list with nothing in it yet) or several elements. It draws
|
|
51
|
+
nothing on the live site. Without marks, blocks are found among `main`'s children, by their words
|
|
52
|
+
when there isn't one for each. Only the business's own CalOS
|
|
49
53
|
addresses, which the build is given with the content, may frame the page or send it anything.
|
|
50
54
|
|
|
51
55
|
Forms are separate and optional. A block can post to any address (a CalOS form, another service, or none);
|
|
@@ -62,7 +66,7 @@ the kit only checks it's a URL.
|
|
|
62
66
|
| **Search** | Titles that fit, canonical addresses, the business as schema.org data (hours, areas, address), FAQ and breadcrumb data, robots.txt, the sitemap. |
|
|
63
67
|
| **Policies** | Privacy, accessibility and terms, written from the business's facts and what the site does. Optional: a site calls `policy()` where it wants them. |
|
|
64
68
|
| **Helpers for components** (`@caltho/site-kit/lib`) | The pieces that touch no files: `md` (Markdown that can't run HTML), `plain`, `policy`, the search data, `imagesIn`, `clock`, `dayRange`, `telHref`. |
|
|
65
|
-
| **Next.js helpers** (`@caltho/site-kit/next`) | The prepared content, a page by its path, page metadata, `Img` (the prepared widths), `Md` (Markdown that can't run HTML), `JsonLd`, `previewOrigins`. The content is a store: on the server it reads `.site/prepared.json`; in the preview, what CalOS sends. |
|
|
69
|
+
| **Next.js helpers** (`@caltho/site-kit/next`) | The prepared content, a page by its path, page metadata, `Img` (the prepared widths), `Md` (Markdown that can't run HTML), `JsonLd`, `BlockMark` (where each block starts, for the preview), `previewOrigins`. The content is a store: on the server it reads `.site/prepared.json`; in the preview, what CalOS sends. |
|
|
66
70
|
| **Preview** (`@caltho/site-kit/next/preview`) | `SitePreview`: CalOS's draft drawn with the site's own components, a click matched to its field, outlines with each section's name. |
|
|
67
71
|
| **Schema** (`@caltho/site-kit/schema`) | What CalOS needs of a declaration with no site code: validate content against the JSON Schema with messages a person can act on, make a blank block or entry, list the kinds of block, and the checks across pages. |
|
|
68
72
|
|
|
@@ -70,14 +74,32 @@ the kit only checks it's a URL.
|
|
|
70
74
|
|
|
71
75
|
Sections any site can use, built once here (accessible, sized for each screen, inside the content
|
|
72
76
|
security policy) so a site gets them with a kit update rather than code of its own. CalOS offers
|
|
73
|
-
them in its editor like the site's own, and Cal can add one. Today:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
them in its editor like the site's own, and Cal can add one. Today:
|
|
78
|
+
|
|
79
|
+
- **gallery**: photos in a grid, each with an optional caption, opening its largest copy; 2 to 4
|
|
80
|
+
across, square, landscape, portrait or as taken.
|
|
81
|
+
- **slideshow**: photos one at a time, fading from one to the next, with captions.
|
|
82
|
+
- **own**: your own section, columns of parts (below).
|
|
83
|
+
- **built**: a section built in CalOS (or by Cal) as a tree of elements styled with Tailwind
|
|
84
|
+
classes: anything a designer builds with HTML and Tailwind (below, "Built sections").
|
|
85
|
+
- **use**: one of the site's components (a tree kept once in the content's `components`) with its
|
|
86
|
+
slots' words, links and pictures filled in; changing the component changes every use.
|
|
87
|
+
|
|
88
|
+
**Your own section** (`own`) is built in CalOS rather than in code: a heading and intro over one to
|
|
89
|
+
four columns (or a wide one beside a narrow one), each a list of parts (heading, text, photo, button,
|
|
90
|
+
list, quote, facts in a row, price, photos in a grid, slideshow, space, table, icon, logos, video),
|
|
91
|
+
left or centred, plain or in the brand colour. A column can be drawn as a card (`card: true`). Its
|
|
92
|
+
parts wear the site's own classes, so what's built looks like the site.
|
|
93
|
+
|
|
94
|
+
- **table**: 2 to 5 columns (the first over the rows' labels, its heading may be ""), 1 to 20 rows,
|
|
95
|
+
a column picked out (`highlight`, counting from 1 after the labels). A cell "yes" or "no" is a tick
|
|
96
|
+
or a dash, read out as the word. A real table from 640px up; below, each row a small card.
|
|
97
|
+
- **icon**: one of the kit's line icons by name (`ICON_NAMES`: check, calendar, clock, users, …),
|
|
98
|
+
drawn inline in the brand colour, small, medium or large.
|
|
99
|
+
- **logos**: 2 to 12, in a row in grey, with a line above if it has one.
|
|
100
|
+
- **video**: a YouTube or Vimeo address and what the video is (its title, for a screen reader), shown
|
|
101
|
+
in YouTube's no-cookie player or Vimeo's with tracking off. `finish` lets the player in
|
|
102
|
+
(`frame-src`) only on a page that shows one, and in the preview. One that's wanted again is saved by
|
|
81
103
|
name in the content's `saved` list, and CalOS offers it beside the site's own kinds.
|
|
82
104
|
|
|
83
105
|
`Slideshow` (from `@caltho/site-kit/next`) is also a building block a site's own sections use: SCV's
|
|
@@ -92,17 +114,125 @@ A site takes them in once, three lines:
|
|
|
92
114
|
import { sections } from "@caltho/site-kit/sections";
|
|
93
115
|
export const blocks = { ...sections, hero, about /* … */ };
|
|
94
116
|
|
|
95
|
-
// the site's block
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
// the site's page: every block after its mark, framed when it has a background, the kit drawing
|
|
118
|
+
// whatever kind the site's own `render` leaves (returns null for)
|
|
119
|
+
<KitBlocks blocks={page.blocks} render={(block, i) => <Block s={block} />} level={(i) => (i === 0 ? 1 : 2)} />
|
|
98
120
|
|
|
99
121
|
// app/layout.tsx
|
|
100
122
|
import "@caltho/site-kit/sections.css";
|
|
101
123
|
```
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
A section of the business's own has a background (`tone`: plain, soft, brand or dark, from the
|
|
126
|
+
Look's `--look-*`, with `--look-page` and `--look-ink` from the Look's `background` and `ink`), space
|
|
127
|
+
above and below (`spacing`: tight, normal, roomy) and can have a photo behind it (`background`, under
|
|
128
|
+
a scrim). On soft, brand or dark it's a card inset within the content's width in the theme's corners
|
|
129
|
+
(`shape: "card"`, the default) or a band across the page (`shape: "band"`). On brand, dark or a photo the kit keeps its words and buttons readable over the site's own
|
|
130
|
+
colour classes. `presets` (from `@caltho/site-kit/sections`) are ready-made ones (an opening, words
|
|
131
|
+
over a photo, three reasons, a client's words, prices, a call to get in touch, contact, common
|
|
132
|
+
questions, a photo and words, facts in a row, a comparison, features with icons, a logo strip, a
|
|
133
|
+
video), each an `own` block to add and edit like any other.
|
|
134
|
+
|
|
135
|
+
From then on each section the kit adds reaches the site with `cal sites kit update`. They're drawn
|
|
136
|
+
from the site's Look (`--look-*`: its type scale, colours, buttons, width and spacing) and need no
|
|
137
|
+
classes of the site's; a site may still give its own for any part, in place of the kit's:
|
|
138
|
+
`<KitBlocks … classes={{ headingLarge: "font-display text-3xl" }} />`.
|
|
139
|
+
|
|
140
|
+
## Built sections
|
|
141
|
+
|
|
142
|
+
A **built** section is a tree of elements (`@caltho/site-kit/tree`: the `Node` type, its zod schema
|
|
143
|
+
`built`, `component` and `use`, the class checker, behaviors' settings, and the messages between
|
|
144
|
+
CalOS's editor and the preview). The kit draws each node as its tag with its Tailwind classes:
|
|
145
|
+
|
|
146
|
+
- **Checked as it's drawn**, whatever the content says: a tag outside the list is left out, a class
|
|
147
|
+
the checker refuses is dropped (`safeClasses`), a link that isn't a page, `#part`, http(s), mailto:
|
|
148
|
+
or tel: goes nowhere (an outside one marked `external` opens in a new tab with
|
|
149
|
+
`rel="noopener noreferrer"`), a picture is one of the site's own through the kit's images, a video
|
|
150
|
+
is the kit's own YouTube (no-cookie) or Vimeo (no tracking) player built from a provider and an
|
|
151
|
+
id, words are light Markdown whose HTML is shown as text, `label` is its `aria-label`, `hidden`
|
|
152
|
+
nodes aren't drawn. A tree's depth, size and ids are checked with the content.
|
|
153
|
+
- **Shared values**: `bind` shows a value from the site's content by its dotted path
|
|
154
|
+
(`company.email`); `repeat` draws a node once for each item of a list (`caseStudies`), its binds
|
|
155
|
+
reading `item.…`.
|
|
156
|
+
- **Classes are prefixed** as drawn (`p-4` is `kit:p-4`), so nothing of the site's own is restyled
|
|
157
|
+
by them, nor they by its classes of the same names. `prepare` compiles exactly the classes the
|
|
158
|
+
content's trees use with Tailwind's own compiler (`tailwindcss` 4.3.3), with **no reset** and in no
|
|
159
|
+
cascade layer, into `public/_kit/built.<hash>.css`; a page with a built section links it (React
|
|
160
|
+
puts the link in the head), a page without doesn't.
|
|
161
|
+
- **The Look is the theme**: colours by role (`bg-brand`, `text-on-brand`, `text-ink`, `bg-page`,
|
|
162
|
+
`text-muted`, `border-line`, `bg-soft`, `text-accent`, `bg-light`, `bg-dark`) and the site's own by
|
|
163
|
+
their slug (`bg-dark-sand`), fonts (`font-display`/`font-heading`, `font-body`/`font-text`), the
|
|
164
|
+
type scale (`text-display`, `text-h1` … `text-small`, each with its line height, tracking and
|
|
165
|
+
weight), corners (`rounded-card`, `rounded-button`), width (`max-w-content`) and spacing
|
|
166
|
+
(`px-gutter`, `py-section`), each reading the Look's `--look-*` variable, so a change of Look needs
|
|
167
|
+
no build of the CSS. Tailwind's own scale and colours are there too. `@caltho/site-kit/theme` has
|
|
168
|
+
the tokens and the input both compilers are given (`tailwindInput`).
|
|
169
|
+
- **Behaviors**, chosen not written: `carousel` (dots, arrows, a stop button, the arrow keys, a
|
|
170
|
+
swipe; its own controls where the design marks nodes with the slots prev, next and dots), `tabs`
|
|
171
|
+
(the first child holds the tabs, the children after it are the panels), `accordion` (each child an
|
|
172
|
+
item, its first child the heading that opens it), `marquee`, `reveal`, `count-up`, and `sticky`
|
|
173
|
+
(CSS alone). Each is a small script of its own, written by `prepare` to `public/_kit` and loaded
|
|
174
|
+
only on a page that uses it. Without scripting a carousel shows its first slide, tabs show every
|
|
175
|
+
panel, every accordion item is open, a marquee's row is still and scrollable; nothing moves for
|
|
176
|
+
someone who asked for less motion.
|
|
177
|
+
- **In CalOS's preview** each element carries `data-node` (never on the published page), so a click
|
|
178
|
+
names its node (`calos:built-select`, with shift), the pointer's node is said (`calos:built-hover`)
|
|
179
|
+
and where each element is (`calos:built-boxes`); the editor outlines nodes (`calos:built-highlight`,
|
|
180
|
+
bringing up the slide, tab or item that holds one) and draws one as hovered or focused
|
|
181
|
+
(`calos:built-state`). Classes take effect as they're typed with Tailwind's browser build
|
|
182
|
+
(`@tailwindcss/browser` 4.3.3, the same input as the build's), loaded by the preview page only;
|
|
183
|
+
visitors never load it. The preview page's policy allows its inline styles for that.
|
|
184
|
+
|
|
185
|
+
**What a site needs to do**: nothing beyond what it has, on 0.3.0: its schema takes the kit's
|
|
186
|
+
`sections` (built and use come with them), it draws blocks with `KitBlocks`, imports
|
|
187
|
+
`sections.css`, and runs `prepare` and `finish`. `public/_kit` is written by every build (add it to
|
|
188
|
+
`.gitignore` with `public/_img`).
|
|
189
|
+
|
|
190
|
+
## Cookie banner
|
|
191
|
+
|
|
192
|
+
Off unless a site turns it on (`consent: { on: true }` in its content, edited in CalOS): visits are
|
|
193
|
+
then counted only once a visitor accepts (finish loads the counter after the choice, never before),
|
|
194
|
+
and `KitFrame` draws a banner in the site's words (`text`, `accept`, `decline`, a `policy` link),
|
|
195
|
+
along the bottom or top or in a corner (`place`), in the kit's design or one of the site's components
|
|
196
|
+
(`design`: a tree with the slots text, accept, decline and policy). The kit's script shows it to a
|
|
197
|
+
visitor who hasn't chosen, keeps the choice in their browser, and brings it back from anything
|
|
198
|
+
marked `data-kit-consent="change"`. The privacy policy says it. Off, visits are counted without
|
|
199
|
+
cookies as before, and no banner is needed.
|
|
200
|
+
|
|
201
|
+
## Content formats
|
|
202
|
+
|
|
203
|
+
A site is the starter plus its configuration, so the configuration's format is the kit's. The
|
|
204
|
+
content says its format (`format`; none is 0, before built sections; this kit's is
|
|
205
|
+
`CONTENT_FORMAT`, 1). When a kit changes a shape, a migration in `@caltho/site-kit/content-migrate`
|
|
206
|
+
moves content to it, as data: `migrateContent(content)` runs those it hasn't had, in order, once
|
|
207
|
+
each, and returns `{ ok: true, content, from, format, applied }`, or `{ ok: false, error: { id,
|
|
208
|
+
message, path } }` with the content as it was when one can't move something (for Caltho to fix in
|
|
209
|
+
the migration, never a site by hand). CalOS runs it on a site's draft and published content when
|
|
210
|
+
the site moves to a newer kit; `prepare` runs it too, so content not moved yet still builds. A new
|
|
211
|
+
kind of block or optional field needs no migration, only the number. Code migrations (a site's own
|
|
212
|
+
files) are `@caltho/site-kit/migrate`.
|
|
213
|
+
|
|
214
|
+
## The theme
|
|
215
|
+
|
|
216
|
+
The Look is the site's theme. Beside its fonts, brand and accent colours, corners and buttons, it has
|
|
217
|
+
the page's colours by role (`page`, `ink`, `muted`, `line`, `soft`), colours of the site's own by name
|
|
218
|
+
(`palette`), a type scale (`type`: display, h1, h2, h3, body and small, each a size in px, a weight,
|
|
219
|
+
tracking in em and a line height), the content's `width` and the `space` between sections. A site's
|
|
220
|
+
`look` options are its design as it is (the defaults CalOS shows); its CSS reads the variables
|
|
221
|
+
(`--look-page`, `--look-swatch-sand`, `--look-type-h2-size`, `--look-width`, `--look-section-pad`, the
|
|
222
|
+
full list in src/content/look.ts) so a change in CalOS reaches every page. `--look-gutter` is the
|
|
223
|
+
space each side of the content (the site's `gutter` option in px on a wide screen, 40 unless said,
|
|
224
|
+
half on a phone): a site's own wrap reads it so its sections and the kit's line up.
|
|
225
|
+
|
|
226
|
+
The menu edited in CalOS can have links under a link (`children`, up to 8, one level): `menuOf`
|
|
227
|
+
returns them, and the site's header draws them (the starter: a dropdown on a wide screen, indented
|
|
228
|
+
in the phone's menu). `blockLabel(block, kindTitle)` (`@caltho/site-kit/schema`) is what a block is
|
|
229
|
+
called in a list: its heading, an `own` section's first heading part, else its kind.
|
|
230
|
+
|
|
231
|
+
Every section's Layout (`placement`, on unless a site sets `placement: false`) has a **Background**: a
|
|
232
|
+
colour (a role, or one of the site's own by its slug) and a photo under a shade. KitBlocks draws it as
|
|
233
|
+
a frame around the section (`frameClass`), its words light when the colour or photo is dark
|
|
234
|
+
(`backgroundIsDark`, from the Look's values at build and in the preview). The schema a site writes
|
|
235
|
+
carries the ready-made sections its `own` kind takes, as `x-presets` at its root.
|
|
106
236
|
|
|
107
237
|
## Share cards
|
|
108
238
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// An accordion: each child of the node is an item, its first child the heading that opens and closes
|
|
2
|
+
// it, the rest what it opens. The heading works as a button (a click, Enter or Space) and says
|
|
3
|
+
// whether it's open. Without scripting every item is open, so all of it can be read.
|
|
4
|
+
//
|
|
5
|
+
// Self-contained: sent to visitors as its own script (./index.ts). The kit marks the items
|
|
6
|
+
// (data-kit-item), headings (data-kit-head) and what they open (data-kit-body) (../next/built.tsx).
|
|
7
|
+
export function accordion(el, s) {
|
|
8
|
+
const items = Array.from(el.querySelectorAll(":scope > [data-kit-item]"));
|
|
9
|
+
const key = Math.random().toString(36).slice(2, 8);
|
|
10
|
+
const parts = items.map((item, i) => {
|
|
11
|
+
const head = item.querySelector(":scope > [data-kit-head]");
|
|
12
|
+
const body = Array.from(item.querySelectorAll(":scope > [data-kit-body]"));
|
|
13
|
+
body.forEach((b, j) => (b.id ||= `kit-acc-${key}-${i}-${j}`));
|
|
14
|
+
return { item, head, body };
|
|
15
|
+
});
|
|
16
|
+
const set = (i, open) => {
|
|
17
|
+
const p = parts[i];
|
|
18
|
+
p.item.toggleAttribute("data-open", open);
|
|
19
|
+
p.head?.setAttribute("aria-expanded", String(open));
|
|
20
|
+
p.body.forEach((b) => (b.hidden = !open));
|
|
21
|
+
};
|
|
22
|
+
parts.forEach((p, i) => {
|
|
23
|
+
const head = p.head;
|
|
24
|
+
if (!head)
|
|
25
|
+
return;
|
|
26
|
+
if (head.tagName !== "BUTTON") {
|
|
27
|
+
head.setAttribute("role", "button");
|
|
28
|
+
head.tabIndex = 0;
|
|
29
|
+
}
|
|
30
|
+
head.setAttribute("aria-controls", p.body.map((b) => b.id).join(" "));
|
|
31
|
+
const toggle = () => {
|
|
32
|
+
const open = head.getAttribute("aria-expanded") !== "true";
|
|
33
|
+
if (open && s.single)
|
|
34
|
+
parts.forEach((_, j) => j !== i && set(j, false));
|
|
35
|
+
set(i, open);
|
|
36
|
+
};
|
|
37
|
+
head.addEventListener("click", (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
toggle();
|
|
40
|
+
});
|
|
41
|
+
head.addEventListener("keydown", (e) => {
|
|
42
|
+
if (head.tagName !== "BUTTON" && (e.key === "Enter" || e.key === " ")) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
toggle();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
set(i, i === s.open);
|
|
48
|
+
});
|
|
49
|
+
// CalOS's preview asks for the item holding what's selected to be opened.
|
|
50
|
+
el.addEventListener("kit:show", (e) => {
|
|
51
|
+
const i = parts.findIndex((p) => p.item.contains(e.target));
|
|
52
|
+
if (i >= 0)
|
|
53
|
+
set(i, true);
|
|
54
|
+
});
|
|
55
|
+
el.setAttribute("data-kit-ready", "");
|
|
56
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// A carousel: its slides one at a time, by its dots and arrows (buttons, each saying what it shows),
|
|
2
|
+
// the arrow keys while it has focus, or a swipe; and every few seconds, where it's set to, while it's
|
|
3
|
+
// on screen and not pointed at or focused, with a button to stop it, and never for someone who asked
|
|
4
|
+
// for less motion. Without scripting the first slide shows (sections.css), and the controls don't.
|
|
5
|
+
//
|
|
6
|
+
// Self-contained (nothing from outside the function): it's sent to visitors as its own script
|
|
7
|
+
// (./index.ts). The kit draws what it works on (../next/built.tsx): the slides in a track
|
|
8
|
+
// (data-kit-track, data-kit-slide, the first data-active), the dots (data-kit-to), arrows
|
|
9
|
+
// (data-kit-prev, data-kit-next) and the stop button (data-kit-pause).
|
|
10
|
+
export function carousel(el, s, still) {
|
|
11
|
+
const track = el.querySelector(":scope > [data-kit-track]");
|
|
12
|
+
if (!track)
|
|
13
|
+
return;
|
|
14
|
+
const slides = Array.from(track.children).filter((c) => c.hasAttribute("data-kit-slide"));
|
|
15
|
+
const n = slides.length;
|
|
16
|
+
const mine = (sel) => Array.from(el.querySelectorAll(sel)).filter((x) => x.closest('[data-kit-behavior="carousel"]') === el);
|
|
17
|
+
const dots = mine("[data-kit-to]");
|
|
18
|
+
const pause = mine("[data-kit-pause]")[0];
|
|
19
|
+
const slideWise = track.getAttribute("data-kit-transition") === "slide";
|
|
20
|
+
const reduce = still || window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
21
|
+
let at = Math.max(0, slides.findIndex((x) => x.hasAttribute("data-active")));
|
|
22
|
+
let timer = 0;
|
|
23
|
+
let held = false;
|
|
24
|
+
let stopped = false;
|
|
25
|
+
let seen = true;
|
|
26
|
+
const show = (i) => {
|
|
27
|
+
at = ((i % n) + n) % n;
|
|
28
|
+
slides.forEach((x, j) => {
|
|
29
|
+
const on = j === at;
|
|
30
|
+
x.toggleAttribute("data-active", on);
|
|
31
|
+
if (on)
|
|
32
|
+
x.removeAttribute("aria-hidden");
|
|
33
|
+
else
|
|
34
|
+
x.setAttribute("aria-hidden", "true");
|
|
35
|
+
x.inert = !on;
|
|
36
|
+
// Sideways: each slide moved by the place shown (a style set from script, which the page's policy allows).
|
|
37
|
+
if (slideWise)
|
|
38
|
+
x.style.transform = `translateX(${-100 * at}%)`;
|
|
39
|
+
});
|
|
40
|
+
dots.forEach((d, j) => d.setAttribute("aria-current", j === at ? "true" : "false"));
|
|
41
|
+
wait();
|
|
42
|
+
};
|
|
43
|
+
const moving = () => !!s.seconds && !reduce && !held && !stopped && seen && !document.hidden && n > 1;
|
|
44
|
+
function wait() {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (moving())
|
|
47
|
+
timer = window.setTimeout(() => (el.isConnected ? show(at + 1) : undefined), Math.max(2, s.seconds ?? 5) * 1000);
|
|
48
|
+
}
|
|
49
|
+
// A control that isn't a button of its own (a designed arrow drawn as a div) still works from the keyboard.
|
|
50
|
+
const press = (b, go) => {
|
|
51
|
+
if (b.tagName !== "BUTTON") {
|
|
52
|
+
b.setAttribute("role", "button");
|
|
53
|
+
b.tabIndex = 0;
|
|
54
|
+
b.addEventListener("keydown", (e) => {
|
|
55
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
go();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
b.addEventListener("click", (e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
go();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
if (n < 2)
|
|
67
|
+
return void el.setAttribute("data-kit-ready", "");
|
|
68
|
+
dots.forEach((d) => press(d, () => show(Number(d.getAttribute("data-kit-to")))));
|
|
69
|
+
mine("[data-kit-prev]").forEach((b) => press(b, () => show(at - 1)));
|
|
70
|
+
mine("[data-kit-next]").forEach((b) => press(b, () => show(at + 1)));
|
|
71
|
+
if (pause) {
|
|
72
|
+
// Nothing turns by itself (no seconds, less motion asked for, CalOS's still preview): no pause to press.
|
|
73
|
+
// Set as a style too, since the element's own display class (inline-flex) would outrank [hidden].
|
|
74
|
+
if (!s.seconds || reduce) {
|
|
75
|
+
pause.hidden = true;
|
|
76
|
+
pause.style.display = "none";
|
|
77
|
+
}
|
|
78
|
+
press(pause, () => {
|
|
79
|
+
stopped = !stopped;
|
|
80
|
+
pause.setAttribute("aria-pressed", String(stopped));
|
|
81
|
+
pause.setAttribute("aria-label", stopped ? "Play the slides" : "Pause the slides");
|
|
82
|
+
pause.toggleAttribute("data-stopped", stopped);
|
|
83
|
+
wait();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
el.addEventListener("keydown", (e) => {
|
|
87
|
+
const t = e.target;
|
|
88
|
+
if (t.closest("input, textarea, select, [contenteditable]"))
|
|
89
|
+
return;
|
|
90
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
show(at + (e.key === "ArrowRight" ? 1 : -1));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (s.pauseOnHover !== false) {
|
|
96
|
+
el.addEventListener("pointerenter", () => ((held = true), wait()));
|
|
97
|
+
el.addEventListener("pointerleave", () => ((held = false), wait()));
|
|
98
|
+
}
|
|
99
|
+
el.addEventListener("focusin", () => ((held = true), wait()));
|
|
100
|
+
el.addEventListener("focusout", (e) => {
|
|
101
|
+
if (!el.contains(e.relatedTarget))
|
|
102
|
+
(held = false), wait();
|
|
103
|
+
});
|
|
104
|
+
// A swipe on a touch screen.
|
|
105
|
+
let from = null;
|
|
106
|
+
track.addEventListener("pointerdown", (e) => (from = e.clientX));
|
|
107
|
+
track.addEventListener("pointerup", (e) => {
|
|
108
|
+
if (from !== null && Math.abs(e.clientX - from) > 40)
|
|
109
|
+
show(at + (e.clientX < from ? 1 : -1));
|
|
110
|
+
from = null;
|
|
111
|
+
});
|
|
112
|
+
// Off screen or in another tab, it waits.
|
|
113
|
+
if (typeof IntersectionObserver !== "undefined")
|
|
114
|
+
new IntersectionObserver(([e]) => {
|
|
115
|
+
seen = !!e?.isIntersecting;
|
|
116
|
+
wait();
|
|
117
|
+
}).observe(el);
|
|
118
|
+
document.addEventListener("visibilitychange", wait);
|
|
119
|
+
// CalOS's preview asks for the slide holding what's selected to be shown.
|
|
120
|
+
el.addEventListener("kit:show", (e) => {
|
|
121
|
+
const i = slides.findIndex((x) => x.contains(e.target));
|
|
122
|
+
if (i >= 0)
|
|
123
|
+
show(i);
|
|
124
|
+
});
|
|
125
|
+
el.setAttribute("data-kit-ready", "");
|
|
126
|
+
show(at);
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function filter(el: HTMLElement): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A filter: the buttons inside it choose which of the items repeated inside it show, by a field of the
|
|
2
|
+
// list (its settings' field): a button matches the items whose value is its words (lowercase, dashes
|
|
3
|
+
// for spaces: "Real estate" is real-estate), "All" (or no words) shows every one. The chosen button says
|
|
4
|
+
// so (aria-pressed), for its design's aria-pressed: classes. Without scripting every item shows.
|
|
5
|
+
//
|
|
6
|
+
// Self-contained: sent to visitors as its own script (./index.ts). The kit marks each item with its value
|
|
7
|
+
// (data-kit-filter-value, ../next/built.tsx).
|
|
8
|
+
export function filter(el) {
|
|
9
|
+
const norm = (v) => v.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10
|
+
const items = () => Array.from(el.querySelectorAll("[data-kit-filter-value]"));
|
|
11
|
+
const buttons = Array.from(el.querySelectorAll("button")).filter((b) => !b.closest("[data-kit-filter-value]"));
|
|
12
|
+
const valueOf = (b) => {
|
|
13
|
+
const v = norm(b.textContent ?? "");
|
|
14
|
+
return v === "all" ? "" : v;
|
|
15
|
+
};
|
|
16
|
+
const choose = (chosen) => {
|
|
17
|
+
const v = chosen ? valueOf(chosen) : "";
|
|
18
|
+
items().forEach((i) => (i.hidden = !!v && i.getAttribute("data-kit-filter-value") !== v));
|
|
19
|
+
buttons.forEach((b) => b.setAttribute("aria-pressed", String(b === chosen)));
|
|
20
|
+
};
|
|
21
|
+
buttons.forEach((b) => b.addEventListener("click", (e) => (e.preventDefault(), choose(b))));
|
|
22
|
+
// Every item to start, its "All" button pressed where it has one.
|
|
23
|
+
choose(buttons.find((b) => valueOf(b) === "") ?? null);
|
|
24
|
+
el.setAttribute("data-kit-ready", "");
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BehaviorName } from "../tree/index.ts";
|
|
2
|
+
type Run = (el: HTMLElement, settings: Record<string, unknown>, still: boolean) => void;
|
|
3
|
+
/** Each behavior that has a script. */
|
|
4
|
+
export declare const BEHAVIOR_RUNS: Partial<Record<BehaviorName, Run>>;
|
|
5
|
+
/** The attribute naming an element's behavior, and the one holding its settings. */
|
|
6
|
+
export declare const BEHAVIOR_ATTR = "data-kit-behavior";
|
|
7
|
+
export declare const SETTINGS_ATTR = "data-kit-settings";
|
|
8
|
+
/** The script a published page carries for one behavior: its own code and the starter, nothing else. */
|
|
9
|
+
export declare function behaviorScript(name: BehaviorName): string | null;
|
|
10
|
+
/** Starts every behavior on the page (CalOS's preview, still: nothing moves by itself). */
|
|
11
|
+
export declare function startBehaviors(still: boolean): void;
|
|
12
|
+
/** Asks whatever behavior holds this element to show it (a carousel's slide, a tab's panel, an accordion's item): the preview, when it's selected. */
|
|
13
|
+
export declare function showWithin(el: Element): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { accordion } from "./accordion.js";
|
|
2
|
+
import { carousel } from "./carousel.js";
|
|
3
|
+
import { countUp, marquee, reveal } from "./motion.js";
|
|
4
|
+
import { tabs } from "./tabs.js";
|
|
5
|
+
import { filter } from "./filter.js";
|
|
6
|
+
/** Each behavior that has a script. */
|
|
7
|
+
export const BEHAVIOR_RUNS = {
|
|
8
|
+
carousel: carousel,
|
|
9
|
+
tabs: tabs,
|
|
10
|
+
accordion: accordion,
|
|
11
|
+
marquee: marquee,
|
|
12
|
+
reveal: reveal,
|
|
13
|
+
"count-up": countUp,
|
|
14
|
+
filter: filter,
|
|
15
|
+
};
|
|
16
|
+
/** The attribute naming an element's behavior, and the one holding its settings. */
|
|
17
|
+
export const BEHAVIOR_ATTR = "data-kit-behavior";
|
|
18
|
+
export const SETTINGS_ATTR = "data-kit-settings";
|
|
19
|
+
/**
|
|
20
|
+
* Starts every element of this behavior not started yet, now and as more are drawn. `still`: nothing
|
|
21
|
+
* moves by itself (CalOS's preview, so what's being edited stays put). Written to be sent as its own
|
|
22
|
+
* script as well as called here, so it uses nothing from outside itself but `run`.
|
|
23
|
+
*/
|
|
24
|
+
function start(name, run, still) {
|
|
25
|
+
const go = () => {
|
|
26
|
+
for (const el of Array.from(document.querySelectorAll(`[data-kit-behavior="${name}"]:not([data-kit-on])`))) {
|
|
27
|
+
el.setAttribute("data-kit-on", "");
|
|
28
|
+
let settings = {};
|
|
29
|
+
try {
|
|
30
|
+
settings = JSON.parse(el.getAttribute("data-kit-settings") || "{}");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Settings the kit wrote can't be broken; the behavior's own defaults if they somehow are.
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
run(el, settings, still);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`site-kit: the ${name} behavior stopped`, err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
let queued = false;
|
|
44
|
+
const soon = () => {
|
|
45
|
+
if (queued)
|
|
46
|
+
return;
|
|
47
|
+
queued = true;
|
|
48
|
+
requestAnimationFrame(() => {
|
|
49
|
+
queued = false;
|
|
50
|
+
go();
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const begin = () => {
|
|
54
|
+
go();
|
|
55
|
+
new MutationObserver(soon).observe(document.body, { childList: true, subtree: true });
|
|
56
|
+
};
|
|
57
|
+
// Loaded before the page is all there (an async script): it waits for the whole page, so a carousel isn't started with half its slides.
|
|
58
|
+
if (document.readyState === "loading")
|
|
59
|
+
document.addEventListener("DOMContentLoaded", begin, { once: true });
|
|
60
|
+
else
|
|
61
|
+
begin();
|
|
62
|
+
}
|
|
63
|
+
/** The script a published page carries for one behavior: its own code and the starter, nothing else. */
|
|
64
|
+
export function behaviorScript(name) {
|
|
65
|
+
const run = BEHAVIOR_RUNS[name];
|
|
66
|
+
if (!run)
|
|
67
|
+
return null;
|
|
68
|
+
return `(()=>{const run=${run.toString()};(${start.toString()})(${JSON.stringify(name)},run,false);})();\n`;
|
|
69
|
+
}
|
|
70
|
+
/** Starts every behavior on the page (CalOS's preview, still: nothing moves by itself). */
|
|
71
|
+
export function startBehaviors(still) {
|
|
72
|
+
for (const [name, run] of Object.entries(BEHAVIOR_RUNS))
|
|
73
|
+
start(name, run, still);
|
|
74
|
+
}
|
|
75
|
+
/** Asks whatever behavior holds this element to show it (a carousel's slide, a tab's panel, an accordion's item): the preview, when it's selected. */
|
|
76
|
+
export function showWithin(el) {
|
|
77
|
+
el.dispatchEvent(new CustomEvent("kit:show", { bubbles: true }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The kit draws the row twice in a track (data-kit-track, its copy data-kit-copy, hidden from screen readers); this moves it by one copy's width. */
|
|
2
|
+
export declare function marquee(el: HTMLElement, s: {
|
|
3
|
+
seconds?: number;
|
|
4
|
+
reverse?: boolean;
|
|
5
|
+
}, still: boolean): void;
|
|
6
|
+
/** Hidden until it's scrolled to (sections.css, by its effect), then in; its children one after another where it staggers them. */
|
|
7
|
+
export declare function reveal(el: HTMLElement, s: {
|
|
8
|
+
stagger?: number;
|
|
9
|
+
}, still: boolean): void;
|
|
10
|
+
/** The first number in its words ("$1,200+", "98%"), counted up from nothing, then left exactly as written. */
|
|
11
|
+
export declare function countUp(el: HTMLElement, s: {
|
|
12
|
+
seconds?: number;
|
|
13
|
+
}, still: boolean): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The behaviors that only move things: a marquee (a row scrolling past without end), reveal (in as
|
|
2
|
+
// it's scrolled to) and count-up (a number counting up to itself as it's scrolled to). Each does
|
|
3
|
+
// nothing for someone who asked for less motion, and without scripting what they hold is simply
|
|
4
|
+
// there: the row still (scrollable), everything shown, the number as written.
|
|
5
|
+
//
|
|
6
|
+
// Each self-contained: sent to visitors as its own script (./index.ts).
|
|
7
|
+
/** The kit draws the row twice in a track (data-kit-track, its copy data-kit-copy, hidden from screen readers); this moves it by one copy's width. */
|
|
8
|
+
export function marquee(el, s, still) {
|
|
9
|
+
const track = el.querySelector(":scope > [data-kit-track]");
|
|
10
|
+
const copy = track?.querySelector(":scope > [data-kit-copy]");
|
|
11
|
+
const first = track?.firstElementChild;
|
|
12
|
+
if (!track || !copy || !first || still || window.matchMedia("(prefers-reduced-motion: reduce)").matches)
|
|
13
|
+
return;
|
|
14
|
+
// Styles set from script (the page's policy allows them): how far one round goes and how long it takes.
|
|
15
|
+
const measure = () => el.style.setProperty("--kit-marquee-shift", `${copy.offsetLeft - first.offsetLeft}px`);
|
|
16
|
+
measure();
|
|
17
|
+
el.style.setProperty("--kit-marquee-seconds", `${s.seconds ?? 30}s`);
|
|
18
|
+
if (s.reverse)
|
|
19
|
+
el.setAttribute("data-kit-reverse", "");
|
|
20
|
+
if (typeof ResizeObserver !== "undefined")
|
|
21
|
+
new ResizeObserver(measure).observe(track);
|
|
22
|
+
el.setAttribute("data-kit-ready", "");
|
|
23
|
+
}
|
|
24
|
+
/** Hidden until it's scrolled to (sections.css, by its effect), then in; its children one after another where it staggers them. */
|
|
25
|
+
export function reveal(el, s, still) {
|
|
26
|
+
if (still || window.matchMedia("(prefers-reduced-motion: reduce)").matches || typeof IntersectionObserver === "undefined")
|
|
27
|
+
return;
|
|
28
|
+
if (s.stagger) {
|
|
29
|
+
el.setAttribute("data-kit-stagger", "");
|
|
30
|
+
Array.from(el.children).forEach((c, i) => (c.style.transitionDelay = `${i * s.stagger}ms`));
|
|
31
|
+
}
|
|
32
|
+
const o = new IntersectionObserver((entries) => {
|
|
33
|
+
if (!entries.some((e) => e.isIntersecting))
|
|
34
|
+
return;
|
|
35
|
+
el.setAttribute("data-kit-shown", "");
|
|
36
|
+
o.disconnect();
|
|
37
|
+
}, { rootMargin: "0px 0px -10% 0px" });
|
|
38
|
+
// Ready first (hidden), then watched: what's already on screen comes in at once.
|
|
39
|
+
el.setAttribute("data-kit-ready", "");
|
|
40
|
+
o.observe(el);
|
|
41
|
+
}
|
|
42
|
+
/** The first number in its words ("$1,200+", "98%"), counted up from nothing, then left exactly as written. */
|
|
43
|
+
export function countUp(el, s, still) {
|
|
44
|
+
if (still || window.matchMedia("(prefers-reduced-motion: reduce)").matches || typeof IntersectionObserver === "undefined")
|
|
45
|
+
return;
|
|
46
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
47
|
+
let text = null;
|
|
48
|
+
for (let t = walker.nextNode(); t; t = walker.nextNode()) {
|
|
49
|
+
if (/\d/.test(t.textContent ?? "")) {
|
|
50
|
+
text = t;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!text)
|
|
55
|
+
return;
|
|
56
|
+
const written = text.textContent;
|
|
57
|
+
const m = /(\d[\d,]*)(\.\d+)?/.exec(written);
|
|
58
|
+
if (!m)
|
|
59
|
+
return;
|
|
60
|
+
const target = Number((m[1] + (m[2] ?? "")).replace(/,/g, ""));
|
|
61
|
+
const decimals = m[2] ? m[2].length - 1 : 0;
|
|
62
|
+
const commas = m[1].includes(",");
|
|
63
|
+
const format = (v) => {
|
|
64
|
+
const fixed = v.toFixed(decimals);
|
|
65
|
+
return commas ? fixed.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : fixed;
|
|
66
|
+
};
|
|
67
|
+
const node = text;
|
|
68
|
+
// Read out as written, not as it counts.
|
|
69
|
+
if (!el.hasAttribute("aria-label"))
|
|
70
|
+
el.setAttribute("aria-label", el.textContent?.trim() ?? written);
|
|
71
|
+
node.textContent = written.slice(0, m.index) + format(0) + written.slice(m.index + m[0].length);
|
|
72
|
+
const o = new IntersectionObserver((entries) => {
|
|
73
|
+
if (!entries.some((e) => e.isIntersecting))
|
|
74
|
+
return;
|
|
75
|
+
o.disconnect();
|
|
76
|
+
const start = performance.now();
|
|
77
|
+
const ms = (s.seconds ?? 1.2) * 1000;
|
|
78
|
+
const step = (now) => {
|
|
79
|
+
const t = Math.min(1, (now - start) / ms);
|
|
80
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
81
|
+
node.textContent = t < 1 ? written.slice(0, m.index) + format(target * eased) + written.slice(m.index + m[0].length) : written;
|
|
82
|
+
if (t < 1)
|
|
83
|
+
requestAnimationFrame(step);
|
|
84
|
+
};
|
|
85
|
+
requestAnimationFrame(step);
|
|
86
|
+
});
|
|
87
|
+
o.observe(el);
|
|
88
|
+
}
|