@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.
Files changed (60) hide show
  1. package/README.md +93 -0
  2. package/package.json +57 -0
  3. package/src/astro-modules.d.ts +11 -0
  4. package/src/blocks/About.astro +94 -0
  5. package/src/blocks/Audience.astro +66 -0
  6. package/src/blocks/BlockRenderer.astro +105 -0
  7. package/src/blocks/Contact.astro +58 -0
  8. package/src/blocks/ContactDetails.astro +91 -0
  9. package/src/blocks/CtaBand.astro +53 -0
  10. package/src/blocks/EmergencyGrid.astro +47 -0
  11. package/src/blocks/EmergencyHelplines.astro +109 -0
  12. package/src/blocks/EmergencyPrimary.astro +51 -0
  13. package/src/blocks/Faq.astro +56 -0
  14. package/src/blocks/FeatureList.astro +73 -0
  15. package/src/blocks/Gallery.astro +95 -0
  16. package/src/blocks/Generic.astro +30 -0
  17. package/src/blocks/Hero.astro +82 -0
  18. package/src/blocks/IntroCta.astro +41 -0
  19. package/src/blocks/LegalDocument.astro +83 -0
  20. package/src/blocks/LocationsOverview.astro +161 -0
  21. package/src/blocks/NotFound.astro +33 -0
  22. package/src/blocks/Notice.astro +49 -0
  23. package/src/blocks/PageBanner.astro +57 -0
  24. package/src/blocks/Pricing.astro +97 -0
  25. package/src/blocks/ProfileDetail.astro +202 -0
  26. package/src/blocks/Projects.astro +91 -0
  27. package/src/blocks/Quote.astro +53 -0
  28. package/src/blocks/Services.astro +46 -0
  29. package/src/blocks/Skills.astro +65 -0
  30. package/src/blocks/Stats.astro +54 -0
  31. package/src/blocks/Steps.astro +76 -0
  32. package/src/blocks/TeamDirectory.astro +164 -0
  33. package/src/blocks/TeamTeaser.astro +84 -0
  34. package/src/blocks/Testimonials.astro +75 -0
  35. package/src/blocks/Timeline.astro +65 -0
  36. package/src/components/Actions.astro +67 -0
  37. package/src/components/Card.astro +104 -0
  38. package/src/components/Logo.astro +57 -0
  39. package/src/components/Media.astro +83 -0
  40. package/src/components/Prose.astro +29 -0
  41. package/src/components/ScrollToTop.astro +49 -0
  42. package/src/components/Section.astro +75 -0
  43. package/src/components/SectionHeader.astro +58 -0
  44. package/src/components/SectionHeading.astro +76 -0
  45. package/src/components/Seo.astro +55 -0
  46. package/src/components/SiteFooter.astro +90 -0
  47. package/src/components/SiteHeader.astro +97 -0
  48. package/src/config.ts +59 -0
  49. package/src/index.ts +75 -0
  50. package/src/layouts/Base.astro +57 -0
  51. package/src/lib/collections.ts +101 -0
  52. package/src/lib/content.ts +51 -0
  53. package/src/lib/format.ts +17 -0
  54. package/src/lib/markdown.ts +5 -0
  55. package/src/profiles/clinic.yaml +35 -0
  56. package/src/profiles/portfolio.yaml +27 -0
  57. package/src/profiles/therapist.yaml +31 -0
  58. package/src/scripts/enhancements.js +459 -0
  59. package/src/tokens/class-inventory.json +284 -0
  60. package/src/tokens/index.css +83 -0
@@ -0,0 +1,53 @@
1
+ ---
2
+ /**
3
+ * A single pull quote band. Present in every therapist site — the `quote`
4
+ * field of `Psychotherapy.astro` (www-utehofer-at), the two blockquotes of
5
+ * `Method.astro` / `Process.astro` (www-bettinageidl-at), the `quote` section
6
+ * of `AboutSection.astro` (www-juliaschaffer-at).
7
+ *
8
+ * The quotation marks belong to the design, not to the content: style them
9
+ * with `.sq-quote__text::before` instead of typing them into the CMS.
10
+ */
11
+ import Section from "../components/Section.astro";
12
+ import Media from "../components/Media.astro";
13
+ import { cmsKey, hasImage, hasText } from "../lib/content";
14
+
15
+ const {
16
+ text,
17
+ author,
18
+ authorRole,
19
+ image,
20
+ section = "quote",
21
+ variant = "muted",
22
+ id,
23
+ } = Astro.props;
24
+
25
+ const cms = section;
26
+ ---
27
+
28
+ {
29
+ hasText(text) && (
30
+ <Section name="quote" variant={variant} id={id} container="narrow" cms={cms}>
31
+ {hasImage(image) && (
32
+ <div class="sq-quote__media" aria-hidden="true">
33
+ <Media src={image} alt="" cms={cmsKey(cms, "image", "src")} />
34
+ </div>
35
+ )}
36
+ <blockquote class="sq-quote__body">
37
+ <p class="sq-quote__text" data-cms={cmsKey(cms, "text")}>
38
+ {text}
39
+ </p>
40
+ {hasText(author) && (
41
+ <footer class="sq-quote__author">
42
+ <cite data-cms={cmsKey(cms, "author")}>{author}</cite>
43
+ {hasText(authorRole) && (
44
+ <span class="sq-quote__author-role" data-cms={cmsKey(cms, "authorRole")}>
45
+ {authorRole}
46
+ </span>
47
+ )}
48
+ </footer>
49
+ )}
50
+ </blockquote>
51
+ </Section>
52
+ )
53
+ }
@@ -0,0 +1,46 @@
1
+ ---
2
+ import Section from "../components/Section.astro";
3
+ import SectionHeader from "../components/SectionHeader.astro";
4
+ import { cmsKey, hasText, list } from "../lib/content";
5
+
6
+ const {
7
+ sectionLabel,
8
+ sectionTitle,
9
+ sectionTitleAccent,
10
+ sectionSubtitle,
11
+ items = [],
12
+ section = "services",
13
+ variant = "muted",
14
+ id = "services",
15
+ } = Astro.props;
16
+
17
+ const cms = section;
18
+ const cards = list<any>(items).filter((item) => hasText(item.title) || hasText(item.body));
19
+ ---
20
+
21
+ <Section name="services" variant={variant} id={id} cms={cms}>
22
+ <SectionHeader
23
+ label={sectionLabel}
24
+ title={sectionTitle}
25
+ titleAccent={sectionTitleAccent}
26
+ lede={sectionSubtitle}
27
+ cms={cms}
28
+ />
29
+ {
30
+ cards.length > 0 && (
31
+ <div class="sq-services__grid">
32
+ {cards.map((item: any, i: number) => (
33
+ <article class="sq-services__card">
34
+ {hasText(item.icon) && (
35
+ <div class={`sq-services__icon sq-services__icon--${item.icon}`} aria-hidden="true">
36
+
37
+ </div>
38
+ )}
39
+ {hasText(item.title) && <h3 data-cms={cmsKey(cms, "items", i, "title")}>{item.title}</h3>}
40
+ {hasText(item.body) && <p data-cms={cmsKey(cms, "items", i, "body")}>{item.body}</p>}
41
+ </article>
42
+ ))}
43
+ </div>
44
+ )
45
+ }
46
+ </Section>
@@ -0,0 +1,65 @@
1
+ ---
2
+ /**
3
+ * Labelled groups of tags. Extracted from `Skills.astro` (www-elingan-dev);
4
+ * the same shape covers the `focusAreas` / `methods` / `activityAreas` tag
5
+ * groups that www-therapieraum-graz-at inlines in its profile pages.
6
+ */
7
+ import Section from "../components/Section.astro";
8
+ import SectionHeader from "../components/SectionHeader.astro";
9
+ import { cmsKey, hasText, list } from "../lib/content";
10
+
11
+ const {
12
+ sectionLabel,
13
+ sectionTitle,
14
+ sectionTitleAccent,
15
+ sectionSubtitle,
16
+ groups = [],
17
+ section = "skills",
18
+ variant = "muted",
19
+ id = "skills",
20
+ } = Astro.props;
21
+
22
+ const cms = section;
23
+ const blocks = list<any>(groups)
24
+ .map((group) => ({
25
+ ...group,
26
+ entries: list<any>(group.items)
27
+ .map((item) => (typeof item === "string" ? item : item.name))
28
+ .filter(hasText),
29
+ }))
30
+ .filter((group) => group.entries.length > 0 || hasText(group.label));
31
+ ---
32
+
33
+ <Section name="skills" variant={variant} id={id} cms={cms}>
34
+ <SectionHeader
35
+ label={sectionLabel}
36
+ title={sectionTitle}
37
+ titleAccent={sectionTitleAccent}
38
+ lede={sectionSubtitle}
39
+ cms={cms}
40
+ />
41
+ {
42
+ blocks.length > 0 && (
43
+ <div class="sq-skills__body">
44
+ {blocks.map((group: any, i: number) => (
45
+ <article class="sq-skills__group">
46
+ {hasText(group.label) && (
47
+ <h3 class="sq-skills__label" data-cms={cmsKey(cms, "groups", i, "label")}>
48
+ {group.label}
49
+ </h3>
50
+ )}
51
+ {group.entries.length > 0 && (
52
+ <ul class="sq-skills__tags" role="list">
53
+ {group.entries.map((entry: string, ei: number) => (
54
+ <li class="sq-tag" data-cms={cmsKey(cms, "groups", i, "items", ei, "name")}>
55
+ {entry}
56
+ </li>
57
+ ))}
58
+ </ul>
59
+ )}
60
+ </article>
61
+ ))}
62
+ </div>
63
+ )
64
+ }
65
+ </Section>
@@ -0,0 +1,54 @@
1
+ ---
2
+ /**
3
+ * A row of figures — the `stats` list that `Hero` renders inline and the
4
+ * `meta` list of `Hero.astro` in www-elingan-dev, as a standalone band so a
5
+ * page can place it anywhere.
6
+ */
7
+ import Section from "../components/Section.astro";
8
+ import SectionHeader from "../components/SectionHeader.astro";
9
+ import { cmsKey, hasText, list } from "../lib/content";
10
+
11
+ const {
12
+ sectionLabel,
13
+ sectionTitle,
14
+ sectionTitleAccent,
15
+ sectionSubtitle,
16
+ items = [],
17
+ section = "stats",
18
+ variant = "muted",
19
+ id,
20
+ } = Astro.props;
21
+
22
+ const cms = section;
23
+ const figures = list<any>(items).filter((item) => hasText(item.value) || hasText(item.label));
24
+ ---
25
+
26
+ <Section name="stats" variant={variant} id={id} cms={cms}>
27
+ <SectionHeader
28
+ label={sectionLabel}
29
+ title={sectionTitle}
30
+ titleAccent={sectionTitleAccent}
31
+ lede={sectionSubtitle}
32
+ cms={cms}
33
+ />
34
+ {
35
+ figures.length > 0 && (
36
+ <dl class="sq-stats__body">
37
+ {figures.map((item: any, i: number) => (
38
+ <div class="sq-stats__item">
39
+ {hasText(item.value) && (
40
+ <dt class="sq-stats__value" data-cms={cmsKey(cms, "items", i, "value")}>
41
+ {item.value}
42
+ </dt>
43
+ )}
44
+ {hasText(item.label) && (
45
+ <dd class="sq-stats__label" data-cms={cmsKey(cms, "items", i, "label")}>
46
+ {item.label}
47
+ </dd>
48
+ )}
49
+ </div>
50
+ ))}
51
+ </dl>
52
+ )
53
+ }
54
+ </Section>
@@ -0,0 +1,76 @@
1
+ ---
2
+ /**
3
+ * Numbered process. Extracted from `Process.astro` + `ItemStep.astro`
4
+ * (www-utehofer-at) and `Process.astro` (www-bettinageidl-at).
5
+ *
6
+ * The number is markup, not content: it comes from the item's position, so
7
+ * reordering steps in the CMS renumbers them.
8
+ */
9
+ import Section from "../components/Section.astro";
10
+ import SectionHeader from "../components/SectionHeader.astro";
11
+ import Media from "../components/Media.astro";
12
+ import { cmsKey, hasAny, hasImage, hasText, list } from "../lib/content";
13
+
14
+ const {
15
+ sectionLabel,
16
+ sectionTitle,
17
+ sectionTitleAccent,
18
+ sectionSubtitle,
19
+ items = [],
20
+ note,
21
+ section = "steps",
22
+ variant = "muted",
23
+ id = "steps",
24
+ } = Astro.props;
25
+
26
+ const cms = section;
27
+ const steps = list<any>(items).filter((item) => hasText(item.title) || hasText(item.body));
28
+ ---
29
+
30
+ <Section name="steps" variant={variant} id={id} cms={cms}>
31
+ <SectionHeader
32
+ label={sectionLabel}
33
+ title={sectionTitle}
34
+ titleAccent={sectionTitleAccent}
35
+ lede={sectionSubtitle}
36
+ cms={cms}
37
+ />
38
+ {
39
+ steps.length > 0 && (
40
+ <ol class="sq-steps__body">
41
+ {steps.map((item: any, i: number) => (
42
+ <li class="sq-steps__item">
43
+ <span class="sq-steps__num" aria-hidden="true">
44
+ {String(i + 1).padStart(2, "0")}
45
+ </span>
46
+ {hasImage(item.icon) && (
47
+ <span class="sq-steps__icon" aria-hidden="true">
48
+ <Media src={item.icon} alt="" cms={cmsKey(cms, "items", i, "icon")} />
49
+ </span>
50
+ )}
51
+ {hasText(item.title) && (
52
+ <h3 class="sq-steps__title" data-cms={cmsKey(cms, "items", i, "title")}>
53
+ {item.title}
54
+ </h3>
55
+ )}
56
+ {hasText(item.body) && (
57
+ <p class="sq-steps__text" data-cms={cmsKey(cms, "items", i, "body")}>
58
+ {item.body}
59
+ </p>
60
+ )}
61
+ </li>
62
+ ))}
63
+ </ol>
64
+ )
65
+ }
66
+ {
67
+ hasAny(note?.title, note?.body) && (
68
+ <div class="sq-steps__note">
69
+ {hasText(note.title) && (
70
+ <h3 data-cms={cmsKey(cms, "note", "title")}>{note.title}</h3>
71
+ )}
72
+ {hasText(note.body) && <p data-cms={cmsKey(cms, "note", "body")}>{note.body}</p>}
73
+ </div>
74
+ )
75
+ }
76
+ </Section>
@@ -0,0 +1,164 @@
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, hasText, list } from "../lib/content";
6
+
7
+ const {
8
+ sectionLabel,
9
+ sectionTitle,
10
+ sectionTitleAccent,
11
+ sectionSubtitle,
12
+ searchPlaceholder,
13
+ searchLabel = "Team-Mitglieder durchsuchen",
14
+ filtersLabel = "Fachgebiete filtern",
15
+ /** `{count}` is replaced at runtime by `scripts/enhancements.js`. */
16
+ counterAllTemplate = "Zeigt alle {count} Fachpersonen",
17
+ counterResultsTemplate = "Suchergebnisse: {count} Treffer",
18
+ filters = [],
19
+ emptyTitle,
20
+ emptyText,
21
+ members = [],
22
+ basePath = "/team",
23
+ section = "teamDirectory",
24
+ variant = "light",
25
+ id,
26
+ } = Astro.props;
27
+
28
+ const cms = section;
29
+ const people = list<any>(members).filter((member) => hasText(member.name));
30
+ const chips = list<any>(filters).filter((filter) => hasText(filter.label) && hasText(filter.value));
31
+ const showSearch = hasText(searchPlaceholder) && people.length > 0;
32
+ const [counterBefore, counterAfter = ""] = String(counterAllTemplate).split("{count}");
33
+ ---
34
+
35
+ <Section name="teamDirectory" variant={variant} id={id} cms={cms}>
36
+ <SectionHeader
37
+ label={sectionLabel}
38
+ title={sectionTitle}
39
+ titleAccent={sectionTitleAccent}
40
+ lede={sectionSubtitle}
41
+ cms={cms}
42
+ />
43
+
44
+ {
45
+ (showSearch || chips.length > 0) && (
46
+ <div class="sq-team__search">
47
+ {showSearch && (
48
+ <div class="sq-team__search-wrap">
49
+ <span class="sq-team__search-icon" aria-hidden="true">
50
+ <svg
51
+ width="20"
52
+ height="20"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ stroke-linecap="round"
58
+ stroke-linejoin="round"
59
+ >
60
+ <circle cx="11" cy="11" r="8" />
61
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
62
+ </svg>
63
+ </span>
64
+ <input
65
+ type="search"
66
+ id="team-search"
67
+ class="sq-team__search-input"
68
+ placeholder={searchPlaceholder}
69
+ aria-label={searchLabel}
70
+ data-cms={cmsKey(cms, "searchPlaceholder")}
71
+ />
72
+ </div>
73
+ )}
74
+
75
+ {chips.length > 0 && (
76
+ <div class="sq-team__filters" role="group" aria-label={filtersLabel}>
77
+ {chips.map((filter: any, i: number) => (
78
+ <button
79
+ type="button"
80
+ class:list={["sq-team__filter", i === 0 && "is-active"]}
81
+ data-filter={filter.value}
82
+ aria-pressed={i === 0 ? "true" : "false"}
83
+ data-cms={cmsKey(cms, "filters", i, "label")}
84
+ >
85
+ {filter.label}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ )}
90
+
91
+ {people.length > 0 && (
92
+ <p
93
+ class="sq-team__counter"
94
+ id="search-counter"
95
+ data-counter-all={counterAllTemplate}
96
+ data-counter-results={counterResultsTemplate}
97
+ >
98
+ {counterBefore}
99
+ <span id="member-count">{people.length}</span>
100
+ {counterAfter}
101
+ </p>
102
+ )}
103
+ </div>
104
+ )
105
+ }
106
+
107
+ {
108
+ people.length > 0 && (
109
+ <div class="sq-team__grid" id="team-grid">
110
+ {people.map((member: any, i: number) => (
111
+ <a
112
+ class="sq-team__card"
113
+ href={member.href || `${basePath}/${member.slug}`}
114
+ data-search={member.searchKeywords}
115
+ data-categories={member.categories}
116
+ >
117
+ <div class="sq-team__photo">
118
+ <Media
119
+ src={member.photo}
120
+ alt={member.photo?.alt || member.name}
121
+ width={640}
122
+ height={640}
123
+ transition:name={`profile-photo-${member.slug}`}
124
+ cms={cmsKey(cms, "members", i, "photo", "src")}
125
+ />
126
+ </div>
127
+ <div class="sq-team__body">
128
+ {hasText(member.titleSuffix) && (
129
+ <span class="sq-team__role" data-cms={cmsKey(cms, "members", i, "titleSuffix")}>
130
+ {member.titleSuffix}
131
+ </span>
132
+ )}
133
+ <h3 class="sq-team__name" data-cms={cmsKey(cms, "members", i, "name")}>
134
+ {member.name}
135
+ </h3>
136
+ {hasText(member.description || member.role) && (
137
+ <p
138
+ class="sq-team__desc"
139
+ data-cms={cmsKey(
140
+ cms,
141
+ "members",
142
+ i,
143
+ hasText(member.description) ? "description" : "role",
144
+ )}
145
+ >
146
+ {member.description || member.role}
147
+ </p>
148
+ )}
149
+ </div>
150
+ </a>
151
+ ))}
152
+ </div>
153
+ )
154
+ }
155
+
156
+ {
157
+ people.length > 0 && hasText(emptyTitle || emptyText) && (
158
+ <div class="sq-team__empty" id="no-results" hidden>
159
+ {hasText(emptyTitle) && <h3 data-cms={cmsKey(cms, "emptyTitle")}>{emptyTitle}</h3>}
160
+ {hasText(emptyText) && <p data-cms={cmsKey(cms, "emptyText")}>{emptyText}</p>}
161
+ </div>
162
+ )
163
+ }
164
+ </Section>
@@ -0,0 +1,84 @@
1
+ ---
2
+ import Section from "../components/Section.astro";
3
+ import SectionHeading from "../components/SectionHeading.astro";
4
+ import Media from "../components/Media.astro";
5
+ import { cmsKey, hasAny, hasImage, hasText, list } from "../lib/content";
6
+
7
+ const {
8
+ sectionLabel,
9
+ sectionTitle,
10
+ sectionTitleAccent,
11
+ sectionSubtitle,
12
+ image,
13
+ badgeNum,
14
+ badgeLabel,
15
+ ctas = [],
16
+ section = "teamTeaser",
17
+ variant = "light",
18
+ id = "team",
19
+ } = Astro.props;
20
+
21
+ const cms = section;
22
+ const hasTitle = hasText(sectionTitle) || hasText(sectionTitleAccent);
23
+ const hasBadge = hasAny(badgeNum, badgeLabel);
24
+ /** The teaser shows a single CTA, straight in the copy column — no actions row. */
25
+ const cta = list<any>(ctas).find((item) => hasText(item.label) && hasText(item.href));
26
+ ---
27
+
28
+ <Section
29
+ name="teamTeaser"
30
+ variant={variant}
31
+ id={id}
32
+ labelledby={hasTitle ? "team-title" : undefined}
33
+ containerClass="sq-split"
34
+ cms={cms}
35
+ >
36
+ <div class="sq-split__copy">
37
+ <SectionHeading
38
+ label={sectionLabel}
39
+ title={sectionTitle}
40
+ titleAccent={sectionTitleAccent}
41
+ lede={sectionSubtitle}
42
+ titleId="team-title"
43
+ cms={cms}
44
+ />
45
+ {
46
+ cta && (
47
+ <a
48
+ class="sq-btn sq-btn--primary"
49
+ href={cta.href}
50
+ target={cta.external ? "_blank" : undefined}
51
+ rel={cta.external ? "noopener" : undefined}
52
+ data-cms={cmsKey(cms, "ctas", 0, "label")}
53
+ >
54
+ {cta.label}
55
+ </a>
56
+ )
57
+ }
58
+ </div>
59
+ {
60
+ (hasImage(image) || hasBadge) && (
61
+ <div class="sq-split__visual">
62
+ {hasImage(image) && (
63
+ <figure class="sq-team-teaser__frame">
64
+ <Media src={image} alt={image?.alt} cms={cmsKey(cms, "image", "src")} />
65
+ </figure>
66
+ )}
67
+ {hasBadge && (
68
+ <div class="sq-team-teaser__badge" aria-hidden="true">
69
+ {hasText(badgeNum) && (
70
+ <span class="sq-team-teaser__badge-num" data-cms={cmsKey(cms, "badgeNum")}>
71
+ {badgeNum}
72
+ </span>
73
+ )}
74
+ {hasText(badgeLabel) && (
75
+ <span class="sq-team-teaser__badge-label" data-cms={cmsKey(cms, "badgeLabel")}>
76
+ {badgeLabel}
77
+ </span>
78
+ )}
79
+ </div>
80
+ )}
81
+ </div>
82
+ )
83
+ }
84
+ </Section>
@@ -0,0 +1,75 @@
1
+ ---
2
+ /**
3
+ * Several quotes as cards.
4
+ *
5
+ * Not extracted from an existing site — none of the five had one — but a
6
+ * standard slot every service site eventually needs, so it ships as part of
7
+ * the catalogue instead of being improvised per client. Same field names as
8
+ * `Quote`, in a list.
9
+ */
10
+ import Section from "../components/Section.astro";
11
+ import SectionHeader from "../components/SectionHeader.astro";
12
+ import Media from "../components/Media.astro";
13
+ import { cmsKey, hasImage, hasText, list } from "../lib/content";
14
+
15
+ const {
16
+ sectionLabel,
17
+ sectionTitle,
18
+ sectionTitleAccent,
19
+ sectionSubtitle,
20
+ items = [],
21
+ section = "testimonials",
22
+ variant = "muted",
23
+ id = "testimonials",
24
+ } = Astro.props;
25
+
26
+ const cms = section;
27
+ const entries = list<any>(items).filter((item) => hasText(item.text));
28
+ ---
29
+
30
+ <Section name="testimonials" variant={variant} id={id} cms={cms}>
31
+ <SectionHeader
32
+ label={sectionLabel}
33
+ title={sectionTitle}
34
+ titleAccent={sectionTitleAccent}
35
+ lede={sectionSubtitle}
36
+ cms={cms}
37
+ />
38
+ {
39
+ entries.length > 0 && (
40
+ <div class="sq-testimonials__body">
41
+ {entries.map((item: any, i: number) => (
42
+ <figure class="sq-testimonials__card">
43
+ {hasImage(item.image) && (
44
+ <div class="sq-testimonials__photo">
45
+ <Media
46
+ src={item.image}
47
+ alt={item.image?.alt || item.author || ""}
48
+ width={128}
49
+ height={128}
50
+ cms={cmsKey(cms, "items", i, "image", "src")}
51
+ />
52
+ </div>
53
+ )}
54
+ <blockquote class="sq-testimonials__text" data-cms={cmsKey(cms, "items", i, "text")}>
55
+ {item.text}
56
+ </blockquote>
57
+ {hasText(item.author) && (
58
+ <figcaption class="sq-testimonials__author">
59
+ <cite data-cms={cmsKey(cms, "items", i, "author")}>{item.author}</cite>
60
+ {hasText(item.authorRole) && (
61
+ <span
62
+ class="sq-testimonials__author-role"
63
+ data-cms={cmsKey(cms, "items", i, "authorRole")}
64
+ >
65
+ {item.authorRole}
66
+ </span>
67
+ )}
68
+ </figcaption>
69
+ )}
70
+ </figure>
71
+ ))}
72
+ </div>
73
+ )
74
+ }
75
+ </Section>
@@ -0,0 +1,65 @@
1
+ ---
2
+ /**
3
+ * Chronological list — training, career, certificates.
4
+ *
5
+ * Extracted from the `timeline` list in `AboutSection.astro`
6
+ * (www-juliaschaffer-at), the `education` lists of www-utehofer-at and
7
+ * www-bettinageidl-at, and the `sq-profile__timeline` markup that
8
+ * www-therapieraum-graz-at inlines in `team/[slug].astro`.
9
+ */
10
+ import Section from "../components/Section.astro";
11
+ import SectionHeader from "../components/SectionHeader.astro";
12
+ import { cmsKey, hasText, list } from "../lib/content";
13
+
14
+ const {
15
+ sectionLabel,
16
+ sectionTitle,
17
+ sectionTitleAccent,
18
+ sectionSubtitle,
19
+ items = [],
20
+ section = "timeline",
21
+ variant = "light",
22
+ id,
23
+ } = Astro.props;
24
+
25
+ const cms = section;
26
+ const entries = list<any>(items)
27
+ .map((item) => (typeof item === "string" ? { text: item } : item))
28
+ .filter((item) => hasText(item.text) || hasText(item.title));
29
+ ---
30
+
31
+ <Section name="timeline" variant={variant} id={id} cms={cms}>
32
+ <SectionHeader
33
+ label={sectionLabel}
34
+ title={sectionTitle}
35
+ titleAccent={sectionTitleAccent}
36
+ lede={sectionSubtitle}
37
+ cms={cms}
38
+ />
39
+ {
40
+ entries.length > 0 && (
41
+ <ol class="sq-timeline__body">
42
+ {entries.map((item: any, i: number) => (
43
+ <li class="sq-timeline__item">
44
+ <span class="sq-timeline__dot" aria-hidden="true" />
45
+ {hasText(item.period) && (
46
+ <p class="sq-timeline__period" data-cms={cmsKey(cms, "items", i, "period")}>
47
+ {item.period}
48
+ </p>
49
+ )}
50
+ {hasText(item.title) && (
51
+ <h3 class="sq-timeline__title" data-cms={cmsKey(cms, "items", i, "title")}>
52
+ {item.title}
53
+ </h3>
54
+ )}
55
+ {hasText(item.text) && (
56
+ <p class="sq-timeline__text" data-cms={cmsKey(cms, "items", i, "text")}>
57
+ {item.text}
58
+ </p>
59
+ )}
60
+ </li>
61
+ ))}
62
+ </ol>
63
+ )
64
+ }
65
+ </Section>