@dloizides/marketing-astro-kit 1.5.1 → 1.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/CHANGELOG.md +12 -0
- package/README.md +24 -0
- package/package.json +10 -3
- package/src/DemoFrame.astro +1 -0
- package/src/HairlineFigure.astro +41 -0
- package/src/PricingComparison.astro +1 -0
- package/src/hairline/figures.ts +54 -0
- package/src/hairline/mount.ts +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `<HairlineFigure />` (`./HairlineFigure.astro`) plus `HAIRLINE_FIGURES`, `TOUCH_SAFE_HAIRLINE_FIGURES` and `clampHairlineIntensity` (`./hairline`). Wraps `@lucasmarkes/hairline` 0.3.0 (exact pin): SSR 5:4 box, lazy mount near the viewport, destroy on page swap, `--hairline-*` mapped from the theming contract. HAIRLINE-1 "Adopt hairline isometric line figures".
|
|
8
|
+
|
|
9
|
+
## 1.5.2
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `.dmk-disclaimer` (DemoFrame) and `.pmk-footnote` (PricingComparison) cap their measure at 65ch; at 1280 px they ran 102-113 characters per line. Mobile is unaffected.
|
|
14
|
+
|
|
3
15
|
## 1.3.0
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ credibility + conversion markers an executive scans for before they trust a SaaS
|
|
|
10
10
|
| `PrimaryCta.astro` | A themeable **primary call-to-action** (link OR spam-safe contact) with an optional secondary CTA. Pair with `buildPrimaryCta` so ONE `SIGNUP_URL` constant drives whether it's a real signup link or lead-capture. |
|
|
11
11
|
| `DemoFrame.astro` | A themeable, **client-side "try it" demo** shell for static sites (no backend): eyebrow/title/intro, a row of clickable example chips, a per-example result panel (bespoke product body via a named `result-<id>` slot), a "sample data" disclaimer, and a primary CTA. Tiny inline vanilla JS toggles which result shows; first example visible without JS. |
|
|
12
12
|
| `GuideCard.astro` | A themeable **card for a guides / SEO content-hub** listing — one guide's tags, title, description and date. Pair with `guideFrontmatterSchema` on an Astro content collection. |
|
|
13
|
+
| `HairlineFigure.astro` | A **Hairline isometric line figure** (`@lucasmarkes/hairline`, pinned 0.3.0) for heroes and empty states: an empty 5:4 box server-side, drawn on the client once it nears the viewport, coloured from the host's tokens. |
|
|
13
14
|
|
|
14
15
|
## Config-driven primary CTA (`PrimaryCta` + `buildPrimaryCta`)
|
|
15
16
|
|
|
@@ -161,6 +162,29 @@ const guides = (await getCollection('guides', ({ data }) => !data.draft))
|
|
|
161
162
|
`updatedDate?`, `tags`, `ogImage?`, `canonical?`, `draft?`. It is built on
|
|
162
163
|
`astro/zod`, so it is the same zod instance Astro's content layer uses.
|
|
163
164
|
|
|
165
|
+
## Hairline line figures (`HairlineFigure`)
|
|
166
|
+
|
|
167
|
+
```astro
|
|
168
|
+
---
|
|
169
|
+
import HairlineFigure from '@dloizides/marketing-astro-kit/HairlineFigure.astro';
|
|
170
|
+
---
|
|
171
|
+
<HairlineFigure figure="terrain" label="Pillars rising over a plinth" intensity={0.6} class="hero-figure" />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Prop | Meaning |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `figure` | One of the 27 names in `HAIRLINE_FIGURES` (`@dloizides/marketing-astro-kit/hairline`). |
|
|
177
|
+
| `label` | Required accessible name. A figure is decoration; it never carries information the reader needs. |
|
|
178
|
+
| `intensity` | 0..1, clamped; default 0.5. |
|
|
179
|
+
| `class` | Host-side sizing / placement. |
|
|
180
|
+
|
|
181
|
+
- **Server render:** an empty `5:4` box, so nothing shifts when it draws. No JS runs until the box is within 200px of the viewport (IntersectionObserver); then the Hairline engine (~170 KB raw, its own lazy chunk) is imported and the figure mounted. Every figure is destroyed on `astro:before-swap`.
|
|
182
|
+
- **Version:** `@lucasmarkes/hairline` is an exact-pinned dependency of this kit. Never add it to a site's own `package.json`; bump it here, deliberately.
|
|
183
|
+
- **Colours:** the component maps the theming contract onto Hairline's variables: `--hairline-plate` <- `--bg`, `--hairline-hi` <- `--ink`, `--hairline-edge` <- `--brand`, `--hairline-mid` <- `--muted`, `--hairline-lo` <- `--hairline`. A missing host token falls through to Hairline's own light/dark defaults. The mapping has zero specificity: on a tinted section set it on the figure, e.g. `.band .hairline-figure { --hairline-plate: var(--bg-tint); }`. Never a hex value.
|
|
184
|
+
- **Touch:** Hairline sets `touch-action: pan-y`, so a vertical finger drag scrolls the page. Figures driven by the pointer's HEIGHT (`elevator`, `laptop`, `terminal`, `cabinet`, `drawer`, `sieve`, `exploded`, `phone`) fight that scroll on a phone; on touch-first pages pick from `TOUCH_SAFE_HAIRLINE_FIGURES` (`terrain`, `riffle`, `keyboard`, `router`, `rail`, `dish`, `turntable`, `plot`). The figure at rest must read on its own: nothing may depend on hover.
|
|
185
|
+
- **Script bundling:** unlike `HeroBackdrop`, this script is a bundled Astro `<script>` (it imports an npm module), so it emits a root-absolute `/_astro/*.js` URL. On a host that serves a tenant from a sub-directory (kefi-landings), set Astro's `build.assetsPrefix` before using it there.
|
|
186
|
+
- **Vite-alias consumers** (the kit aliased to source, not installed) must have `@lucasmarkes/hairline@0.3.0` resolvable from the site.
|
|
187
|
+
|
|
164
188
|
## Theming contract
|
|
165
189
|
|
|
166
190
|
The components read these CSS vars off the host `:root` (each has a sensible
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/marketing-astro-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Reusable, themeable Astro marketing sections for product sites — a Free-vs-Pro pricing comparison table, a trust / credibility section (security cards, customer-logo + testimonial placeholders, discreet built-by), a client-side \"try it\" demo shell, and an SEO guides/content-hub card + frontmatter schema. Data-driven via a shared TypeScript schema; themed entirely through the host site's CSS custom properties.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"hero",
|
|
18
18
|
"background",
|
|
19
19
|
"canvas",
|
|
20
|
-
"animation"
|
|
20
|
+
"animation",
|
|
21
|
+
"hairline",
|
|
22
|
+
"illustration"
|
|
21
23
|
],
|
|
22
24
|
"author": "dloizides",
|
|
23
25
|
"license": "MIT",
|
|
@@ -42,7 +44,12 @@
|
|
|
42
44
|
"./icons": "./src/icons.ts",
|
|
43
45
|
"./HeroBackdrop.astro": "./src/HeroBackdrop.astro",
|
|
44
46
|
"./heroBackdrop": "./src/heroBackdrop/presets.ts",
|
|
45
|
-
"./heroBackdrop/types": "./src/heroBackdrop/types.ts"
|
|
47
|
+
"./heroBackdrop/types": "./src/heroBackdrop/types.ts",
|
|
48
|
+
"./HairlineFigure.astro": "./src/HairlineFigure.astro",
|
|
49
|
+
"./hairline": "./src/hairline/figures.ts"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@lucasmarkes/hairline": "0.3.0"
|
|
46
53
|
},
|
|
47
54
|
"peerDependencies": {
|
|
48
55
|
"astro": ">=4"
|
package/src/DemoFrame.astro
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { clampHairlineIntensity, type HairlineFigureName } from './hairline/figures';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Which Hairline figure to draw; on touch-first surfaces prefer `TOUCH_SAFE_HAIRLINE_FIGURES`. */
|
|
6
|
+
figure: HairlineFigureName;
|
|
7
|
+
/** The accessible name; the figure is decoration and must never carry information. */
|
|
8
|
+
label: string;
|
|
9
|
+
intensity?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { figure, label, intensity, class: className = '' } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
class:list={['hairline-figure', className]}
|
|
18
|
+
data-hairline-figure={figure}
|
|
19
|
+
data-hairline-intensity={clampHairlineIntensity(intensity)}
|
|
20
|
+
data-hairline-label={label}
|
|
21
|
+
>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
:where(.hairline-figure) {
|
|
26
|
+
display: block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
aspect-ratio: 5 / 4;
|
|
29
|
+
--hairline-plate: var(--bg);
|
|
30
|
+
--hairline-hi: var(--ink);
|
|
31
|
+
--hairline-edge: var(--brand);
|
|
32
|
+
--hairline-mid: var(--muted);
|
|
33
|
+
--hairline-lo: var(--hairline);
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
import { startHairlineFigures } from './hairline/mount';
|
|
39
|
+
|
|
40
|
+
startHairlineFigures();
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const HAIRLINE_FIGURES = [
|
|
2
|
+
'basket',
|
|
3
|
+
'branches',
|
|
4
|
+
'cabinet',
|
|
5
|
+
'dish',
|
|
6
|
+
'drawer',
|
|
7
|
+
'elevator',
|
|
8
|
+
'exploded',
|
|
9
|
+
'keyboard',
|
|
10
|
+
'laptop',
|
|
11
|
+
'lockers',
|
|
12
|
+
'loupe',
|
|
13
|
+
'padlock',
|
|
14
|
+
'patch',
|
|
15
|
+
'phone',
|
|
16
|
+
'phosphor',
|
|
17
|
+
'plot',
|
|
18
|
+
'plug',
|
|
19
|
+
'query',
|
|
20
|
+
'rail',
|
|
21
|
+
'riffle',
|
|
22
|
+
'router',
|
|
23
|
+
'sieve',
|
|
24
|
+
'slow',
|
|
25
|
+
'terminal',
|
|
26
|
+
'terrain',
|
|
27
|
+
'turntable',
|
|
28
|
+
'vault',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type HairlineFigureName = (typeof HAIRLINE_FIGURES)[number];
|
|
32
|
+
|
|
33
|
+
export const TOUCH_SAFE_HAIRLINE_FIGURES: readonly HairlineFigureName[] = [
|
|
34
|
+
'terrain',
|
|
35
|
+
'riffle',
|
|
36
|
+
'keyboard',
|
|
37
|
+
'router',
|
|
38
|
+
'rail',
|
|
39
|
+
'dish',
|
|
40
|
+
'turntable',
|
|
41
|
+
'plot',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_HAIRLINE_INTENSITY = 0.5;
|
|
45
|
+
|
|
46
|
+
export function isHairlineFigureName(value: string): value is HairlineFigureName {
|
|
47
|
+
return (HAIRLINE_FIGURES as readonly string[]).includes(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Clamps an intensity to the 0..1 range Hairline accepts; a non-finite value becomes the default. */
|
|
51
|
+
export function clampHairlineIntensity(value: number | undefined): number {
|
|
52
|
+
if (value === undefined || !Number.isFinite(value)) return DEFAULT_HAIRLINE_INTENSITY;
|
|
53
|
+
return Math.min(1, Math.max(0, value));
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Figure } from '@lucasmarkes/hairline';
|
|
2
|
+
import { clampHairlineIntensity, isHairlineFigureName } from './figures';
|
|
3
|
+
|
|
4
|
+
const HOST_SELECTOR = '[data-hairline-figure]';
|
|
5
|
+
const NEAR_VIEWPORT_MARGIN = '200px 0px';
|
|
6
|
+
|
|
7
|
+
const running = new Map<HTMLElement, Figure>();
|
|
8
|
+
let observer: IntersectionObserver | null = null;
|
|
9
|
+
|
|
10
|
+
async function mountFigure(host: HTMLElement): Promise<void> {
|
|
11
|
+
const name = host.dataset.hairlineFigure ?? '';
|
|
12
|
+
if (!isHairlineFigureName(name) || running.has(host)) return;
|
|
13
|
+
const figures = await import('@lucasmarkes/hairline');
|
|
14
|
+
if (!host.isConnected || running.has(host)) return;
|
|
15
|
+
const figure = figures[name](host, {
|
|
16
|
+
intensity: clampHairlineIntensity(Number(host.dataset.hairlineIntensity)),
|
|
17
|
+
label: host.dataset.hairlineLabel,
|
|
18
|
+
});
|
|
19
|
+
running.set(host, figure);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function onIntersect(entries: IntersectionObserverEntry[], watcher: IntersectionObserver): void {
|
|
23
|
+
entries.forEach((entry) => {
|
|
24
|
+
if (!entry.isIntersecting) return;
|
|
25
|
+
watcher.unobserve(entry.target);
|
|
26
|
+
void mountFigure(entry.target as HTMLElement);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function observeFigures(): void {
|
|
31
|
+
const hosts = document.querySelectorAll<HTMLElement>(HOST_SELECTOR);
|
|
32
|
+
if (hosts.length === 0) return;
|
|
33
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
34
|
+
hosts.forEach((host) => void mountFigure(host));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
observer ??= new IntersectionObserver(onIntersect, { rootMargin: NEAR_VIEWPORT_MARGIN });
|
|
38
|
+
hosts.forEach((host) => observer?.observe(host));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function destroyFigures(): void {
|
|
42
|
+
observer?.disconnect();
|
|
43
|
+
observer = null;
|
|
44
|
+
running.forEach((figure) => figure.destroy());
|
|
45
|
+
running.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Mounts every Hairline host on the page as it nears the viewport and tears them down on an Astro page swap. */
|
|
49
|
+
export function startHairlineFigures(): void {
|
|
50
|
+
observeFigures();
|
|
51
|
+
document.addEventListener('astro:page-load', observeFigures);
|
|
52
|
+
document.addEventListener('astro:before-swap', destroyFigures);
|
|
53
|
+
}
|