ferst-core 0.7.0 → 0.7.4

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 (38) hide show
  1. package/components/BlockRenderer.astro +6 -0
  2. package/components/Pagination.astro +51 -0
  3. package/components/blocks/Heading.astro +47 -46
  4. package/components/blocks/Prose.astro +10 -2
  5. package/components/blocks/Quote.astro +40 -38
  6. package/components/recipes/Accordion.astro +91 -84
  7. package/components/recipes/ContactCard.astro +172 -170
  8. package/components/recipes/Document.astro +152 -0
  9. package/components/recipes/FeatureCards.astro +112 -111
  10. package/components/recipes/Hero.astro +120 -8
  11. package/components/recipes/LinkRow.astro +91 -0
  12. package/components/recipes/MediaCards.astro +213 -212
  13. package/components/recipes/Notice.astro +90 -89
  14. package/components/recipes/Stats.astro +152 -145
  15. package/components/recipes/Table.astro +98 -97
  16. package/components/recipes/Tabs.astro +134 -133
  17. package/components/recipes/Tiles.astro +287 -271
  18. package/components/recipes/Timetable.astro +189 -0
  19. package/components/recipes/Video.astro +177 -157
  20. package/content/blocks.ts +68 -2
  21. package/content/cookies.ts +10 -0
  22. package/examples/primitives.ts +2 -0
  23. package/examples/recipes.ts +42 -36
  24. package/integration.mjs +83 -64
  25. package/layouts/Base.astro +481 -475
  26. package/lib/documents.ts +68 -62
  27. package/lib/embed-consent.ts +78 -73
  28. package/lib/inline-markdown.ts +12 -0
  29. package/lib/links.ts +12 -0
  30. package/lib/paginate.ts +23 -0
  31. package/lib/posts.ts +20 -2
  32. package/lib/timetable.ts +27 -0
  33. package/lib/video.ts +101 -56
  34. package/package.json +1 -1
  35. package/routes/cookie-policy.astro +27 -0
  36. package/routes/posts/index.astro +36 -31
  37. package/routes/posts/page/[page].astro +36 -0
  38. package/routes/posts/tag/[...tag].astro +41 -37
@@ -1,133 +1,134 @@
1
- ---
2
- /** Tabs — a Level-2 recipe: labelled panels, one open at a time. Progressive:
3
- * the first panel is visible with no JavaScript; a small script adds selection,
4
- * ARIA state and arrow-key navigation. Composes the Heading primitive. */
5
- import Heading from '../blocks/Heading.astro';
6
-
7
- interface Item { label: string; heading?: string; text?: string; image?: { src: string; alt?: string } }
8
- interface Props {
9
- title?: string;
10
- items?: Item[];
11
- }
12
- const { title, items = [] } = Astro.props;
13
- const uid = 'tabs-' + Math.random().toString(36).slice(2, 9);
14
- ---
15
-
16
- <section class="b-tabs">
17
- {title && <Heading text={title} level={2} align="center" />}
18
- <div class="b-tabs__inner">
19
- <div class="b-tabs__list" role="tablist" aria-label={title ?? 'Tabs'}>
20
- {items.map((item, i) => (
21
- <button
22
- type="button"
23
- role="tab"
24
- id={`${uid}-tab-${i}`}
25
- aria-controls={`${uid}-panel-${i}`}
26
- aria-selected={i === 0 ? 'true' : 'false'}
27
- tabindex={i === 0 ? 0 : -1}
28
- class="b-tabs__tab"
29
- >
30
- {item.label}
31
- </button>
32
- ))}
33
- </div>
34
- {items.map((item, i) => (
35
- <div
36
- role="tabpanel"
37
- id={`${uid}-panel-${i}`}
38
- aria-labelledby={`${uid}-tab-${i}`}
39
- class="b-tabs__panel"
40
- hidden={i !== 0}
41
- tabindex="0"
42
- >
43
- {item.image && (
44
- <img class="b-tabs__img" src={item.image.src} alt={item.image.alt ?? ''} loading="lazy" decoding="async" />
45
- )}
46
- <div class="b-tabs__copy">
47
- {item.heading && <h3 class="b-tabs__heading">{item.heading}</h3>}
48
- {item.text && <p class="b-tabs__text">{item.text}</p>}
49
- </div>
50
- </div>
51
- ))}
52
- </div>
53
- </section>
54
-
55
- <style>
56
- .b-tabs {
57
- display: flex;
58
- flex-direction: column;
59
- gap: 1.5rem;
60
- max-width: 60rem;
61
- margin-inline: auto;
62
- padding-inline: 1rem;
63
- }
64
- .b-tabs__list {
65
- display: flex;
66
- flex-wrap: wrap;
67
- gap: 0.25rem;
68
- border-bottom: 1px solid var(--border);
69
- }
70
- .b-tabs__tab {
71
- appearance: none;
72
- background: none;
73
- border: none;
74
- border-bottom: 2px solid transparent;
75
- margin-bottom: -1px;
76
- padding: 0.7rem 1rem;
77
- font-family: var(--font-heading);
78
- font-size: 0.95rem;
79
- font-weight: 600;
80
- color: var(--muted);
81
- cursor: pointer;
82
- transition: color 0.15s, border-color 0.15s;
83
- }
84
- .b-tabs__tab:hover { color: var(--fg); }
85
- .b-tabs__tab[aria-selected='true'] { color: var(--gold); border-bottom-color: var(--gold); }
86
- .b-tabs__tab:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 4px; }
87
- .b-tabs__panel {
88
- display: grid;
89
- gap: 1.25rem;
90
- }
91
- /* A class `display` beats the UA `[hidden]{display:none}` (equal specificity,
92
- author origin wins), so make the hidden state explicit — for the first paint
93
- and every JS toggle. */
94
- .b-tabs__panel[hidden] {
95
- display: none;
96
- }
97
- @media (min-width: 48em) {
98
- .b-tabs__panel:has(.b-tabs__img) { grid-template-columns: 1fr 1fr; align-items: center; }
99
- }
100
- .b-tabs__panel:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; border-radius: var(--radius-sm); }
101
- .b-tabs__img { width: 100%; height: auto; border-radius: var(--radius); }
102
- .b-tabs__heading { margin: 0 0 0.5rem; color: var(--fg); }
103
- .b-tabs__text { margin: 0; color: var(--muted); line-height: 1.65; }
104
- </style>
105
-
106
- <script>
107
- // Wire every tabs group on the page: selection, ARIA state, arrow-key nav.
108
- document.querySelectorAll<HTMLElement>('.b-tabs').forEach((root) => {
109
- const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
110
- const panels = Array.from(root.querySelectorAll<HTMLElement>('[role="tabpanel"]'));
111
- if (!tabs.length) return;
112
-
113
- function select(index: number, focus = true) {
114
- tabs.forEach((tab, i) => {
115
- const on = i === index;
116
- tab.setAttribute('aria-selected', on ? 'true' : 'false');
117
- tab.tabIndex = on ? 0 : -1;
118
- panels[i]?.toggleAttribute('hidden', !on);
119
- });
120
- if (focus) tabs[index]?.focus();
121
- }
122
-
123
- tabs.forEach((tab, i) => {
124
- tab.addEventListener('click', () => select(i, false));
125
- tab.addEventListener('keydown', (e) => {
126
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); select((i + 1) % tabs.length); }
127
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); select((i - 1 + tabs.length) % tabs.length); }
128
- else if (e.key === 'Home') { e.preventDefault(); select(0); }
129
- else if (e.key === 'End') { e.preventDefault(); select(tabs.length - 1); }
130
- });
131
- });
132
- });
133
- </script>
1
+ ---
2
+ /** Tabs — a Level-2 recipe: labelled panels, one open at a time. Progressive:
3
+ * the first panel is visible with no JavaScript; a small script adds selection,
4
+ * ARIA state and arrow-key navigation. Composes the Heading primitive. */
5
+ import Heading from '../blocks/Heading.astro';
6
+
7
+ import { inlineMarkdown } from '../../lib/inline-markdown';
8
+ interface Item { label: string; heading?: string; text?: string; image?: { src: string; alt?: string } }
9
+ interface Props {
10
+ title?: string;
11
+ items?: Item[];
12
+ }
13
+ const { title, items = [] } = Astro.props;
14
+ const uid = 'tabs-' + Math.random().toString(36).slice(2, 9);
15
+ ---
16
+
17
+ <section class="b-tabs">
18
+ {title && <Heading text={title} level={2} align="center" />}
19
+ <div class="b-tabs__inner">
20
+ <div class="b-tabs__list" role="tablist" aria-label={title ?? 'Tabs'}>
21
+ {items.map((item, i) => (
22
+ <button
23
+ type="button"
24
+ role="tab"
25
+ id={`${uid}-tab-${i}`}
26
+ aria-controls={`${uid}-panel-${i}`}
27
+ aria-selected={i === 0 ? 'true' : 'false'}
28
+ tabindex={i === 0 ? 0 : -1}
29
+ class="b-tabs__tab"
30
+ >
31
+ {item.label}
32
+ </button>
33
+ ))}
34
+ </div>
35
+ {items.map((item, i) => (
36
+ <div
37
+ role="tabpanel"
38
+ id={`${uid}-panel-${i}`}
39
+ aria-labelledby={`${uid}-tab-${i}`}
40
+ class="b-tabs__panel"
41
+ hidden={i !== 0}
42
+ tabindex="0"
43
+ >
44
+ {item.image && (
45
+ <img class="b-tabs__img" src={item.image.src} alt={item.image.alt ?? ''} loading="lazy" decoding="async" />
46
+ )}
47
+ <div class="b-tabs__copy">
48
+ {item.heading && <h3 class="b-tabs__heading">{item.heading}</h3>}
49
+ {item.text && <p class="b-tabs__text" set:html={inlineMarkdown(item.text)} />}
50
+ </div>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </section>
55
+
56
+ <style>
57
+ .b-tabs {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 1.5rem;
61
+ max-width: 60rem;
62
+ margin-inline: auto;
63
+ padding-inline: 1rem;
64
+ }
65
+ .b-tabs__list {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: 0.25rem;
69
+ border-bottom: 1px solid var(--border);
70
+ }
71
+ .b-tabs__tab {
72
+ appearance: none;
73
+ background: none;
74
+ border: none;
75
+ border-bottom: 2px solid transparent;
76
+ margin-bottom: -1px;
77
+ padding: 0.7rem 1rem;
78
+ font-family: var(--font-heading);
79
+ font-size: 0.95rem;
80
+ font-weight: 600;
81
+ color: var(--muted);
82
+ cursor: pointer;
83
+ transition: color 0.15s, border-color 0.15s;
84
+ }
85
+ .b-tabs__tab:hover { color: var(--fg); }
86
+ .b-tabs__tab[aria-selected='true'] { color: var(--gold); border-bottom-color: var(--gold); }
87
+ .b-tabs__tab:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 4px; }
88
+ .b-tabs__panel {
89
+ display: grid;
90
+ gap: 1.25rem;
91
+ }
92
+ /* A class `display` beats the UA `[hidden]{display:none}` (equal specificity,
93
+ author origin wins), so make the hidden state explicit — for the first paint
94
+ and every JS toggle. */
95
+ .b-tabs__panel[hidden] {
96
+ display: none;
97
+ }
98
+ @media (min-width: 48em) {
99
+ .b-tabs__panel:has(.b-tabs__img) { grid-template-columns: 1fr 1fr; align-items: center; }
100
+ }
101
+ .b-tabs__panel:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; border-radius: var(--radius-sm); }
102
+ .b-tabs__img { width: 100%; height: auto; border-radius: var(--radius); }
103
+ .b-tabs__heading { margin: 0 0 0.5rem; color: var(--fg); }
104
+ .b-tabs__text { margin: 0; color: var(--muted); line-height: 1.65; }
105
+ </style>
106
+
107
+ <script>
108
+ // Wire every tabs group on the page: selection, ARIA state, arrow-key nav.
109
+ document.querySelectorAll<HTMLElement>('.b-tabs').forEach((root) => {
110
+ const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
111
+ const panels = Array.from(root.querySelectorAll<HTMLElement>('[role="tabpanel"]'));
112
+ if (!tabs.length) return;
113
+
114
+ function select(index: number, focus = true) {
115
+ tabs.forEach((tab, i) => {
116
+ const on = i === index;
117
+ tab.setAttribute('aria-selected', on ? 'true' : 'false');
118
+ tab.tabIndex = on ? 0 : -1;
119
+ panels[i]?.toggleAttribute('hidden', !on);
120
+ });
121
+ if (focus) tabs[index]?.focus();
122
+ }
123
+
124
+ tabs.forEach((tab, i) => {
125
+ tab.addEventListener('click', () => select(i, false));
126
+ tab.addEventListener('keydown', (e) => {
127
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); select((i + 1) % tabs.length); }
128
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); select((i - 1 + tabs.length) % tabs.length); }
129
+ else if (e.key === 'Home') { e.preventDefault(); select(0); }
130
+ else if (e.key === 'End') { e.preventDefault(); select(tabs.length - 1); }
131
+ });
132
+ });
133
+ });
134
+ </script>