@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
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @hubpav/hwio-ui
|
|
2
|
+
|
|
3
|
+
HARDWARIO web design system for Astro sites: the Tailwind 4 entry stylesheet with DaisyUI 5,
|
|
4
|
+
theme files generated from `@hubpav/hwio-brand` (`hwio`, `hwio-dark`, `hwio-forestry`, `er3o`),
|
|
5
|
+
the `HWio*` components (`@hubpav/hwio-ui/components/HWioHeader.astro` and friends, subpath
|
|
6
|
+
imports only), shared types and content-schema fragments, and the `hwioUi()` integration
|
|
7
|
+
that writes `_headers` and ships the brand fonts. Components never carry copy: every visible
|
|
8
|
+
string is a prop.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hubpav/hwio-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "HARDWARIO web design system for Astro: DaisyUI 5 themes generated from the brand tokens, the Tailwind entry stylesheet and the HWio* components.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hardwario/website-ui.git",
|
|
10
|
+
"directory": "packages/ui"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
"./styles.css": "./src/styles/styles.css",
|
|
18
|
+
"./prose.css": "./src/styles/prose.css",
|
|
19
|
+
"./themes/*": "./src/styles/themes/*",
|
|
20
|
+
"./components/*": "./src/components/*",
|
|
21
|
+
"./types": "./src/types.ts",
|
|
22
|
+
"./schemas": "./src/schemas.ts",
|
|
23
|
+
"./integration": "./src/integration.mjs",
|
|
24
|
+
"./package.json": "./package.json",
|
|
25
|
+
"./html": "./src/html.ts"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "node scripts/build-themes.mjs"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@hubpav/hwio-brand": "0.1.0",
|
|
32
|
+
"@hubpav/hwio-web-runtime": "0.1.0",
|
|
33
|
+
"@hubpav/hwio-web-worker": "0.1.0",
|
|
34
|
+
"daisyui": "5.7.28"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"astro": "^7.0.0",
|
|
38
|
+
"tailwindcss": "^4.3.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Renders breadcrumb items (the site derives them) with optional BreadcrumbList JSON-LD. */
|
|
3
|
+
import type { HWioLinkItem } from '../types';
|
|
4
|
+
interface Props { items: (HWioLinkItem & { noLink?: boolean })[]; label: string; siteUrl?: string; jsonLd?: boolean; class?: string }
|
|
5
|
+
const { items, label, siteUrl = '', jsonLd = true, class: className = '' } = Astro.props;
|
|
6
|
+
const ld = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.filter((i) => !i.noLink).map((i, n) => ({ '@type': 'ListItem', position: n + 1, name: i.label, item: i.href.startsWith('http') ? i.href : `${siteUrl}${i.href}` })) };
|
|
7
|
+
---
|
|
8
|
+
{items.length > 1 && (
|
|
9
|
+
<nav aria-label={label} class:list={['container pt-2 text-sm', className]}>
|
|
10
|
+
<div class="breadcrumbs">
|
|
11
|
+
<ul>
|
|
12
|
+
{items.map((item, i) => (
|
|
13
|
+
<li>{i === items.length - 1 || item.noLink ? <span aria-current={i === items.length - 1 ? 'page' : undefined}>{item.label}</span> : <a href={item.href}>{item.label}</a>}</li>
|
|
14
|
+
))}
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
17
|
+
{jsonLd && <script is:inline type="application/ld+json" set:html={JSON.stringify(ld)} />}
|
|
18
|
+
</nav>
|
|
19
|
+
)}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HWioTrack } from '../types';
|
|
3
|
+
import HWioIcon from './HWioIcon.astro';
|
|
4
|
+
/**
|
|
5
|
+
* Link or button on DaisyUI `btn`. Labels are slot content (sites own their copy; owner rule:
|
|
6
|
+
* imperative verb, no article). `track` writes the measurement attributes data-track / data-track-location.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
href?: string;
|
|
10
|
+
external?: boolean;
|
|
11
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'neutral' | 'inverse' | 'link';
|
|
12
|
+
size?: 'sm' | 'md' | 'lg' | 'wide';
|
|
13
|
+
track?: HWioTrack;
|
|
14
|
+
arrow?: boolean;
|
|
15
|
+
type?: 'button' | 'submit';
|
|
16
|
+
class?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
const VARIANT = {
|
|
20
|
+
primary: 'btn btn-primary',
|
|
21
|
+
secondary: 'btn btn-outline btn-primary',
|
|
22
|
+
ghost: 'btn btn-ghost',
|
|
23
|
+
neutral: 'btn btn-neutral',
|
|
24
|
+
inverse: 'btn hwio-btn-inverse',
|
|
25
|
+
link: 'btn btn-link',
|
|
26
|
+
} as const;
|
|
27
|
+
const SIZE = { sm: 'btn-sm', md: '', lg: 'btn-lg', wide: 'btn-wide' } as const;
|
|
28
|
+
const { href, external = false, variant = 'primary', size = 'md', track, arrow = false, type = 'button', class: className = '', ...rest } = Astro.props;
|
|
29
|
+
const classes = [VARIANT[variant], SIZE[size], className];
|
|
30
|
+
const trackAttrs = track ? { 'data-track': track.id, 'data-track-location': track.location } : {};
|
|
31
|
+
---
|
|
32
|
+
{href ? (
|
|
33
|
+
<a href={href} class:list={classes} target={external ? '_blank' : undefined} rel={external ? 'noopener noreferrer' : undefined} {...trackAttrs} {...rest}>
|
|
34
|
+
<slot />
|
|
35
|
+
{arrow && <HWioIcon name="arrow-right" size="sm" />}
|
|
36
|
+
{external && !arrow && <HWioIcon name="external" size="sm" />}
|
|
37
|
+
</a>
|
|
38
|
+
) : (
|
|
39
|
+
<button type={type} class:list={classes} {...trackAttrs} {...rest}>
|
|
40
|
+
<slot />
|
|
41
|
+
{arrow && <HWioIcon name="arrow-right" size="sm" />}
|
|
42
|
+
</button>
|
|
43
|
+
)}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Two or three parallel offers, one of them the lead (www's "A live project or a technical evaluation?"). */
|
|
3
|
+
import type { HWioCta } from '../types';
|
|
4
|
+
import HWioButton from './HWioButton.astro';
|
|
5
|
+
import HWioIcon from './HWioIcon.astro';
|
|
6
|
+
interface Choice { label: string; title: string; text: string; points?: string[]; cta: HWioCta }
|
|
7
|
+
interface Props { items: Choice[]; lead?: number; class?: string }
|
|
8
|
+
const { items, lead = 0, class: className = '' } = Astro.props;
|
|
9
|
+
const COLS = { 1: 'grid gap-6', 2: 'grid gap-6 md:grid-cols-2', 3: 'grid gap-6 md:grid-cols-3' } as const;
|
|
10
|
+
const cols = COLS[(Math.min(Math.max(items.length, 1), 3) as 1 | 2 | 3)];
|
|
11
|
+
---
|
|
12
|
+
<div class:list={[cols, 'mx-auto max-w-[65rem]', className]}>
|
|
13
|
+
{items.map((c, i) => {
|
|
14
|
+
const isLead = i === lead;
|
|
15
|
+
return (
|
|
16
|
+
<article class:list={['card card-border flex h-full flex-col bg-base-100 border-t-[3px]', isLead ? 'border-t-accent' : 'border-t-base-300']}>
|
|
17
|
+
<div class="card-body gap-0 p-10">
|
|
18
|
+
<p class:list={['mb-4 text-sm font-semibold', isLead ? 'text-accent' : 'hwio-muted']}>{c.label}</p>
|
|
19
|
+
<h3 class="mb-3.5 text-[1.75rem] leading-[1.15] tracking-[-0.02em]">{c.title}</h3>
|
|
20
|
+
<p class="hwio-muted mb-6">{c.text}</p>
|
|
21
|
+
{c.points && c.points.length > 0 && (
|
|
22
|
+
<ul class="hwio-muted mb-8 flex flex-col gap-3 text-[15px]">
|
|
23
|
+
{c.points.map((pt) => <li class="flex items-start gap-3"><HWioIcon name="check" class="mt-0.5 shrink-0 text-accent" />{pt}</li>)}
|
|
24
|
+
</ul>
|
|
25
|
+
)}
|
|
26
|
+
<div class="mt-auto"><HWioButton href={c.cta.href} external={c.cta.external} variant={isLead ? 'primary' : 'secondary'} arrow={isLead} track={c.cta.track}>{c.cta.label}</HWioButton></div>
|
|
27
|
+
</div>
|
|
28
|
+
</article>
|
|
29
|
+
);
|
|
30
|
+
})}
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Contact block: copy and a contact person on the left, the HubSpot form on the right. */
|
|
3
|
+
import type { HWioTone } from '../types';
|
|
4
|
+
import HWioIcon from './HWioIcon.astro';
|
|
5
|
+
import HWioSection from './HWioSection.astro';
|
|
6
|
+
import HWioSectionHeader from './HWioSectionHeader.astro';
|
|
7
|
+
interface Props {
|
|
8
|
+
id?: string;
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
lead?: string;
|
|
12
|
+
person?: { name: string; role?: string; email?: string; phone?: string; phoneHref?: string; image?: { src: string; alt: string; width: number; height: number } };
|
|
13
|
+
tone?: HWioTone;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
const { id = 'contact', eyebrow, title, lead, person, tone = 'alt', class: className = '' } = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
<HWioSection id={id} tone={tone} class={className}>
|
|
19
|
+
<div class="grid gap-12 lg:grid-cols-[2fr_3fr]">
|
|
20
|
+
<div>
|
|
21
|
+
<HWioSectionHeader eyebrow={eyebrow} title={title} lead={lead} align="left" />
|
|
22
|
+
{person && (
|
|
23
|
+
<div class="mt-8 flex items-center gap-4">
|
|
24
|
+
{person.image && <img src={person.image.src} alt={person.image.alt} width={person.image.width} height={person.image.height} class="size-16 rounded-full object-cover" loading="lazy" />}
|
|
25
|
+
<div class="text-sm">
|
|
26
|
+
<p class="font-semibold">{person.name}</p>
|
|
27
|
+
{person.role && <p class="opacity-80">{person.role}</p>}
|
|
28
|
+
{person.email && <a href={`mailto:${person.email}`} class="link link-hover inline-flex items-center gap-1"><HWioIcon name="mail" size="sm" />{person.email}</a>}
|
|
29
|
+
{person.phone && <a href={person.phoneHref ?? `tel:${person.phone.replace(/\s+/g, '')}`} class="link link-hover ml-3 inline-flex items-center gap-1"><HWioIcon name="phone" size="sm" />{person.phone}</a>}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
<slot name="aside" />
|
|
34
|
+
</div>
|
|
35
|
+
<div class="card card-border bg-base-100"><div class="card-body"><slot /></div></div>
|
|
36
|
+
</div>
|
|
37
|
+
</HWioSection>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Consent banner + settings dialog (Consent Mode v2, consent-gated GTM). Two variants:
|
|
4
|
+
* `categories` (the HARDWARIO sites: four purposes, granular save) and `simple` (enerooo today:
|
|
5
|
+
* accept / necessary only). Runtime: @hubpav/hwio-web-runtime/consent.
|
|
6
|
+
*/
|
|
7
|
+
interface CategoryCopy { title: string; description: string }
|
|
8
|
+
interface Props {
|
|
9
|
+
version: string;
|
|
10
|
+
locale: string;
|
|
11
|
+
gtmId?: string | null;
|
|
12
|
+
policyHref: string;
|
|
13
|
+
variant?: 'categories' | 'simple';
|
|
14
|
+
labels: {
|
|
15
|
+
title: string; short: string; policy: string; acceptAll: string; rejectAll: string; customize?: string; close?: string; save?: string;
|
|
16
|
+
settingsTitle?: string; settingsIntro?: string; alwaysOn?: string;
|
|
17
|
+
categories?: { necessary: CategoryCopy; preferences: CategoryCopy; statistics: CategoryCopy; marketing: CategoryCopy };
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const { version, locale, gtmId = null, policyHref, variant = 'categories', labels } = Astro.props;
|
|
21
|
+
const optional = ['preferences', 'statistics', 'marketing'] as const;
|
|
22
|
+
---
|
|
23
|
+
<div data-hwio-consent data-version={version} data-gtm={gtmId ?? undefined} data-locale={locale} hidden role="region" aria-label={labels.title} class="fixed inset-x-0 bottom-0 z-[120] px-4 pb-4 sm:px-6 sm:pb-6" data-hwio-font-exempt>
|
|
24
|
+
<div class="card card-border mx-auto max-w-5xl bg-base-100 shadow-2xl">
|
|
25
|
+
<div class="card-body gap-4 md:flex-row md:items-center">
|
|
26
|
+
<div class="min-w-0 flex-1">
|
|
27
|
+
<p class="text-base font-semibold">{labels.title}</p>
|
|
28
|
+
<p class="mt-1 text-sm leading-6 opacity-80">
|
|
29
|
+
{labels.short}
|
|
30
|
+
<a href={policyHref} class="link link-secondary font-semibold">{labels.policy}</a>
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex flex-col gap-2 sm:flex-row md:shrink-0">
|
|
34
|
+
<button type="button" data-hwio-consent-reject class="btn btn-outline btn-secondary">{labels.rejectAll}</button>
|
|
35
|
+
{variant === 'categories' && labels.customize && <button type="button" data-hwio-consent-customize class="btn btn-outline btn-secondary">{labels.customize}</button>}
|
|
36
|
+
<button type="button" data-hwio-consent-accept class="btn btn-primary">{labels.acceptAll}</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{variant === 'categories' && labels.categories && (
|
|
43
|
+
<div data-hwio-consent-modal hidden class="fixed inset-0 z-[130] flex items-center justify-center bg-neutral/60 px-4 py-6" data-hwio-font-exempt>
|
|
44
|
+
<section role="dialog" aria-modal="true" aria-labelledby="hwio-consent-settings-title" class="card max-h-[calc(100vh-3rem)] w-full max-w-2xl overflow-y-auto bg-base-100 shadow-2xl">
|
|
45
|
+
<div class="card-body gap-6">
|
|
46
|
+
<div class="flex items-start justify-between gap-6">
|
|
47
|
+
<div>
|
|
48
|
+
<h2 id="hwio-consent-settings-title" class="text-2xl font-bold">{labels.settingsTitle}</h2>
|
|
49
|
+
<p class="mt-2 text-sm leading-6 opacity-80">{labels.settingsIntro}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<button type="button" data-hwio-consent-close class="btn btn-ghost btn-square btn-sm" aria-label={labels.close}>
|
|
52
|
+
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="space-y-3">
|
|
56
|
+
<div class="rounded-box border border-base-300 p-4">
|
|
57
|
+
<div class="flex items-start justify-between gap-4">
|
|
58
|
+
<div>
|
|
59
|
+
<p class="font-semibold">{labels.categories.necessary.title}</p>
|
|
60
|
+
<p class="mt-1 text-sm leading-6 opacity-80">{labels.categories.necessary.description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<span class="badge badge-ghost shrink-0">{labels.alwaysOn}</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{optional.map((category) => (
|
|
66
|
+
<label class="flex cursor-pointer items-start justify-between gap-4 rounded-box border border-base-300 p-4 transition-colors hover:border-secondary">
|
|
67
|
+
<span>
|
|
68
|
+
<span class="block font-semibold">{labels.categories![category].title}</span>
|
|
69
|
+
<span class="mt-1 block text-sm leading-6 opacity-80">{labels.categories![category].description}</span>
|
|
70
|
+
</span>
|
|
71
|
+
<input type="checkbox" class="toggle toggle-primary mt-1 shrink-0" id={`hwio-consent-${category}`} name={`hwio-consent-${category}`} autocomplete="off" data-hwio-consent-toggle={category} />
|
|
72
|
+
</label>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
76
|
+
<button type="button" data-hwio-consent-reject class="btn btn-outline btn-secondary">{labels.rejectAll}</button>
|
|
77
|
+
<button type="button" data-hwio-consent-save class="btn btn-outline btn-secondary">{labels.save}</button>
|
|
78
|
+
<button type="button" data-hwio-consent-accept class="btn btn-primary">{labels.acceptAll}</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
<script>
|
|
85
|
+
import { hwioConsentRuntime } from '@hubpav/hwio-web-runtime/consent';
|
|
86
|
+
hwioConsentRuntime();
|
|
87
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Closing call-to-action band (www CTASection, direction A). The low-commitment fallback line is the `below` slot. */
|
|
3
|
+
import type { HWioCta, HWioTone } from '../types';
|
|
4
|
+
import HWioButton from './HWioButton.astro';
|
|
5
|
+
import HWioSection from './HWioSection.astro';
|
|
6
|
+
interface Props { id?: string; eyebrow?: string; headline: string; description?: string; primary: HWioCta; secondary?: HWioCta; tone?: HWioTone; class?: string }
|
|
7
|
+
const { id, eyebrow, headline, description, primary, secondary, tone = 'base', class: className = '' } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
<HWioSection id={id} tone={tone} class={className}>
|
|
10
|
+
<div class="mx-auto max-w-3xl text-center">
|
|
11
|
+
{eyebrow && <p class="hwio-kicker mb-4">{eyebrow}</p>}
|
|
12
|
+
<h2 class="mb-5">{headline}</h2>
|
|
13
|
+
{description && <p class="hwio-lead mx-auto mb-10 max-w-[40rem]">{description}</p>}
|
|
14
|
+
<div class="flex flex-wrap justify-center gap-3">
|
|
15
|
+
<HWioButton href={primary.href} external={primary.external} variant={tone === 'neutral' ? 'inverse' : 'primary'} size="lg" arrow track={primary.track}>{primary.label}</HWioButton>
|
|
16
|
+
{secondary && <HWioButton href={secondary.href} external={secondary.external} variant="secondary" size="lg" track={secondary.track}>{secondary.label}</HWioButton>}
|
|
17
|
+
</div>
|
|
18
|
+
<div class="hwio-muted mt-8 text-[15px]"><slot name="below" /></div>
|
|
19
|
+
</div>
|
|
20
|
+
</HWioSection>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Exclusive accordion on native <details name>, optional FAQPage JSON-LD. Answers are text unless `html` is set. */
|
|
3
|
+
interface Props { items: { q: string; a: string }[]; name?: string; html?: boolean; jsonLd?: boolean; class?: string }
|
|
4
|
+
const { items, name = 'hwio-faq', html = false, jsonLd = true, class: className = '' } = Astro.props;
|
|
5
|
+
const ld = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: items.map((i) => ({ '@type': 'Question', name: i.q, acceptedAnswer: { '@type': 'Answer', text: i.a } })) };
|
|
6
|
+
---
|
|
7
|
+
<div class:list={['space-y-3', className]}>
|
|
8
|
+
{items.map((item) => (
|
|
9
|
+
<details class="collapse collapse-arrow border border-base-300 bg-base-100" name={name}>
|
|
10
|
+
<summary class="collapse-title font-semibold">{item.q}</summary>
|
|
11
|
+
<div class="collapse-content opacity-90">{html ? <div set:html={item.a} /> : <p>{item.a}</p>}</div>
|
|
12
|
+
</details>
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
{jsonLd && <script is:inline type="application/ld+json" set:html={JSON.stringify(ld)} />}
|
|
16
|
+
<script>
|
|
17
|
+
import { hwioAccordionRuntime } from '@hubpav/hwio-web-runtime/accordion';
|
|
18
|
+
hwioAccordionRuntime();
|
|
19
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import HWioIcon from './HWioIcon.astro';
|
|
3
|
+
interface Props { title: string; text: string; icon?: string; href?: string; class?: string }
|
|
4
|
+
const { title, text, icon, href, class: className = '' } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
<article class:list={['card card-border h-full bg-base-100', href ? 'transition-shadow hover:shadow-lg' : '', className]}>
|
|
7
|
+
<div class="card-body gap-0">
|
|
8
|
+
<slot name="icon">
|
|
9
|
+
{icon && <span class="mb-6 inline-flex size-12 items-center justify-center rounded-xl bg-accent/8 text-accent"><HWioIcon name={icon as 'check'} size="lg" /></span>}
|
|
10
|
+
</slot>
|
|
11
|
+
<h3 class="mb-2.5">{href ? <a href={href} class="link link-hover">{title}</a> : title}</h3>
|
|
12
|
+
<p class="hwio-muted text-base">{text}</p>
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</article>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
import HWioFeatureCard from './HWioFeatureCard.astro';
|
|
3
|
+
interface Props { items: { title: string; text: string; icon?: string; href?: string }[]; columns?: 2 | 3 | 4; class?: string }
|
|
4
|
+
const COLS = { 2: 'grid gap-6 md:grid-cols-2', 3: 'grid gap-6 md:grid-cols-2 lg:grid-cols-3', 4: 'grid gap-6 md:grid-cols-2 lg:grid-cols-4' } as const;
|
|
5
|
+
const { items, columns = 3, class: className = '' } = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
<div class:list={[COLS[columns], className]}>
|
|
8
|
+
{items.map((item) => <HWioFeatureCard {...item} />)}
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Font policy (owner ruling R3, 2026-09-05): font-display block plus a pre-paint gate. Rendered by
|
|
4
|
+
* HWioSeoHead; preloads both faces, runs the static gate bootstrap (hash-stable; it reads
|
|
5
|
+
* data-hwio-font-* from <html>) and restores visibility without JavaScript.
|
|
6
|
+
*/
|
|
7
|
+
import { fontGate } from '@hubpav/hwio-web-runtime/inline';
|
|
8
|
+
interface Props { files: string[] }
|
|
9
|
+
const { files } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
{files.map((f) => <link rel="preload" href={f} as="font" type="font/woff2" crossorigin fetchpriority="high" />)}
|
|
12
|
+
<script is:inline set:html={fontGate.code} />
|
|
13
|
+
<noscript>
|
|
14
|
+
<style is:inline>
|
|
15
|
+
html.fonts-loading body > * { visibility: visible !important; }
|
|
16
|
+
html.fonts-loading { --hwio-font-sans: 'Inter Fallback', ui-sans-serif, system-ui, sans-serif; }
|
|
17
|
+
</style>
|
|
18
|
+
</noscript>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Site footer on the neutral surface: social bar, logo slot, link columns, branches, legal row
|
|
4
|
+
* with the cookie-settings button, the cross-site strip, copyright and tagline. All strings are props.
|
|
5
|
+
*/
|
|
6
|
+
import type { HWioLinkItem } from '../types';
|
|
7
|
+
import HWioIcon from './HWioIcon.astro';
|
|
8
|
+
import HWioFooterSites from './HWioFooterSites.astro';
|
|
9
|
+
|
|
10
|
+
interface Column { title: string; links: HWioLinkItem[] }
|
|
11
|
+
interface Props {
|
|
12
|
+
columns: Column[];
|
|
13
|
+
social?: { icon: 'linkedin' | 'x' | 'instagram' | 'youtube' | 'github' | 'forum'; label: string; href: string }[];
|
|
14
|
+
branches?: { title: string; items: { name: string; country: string; mapUrl: string }[] };
|
|
15
|
+
legal?: HWioLinkItem[];
|
|
16
|
+
cookieSettingsLabel?: string;
|
|
17
|
+
sites?: { items: { key: string; label: string; href: string }[]; label: string };
|
|
18
|
+
copyright: string;
|
|
19
|
+
tagline?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
const { columns, social = [], branches, legal = [], cookieSettingsLabel, sites, copyright, tagline, class: className = '' } = Astro.props;
|
|
23
|
+
const linkClass = 'link link-hover inline-flex items-center gap-1';
|
|
24
|
+
---
|
|
25
|
+
<footer class:list={['bg-neutral text-neutral-content', className]} data-hwio-footer>
|
|
26
|
+
{social.length > 0 && (
|
|
27
|
+
<div class="border-b border-neutral-content/10">
|
|
28
|
+
<div class="container flex justify-center gap-6 py-6">
|
|
29
|
+
{social.filter((s) => s.icon !== 'forum').map((s) => (
|
|
30
|
+
<a href={s.href} target="_blank" rel="noopener noreferrer" class="hover:text-accent transition-colors" aria-label={s.label}>
|
|
31
|
+
<HWioIcon name={s.icon} class="size-9" />
|
|
32
|
+
</a>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<div class="container py-12 lg:py-16">
|
|
39
|
+
<div class="grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-6">
|
|
40
|
+
<div class="hidden md:col-span-3 md:flex lg:col-span-1 lg:block">
|
|
41
|
+
<slot name="logo" />
|
|
42
|
+
</div>
|
|
43
|
+
{branches && (
|
|
44
|
+
<div>
|
|
45
|
+
<p class="mb-4 font-medium">{branches.title}</p>
|
|
46
|
+
<div class="space-y-2 text-base">
|
|
47
|
+
{branches.items.map((b) => (
|
|
48
|
+
<div>
|
|
49
|
+
<p class="opacity-80">{b.name}</p>
|
|
50
|
+
<a href={b.mapUrl} target="_blank" rel="noopener noreferrer" class={linkClass}>{b.country}<HWioIcon name="external" size="sm" /></a>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{columns.map((col) => (
|
|
57
|
+
<div>
|
|
58
|
+
<p class="mb-4 font-medium">{col.title}</p>
|
|
59
|
+
<ul class="space-y-1 text-base">
|
|
60
|
+
{col.links.map((l) => (
|
|
61
|
+
<li>
|
|
62
|
+
<a href={l.href} target={l.external ? '_blank' : undefined} rel={l.external ? 'noopener noreferrer' : undefined} class={linkClass} data-track={l.track?.id} data-track-location={l.track?.location ?? 'footer'}>
|
|
63
|
+
{l.label}{l.external && <HWioIcon name="external" size="sm" />}
|
|
64
|
+
</a>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
{(legal.length > 0 || cookieSettingsLabel) && (
|
|
71
|
+
<div>
|
|
72
|
+
<slot name="legal-title" />
|
|
73
|
+
<ul class="space-y-1 text-base">
|
|
74
|
+
{legal.map((l) => (
|
|
75
|
+
<li><a href={l.href} target={l.external ? '_blank' : undefined} rel={l.external ? 'noopener noreferrer' : undefined} class={linkClass}>{l.label}{l.external && <HWioIcon name="download" size="sm" />}</a></li>
|
|
76
|
+
))}
|
|
77
|
+
{cookieSettingsLabel && <li><button type="button" data-hwio-cookie-settings class="link link-hover text-left">{cookieSettingsLabel}</button></li>}
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{sites && sites.items.length > 0 && (
|
|
85
|
+
<div class="border-t border-neutral-content/10">
|
|
86
|
+
<div class="container py-5"><HWioFooterSites items={sites.items} label={sites.label} /></div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div class="border-t border-neutral-content/10">
|
|
91
|
+
<div class="container flex flex-col items-center justify-between gap-4 py-6 text-sm opacity-80 md:flex-row">
|
|
92
|
+
<p>{copyright}</p>
|
|
93
|
+
{tagline && <p class="text-center">{tagline}</p>}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</footer>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** The "HARDWARIO websites" strip. Pass the entries from hwioFooterSites() in @hubpav/hwio-brand. */
|
|
3
|
+
interface Props { items: { key: string; label: string; href: string }[]; label: string; class?: string }
|
|
4
|
+
const { items, label, class: className = '' } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
{items.length > 0 && (
|
|
7
|
+
<nav class:list={['flex flex-wrap items-baseline justify-center gap-x-3 gap-y-1 text-sm', className]} aria-label={label}>
|
|
8
|
+
<span class="w-full text-center opacity-70 sm:w-auto sm:text-left">{label}:</span>
|
|
9
|
+
{items.map((site, i) => (
|
|
10
|
+
<>
|
|
11
|
+
{i > 0 && <span class="opacity-50" aria-hidden="true">·</span>}
|
|
12
|
+
<a href={site.href} target="_blank" rel="noopener noreferrer" class="link link-hover">{site.label}</a>
|
|
13
|
+
</>
|
|
14
|
+
))}
|
|
15
|
+
</nav>
|
|
16
|
+
)}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HWioFieldDef } from '../types';
|
|
3
|
+
interface Props { field: HWioFieldDef }
|
|
4
|
+
const { field } = Astro.props;
|
|
5
|
+
const id = `hwio-field-${field.name}`;
|
|
6
|
+
const message = field.messageField ? { 'data-message-field': 'true', 'data-message-label': field.label } : {};
|
|
7
|
+
---
|
|
8
|
+
{field.type === 'hidden' ? (
|
|
9
|
+
<input type="hidden" name={field.name} value={field.value ?? ''} />
|
|
10
|
+
) : field.type === 'checkbox' ? (
|
|
11
|
+
<label class:list={['label cursor-pointer items-start gap-3 text-base-content', field.half ? '' : 'md:col-span-2']}>
|
|
12
|
+
<input type="checkbox" id={id} name={field.name} class="checkbox checkbox-primary mt-0.5" required={field.required} {...message} />
|
|
13
|
+
<span class="text-sm"><slot name="label">{field.label}</slot></span>
|
|
14
|
+
</label>
|
|
15
|
+
) : (
|
|
16
|
+
<fieldset class:list={['fieldset', field.half ? '' : 'md:col-span-2']}>
|
|
17
|
+
<legend class="fieldset-legend">{field.label}{field.required && <span aria-hidden="true"> *</span>}</legend>
|
|
18
|
+
{field.type === 'textarea' ? (
|
|
19
|
+
<textarea id={id} name={field.name} class="textarea w-full" rows={field.rows ?? 5} required={field.required} placeholder={field.placeholder} {...message}>{field.value ?? ''}</textarea>
|
|
20
|
+
) : field.type === 'select' ? (
|
|
21
|
+
<select id={id} name={field.name} class="select w-full" required={field.required} {...message}>
|
|
22
|
+
{field.placeholder && <option value="" disabled selected>{field.placeholder}</option>}
|
|
23
|
+
{(field.options ?? []).map((o) => <option value={o.value}>{o.label}</option>)}
|
|
24
|
+
</select>
|
|
25
|
+
) : (
|
|
26
|
+
<input id={id} type={field.type ?? 'text'} name={field.name} class="input w-full" required={field.required} placeholder={field.placeholder} autocomplete={field.autocomplete} value={field.value} {...message} />
|
|
27
|
+
)}
|
|
28
|
+
</fieldset>
|
|
29
|
+
)}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Sticky site header: logo slot, primary nav (flat links; mega-menu content via `mega:<key>` slots),
|
|
4
|
+
* CTAs, language switcher, theme toggle, and a DaisyUI drawer for small screens.
|
|
5
|
+
*/
|
|
6
|
+
import type { HWioCta, HWioLinkItem } from '../types';
|
|
7
|
+
import HWioButton from './HWioButton.astro';
|
|
8
|
+
import HWioIcon from './HWioIcon.astro';
|
|
9
|
+
import HWioLanguageSwitcher from './HWioLanguageSwitcher.astro';
|
|
10
|
+
import HWioThemeToggle from './HWioThemeToggle.astro';
|
|
11
|
+
|
|
12
|
+
interface NavItem extends HWioLinkItem { mega?: string }
|
|
13
|
+
interface Props {
|
|
14
|
+
nav: NavItem[];
|
|
15
|
+
ctas?: HWioCta[];
|
|
16
|
+
languages?: { code: string; label: string; href: string; current?: boolean }[];
|
|
17
|
+
showThemeToggle?: boolean;
|
|
18
|
+
scrollspy?: boolean;
|
|
19
|
+
labels: { nav: string; menu: string; close: string; theme?: string; languages?: string };
|
|
20
|
+
id?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
const { nav, ctas = [], languages = [], showThemeToggle = true, scrollspy = false, labels, id = 'hwio-nav-drawer', class: className = '' } = Astro.props;
|
|
24
|
+
const CTA_VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost' } as const;
|
|
25
|
+
---
|
|
26
|
+
<header class:list={['sticky top-0 z-50 border-b border-base-300 bg-base-100/90 backdrop-blur supports-[backdrop-filter]:bg-base-100/80', className]} data-hwio-font-exempt>
|
|
27
|
+
<div class="container flex h-16 items-center gap-4 lg:h-[72px]">
|
|
28
|
+
<div class="shrink-0">
|
|
29
|
+
<slot name="logo" />
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<nav class="hidden flex-1 justify-center lg:flex" aria-label={labels.nav} data-hwio-scrollspy={scrollspy ? 'true' : undefined}>
|
|
33
|
+
<ul class="menu menu-horizontal gap-1 px-1">
|
|
34
|
+
{nav.map((item) => (
|
|
35
|
+
<li>
|
|
36
|
+
{item.mega ? (
|
|
37
|
+
<details>
|
|
38
|
+
<summary class="font-medium">{item.label}</summary>
|
|
39
|
+
<div class="dropdown-content bg-base-100 rounded-box z-50 mt-2 p-4 shadow-lg border border-base-300">
|
|
40
|
+
<slot name={`mega:${item.mega}`} />
|
|
41
|
+
</div>
|
|
42
|
+
</details>
|
|
43
|
+
) : (
|
|
44
|
+
<a href={item.href} class="text-[15px] font-medium text-base-content/80 hover:text-accent" aria-current={item.current ? 'page' : undefined} data-track={item.track?.id} data-track-location={item.track?.location ?? 'header'}>{item.label}</a>
|
|
45
|
+
)}
|
|
46
|
+
</li>
|
|
47
|
+
))}
|
|
48
|
+
</ul>
|
|
49
|
+
</nav>
|
|
50
|
+
|
|
51
|
+
<div class="ml-auto flex items-center gap-2">
|
|
52
|
+
{showThemeToggle && <HWioThemeToggle label={labels.theme ?? 'Theme'} />}
|
|
53
|
+
{languages.length > 1 && <HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} />}
|
|
54
|
+
<div class="hidden items-center gap-2 lg:flex">
|
|
55
|
+
{ctas.map((cta) => (
|
|
56
|
+
<HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} size="sm" track={cta.track ?? { id: 'header-cta', location: 'header' }}>{cta.label}</HWioButton>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
<div class="drawer drawer-end w-auto lg:hidden">
|
|
60
|
+
<input id={id} type="checkbox" class="drawer-toggle" />
|
|
61
|
+
<div class="drawer-content">
|
|
62
|
+
<label for={id} class="btn btn-ghost btn-square drawer-button" aria-label={labels.menu} aria-controls={`${id}-panel`} aria-expanded="false" tabindex="0" role="button">
|
|
63
|
+
<HWioIcon name="menu" size="lg" />
|
|
64
|
+
</label>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="drawer-side z-[60]">
|
|
67
|
+
<label for={id} aria-label={labels.close} class="drawer-overlay"></label>
|
|
68
|
+
<div id={`${id}-panel`} class="menu min-h-full w-80 max-w-[85vw] gap-2 bg-base-100 p-6 text-base-content">
|
|
69
|
+
<div class="flex justify-end">
|
|
70
|
+
<label for={id} class="btn btn-ghost btn-square btn-sm" aria-label={labels.close} tabindex="0" role="button"><HWioIcon name="close" /></label>
|
|
71
|
+
</div>
|
|
72
|
+
<ul class="menu w-full text-base">
|
|
73
|
+
{nav.map((item) => (
|
|
74
|
+
<li><a href={item.href} aria-current={item.current ? 'page' : undefined} data-track={item.track?.id} data-track-location="mobile-menu">{item.label}</a></li>
|
|
75
|
+
))}
|
|
76
|
+
</ul>
|
|
77
|
+
<div class="mt-4 flex flex-col gap-2">
|
|
78
|
+
{ctas.map((cta) => (
|
|
79
|
+
<HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} track={{ id: cta.track?.id ?? 'header-cta', location: 'mobile-menu' }}>{cta.label}</HWioButton>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
{languages.length > 1 && <div class="mt-4"><HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} variant="pills" /></div>}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</header>
|
|
89
|
+
<script>
|
|
90
|
+
import { hwioNavRuntime } from '@hubpav/hwio-web-runtime/nav';
|
|
91
|
+
hwioNavRuntime();
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Two-column hero (direction A): kicker, headline with optional accent line, lead, actions, checklist; media on a soft sky glow. */
|
|
3
|
+
import type { HWioCta } from '../types';
|
|
4
|
+
import HWioButton from './HWioButton.astro';
|
|
5
|
+
import HWioIcon from './HWioIcon.astro';
|
|
6
|
+
interface Props { id?: string; eyebrow?: string; title: string; accent?: string; lead?: string; actions?: HWioCta[]; checklist?: string[]; reverse?: boolean; glow?: boolean; class?: string }
|
|
7
|
+
const { id, eyebrow, title, accent, lead, actions = [], checklist = [], reverse = false, glow = true, class: className = '' } = Astro.props;
|
|
8
|
+
const VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost' } as const;
|
|
9
|
+
---
|
|
10
|
+
<section id={id} class:list={['bg-base-100 text-base-content', className]}>
|
|
11
|
+
<div class:list={['container grid items-center gap-12 py-16 lg:grid-cols-[7fr_5fr] lg:gap-16 lg:py-24', reverse ? 'lg:[&>*:first-child]:order-2' : '']}>
|
|
12
|
+
<div>
|
|
13
|
+
{eyebrow && <p class="hwio-kicker mb-5">{eyebrow}</p>}
|
|
14
|
+
<h1><slot name="title"><span class="block">{title}</span>{accent && <span class="block text-accent">{accent}</span>}</slot></h1>
|
|
15
|
+
{lead && <p class="hwio-lead mt-7 max-w-[37.5rem] text-xl">{lead}</p>}
|
|
16
|
+
{actions.length > 0 && (
|
|
17
|
+
<div class="mt-9 flex flex-wrap gap-3">
|
|
18
|
+
{actions.map((a, i) => <HWioButton href={a.href} external={a.external} variant={VARIANT[a.variant ?? (i === 0 ? 'primary' : 'secondary')]} size="lg" arrow={i === 0} track={a.track}>{a.label}</HWioButton>)}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
{checklist.length > 0 && (
|
|
22
|
+
<ul class="hwio-muted mt-8 flex flex-wrap gap-x-8 gap-y-3 text-[15px]">
|
|
23
|
+
{checklist.map((c) => <li class="inline-flex items-center gap-2"><HWioIcon name="check" size="sm" class="text-accent" />{c}</li>)}
|
|
24
|
+
</ul>
|
|
25
|
+
)}
|
|
26
|
+
<slot name="extra" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class:list={['relative flex items-center justify-center', glow ? 'hwio-hero-glow aspect-square rounded-3xl' : '']}><slot name="media" /></div>
|
|
29
|
+
</div>
|
|
30
|
+
</section>
|
|
31
|
+
<style>
|
|
32
|
+
.hwio-hero-glow { background: radial-gradient(closest-side, var(--color-base-200) 0%, var(--color-base-200) 55%, var(--color-base-100) 100%); }
|
|
33
|
+
</style>
|