@hubpav/hwio-ui 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 +8 -0
- package/package.json +40 -0
- package/src/components/HWioBreadcrumbs.astro +19 -0
- package/src/components/HWioButton.astro +43 -0
- package/src/components/HWioChoiceCards.astro +31 -0
- package/src/components/HWioContactSection.astro +37 -0
- package/src/components/HWioCookieConsent.astro +87 -0
- package/src/components/HWioCtaBand.astro +20 -0
- package/src/components/HWioFaq.astro +19 -0
- package/src/components/HWioFeatureCard.astro +15 -0
- package/src/components/HWioFeatureGrid.astro +9 -0
- package/src/components/HWioFontGate.astro +18 -0
- package/src/components/HWioFooter.astro +96 -0
- package/src/components/HWioFooterSites.astro +16 -0
- package/src/components/HWioFormField.astro +29 -0
- package/src/components/HWioHeader.astro +92 -0
- package/src/components/HWioHeroSplit.astro +33 -0
- package/src/components/HWioHubSpotForm.astro +58 -0
- package/src/components/HWioIcon.astro +44 -0
- package/src/components/HWioLanguageSwitcher.astro +27 -0
- package/src/components/HWioLogoStrip.astro +29 -0
- package/src/components/HWioNotFound.astro +12 -0
- package/src/components/HWioPricingTiers.astro +28 -0
- package/src/components/HWioProcessSteps.astro +17 -0
- package/src/components/HWioSection.astro +10 -0
- package/src/components/HWioSectionHeader.astro +24 -0
- package/src/components/HWioSeoHead.astro +83 -0
- package/src/components/HWioShell.astro +20 -0
- package/src/components/HWioSkipLink.astro +5 -0
- package/src/components/HWioStatsBand.astro +27 -0
- package/src/components/HWioThemeToggle.astro +22 -0
- package/src/components/HWioTurnstile.astro +10 -0
- package/src/html.ts +31 -0
- package/src/integration.mjs +61 -0
- package/src/schemas.ts +51 -0
- package/src/styles/prose.css +24 -0
- package/src/styles/styles.css +221 -0
- package/src/styles/themes/er3o.css +55 -0
- package/src/styles/themes/hwio-dark.css +49 -0
- package/src/styles/themes/hwio-forestry-dark.css +42 -0
- package/src/styles/themes/hwio-forestry.css +42 -0
- package/src/styles/themes/hwio.css +42 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Declarative HubSpot form posting through the website-forms Worker. Honeypot (class, not inline
|
|
4
|
+
* style), hs_language, lazy Turnstile, status line and dataLayer event are built in.
|
|
5
|
+
*/
|
|
6
|
+
import type { HWioFieldDef } from '../types';
|
|
7
|
+
import HWioFormField from './HWioFormField.astro';
|
|
8
|
+
import HWioTurnstile from './HWioTurnstile.astro';
|
|
9
|
+
interface Props {
|
|
10
|
+
form: { key: string; guid: string; endpoint: string; event?: 'generate_lead' | 'newsletter_signup'; enrich?: boolean; kind?: string; source?: string };
|
|
11
|
+
fields: HWioFieldDef[];
|
|
12
|
+
labels: { submit: string; sending: string; success: string; error: string };
|
|
13
|
+
locale: string;
|
|
14
|
+
turnstileSiteKey?: string;
|
|
15
|
+
layout?: 'grid' | 'inline';
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
const { form, fields, labels, locale, turnstileSiteKey, layout = 'grid', class: className = '' } = Astro.props;
|
|
19
|
+
const LAYOUT = { grid: 'grid gap-4 md:grid-cols-2', inline: 'flex flex-col gap-3 sm:flex-row sm:items-end' } as const;
|
|
20
|
+
---
|
|
21
|
+
<form
|
|
22
|
+
class:list={['hwio-form', className]}
|
|
23
|
+
method="post"
|
|
24
|
+
action={form.endpoint}
|
|
25
|
+
novalidate={undefined}
|
|
26
|
+
data-hs-form={form.key}
|
|
27
|
+
data-hs-form-guid={form.guid}
|
|
28
|
+
data-hs-endpoint={form.endpoint}
|
|
29
|
+
data-hs-event={form.event}
|
|
30
|
+
data-hs-enrich={form.enrich ? 'true' : undefined}
|
|
31
|
+
data-form-kind={form.kind}
|
|
32
|
+
data-inquiry-source={form.source}
|
|
33
|
+
data-hs-sending-msg={labels.sending}
|
|
34
|
+
data-hs-success-msg={labels.success}
|
|
35
|
+
data-hs-error-msg={labels.error}
|
|
36
|
+
>
|
|
37
|
+
<div class={LAYOUT[layout]}>
|
|
38
|
+
{fields.map((field) => <HWioFormField field={field} />)}
|
|
39
|
+
{layout === 'inline' && <button type="submit" class="btn btn-primary">{labels.submit}</button>}
|
|
40
|
+
</div>
|
|
41
|
+
<input type="hidden" name="hs_language" value={locale} />
|
|
42
|
+
<div class="hwio-hp" aria-hidden="true">
|
|
43
|
+
<label>Leave this field empty <input type="text" name="hwio_hp" tabindex="-1" autocomplete="off" /></label>
|
|
44
|
+
</div>
|
|
45
|
+
{turnstileSiteKey && <div class="mt-4"><HWioTurnstile siteKey={turnstileSiteKey} /></div>}
|
|
46
|
+
<slot name="consent" />
|
|
47
|
+
{layout === 'grid' && (
|
|
48
|
+
<div class="mt-6 flex flex-wrap items-center gap-4">
|
|
49
|
+
<button type="submit" class="btn btn-primary btn-lg">{labels.submit}</button>
|
|
50
|
+
<slot name="after-submit" />
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<p data-hs-status hidden role="status" aria-live="polite"></p>
|
|
54
|
+
</form>
|
|
55
|
+
<script>
|
|
56
|
+
import { hwioFormsRuntime } from '@hubpav/hwio-web-runtime/forms';
|
|
57
|
+
hwioFormsRuntime();
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Curated inline SVG icons (Heroicons outline paths, MIT; brand social marks). No icon library dependency. */
|
|
3
|
+
interface Props {
|
|
4
|
+
name: keyof typeof STROKE | keyof typeof FILL;
|
|
5
|
+
class?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
const STROKE = {
|
|
10
|
+
'arrow-right': 'M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3',
|
|
11
|
+
'arrow-up-right': 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
|
|
12
|
+
external: 'M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25',
|
|
13
|
+
'chevron-down': 'm19.5 8.25-7.5 7.5-7.5-7.5',
|
|
14
|
+
'chevron-left': 'M15.75 19.5 8.25 12l7.5-7.5',
|
|
15
|
+
'chevron-right': 'm8.25 4.5 7.5 7.5-7.5 7.5',
|
|
16
|
+
check: 'm4.5 12.75 6 6 9-13.5',
|
|
17
|
+
close: 'M6 18 18 6M6 6l12 12',
|
|
18
|
+
menu: 'M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5',
|
|
19
|
+
sun: 'M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z',
|
|
20
|
+
moon: 'M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z',
|
|
21
|
+
globe: 'M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5a17.92 17.92 0 0 1-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418',
|
|
22
|
+
bag: 'M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 9.75h12.974c.576 0 1.059.435 1.119 1.007Z',
|
|
23
|
+
phone: 'M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z',
|
|
24
|
+
mail: 'M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75',
|
|
25
|
+
download: 'M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3',
|
|
26
|
+
} as const;
|
|
27
|
+
const FILL = {
|
|
28
|
+
linkedin: 'M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77Z',
|
|
29
|
+
x: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
|
|
30
|
+
instagram: 'M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4H7.6m9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8 1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3Z',
|
|
31
|
+
youtube: 'M10 15l5.19-3L10 9v6m11.56-7.83c.13.47.22 1.1.28 1.9.07.8.1 1.49.1 2.09L22 12c0 2.19-.16 3.8-.44 4.83-.25.9-.83 1.48-1.73 1.73-.47.13-1.33.22-2.65.28-1.3.07-2.49.1-3.59.1L12 19c-4.19 0-6.8-.16-7.83-.44-.9-.25-1.48-.83-1.73-1.73-.13-.47-.22-1.1-.28-1.9-.07-.8-.1-1.49-.1-2.09L2 12c0-2.19.16-3.8.44-4.83.25-.9.83-1.48 1.73-1.73.47-.13 1.33-.22 2.65-.28 1.3-.07 2.49-.1 3.59-.1L12 5c4.19 0 6.8.16 7.83.44.9.25 1.48.83 1.73 1.73Z',
|
|
32
|
+
github: 'M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z',
|
|
33
|
+
forum: 'M12 3C6.5 3 2 6.58 2 11a7.218 7.218 0 0 0 2.75 5.5c0 .6-.42 2.17-2.75 4.5 2.37-.11 4.64-1 6.47-2.5H12c5.5 0 10-3.58 10-8s-4.5-8-10-8Z',
|
|
34
|
+
} as const;
|
|
35
|
+
const SIZE = { sm: 'size-4', md: 'size-5', lg: 'size-6' } as const;
|
|
36
|
+
const { name, class: className = '', size = 'md', label } = Astro.props;
|
|
37
|
+
const stroke = (STROKE as Record<string, string>)[name];
|
|
38
|
+
const fill = (FILL as Record<string, string>)[name];
|
|
39
|
+
---
|
|
40
|
+
{stroke ? (
|
|
41
|
+
<svg class:list={[SIZE[size], className]} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden={label ? undefined : 'true'} role={label ? 'img' : undefined} aria-label={label}><path d={stroke} /></svg>
|
|
42
|
+
) : (
|
|
43
|
+
<svg class:list={[SIZE[size], className]} viewBox="0 0 24 24" fill="currentColor" aria-hidden={label ? undefined : 'true'} role={label ? 'img' : undefined} aria-label={label}><path d={fill} /></svg>
|
|
44
|
+
)}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import HWioIcon from './HWioIcon.astro';
|
|
3
|
+
interface Item { code: string; label: string; href: string; current?: boolean }
|
|
4
|
+
interface Props { items: Item[]; label: string; variant?: 'pills' | 'dropdown'; class?: string }
|
|
5
|
+
const { items, label, variant = 'dropdown', class: className = '' } = Astro.props;
|
|
6
|
+
const current = items.find((i) => i.current);
|
|
7
|
+
---
|
|
8
|
+
{variant === 'pills' ? (
|
|
9
|
+
<nav aria-label={label} class:list={['join', className]}>
|
|
10
|
+
{items.map((i) => (
|
|
11
|
+
<a href={i.href} hreflang={i.code} lang={i.code} class:list={['btn btn-sm join-item', i.current ? 'btn-primary' : 'btn-ghost']} aria-current={i.current ? 'page' : undefined}>{i.label}</a>
|
|
12
|
+
))}
|
|
13
|
+
</nav>
|
|
14
|
+
) : (
|
|
15
|
+
<details class:list={['dropdown dropdown-end', className]}>
|
|
16
|
+
<summary class="btn btn-ghost btn-sm gap-1" aria-label={label}>
|
|
17
|
+
<HWioIcon name="globe" size="sm" />
|
|
18
|
+
<span class="uppercase">{current?.code ?? items[0]?.code}</span>
|
|
19
|
+
<HWioIcon name="chevron-down" size="sm" />
|
|
20
|
+
</summary>
|
|
21
|
+
<ul class="menu dropdown-content bg-base-100 rounded-box z-50 mt-2 w-44 p-2 shadow-lg border border-base-300" aria-label={label}>
|
|
22
|
+
{items.map((i) => (
|
|
23
|
+
<li><a href={i.href} hreflang={i.code} lang={i.code} class:list={[i.current ? 'menu-active' : '']} aria-current={i.current ? 'page' : undefined}>{i.label}</a></li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
26
|
+
</details>
|
|
27
|
+
)}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Customer/partner logos as a static row or an infinite marquee (paused on hover, static under reduced motion). */
|
|
3
|
+
import type { HWioTone } from '../types';
|
|
4
|
+
interface Logo { src: string; alt: string; href?: string; width: number; height: number; class?: string }
|
|
5
|
+
interface Props { id?: string; logos: Logo[]; label?: string; marquee?: boolean; grey?: boolean; tone?: HWioTone; class?: string }
|
|
6
|
+
const { id, logos, label, marquee = true, grey = true, tone = 'base', class: className = '' } = Astro.props;
|
|
7
|
+
const TONE = { base: 'bg-base-100', alt: 'bg-base-200', neutral: 'bg-neutral text-neutral-content' } as const;
|
|
8
|
+
const FADE = { base: 'from-base-100', alt: 'from-base-200', neutral: 'from-neutral' } as const;
|
|
9
|
+
const groups = marquee ? [0, 1, 2, 3] : [0];
|
|
10
|
+
---
|
|
11
|
+
<section id={id} class:list={['overflow-hidden border-y border-base-300 py-10', TONE[tone], className]}>
|
|
12
|
+
{label && <div class="container"><p class="mb-8 text-center text-sm font-semibold opacity-70">{label}</p></div>}
|
|
13
|
+
<div class="relative">
|
|
14
|
+
{marquee && <div class:list={['pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r to-transparent md:w-32', FADE[tone]]}></div>}
|
|
15
|
+
{marquee && <div class:list={['pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l to-transparent md:w-32', FADE[tone]]}></div>}
|
|
16
|
+
<div class:list={[marquee ? 'hwio-marquee-track flex' : 'container flex flex-wrap items-center justify-center gap-x-12 gap-y-6']} role="group" aria-label={label}>
|
|
17
|
+
{groups.map((g) => (
|
|
18
|
+
<div class="flex shrink-0 items-center" aria-hidden={g > 0 ? 'true' : undefined}>
|
|
19
|
+
{logos.map((logo) => {
|
|
20
|
+
const img = <img src={logo.src} alt={logo.alt} width={logo.width} height={logo.height} loading="lazy" decoding="async" class:list={['h-7 w-auto transition-transform duration-300 hover:scale-105', grey ? 'hwio-logo-grey' : '', logo.class]} />;
|
|
21
|
+
return logo.href
|
|
22
|
+
? <a href={logo.href} target="_blank" rel="noopener noreferrer" class="mx-8" tabindex={g > 0 ? -1 : undefined}>{img}</a>
|
|
23
|
+
: <span class="mx-8">{img}</span>;
|
|
24
|
+
})}
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HWioCta } from '../types';
|
|
3
|
+
import HWioButton from './HWioButton.astro';
|
|
4
|
+
interface Props { code?: string; title: string; text?: string; cta: HWioCta }
|
|
5
|
+
const { code = '404', title, text, cta } = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
<section class="container flex min-h-[60vh] flex-col items-center justify-center py-24 text-center">
|
|
8
|
+
<p class="hwio-eyebrow mb-4">{code}</p>
|
|
9
|
+
<h1>{title}</h1>
|
|
10
|
+
{text && <p class="hwio-lead mt-4 max-w-xl">{text}</p>}
|
|
11
|
+
<div class="mt-8"><HWioButton href={cta.href} variant="primary" arrow track={cta.track}>{cta.label}</HWioButton></div>
|
|
12
|
+
</section>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HWioCta } from '../types';
|
|
3
|
+
import HWioButton from './HWioButton.astro';
|
|
4
|
+
import HWioIcon from './HWioIcon.astro';
|
|
5
|
+
interface Tier { name: string; price?: string; description?: string; features: string[]; cta?: HWioCta; featured?: boolean; badge?: string }
|
|
6
|
+
interface Props { tiers: Tier[]; class?: string }
|
|
7
|
+
const { tiers, class: className = '' } = Astro.props;
|
|
8
|
+
const COLS = { 1: 'grid gap-6', 2: 'grid gap-6 md:grid-cols-2', 3: 'grid gap-6 md:grid-cols-3', 4: 'grid gap-6 md:grid-cols-2 lg:grid-cols-4' } as const;
|
|
9
|
+
const cols = (Math.min(Math.max(tiers.length, 1), 4) as 1 | 2 | 3 | 4);
|
|
10
|
+
---
|
|
11
|
+
<div class:list={[COLS[cols], className]}>
|
|
12
|
+
{tiers.map((tier) => (
|
|
13
|
+
<article class:list={['card card-border bg-base-100 h-full', tier.featured ? 'border-primary shadow-lg' : '']}>
|
|
14
|
+
<div class="card-body">
|
|
15
|
+
<div class="flex items-start justify-between gap-2">
|
|
16
|
+
<h3 class="card-title text-[1.375rem]">{tier.name}</h3>
|
|
17
|
+
{tier.badge && <span class="badge badge-primary">{tier.badge}</span>}
|
|
18
|
+
</div>
|
|
19
|
+
{tier.price && <p class="text-3xl font-bold">{tier.price}</p>}
|
|
20
|
+
{tier.description && <p class="opacity-80">{tier.description}</p>}
|
|
21
|
+
<ul class="my-4 space-y-2 text-sm">
|
|
22
|
+
{tier.features.map((f) => <li class="flex items-start gap-2"><HWioIcon name="check" size="sm" class="mt-0.5 shrink-0 text-accent" /><span>{f}</span></li>)}
|
|
23
|
+
</ul>
|
|
24
|
+
{tier.cta && <div class="card-actions mt-auto"><HWioButton href={tier.cta.href} external={tier.cta.external} variant={tier.featured ? 'primary' : 'secondary'} class="w-full" track={tier.cta.track}>{tier.cta.label}</HWioButton></div>}
|
|
25
|
+
</div>
|
|
26
|
+
</article>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Numbered process steps. Order carries meaning here, so the numbers are content, not decoration. */
|
|
3
|
+
interface Props { items: { title: string; text: string }[]; orientation?: 'horizontal' | 'vertical'; class?: string }
|
|
4
|
+
const { items, orientation = 'horizontal', class: className = '' } = Astro.props;
|
|
5
|
+
const LAYOUT = { horizontal: 'grid gap-8 md:grid-cols-2 lg:grid-cols-4', vertical: 'grid gap-8' } as const;
|
|
6
|
+
---
|
|
7
|
+
<ol class:list={[LAYOUT[orientation], className]} data-hwio-steps>
|
|
8
|
+
{items.map((step, i) => (
|
|
9
|
+
<li class="flex gap-4">
|
|
10
|
+
<span class="badge badge-primary badge-lg shrink-0 font-semibold" aria-hidden="true">{i + 1}</span>
|
|
11
|
+
<div>
|
|
12
|
+
<h3 class="text-lg font-semibold"><span class="sr-only">{i + 1}. </span>{step.title}</h3>
|
|
13
|
+
<p class="mt-1 opacity-80">{step.text}</p>
|
|
14
|
+
</div>
|
|
15
|
+
</li>
|
|
16
|
+
))}
|
|
17
|
+
</ol>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HWioTone } from '../types';
|
|
3
|
+
interface Props { id?: string; tone?: HWioTone; spacing?: 'normal' | 'compact' | 'none'; container?: boolean; as?: 'section' | 'div' | 'article'; reveal?: boolean; class?: string; [key: string]: unknown }
|
|
4
|
+
const TONE = { base: 'bg-base-100 text-base-content hwio-tone-base', alt: 'bg-base-200 text-base-content hwio-tone-alt', neutral: 'bg-neutral text-neutral-content hwio-tone-neutral' } as const;
|
|
5
|
+
const SPACING = { normal: 'hwio-section', compact: 'hwio-section-compact', none: '' } as const;
|
|
6
|
+
const { id, tone = 'base', spacing = 'normal', container = true, as: Tag = 'section', reveal = false, class: className = '', ...rest } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<Tag id={id} class:list={[TONE[tone], SPACING[spacing], className]} data-hwio-reveal={reveal ? 'true' : undefined} {...rest}>
|
|
9
|
+
{container ? <div class="container"><slot /></div> : <slot />}
|
|
10
|
+
</Tag>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Kicker + heading + lead. `layout="split"` puts the lead beside the heading (direction A's section opener). */
|
|
3
|
+
interface Props { eyebrow?: string; title: string; lead?: string; align?: 'left' | 'center'; layout?: 'stack' | 'split'; level?: 1 | 2 | 3; class?: string }
|
|
4
|
+
const ALIGN = { left: 'text-left', center: 'mx-auto text-center' } as const;
|
|
5
|
+
const { eyebrow, title, lead, align = 'center', layout = 'stack', level = 2, class: className = '' } = Astro.props;
|
|
6
|
+
const Heading = `h${level}` as 'h1' | 'h2' | 'h3';
|
|
7
|
+
---
|
|
8
|
+
{layout === 'split' ? (
|
|
9
|
+
<div class:list={['grid gap-8 lg:grid-cols-[5fr_7fr] lg:items-end', className]}>
|
|
10
|
+
<div>
|
|
11
|
+
{eyebrow && <p class="hwio-kicker mb-4">{eyebrow}</p>}
|
|
12
|
+
<Heading><slot name="title">{title}</slot></Heading>
|
|
13
|
+
</div>
|
|
14
|
+
{lead && <p class="hwio-lead lg:max-w-[36rem]">{lead}</p>}
|
|
15
|
+
<slot />
|
|
16
|
+
</div>
|
|
17
|
+
) : (
|
|
18
|
+
<div class:list={['max-w-3xl', ALIGN[align], className]}>
|
|
19
|
+
{eyebrow && <p class="hwio-kicker mb-4">{eyebrow}</p>}
|
|
20
|
+
<Heading><slot name="title">{title}</slot></Heading>
|
|
21
|
+
{lead && <p class="hwio-lead mt-4">{lead}</p>}
|
|
22
|
+
<slot />
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Everything in <head> for a site page. Sites keep a thin BaseLayout that renders
|
|
4
|
+
* <html {...hwioHtmlAttrs(...)}><head><HWioSeoHead …/></head><body><HWioShell …/></body></html>.
|
|
5
|
+
* Copy-free: title, description, alternates, JSON-LD and the OG image all come from the site.
|
|
6
|
+
*/
|
|
7
|
+
import { consentDefaults, themeBootstrap } from '@hubpav/hwio-web-runtime/inline';
|
|
8
|
+
import type { HWioAlternate } from '../types';
|
|
9
|
+
import HWioFontGate from './HWioFontGate.astro';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
siteName: string;
|
|
13
|
+
siteUrl: string;
|
|
14
|
+
title: string;
|
|
15
|
+
/** Appended as " | <siteName>" unless the title already contains it. */
|
|
16
|
+
suffix?: boolean;
|
|
17
|
+
description: string;
|
|
18
|
+
canonical: string;
|
|
19
|
+
locale: string;
|
|
20
|
+
ogLocale?: string;
|
|
21
|
+
alternates?: HWioAlternate[];
|
|
22
|
+
ogImage: { url: string; width?: number; height?: number; alt?: string };
|
|
23
|
+
pageType?: 'website' | 'article' | 'product' | 'profile';
|
|
24
|
+
article?: { publishedTime?: string; modifiedTime?: string; authors?: string[]; section?: string; tags?: string[] };
|
|
25
|
+
noindex?: boolean;
|
|
26
|
+
jsonLd?: object[];
|
|
27
|
+
preloadImage?: string;
|
|
28
|
+
twitterSite?: string;
|
|
29
|
+
favicons?: { svg?: string; png32?: string; apple?: string };
|
|
30
|
+
/** Font files to preload and gate on (null disables the gate). */
|
|
31
|
+
fonts?: string[] | null;
|
|
32
|
+
/** Emit Consent Mode v2 denied defaults (sites with GTM). */
|
|
33
|
+
consentMode?: boolean;
|
|
34
|
+
themeColor?: string;
|
|
35
|
+
}
|
|
36
|
+
const {
|
|
37
|
+
siteName, siteUrl, title, suffix = true, description, canonical, locale, ogLocale, alternates = [], ogImage,
|
|
38
|
+
pageType = 'website', article, noindex = false, jsonLd = [], preloadImage, twitterSite,
|
|
39
|
+
favicons = { svg: '/favicon.svg', png32: '/favicon-32x32.png', apple: '/apple-touch-icon.png' },
|
|
40
|
+
fonts = null, consentMode = false, themeColor,
|
|
41
|
+
} = Astro.props;
|
|
42
|
+
const fullTitle = suffix && !title.includes(siteName) ? `${title} | ${siteName}` : title;
|
|
43
|
+
const abs = (u: string) => (u.startsWith('http') ? u : `${siteUrl}${u}`);
|
|
44
|
+
---
|
|
45
|
+
<meta charset="UTF-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
47
|
+
{fonts && <HWioFontGate files={fonts} />}
|
|
48
|
+
<script is:inline set:html={themeBootstrap.code} />
|
|
49
|
+
{consentMode && <script is:inline set:html={consentDefaults.code} />}
|
|
50
|
+
<meta name="generator" content={Astro.generator} />
|
|
51
|
+
<title>{fullTitle}</title>
|
|
52
|
+
<meta name="description" content={description} />
|
|
53
|
+
<link rel="canonical" href={canonical} />
|
|
54
|
+
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
|
55
|
+
{themeColor && <meta name="theme-color" content={themeColor} />}
|
|
56
|
+
{favicons.svg && <link rel="icon" type="image/svg+xml" href={favicons.svg} />}
|
|
57
|
+
{favicons.png32 && <link rel="icon" type="image/png" sizes="32x32" href={favicons.png32} />}
|
|
58
|
+
{favicons.apple && <link rel="apple-touch-icon" sizes="180x180" href={favicons.apple} />}
|
|
59
|
+
{!noindex && alternates.map((a) => <link rel="alternate" hreflang={a.lang} href={a.href} />)}
|
|
60
|
+
<meta property="og:type" content={pageType} />
|
|
61
|
+
<meta property="og:url" content={canonical} />
|
|
62
|
+
<meta property="og:title" content={fullTitle} />
|
|
63
|
+
<meta property="og:description" content={description} />
|
|
64
|
+
<meta property="og:image" content={abs(ogImage.url)} />
|
|
65
|
+
{ogImage.width && <meta property="og:image:width" content={String(ogImage.width)} />}
|
|
66
|
+
{ogImage.height && <meta property="og:image:height" content={String(ogImage.height)} />}
|
|
67
|
+
<meta property="og:image:alt" content={ogImage.alt ?? title} />
|
|
68
|
+
<meta property="og:site_name" content={siteName} />
|
|
69
|
+
{ogLocale && <meta property="og:locale" content={ogLocale} />}
|
|
70
|
+
{pageType === 'article' && article?.publishedTime && <meta property="article:published_time" content={article.publishedTime} />}
|
|
71
|
+
{pageType === 'article' && article?.modifiedTime && <meta property="article:modified_time" content={article.modifiedTime} />}
|
|
72
|
+
{pageType === 'article' && article?.section && <meta property="article:section" content={article.section} />}
|
|
73
|
+
{pageType === 'article' && article?.authors?.map((a) => <meta property="article:author" content={a} />)}
|
|
74
|
+
{pageType === 'article' && article?.tags?.map((t) => <meta property="article:tag" content={t} />)}
|
|
75
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
76
|
+
{twitterSite && <meta name="twitter:site" content={twitterSite} />}
|
|
77
|
+
<meta name="twitter:title" content={fullTitle} />
|
|
78
|
+
<meta name="twitter:description" content={description} />
|
|
79
|
+
<meta name="twitter:image" content={abs(ogImage.url)} />
|
|
80
|
+
<meta name="twitter:image:alt" content={ogImage.alt ?? title} />
|
|
81
|
+
{preloadImage && <link rel="preload" href={preloadImage} as="image" fetchpriority="high" />}
|
|
82
|
+
{jsonLd.map((ld) => <script is:inline type="application/ld+json" set:html={JSON.stringify(ld)} />)}
|
|
83
|
+
<slot />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Body skeleton: skip link, header, optional breadcrumb, <main id="main-content">, footer, consent. Also mounts CTA tracking. */
|
|
3
|
+
import HWioSkipLink from './HWioSkipLink.astro';
|
|
4
|
+
interface Props { skipLabel: string; class?: string }
|
|
5
|
+
const { skipLabel, class: className = '' } = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
<div class:list={['min-h-screen flex flex-col bg-base-100 text-base-content', className]}>
|
|
8
|
+
<HWioSkipLink label={skipLabel} />
|
|
9
|
+
<slot name="header" />
|
|
10
|
+
<slot name="breadcrumb" />
|
|
11
|
+
<main id="main-content" class="flex-1">
|
|
12
|
+
<slot />
|
|
13
|
+
</main>
|
|
14
|
+
<slot name="footer" />
|
|
15
|
+
<slot name="consent" />
|
|
16
|
+
</div>
|
|
17
|
+
<script>
|
|
18
|
+
import { hwioTrackRuntime } from '@hubpav/hwio-web-runtime/track';
|
|
19
|
+
hwioTrackRuntime();
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Proof numbers in a hairline band with dividers (direction A); counts up on first view unless `animate` is false. */
|
|
3
|
+
interface Props { id?: string; items: { value: string | number; suffix?: string; label: string; description?: string }[]; animate?: boolean; class?: string }
|
|
4
|
+
const { id, items, animate = true, class: className = '' } = Astro.props;
|
|
5
|
+
const COLS = { 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-3', 4: 'sm:grid-cols-2 lg:grid-cols-4' } as const;
|
|
6
|
+
const cols = COLS[(Math.min(Math.max(items.length, 2), 4) as 2 | 3 | 4)];
|
|
7
|
+
---
|
|
8
|
+
<section id={id} class:list={['border-y border-base-300 bg-base-100 text-base-content', className]}>
|
|
9
|
+
<div class:list={['container grid py-9', cols]}>
|
|
10
|
+
{items.map((s, i) => {
|
|
11
|
+
const numeric = typeof s.value === 'number';
|
|
12
|
+
return (
|
|
13
|
+
<div class:list={['px-8 py-2', i > 0 ? 'sm:border-l sm:border-base-300' : '']}>
|
|
14
|
+
<div class="mb-2.5 text-[2.5rem] font-bold leading-none tracking-[-0.03em]">
|
|
15
|
+
<span class="hwio-stat-value" data-hwio-counter={animate && numeric ? 'true' : undefined} data-target={numeric ? String(s.value) : undefined} data-suffix={s.suffix ?? ''}>{s.value}{s.suffix ?? ''}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="text-[15px] font-semibold">{s.label}</div>
|
|
18
|
+
{s.description && <div class="hwio-muted mt-0.5 text-sm">{s.description}</div>}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
})}
|
|
22
|
+
</div>
|
|
23
|
+
</section>
|
|
24
|
+
<script>
|
|
25
|
+
import { hwioCounterRuntime } from '@hubpav/hwio-web-runtime/counter';
|
|
26
|
+
hwioCounterRuntime();
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import HWioIcon from './HWioIcon.astro';
|
|
3
|
+
interface Props { label: string; class?: string }
|
|
4
|
+
const { label, class: className = '' } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
<button type="button" data-hwio-theme-toggle class:list={['btn btn-ghost btn-square btn-sm hwio-theme-toggle', className]} aria-label={label} title={label} aria-pressed="false">
|
|
7
|
+
<HWioIcon name="moon" class="hwio-theme-toggle__moon" />
|
|
8
|
+
<HWioIcon name="sun" class="hwio-theme-toggle__sun" />
|
|
9
|
+
</button>
|
|
10
|
+
<style>
|
|
11
|
+
.hwio-theme-toggle :global(.hwio-theme-toggle__sun) { display: none; }
|
|
12
|
+
:global([data-theme$='-dark']) .hwio-theme-toggle :global(.hwio-theme-toggle__sun) { display: block; }
|
|
13
|
+
:global([data-theme$='-dark']) .hwio-theme-toggle :global(.hwio-theme-toggle__moon) { display: none; }
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:global(html:not([data-theme])) .hwio-theme-toggle :global(.hwio-theme-toggle__sun) { display: block; }
|
|
16
|
+
:global(html:not([data-theme])) .hwio-theme-toggle :global(.hwio-theme-toggle__moon) { display: none; }
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
<script>
|
|
20
|
+
import { hwioThemeRuntime } from '@hubpav/hwio-web-runtime/theme';
|
|
21
|
+
hwioThemeRuntime();
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Lazy explicit Cloudflare Turnstile widget; place inside a <form>. The site key is public by design. */
|
|
3
|
+
interface Props { siteKey: string }
|
|
4
|
+
const { siteKey } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
<div class="cf-turnstile" data-sitekey={siteKey}></div>
|
|
7
|
+
<script>
|
|
8
|
+
import { hwioTurnstileRuntime } from '@hubpav/hwio-web-runtime/turnstile';
|
|
9
|
+
hwioTurnstileRuntime();
|
|
10
|
+
</script>
|
package/src/html.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attributes for a site's <html> element. The runtime bootstraps (theme, font gate, consent)
|
|
3
|
+
* read these instead of being templated per site, which keeps their CSP hashes stable.
|
|
4
|
+
* <html {...hwioHtmlAttrs({ lang, consentVersion, themes: { light: 'hwio', dark: 'hwio-dark' }, font: { family: 'Inter', sample, sampleExt } })}>
|
|
5
|
+
*/
|
|
6
|
+
export interface HWioHtmlOptions {
|
|
7
|
+
lang: string;
|
|
8
|
+
consentVersion: string;
|
|
9
|
+
themes: { light: string; dark?: string };
|
|
10
|
+
/** Pin data-theme on <html> (single-theme sites such as enerooo, or a fixed catalog page). */
|
|
11
|
+
pinTheme?: boolean;
|
|
12
|
+
font?: { family: string; sample?: string; sampleExt?: string } | null;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hwioHtmlAttrs(o: HWioHtmlOptions): Record<string, string> {
|
|
17
|
+
const attrs: Record<string, string> = {
|
|
18
|
+
lang: o.lang,
|
|
19
|
+
class: [o.font ? 'fonts-loading' : '', o.class ?? ''].filter(Boolean).join(' '),
|
|
20
|
+
'data-hwio-consent-version': o.consentVersion,
|
|
21
|
+
'data-hwio-theme-light': o.themes.light,
|
|
22
|
+
};
|
|
23
|
+
if (o.themes.dark) attrs['data-hwio-theme-dark'] = o.themes.dark;
|
|
24
|
+
if (o.pinTheme || !o.themes.dark) attrs['data-theme'] = o.themes.light;
|
|
25
|
+
if (o.font) {
|
|
26
|
+
attrs['data-hwio-font-family'] = o.font.family;
|
|
27
|
+
if (o.font.sample) attrs['data-hwio-font-sample'] = o.font.sample;
|
|
28
|
+
if (o.font.sampleExt) attrs['data-hwio-font-sample-ext'] = o.font.sampleExt;
|
|
29
|
+
}
|
|
30
|
+
return attrs;
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Astro integration for sites on the design system.
|
|
2
|
+
// import { hwioUi } from '@hubpav/hwio-ui/integration';
|
|
3
|
+
// integrations: [hwioUi({ headers: { gtm: true, turnstile: true, submitEndpoint: 'https://submit.hardwario.com', earlyHints: ['/fonts/inter-latin.woff2', '/fonts/inter-latin-ext.woff2'] } })]
|
|
4
|
+
// It (1) forces component scripts to stay external (script-src 'self' covers them), (2) serves the
|
|
5
|
+
// brand fonts and logos in dev and copies them into the build output, (3) writes `_headers`
|
|
6
|
+
// from one options object so security headers have a single source of truth.
|
|
7
|
+
import { copyFileSync, mkdirSync, readdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join, dirname, extname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { hwioRenderHeadersFile } from '@hubpav/hwio-web-worker/headers';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
function brandDir() {
|
|
16
|
+
return dirname(require.resolve('@hubpav/hwio-brand/package.json'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MIME = { '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon' };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{ headers?: import('@hubpav/hwio-web-worker/headers').HWioHeadersOptions | false, assets?: boolean }} [options]
|
|
23
|
+
* @returns {import('astro').AstroIntegration}
|
|
24
|
+
*/
|
|
25
|
+
export function hwioUi(options = {}) {
|
|
26
|
+
const { headers = false, assets = true } = options;
|
|
27
|
+
const brand = brandDir();
|
|
28
|
+
const fontsDir = join(brand, 'dist/fonts');
|
|
29
|
+
const logosDir = join(brand, 'logos');
|
|
30
|
+
return {
|
|
31
|
+
name: '@hubpav/hwio-ui',
|
|
32
|
+
hooks: {
|
|
33
|
+
'astro:config:setup': ({ updateConfig }) => {
|
|
34
|
+
updateConfig({ vite: { build: { assetsInlineLimit: 0 } } });
|
|
35
|
+
},
|
|
36
|
+
'astro:server:setup': ({ server }) => {
|
|
37
|
+
if (!assets) return;
|
|
38
|
+
server.middlewares.use((req, res, next) => {
|
|
39
|
+
const url = req.url ?? '';
|
|
40
|
+
const map = url.startsWith('/fonts/') ? [fontsDir, url.slice('/fonts/'.length)] : url.startsWith('/brand/') ? [logosDir, url.slice('/brand/'.length)] : null;
|
|
41
|
+
if (!map) return next();
|
|
42
|
+
const file = join(map[0], map[1].split('?')[0]);
|
|
43
|
+
if (!file.startsWith(map[0]) || !existsSync(file)) return next();
|
|
44
|
+
res.setHeader('Content-Type', MIME[extname(file)] ?? 'application/octet-stream');
|
|
45
|
+
res.end(readFileSync(file));
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
'astro:build:done': ({ dir }) => {
|
|
49
|
+
const out = fileURLToPath(dir);
|
|
50
|
+
if (assets) {
|
|
51
|
+
for (const [src, dest] of [[fontsDir, 'fonts'], [logosDir, 'brand']]) {
|
|
52
|
+
if (!existsSync(src)) continue;
|
|
53
|
+
mkdirSync(join(out, dest), { recursive: true });
|
|
54
|
+
for (const f of readdirSync(src)) if (extname(f) !== '.css') copyFileSync(join(src, f), join(out, dest, f));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (headers) writeFileSync(join(out, '_headers'), hwioRenderHeadersFile(headers));
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Zod fragments for site content collections (Astro's bundled zod), so rebuilt micro-sites
|
|
2
|
+
// validate the YAML that feeds the composed components.
|
|
3
|
+
import { z } from 'astro/zod';
|
|
4
|
+
|
|
5
|
+
export const hwioCtaSchema = z.object({
|
|
6
|
+
label: z.string(),
|
|
7
|
+
href: z.string(),
|
|
8
|
+
external: z.boolean().optional(),
|
|
9
|
+
track: z.object({ id: z.string(), location: z.string().optional() }).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const hwioStatSchema = z.object({
|
|
13
|
+
value: z.number(),
|
|
14
|
+
suffix: z.string().optional(),
|
|
15
|
+
label: z.string(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const hwioStepSchema = z.object({
|
|
19
|
+
title: z.string(),
|
|
20
|
+
text: z.string(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const hwioFaqItemSchema = z.object({
|
|
24
|
+
q: z.string(),
|
|
25
|
+
a: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const hwioFeatureSchema = z.object({
|
|
29
|
+
icon: z.string().optional(),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
text: z.string(),
|
|
32
|
+
href: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const hwioTierSchema = z.object({
|
|
36
|
+
name: z.string(),
|
|
37
|
+
price: z.string().optional(),
|
|
38
|
+
description: z.string().optional(),
|
|
39
|
+
features: z.array(z.string()),
|
|
40
|
+
cta: hwioCtaSchema.optional(),
|
|
41
|
+
featured: z.boolean().optional(),
|
|
42
|
+
badge: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const hwioLogoSchema = z.object({
|
|
46
|
+
src: z.string(),
|
|
47
|
+
alt: z.string(),
|
|
48
|
+
href: z.string().optional(),
|
|
49
|
+
width: z.number(),
|
|
50
|
+
height: z.number(),
|
|
51
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Markdown/long-form typography on semantic colours. Import next to styles.css when a site renders prose. */
|
|
2
|
+
@layer components {
|
|
3
|
+
.hwio-prose { line-height: 1.7; color: var(--color-base-content); }
|
|
4
|
+
.hwio-prose p { margin-bottom: calc(var(--spacing) * 4); }
|
|
5
|
+
.hwio-prose h2 { margin-top: calc(var(--spacing) * 10); margin-bottom: calc(var(--spacing) * 4); }
|
|
6
|
+
.hwio-prose h3 { margin-top: calc(var(--spacing) * 8); margin-bottom: calc(var(--spacing) * 3); }
|
|
7
|
+
.hwio-prose h4 { margin-top: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 2); }
|
|
8
|
+
.hwio-prose ul { list-style: disc; padding-left: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 4); }
|
|
9
|
+
.hwio-prose ol { list-style: decimal; padding-left: calc(var(--spacing) * 6); margin-bottom: calc(var(--spacing) * 4); }
|
|
10
|
+
.hwio-prose li { padding-left: calc(var(--spacing) * 1); margin-bottom: calc(var(--spacing) * 2); }
|
|
11
|
+
.hwio-prose a { color: var(--color-secondary); text-decoration: underline; text-underline-offset: 0.2em; }
|
|
12
|
+
.hwio-prose a:hover { color: var(--color-primary); }
|
|
13
|
+
.hwio-prose strong { font-weight: 600; }
|
|
14
|
+
.hwio-prose blockquote { border-left: 4px solid var(--color-secondary); padding-left: calc(var(--spacing) * 4); font-style: italic; margin-block: calc(var(--spacing) * 6); }
|
|
15
|
+
.hwio-prose code { background: var(--color-base-200); padding: 0.125em 0.375em; border-radius: var(--radius-field); font-size: 0.9em; }
|
|
16
|
+
.hwio-prose pre { background: var(--color-neutral); color: var(--color-neutral-content); padding: calc(var(--spacing) * 4); border-radius: var(--radius-box); overflow-x: auto; margin-block: calc(var(--spacing) * 6); }
|
|
17
|
+
.hwio-prose pre code { background: transparent; padding: 0; color: inherit; }
|
|
18
|
+
.hwio-prose hr { border-color: var(--color-base-300); margin-block: calc(var(--spacing) * 8); }
|
|
19
|
+
.hwio-prose img { border-radius: var(--radius-box); margin-block: calc(var(--spacing) * 6); margin-inline: auto; max-width: 100%; }
|
|
20
|
+
.hwio-prose table { width: 100%; border-collapse: collapse; margin-block: calc(var(--spacing) * 6); }
|
|
21
|
+
.hwio-prose th { background: var(--color-base-200); border: 1px solid var(--color-base-300); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); text-align: left; font-weight: 600; }
|
|
22
|
+
.hwio-prose td { border: 1px solid var(--color-base-300); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); }
|
|
23
|
+
.hwio-prose-lg { font-size: var(--text-lg); }
|
|
24
|
+
}
|