@sumaq/site-kit 0.2.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 +93 -0
- package/package.json +57 -0
- package/src/astro-modules.d.ts +11 -0
- package/src/blocks/About.astro +94 -0
- package/src/blocks/Audience.astro +66 -0
- package/src/blocks/BlockRenderer.astro +105 -0
- package/src/blocks/Contact.astro +58 -0
- package/src/blocks/ContactDetails.astro +91 -0
- package/src/blocks/CtaBand.astro +53 -0
- package/src/blocks/EmergencyGrid.astro +47 -0
- package/src/blocks/EmergencyHelplines.astro +109 -0
- package/src/blocks/EmergencyPrimary.astro +51 -0
- package/src/blocks/Faq.astro +56 -0
- package/src/blocks/FeatureList.astro +73 -0
- package/src/blocks/Gallery.astro +95 -0
- package/src/blocks/Generic.astro +30 -0
- package/src/blocks/Hero.astro +82 -0
- package/src/blocks/IntroCta.astro +41 -0
- package/src/blocks/LegalDocument.astro +83 -0
- package/src/blocks/LocationsOverview.astro +161 -0
- package/src/blocks/NotFound.astro +33 -0
- package/src/blocks/Notice.astro +49 -0
- package/src/blocks/PageBanner.astro +57 -0
- package/src/blocks/Pricing.astro +97 -0
- package/src/blocks/ProfileDetail.astro +202 -0
- package/src/blocks/Projects.astro +91 -0
- package/src/blocks/Quote.astro +53 -0
- package/src/blocks/Services.astro +46 -0
- package/src/blocks/Skills.astro +65 -0
- package/src/blocks/Stats.astro +54 -0
- package/src/blocks/Steps.astro +76 -0
- package/src/blocks/TeamDirectory.astro +164 -0
- package/src/blocks/TeamTeaser.astro +84 -0
- package/src/blocks/Testimonials.astro +75 -0
- package/src/blocks/Timeline.astro +65 -0
- package/src/components/Actions.astro +67 -0
- package/src/components/Card.astro +104 -0
- package/src/components/Logo.astro +57 -0
- package/src/components/Media.astro +83 -0
- package/src/components/Prose.astro +29 -0
- package/src/components/ScrollToTop.astro +49 -0
- package/src/components/Section.astro +75 -0
- package/src/components/SectionHeader.astro +58 -0
- package/src/components/SectionHeading.astro +76 -0
- package/src/components/Seo.astro +55 -0
- package/src/components/SiteFooter.astro +90 -0
- package/src/components/SiteHeader.astro +97 -0
- package/src/config.ts +59 -0
- package/src/index.ts +75 -0
- package/src/layouts/Base.astro +57 -0
- package/src/lib/collections.ts +101 -0
- package/src/lib/content.ts +51 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/markdown.ts +5 -0
- package/src/profiles/clinic.yaml +35 -0
- package/src/profiles/portfolio.yaml +27 -0
- package/src/profiles/therapist.yaml +31 -0
- package/src/scripts/enhancements.js +459 -0
- package/src/tokens/class-inventory.json +284 -0
- package/src/tokens/index.css +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# `@sumaq/site-kit`
|
|
2
|
+
|
|
3
|
+
Layouts, blocks, helpers and Astro config factory for Sumaq client sites.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @sumaq/site-kit@0.2.0
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Public on [npmjs.com](https://www.npmjs.com/package/@sumaq/site-kit). Pin an exact version in each `www-*` site — never a range.
|
|
10
|
+
|
|
11
|
+
The kit owns the **markup**. Colours, sizes and typography stay in the client's `src/styles/site.css`; content stays in `content/*.json`. See [`block-contract.md`](../../amauta/packages/block-contract.md) for the class contract and [`inventario-componentes.md`](../../amauta/packages/inventario-componentes.md) for where each component came from.
|
|
12
|
+
|
|
13
|
+
## The rendering rule
|
|
14
|
+
|
|
15
|
+
**If a field has content the element renders; if it does not, nothing is emitted.** No empty `<p>`, no empty `<ul>`, no section header with three blank lines — and no empty `<section>` either: `Section` renders its children first and skips the whole band when the result is blank. Because of that, kit blocks are server-only — do not put a `client:*` component inside a `Section`.
|
|
16
|
+
|
|
17
|
+
The checks live in `lib/content.ts` (`hasText`, `list`, `hasItems`, `hasImage`, `hasAny`, `cmsKey`) so every block decides the same way.
|
|
18
|
+
|
|
19
|
+
## Catalogue
|
|
20
|
+
|
|
21
|
+
### Layout
|
|
22
|
+
|
|
23
|
+
`Base` — `<html>` shell with SEO, header, footer, skip link, optional floating back-to-top, and a `head` slot for per-site fonts.
|
|
24
|
+
|
|
25
|
+
### Chrome and primitives — `@sumaq/site-kit/components/*`
|
|
26
|
+
|
|
27
|
+
| Component | What it is |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `Seo` | title, description, canonical, Open Graph, Twitter, optional JSON-LD |
|
|
30
|
+
| `SiteHeader` / `SiteFooter` | brand, navigation, footer columns, copyright |
|
|
31
|
+
| `Section` | the `<section class="sq-section sq-{name}">` + `sq-container` skeleton |
|
|
32
|
+
| `SectionHeader` / `SectionHeading` | label, title (+ accent), lede, note — with and without the `<header>` wrapper |
|
|
33
|
+
| `Actions` | a row of CTAs; honours `external` |
|
|
34
|
+
| `Card` | media, icon, badge, title, text, bullets; becomes an `<a>` when given `href` |
|
|
35
|
+
| `Logo` | brand link, falls back to the brand name when there is no image |
|
|
36
|
+
| `Media` | `<img>` for CMS strings, `astro:assets` `<Image>` for imported assets |
|
|
37
|
+
| `Prose` | markdown or HTML into `sq-prose` |
|
|
38
|
+
| `ScrollToTop` | floating back-to-top control |
|
|
39
|
+
|
|
40
|
+
### Blocks — `@sumaq/site-kit/blocks/*`
|
|
41
|
+
|
|
42
|
+
| Group | Blocks |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Openers | `Hero`, `PageBanner` |
|
|
45
|
+
| Content | `About`, `Audience`, `Faq`, `FeatureList`, `Gallery`, `Generic`, `Notice`, `Pricing`, `Projects`, `Quote`, `Services`, `Skills`, `Stats`, `Steps`, `Testimonials`, `Timeline` |
|
|
46
|
+
| Conversion | `Contact` (people), `ContactDetails` (the practice), `CtaBand`, `IntroCta` |
|
|
47
|
+
| Collections | `LocationsOverview`, `ProfileDetail`, `TeamDirectory`, `TeamTeaser` |
|
|
48
|
+
| Standalone pages | `LegalDocument`, `NotFound` |
|
|
49
|
+
| Therapist profile | `EmergencyPrimary`, `EmergencyGrid`, `EmergencyHelplines` |
|
|
50
|
+
| Dispatcher | `BlockRenderer` — maps `block.type` to a component, `Generic` as fallback |
|
|
51
|
+
|
|
52
|
+
Every block accepts `section` (the `data-cms` base key), `variant` (`light` / `muted` / …) and `id`, so the same component can appear twice on a page under different CMS keys.
|
|
53
|
+
|
|
54
|
+
## Site-local components
|
|
55
|
+
|
|
56
|
+
The kit is where a site **starts**, not where it is confined — a client paying for something of their own gets it in their own repo. Two seams make that work without forking:
|
|
57
|
+
|
|
58
|
+
```astro
|
|
59
|
+
---
|
|
60
|
+
import { Base, BlockRenderer } from "@sumaq/site-kit";
|
|
61
|
+
import Rooms from "../components/Rooms.astro"; // only this client
|
|
62
|
+
---
|
|
63
|
+
<BlockRenderer blocks={blocks} components={{ rooms: Rooms }} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`components` merges into the catalogue **and wins over it**, so a site can add a block the kit does not have or replace one it does. An unmatched `type` still falls back to `Generic`, and warns in dev so a custom block never fails silently. Data-side there is nothing to register: `@sumaq/cms-schema` validates each block against the site's own `cms/*.yaml`, never against a central list.
|
|
67
|
+
|
|
68
|
+
**The namespace rule:** `sq-*` belongs to the kit and is inventoried in `class-inventory.json`. Site-local components use their own prefix. That is what keeps the shared markup verifiable without taxing what makes a client's site theirs.
|
|
69
|
+
|
|
70
|
+
Promotion: if a local block shows up in three clients, it earns a place in the catalogue — see the checklist below.
|
|
71
|
+
|
|
72
|
+
## Profiles
|
|
73
|
+
|
|
74
|
+
`profiles/{therapist,clinic,portfolio}.yaml` — a curated block subset, the required/optional pages, and a default composition per niche. Adding a niche is a profile plus whatever blocks it is missing, not a site from scratch.
|
|
75
|
+
|
|
76
|
+
## Working on the kit
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pnpm typecheck # tsc over the TS surface
|
|
80
|
+
pnpm check:catalogue # every block is exported, reachable and in the profiles
|
|
81
|
+
pnpm classes # regenerate src/tokens/class-inventory.json
|
|
82
|
+
pnpm test # typecheck + catalogue
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`class-inventory.json` is derived from the source — never hand-edit it; run `pnpm classes` after touching markup. To check the data contract too:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node scripts/check-catalogue.mjs --registry ../../../sumaq-cli/skills/sumaq-site-builder/blocks-registry.yaml
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Adding a block
|
|
92
|
+
|
|
93
|
+
Follow the checklist in [`block-contract.md` §8](../../amauta/packages/block-contract.md): the regular skeleton, `data-cms` on every editable node, no `<style>`, no colours, no measurements, and no hardcoded copy — a label the client can read is content, not markup.
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sumaq/site-kit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Layouts, blocks, helpers and Astro config factory for Sumaq client sites.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"author": "elingan",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/elingan/sumaq-packages.git",
|
|
10
|
+
"directory": "packages/site-kit"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.ts",
|
|
15
|
+
"./config": "./src/config.ts",
|
|
16
|
+
"./blocks/*": "./src/blocks/*.astro",
|
|
17
|
+
"./components/*": "./src/components/*.astro",
|
|
18
|
+
"./layouts/*": "./src/layouts/*.astro",
|
|
19
|
+
"./collections": "./src/lib/collections.ts",
|
|
20
|
+
"./content": "./src/lib/content.ts",
|
|
21
|
+
"./format": "./src/lib/format.ts",
|
|
22
|
+
"./markdown": "./src/lib/markdown.ts",
|
|
23
|
+
"./tokens.css": "./src/tokens/index.css",
|
|
24
|
+
"./class-inventory.json": "./src/tokens/class-inventory.json",
|
|
25
|
+
"./profiles/*": "./src/profiles/*.yaml",
|
|
26
|
+
"./scripts/*": "./src/scripts/*"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"astro": "^7.1.0",
|
|
30
|
+
"sharp": "^0.35.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@astrojs/sitemap": "3.7.3",
|
|
34
|
+
"marked": "15.0.7",
|
|
35
|
+
"@sumaq/cms-schema": "0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"astro": "7.1.4",
|
|
39
|
+
"typescript": "5.8.3"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"src"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.12.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
53
|
+
"classes": "node scripts/build-class-inventory.mjs",
|
|
54
|
+
"check:catalogue": "node scripts/check-catalogue.mjs",
|
|
55
|
+
"test": "pnpm run typecheck && pnpm run check:catalogue"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets plain `tsc` resolve the `.astro` re-exports in `index.ts`.
|
|
3
|
+
*
|
|
4
|
+
* It does not typecheck the components themselves — that needs `@astrojs/check`,
|
|
5
|
+
* which the package does not depend on yet. When it does, this shim goes away
|
|
6
|
+
* and the real prop types take over.
|
|
7
|
+
*/
|
|
8
|
+
declare module "*.astro" {
|
|
9
|
+
const component: (props: Record<string, any>) => any;
|
|
10
|
+
export default component;
|
|
11
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Practitioner / company introduction with a portrait.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `AboutMe.astro` (www-utehofer-at), `About.astro`
|
|
6
|
+
* (www-bettinageidl-at) and `AboutSection.astro` (www-juliaschaffer-at): the
|
|
7
|
+
* three are the same split — copy on one side, portrait on the other, plus an
|
|
8
|
+
* identity line (name + qualifications).
|
|
9
|
+
*/
|
|
10
|
+
import Section from "../components/Section.astro";
|
|
11
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
12
|
+
import Media from "../components/Media.astro";
|
|
13
|
+
import Prose from "../components/Prose.astro";
|
|
14
|
+
import Actions from "../components/Actions.astro";
|
|
15
|
+
import { cmsKey, hasAny, hasImage, hasText, list } from "../lib/content";
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
sectionLabel,
|
|
19
|
+
sectionTitle,
|
|
20
|
+
sectionTitleAccent,
|
|
21
|
+
sectionSubtitle,
|
|
22
|
+
body,
|
|
23
|
+
image,
|
|
24
|
+
name,
|
|
25
|
+
role,
|
|
26
|
+
qualifications = [],
|
|
27
|
+
ctas = [],
|
|
28
|
+
/** Portrait first instead of copy first. */
|
|
29
|
+
reverse = false,
|
|
30
|
+
section = "about",
|
|
31
|
+
variant = "light",
|
|
32
|
+
id = "about",
|
|
33
|
+
} = Astro.props;
|
|
34
|
+
|
|
35
|
+
const cms = section;
|
|
36
|
+
const badges = list<any>(qualifications)
|
|
37
|
+
.map((item) => (typeof item === "string" ? item : item.text))
|
|
38
|
+
.filter(hasText);
|
|
39
|
+
const hasCopy = hasAny(body, name, role, badges, ctas);
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<Section name="about" variant={variant} id={id} cms={cms}>
|
|
43
|
+
<SectionHeader
|
|
44
|
+
label={sectionLabel}
|
|
45
|
+
title={sectionTitle}
|
|
46
|
+
titleAccent={sectionTitleAccent}
|
|
47
|
+
lede={sectionSubtitle}
|
|
48
|
+
cms={cms}
|
|
49
|
+
/>
|
|
50
|
+
{
|
|
51
|
+
(hasCopy || hasImage(image)) && (
|
|
52
|
+
<div class:list={["sq-about__body", reverse && "sq-about__body--reverse"]}>
|
|
53
|
+
{hasCopy && (
|
|
54
|
+
<div class="sq-about__copy">
|
|
55
|
+
{hasAny(name, role, badges) && (
|
|
56
|
+
<div class="sq-about__identity">
|
|
57
|
+
{hasText(name) && (
|
|
58
|
+
<p class="sq-about__name" data-cms={cmsKey(cms, "name")}>
|
|
59
|
+
{name}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
{hasText(role) && (
|
|
63
|
+
<p class="sq-about__role" data-cms={cmsKey(cms, "role")}>
|
|
64
|
+
{role}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
{badges.length > 0 && (
|
|
68
|
+
<ul class="sq-about__qualifications">
|
|
69
|
+
{badges.map((badge, i) => (
|
|
70
|
+
<li data-cms={cmsKey(cms, "qualifications", i, "text")}>{badge}</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
<Prose body={body} cms={cmsKey(cms, "body")} />
|
|
77
|
+
<Actions ctas={ctas} cms={cmsKey(cms, "ctas")} />
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
{hasImage(image) && (
|
|
81
|
+
<figure class="sq-about__media">
|
|
82
|
+
<Media
|
|
83
|
+
src={image}
|
|
84
|
+
alt={image?.alt || name}
|
|
85
|
+
width={720}
|
|
86
|
+
height={900}
|
|
87
|
+
cms={cmsKey(cms, "image", "src")}
|
|
88
|
+
/>
|
|
89
|
+
</figure>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
</Section>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Section from "../components/Section.astro";
|
|
3
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
4
|
+
import Media from "../components/Media.astro";
|
|
5
|
+
import { cmsKey, hasImage, hasText, list } from "../lib/content";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sectionLabel,
|
|
9
|
+
sectionTitle,
|
|
10
|
+
sectionTitleAccent,
|
|
11
|
+
sectionSubtitle,
|
|
12
|
+
sectionNote,
|
|
13
|
+
items = [],
|
|
14
|
+
section = "audience",
|
|
15
|
+
variant = "light",
|
|
16
|
+
id = "audience",
|
|
17
|
+
} = Astro.props;
|
|
18
|
+
|
|
19
|
+
const cms = section;
|
|
20
|
+
const cards = list<any>(items).filter(
|
|
21
|
+
(item) => hasText(item.title) || hasImage(item.image) || list(item.bullets).length > 0,
|
|
22
|
+
);
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<Section name="audience" variant={variant} id={id} cms={cms}>
|
|
26
|
+
<SectionHeader
|
|
27
|
+
label={sectionLabel}
|
|
28
|
+
title={sectionTitle}
|
|
29
|
+
titleAccent={sectionTitleAccent}
|
|
30
|
+
lede={sectionSubtitle}
|
|
31
|
+
note={sectionNote}
|
|
32
|
+
cms={cms}
|
|
33
|
+
/>
|
|
34
|
+
{
|
|
35
|
+
cards.length > 0 && (
|
|
36
|
+
<div class="sq-audience__grid">
|
|
37
|
+
{cards.map((item: any, i: number) => {
|
|
38
|
+
const bullets = list<any>(item.bullets).filter((bullet) => hasText(bullet.text));
|
|
39
|
+
return (
|
|
40
|
+
<article class="sq-audience__card">
|
|
41
|
+
<Media
|
|
42
|
+
src={item.image}
|
|
43
|
+
alt={item.image?.alt}
|
|
44
|
+
cms={cmsKey(cms, "items", i, "image", "src")}
|
|
45
|
+
/>
|
|
46
|
+
<div class="sq-audience__body">
|
|
47
|
+
{hasText(item.title) && (
|
|
48
|
+
<h3 data-cms={cmsKey(cms, "items", i, "title")}>{item.title}</h3>
|
|
49
|
+
)}
|
|
50
|
+
{bullets.length > 0 && (
|
|
51
|
+
<ul>
|
|
52
|
+
{bullets.map((bullet: any, bi: number) => (
|
|
53
|
+
<li data-cms={cmsKey(cms, "items", i, "bullets", bi, "text")}>
|
|
54
|
+
{bullet.text}
|
|
55
|
+
</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</article>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
</Section>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
import About from "./About.astro";
|
|
3
|
+
import Audience from "./Audience.astro";
|
|
4
|
+
import Contact from "./Contact.astro";
|
|
5
|
+
import ContactDetails from "./ContactDetails.astro";
|
|
6
|
+
import CtaBand from "./CtaBand.astro";
|
|
7
|
+
import EmergencyGrid from "./EmergencyGrid.astro";
|
|
8
|
+
import EmergencyHelplines from "./EmergencyHelplines.astro";
|
|
9
|
+
import EmergencyPrimary from "./EmergencyPrimary.astro";
|
|
10
|
+
import Faq from "./Faq.astro";
|
|
11
|
+
import FeatureList from "./FeatureList.astro";
|
|
12
|
+
import Gallery from "./Gallery.astro";
|
|
13
|
+
import Generic from "./Generic.astro";
|
|
14
|
+
import Hero from "./Hero.astro";
|
|
15
|
+
import IntroCta from "./IntroCta.astro";
|
|
16
|
+
import LegalDocument from "./LegalDocument.astro";
|
|
17
|
+
import LocationsOverview from "./LocationsOverview.astro";
|
|
18
|
+
import NotFound from "./NotFound.astro";
|
|
19
|
+
import Notice from "./Notice.astro";
|
|
20
|
+
import PageBanner from "./PageBanner.astro";
|
|
21
|
+
import Pricing from "./Pricing.astro";
|
|
22
|
+
import ProfileDetail from "./ProfileDetail.astro";
|
|
23
|
+
import Projects from "./Projects.astro";
|
|
24
|
+
import Quote from "./Quote.astro";
|
|
25
|
+
import Services from "./Services.astro";
|
|
26
|
+
import Skills from "./Skills.astro";
|
|
27
|
+
import Stats from "./Stats.astro";
|
|
28
|
+
import Steps from "./Steps.astro";
|
|
29
|
+
import TeamDirectory from "./TeamDirectory.astro";
|
|
30
|
+
import TeamTeaser from "./TeamTeaser.astro";
|
|
31
|
+
import Testimonials from "./Testimonials.astro";
|
|
32
|
+
import Timeline from "./Timeline.astro";
|
|
33
|
+
import { list } from "../lib/content";
|
|
34
|
+
|
|
35
|
+
const BLOCK_COMPONENTS: Record<string, any> = {
|
|
36
|
+
about: About,
|
|
37
|
+
audience: Audience,
|
|
38
|
+
contact: Contact,
|
|
39
|
+
contactDetails: ContactDetails,
|
|
40
|
+
cta: CtaBand,
|
|
41
|
+
emergencyGrid: EmergencyGrid,
|
|
42
|
+
emergencyHelplines: EmergencyHelplines,
|
|
43
|
+
emergencyPrimary: EmergencyPrimary,
|
|
44
|
+
faq: Faq,
|
|
45
|
+
featureList: FeatureList,
|
|
46
|
+
gallery: Gallery,
|
|
47
|
+
generic: Generic,
|
|
48
|
+
hero: Hero,
|
|
49
|
+
introCta: IntroCta,
|
|
50
|
+
legal: LegalDocument,
|
|
51
|
+
locationsOverview: LocationsOverview,
|
|
52
|
+
notFound: NotFound,
|
|
53
|
+
notice: Notice,
|
|
54
|
+
pageBanner: PageBanner,
|
|
55
|
+
pricing: Pricing,
|
|
56
|
+
profileDetail: ProfileDetail,
|
|
57
|
+
projects: Projects,
|
|
58
|
+
quote: Quote,
|
|
59
|
+
services: Services,
|
|
60
|
+
skills: Skills,
|
|
61
|
+
stats: Stats,
|
|
62
|
+
steps: Steps,
|
|
63
|
+
teamDirectory: TeamDirectory,
|
|
64
|
+
teamTeaser: TeamTeaser,
|
|
65
|
+
testimonials: Testimonials,
|
|
66
|
+
timeline: Timeline,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Renders CMS blocks. Collection-backed blocks must already carry their items
|
|
71
|
+
* (members / locations) — resolve them in the page with @sumaq/site-kit/collections.
|
|
72
|
+
*
|
|
73
|
+
* `components` merges the site's own blocks into the map and wins over the
|
|
74
|
+
* catalogue, so a client repo can add a block the kit does not have — or
|
|
75
|
+
* replace one it does — without forking (block-contract §7):
|
|
76
|
+
*
|
|
77
|
+
* import Rooms from "../components/Rooms.astro";
|
|
78
|
+
* <BlockRenderer blocks={blocks} components={{ rooms: Rooms }} />
|
|
79
|
+
*
|
|
80
|
+
* A `type` that matches nothing falls back to `Generic` so a schema that runs
|
|
81
|
+
* ahead of the kit still builds; in dev it also warns, because silently
|
|
82
|
+
* rendering the wrong markup is how a custom block goes unnoticed.
|
|
83
|
+
*/
|
|
84
|
+
const { blocks = [], components = {} } = Astro.props;
|
|
85
|
+
const registry: Record<string, any> = { ...BLOCK_COMPONENTS, ...components };
|
|
86
|
+
const entries = list<any>(blocks).filter((block) => block && typeof block === "object");
|
|
87
|
+
|
|
88
|
+
if (import.meta.env.DEV) {
|
|
89
|
+
for (const block of entries) {
|
|
90
|
+
if (block.type && !registry[block.type]) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[site-kit] Unknown block type "${block.type}" — falling back to Generic. ` +
|
|
93
|
+
`Pass it in <BlockRenderer components={{ ${block.type}: … }} /> if it is a site-local block.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
entries.map((block: any) => {
|
|
102
|
+
const Comp = registry[block.type] ?? Generic;
|
|
103
|
+
return <Comp {...block} />;
|
|
104
|
+
})
|
|
105
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Section from "../components/Section.astro";
|
|
3
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
4
|
+
import { telHref } from "../lib/format";
|
|
5
|
+
import { cmsKey, hasText, list } from "../lib/content";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sectionLabel,
|
|
9
|
+
sectionTitle,
|
|
10
|
+
sectionTitleAccent,
|
|
11
|
+
sectionSubtitle,
|
|
12
|
+
cards = [],
|
|
13
|
+
section = "contact",
|
|
14
|
+
variant = "muted",
|
|
15
|
+
id = "contact",
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
|
|
18
|
+
const cms = section;
|
|
19
|
+
const people = list<any>(cards).filter(
|
|
20
|
+
(card) => hasText(card.name) || hasText(card.email) || hasText(card.phone),
|
|
21
|
+
);
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<Section name="contact" variant={variant} id={id} cms={cms}>
|
|
25
|
+
<SectionHeader
|
|
26
|
+
label={sectionLabel}
|
|
27
|
+
title={sectionTitle}
|
|
28
|
+
titleAccent={sectionTitleAccent}
|
|
29
|
+
lede={sectionSubtitle}
|
|
30
|
+
cms={cms}
|
|
31
|
+
/>
|
|
32
|
+
{
|
|
33
|
+
people.length > 0 && (
|
|
34
|
+
<div class:list={["sq-contact__grid", people.length === 1 && "sq-contact__grid--single"]}>
|
|
35
|
+
{people.map((card: any, i: number) => (
|
|
36
|
+
<article class="sq-contact__card">
|
|
37
|
+
{hasText(card.name) && <h3 data-cms={cmsKey(cms, "cards", i, "name")}>{card.name}</h3>}
|
|
38
|
+
{hasText(card.role) && (
|
|
39
|
+
<p class="sq-contact__role" data-cms={cmsKey(cms, "cards", i, "role")}>
|
|
40
|
+
{card.role}
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
{hasText(card.email) && (
|
|
44
|
+
<a href={`mailto:${card.email}`} data-cms={cmsKey(cms, "cards", i, "email")}>
|
|
45
|
+
{card.email}
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
{hasText(card.phone) && (
|
|
49
|
+
<a href={telHref(card.phone)} data-cms={cmsKey(cms, "cards", i, "phone")}>
|
|
50
|
+
{card.phone}
|
|
51
|
+
</a>
|
|
52
|
+
)}
|
|
53
|
+
</article>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
</Section>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Address / phone / email / hours as a definition list.
|
|
4
|
+
*
|
|
5
|
+
* The kit's `Contact` block lists *people* (name + role + reachability); this
|
|
6
|
+
* one lists the *practice*, which is what `ContactSection.astro`
|
|
7
|
+
* (www-juliaschaffer-at), `Contact.astro` (www-utehofer-at) and
|
|
8
|
+
* `Contact.astro` (www-bettinageidl-at) each solved separately.
|
|
9
|
+
*
|
|
10
|
+
* `type` derives the href, so the CMS stores a value, never a `mailto:`.
|
|
11
|
+
*/
|
|
12
|
+
import Section from "../components/Section.astro";
|
|
13
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
14
|
+
import Actions from "../components/Actions.astro";
|
|
15
|
+
import Media from "../components/Media.astro";
|
|
16
|
+
import { telHref } from "../lib/format";
|
|
17
|
+
import { cmsKey, hasImage, hasText, list } from "../lib/content";
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
sectionLabel,
|
|
21
|
+
sectionTitle,
|
|
22
|
+
sectionTitleAccent,
|
|
23
|
+
sectionSubtitle,
|
|
24
|
+
items = [],
|
|
25
|
+
ctas = [],
|
|
26
|
+
section = "contactDetails",
|
|
27
|
+
variant = "muted",
|
|
28
|
+
id = "contact",
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const cms = section;
|
|
32
|
+
|
|
33
|
+
const hrefFor = (item: any): string | undefined => {
|
|
34
|
+
if (hasText(item.href)) return item.href;
|
|
35
|
+
if (!hasText(item.value)) return undefined;
|
|
36
|
+
if (item.type === "email") return `mailto:${item.value}`;
|
|
37
|
+
if (item.type === "phone") return telHref(item.value);
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const rows = list<any>(items)
|
|
42
|
+
.filter((item) => hasText(item.value) || hasText(item.label))
|
|
43
|
+
.map((item) => ({ ...item, resolvedHref: hrefFor(item) }));
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<Section name="contactDetails" variant={variant} id={id} cms={cms}>
|
|
47
|
+
<SectionHeader
|
|
48
|
+
label={sectionLabel}
|
|
49
|
+
title={sectionTitle}
|
|
50
|
+
titleAccent={sectionTitleAccent}
|
|
51
|
+
lede={sectionSubtitle}
|
|
52
|
+
cms={cms}
|
|
53
|
+
/>
|
|
54
|
+
{
|
|
55
|
+
rows.length > 0 && (
|
|
56
|
+
<dl class="sq-contact-details__body">
|
|
57
|
+
{rows.map((item: any, i: number) => (
|
|
58
|
+
<div class="sq-contact-details__row">
|
|
59
|
+
{hasImage(item.icon) && (
|
|
60
|
+
<span class="sq-contact-details__icon" aria-hidden="true">
|
|
61
|
+
<Media src={item.icon} alt="" cms={cmsKey(cms, "items", i, "icon")} />
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{hasText(item.label) && (
|
|
65
|
+
<dt class="sq-contact-details__label" data-cms={cmsKey(cms, "items", i, "label")}>
|
|
66
|
+
{item.label}
|
|
67
|
+
</dt>
|
|
68
|
+
)}
|
|
69
|
+
{hasText(item.value) && (
|
|
70
|
+
<dd class="sq-contact-details__value">
|
|
71
|
+
{item.resolvedHref ? (
|
|
72
|
+
<a
|
|
73
|
+
href={item.resolvedHref}
|
|
74
|
+
target={item.external ? "_blank" : undefined}
|
|
75
|
+
rel={item.external ? "noopener" : undefined}
|
|
76
|
+
data-cms={cmsKey(cms, "items", i, "value")}
|
|
77
|
+
>
|
|
78
|
+
{item.value}
|
|
79
|
+
</a>
|
|
80
|
+
) : (
|
|
81
|
+
<span data-cms={cmsKey(cms, "items", i, "value")}>{item.value}</span>
|
|
82
|
+
)}
|
|
83
|
+
</dd>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</dl>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
<Actions ctas={ctas} cms={cmsKey(cms, "ctas")} />
|
|
91
|
+
</Section>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Actions from "../components/Actions.astro";
|
|
3
|
+
import { cmsKey, hasAny, hasText } from "../lib/content";
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
sectionTitle,
|
|
7
|
+
sectionTitleAccent,
|
|
8
|
+
sectionSubtitle,
|
|
9
|
+
ctas = [],
|
|
10
|
+
section = "cta",
|
|
11
|
+
} = Astro.props;
|
|
12
|
+
|
|
13
|
+
const cms = section;
|
|
14
|
+
const hasTitle = hasText(sectionTitle) || hasText(sectionTitleAccent);
|
|
15
|
+
const hasCopy = hasAny(sectionTitle, sectionTitleAccent, sectionSubtitle);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
hasAny(sectionTitle, sectionTitleAccent, sectionSubtitle, ctas) && (
|
|
20
|
+
<section
|
|
21
|
+
class="sq-cta"
|
|
22
|
+
aria-labelledby={hasTitle ? "cta-title" : undefined}
|
|
23
|
+
data-cms={cms}
|
|
24
|
+
>
|
|
25
|
+
<div class="sq-container sq-cta__inner">
|
|
26
|
+
{hasCopy && (
|
|
27
|
+
<div>
|
|
28
|
+
{hasTitle && (
|
|
29
|
+
<h2 id="cta-title" class="sq-cta__title" data-cms={cmsKey(cms, "sectionTitle")}>
|
|
30
|
+
{sectionTitle}
|
|
31
|
+
{hasText(sectionTitleAccent) && (
|
|
32
|
+
<span class="sq-accent" data-cms={cmsKey(cms, "sectionTitleAccent")}>
|
|
33
|
+
{sectionTitleAccent}
|
|
34
|
+
</span>
|
|
35
|
+
)}
|
|
36
|
+
</h2>
|
|
37
|
+
)}
|
|
38
|
+
{hasText(sectionSubtitle) && (
|
|
39
|
+
<p data-cms={cmsKey(cms, "sectionSubtitle")}>{sectionSubtitle}</p>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<Actions
|
|
44
|
+
ctas={ctas}
|
|
45
|
+
class="sq-cta__actions"
|
|
46
|
+
cms={cmsKey(cms, "ctas")}
|
|
47
|
+
secondaryClass="sq-btn--outline-light"
|
|
48
|
+
primaryFirst
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cmsKey, hasText, list } from "../lib/content";
|
|
3
|
+
|
|
4
|
+
const { sectionTitle, items = [], section = "emergencyGrid", callLabel } = Astro.props;
|
|
5
|
+
const cms = section;
|
|
6
|
+
const entries = list<any>(items).filter((item) => hasText(item.name) || hasText(item.number));
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
hasText(sectionTitle) && (
|
|
11
|
+
<h2
|
|
12
|
+
class="sq-section__title sq-center sq-section__title--compact"
|
|
13
|
+
data-cms={cmsKey(cms, "sectionTitle")}
|
|
14
|
+
>
|
|
15
|
+
{sectionTitle}
|
|
16
|
+
</h2>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
{
|
|
20
|
+
entries.length > 0 && (
|
|
21
|
+
<div class="sq-emergency__grid" data-cms={cms}>
|
|
22
|
+
{entries.map((item: any, i: number) => (
|
|
23
|
+
<a
|
|
24
|
+
class="sq-emergency__item"
|
|
25
|
+
href={item.href}
|
|
26
|
+
aria-label={hasText(callLabel) ? `${item.name} — ${callLabel}` : item.name}
|
|
27
|
+
>
|
|
28
|
+
{hasText(item.name) && (
|
|
29
|
+
<span class="sq-emergency__item-name" data-cms={cmsKey(cms, "items", i, "name")}>
|
|
30
|
+
{item.name}
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
{hasText(item.desc) && (
|
|
34
|
+
<span class="sq-emergency__item-desc" data-cms={cmsKey(cms, "items", i, "desc")}>
|
|
35
|
+
{item.desc}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
{hasText(item.number) && (
|
|
39
|
+
<span class="sq-emergency__item-number" data-cms={cmsKey(cms, "items", i, "number")}>
|
|
40
|
+
{item.number}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</a>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|