@contentrain/astro-kit 0.31.0 → 0.33.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/catalog.json +4600 -1584
- package/components/_shared/Button.astro +2 -1
- package/components/_shared/KitImage.astro +9 -4
- package/components/_shared/Section.astro +3 -1
- package/components/_shared/SectionHeader.astro +27 -0
- package/components/_shared/measured.ts +23 -0
- package/components/_shared/safe-url.ts +67 -0
- package/components/_shared/social.ts +73 -0
- package/components/announcement/Announcement.astro +2 -1
- package/components/announcement/meta.json +1 -1
- package/components/band/Band.astro +3 -2
- package/components/band/meta.json +1 -0
- package/components/breadcrumb/Breadcrumb.astro +2 -1
- package/components/breadcrumb/meta.json +1 -1
- package/components/card-grid/CardGrid.astro +2 -1
- package/components/card-grid/meta.json +1 -0
- package/components/consent/Consent.astro +2 -1
- package/components/consent/meta.json +1 -1
- package/components/contact-details/ContactDetails.astro +3 -2
- package/components/contact-details/meta.json +1 -0
- package/components/contact-form/ContactForm.astro +51 -14
- package/components/contact-form/fixtures.json +169 -9
- package/components/contact-form/meta.json +284 -15
- package/components/cta/Cta.astro +52 -12
- package/components/cta/fixtures.json +5 -1
- package/components/cta/meta.json +11 -2
- package/components/dialog/Dialog.astro +113 -0
- package/components/dialog/fixtures.json +84 -0
- package/components/dialog/meta.json +252 -0
- package/components/embed/meta.json +1 -1
- package/components/faq/Faq.astro +60 -17
- package/components/faq/fixtures.json +79 -0
- package/components/faq/meta.json +9 -3
- package/components/feature-list/FeatureList.astro +28 -4
- package/components/feature-list/meta.json +16 -3
- package/components/footer/Footer.astro +5 -4
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +49 -14
- package/components/gallery/fixtures.json +275 -0
- package/components/gallery/meta.json +267 -14
- package/components/header/Header.astro +17 -5
- package/components/header/fixtures.json +90 -1
- package/components/header/meta.json +5 -3
- package/components/hero/meta.json +1 -0
- package/components/language-switcher/LanguageSwitcher.astro +68 -0
- package/components/language-switcher/fixtures.json +51 -0
- package/components/language-switcher/meta.json +73 -0
- package/components/logo-cloud/LogoCloud.astro +86 -21
- package/components/logo-cloud/fixtures.json +239 -0
- package/components/logo-cloud/meta.json +145 -5
- package/components/nav/Nav.astro +77 -24
- package/components/nav/NavTree.astro +40 -0
- package/components/nav/fixtures.json +165 -0
- package/components/nav/meta.json +11 -2
- package/components/newsletter/Newsletter.astro +2 -1
- package/components/newsletter/meta.json +1 -0
- package/components/pagination/Pagination.astro +4 -3
- package/components/pagination/meta.json +1 -1
- package/components/post-card/PostCard.astro +8 -5
- package/components/post-card/meta.json +3 -2
- package/components/post-list/PostList.astro +115 -0
- package/components/post-list/fixtures.json +387 -0
- package/components/post-list/meta.json +309 -0
- package/components/pricing/Pricing.astro +128 -24
- package/components/pricing/fixtures.json +288 -4
- package/components/pricing/meta.json +188 -1
- package/components/search-box/SearchBox.astro +51 -0
- package/components/search-box/fixtures.json +31 -0
- package/components/search-box/meta.json +75 -0
- package/components/slider/Slider.astro +19 -115
- package/components/slider/SliderTrack.astro +178 -0
- package/components/slider/fixtures.json +2 -1
- package/components/slider/meta.json +11 -5
- package/components/social-links/SocialLinks.astro +74 -0
- package/components/social-links/fixtures.json +67 -0
- package/components/social-links/meta.json +94 -0
- package/components/split/Split.astro +76 -14
- package/components/split/fixtures.json +8 -1
- package/components/split/meta.json +226 -4
- package/components/stats/Stats.astro +54 -11
- package/components/stats/fixtures.json +260 -0
- package/components/stats/meta.json +151 -4
- package/components/steps/Steps.astro +36 -8
- package/components/steps/fixtures.json +217 -5
- package/components/steps/meta.json +157 -3
- package/components/tabs/Tabs.astro +10 -3
- package/components/tabs/fixtures.json +4 -0
- package/components/tabs/meta.json +5 -2
- package/components/team/Team.astro +54 -9
- package/components/team/fixtures.json +217 -4
- package/components/team/meta.json +169 -3
- package/components/team/team.ts +4 -0
- package/components/testimonial/Testimonial.astro +71 -21
- package/components/testimonial/fixtures.json +73 -0
- package/components/testimonial/meta.json +168 -6
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +124 -3
- package/mapping/divi.json +26 -8
- package/mapping/elementor.json +133 -14
- package/mapping/gutenberg.json +38 -10
- package/package.json +2 -2
|
@@ -77,5 +77,170 @@
|
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
79
|
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "ps-flyout",
|
|
83
|
+
"props": {
|
|
84
|
+
"id": "fx-nav-ps",
|
|
85
|
+
"items": [
|
|
86
|
+
{
|
|
87
|
+
"label": "Hakkımızda",
|
|
88
|
+
"href": "/hakkimizda/"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"label": "Ürünlerimiz",
|
|
92
|
+
"href": "#",
|
|
93
|
+
"children": [
|
|
94
|
+
{
|
|
95
|
+
"label": "Aracım İçin",
|
|
96
|
+
"href": "#",
|
|
97
|
+
"children": [
|
|
98
|
+
{
|
|
99
|
+
"label": "Trafik Sigortası",
|
|
100
|
+
"href": "/urunlerimiz/trafik-sigortasi/"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"label": "Kasko",
|
|
104
|
+
"href": "/urunlerimiz/kasko/"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"label": "Evim İçin",
|
|
110
|
+
"href": "#",
|
|
111
|
+
"children": [
|
|
112
|
+
{
|
|
113
|
+
"label": "Dask",
|
|
114
|
+
"href": "/urunlerimiz/dask/"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"label": "Konut Sigortası",
|
|
118
|
+
"href": "/urunlerimiz/konut-sigortasi/"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"label": "Sağlığım İçin",
|
|
124
|
+
"href": "#",
|
|
125
|
+
"children": [
|
|
126
|
+
{
|
|
127
|
+
"label": "Sağlık Sigortası",
|
|
128
|
+
"href": "/urunlerimiz/saglik-sigortasi/"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"label": "Tamamlayıcı Sağlık",
|
|
132
|
+
"href": "/urunlerimiz/tamamlayici-saglik/"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"label": "Diğer",
|
|
138
|
+
"href": "#",
|
|
139
|
+
"children": [
|
|
140
|
+
{
|
|
141
|
+
"label": "İş Yeri Sigortası",
|
|
142
|
+
"href": "/urunlerimiz/is-yeri-sigortasi/"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"label": "Seyahat Sigortası",
|
|
146
|
+
"href": "/urunlerimiz/seyahat-sigortasi/"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"label": "Anlaşmalı Sigorta Şirketleri",
|
|
154
|
+
"href": "/anlasmali-sigorta-sirketleri/"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"label": "İletişim",
|
|
158
|
+
"href": "/iletisim/"
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "ps-mega",
|
|
165
|
+
"props": {
|
|
166
|
+
"id": "fx-nav-ps-mega",
|
|
167
|
+
"menu": "mega",
|
|
168
|
+
"items": [
|
|
169
|
+
{
|
|
170
|
+
"label": "Hakkımızda",
|
|
171
|
+
"href": "/hakkimizda/"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"label": "Ürünlerimiz",
|
|
175
|
+
"href": "#",
|
|
176
|
+
"children": [
|
|
177
|
+
{
|
|
178
|
+
"label": "Aracım İçin",
|
|
179
|
+
"href": "#",
|
|
180
|
+
"children": [
|
|
181
|
+
{
|
|
182
|
+
"label": "Trafik Sigortası",
|
|
183
|
+
"href": "/urunlerimiz/trafik-sigortasi/"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"label": "Kasko",
|
|
187
|
+
"href": "/urunlerimiz/kasko/"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"label": "Evim İçin",
|
|
193
|
+
"href": "#",
|
|
194
|
+
"children": [
|
|
195
|
+
{
|
|
196
|
+
"label": "Dask",
|
|
197
|
+
"href": "/urunlerimiz/dask/"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"label": "Konut Sigortası",
|
|
201
|
+
"href": "/urunlerimiz/konut-sigortasi/"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"label": "Sağlığım İçin",
|
|
207
|
+
"href": "#",
|
|
208
|
+
"children": [
|
|
209
|
+
{
|
|
210
|
+
"label": "Sağlık Sigortası",
|
|
211
|
+
"href": "/urunlerimiz/saglik-sigortasi/"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"label": "Tamamlayıcı Sağlık",
|
|
215
|
+
"href": "/urunlerimiz/tamamlayici-saglik/"
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"label": "Diğer",
|
|
221
|
+
"href": "#",
|
|
222
|
+
"children": [
|
|
223
|
+
{
|
|
224
|
+
"label": "İş Yeri Sigortası",
|
|
225
|
+
"href": "/urunlerimiz/is-yeri-sigortasi/"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"label": "Seyahat Sigortası",
|
|
229
|
+
"href": "/urunlerimiz/seyahat-sigortasi/"
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"label": "Anlaşmalı Sigorta Şirketleri",
|
|
237
|
+
"href": "/anlasmali-sigorta-sirketleri/"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"label": "İletişim",
|
|
241
|
+
"href": "/iletisim/"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
80
245
|
}
|
|
81
246
|
]
|
package/components/nav/meta.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nav",
|
|
3
3
|
"name": "Navigation",
|
|
4
|
-
"description": "Site navigation: an inline list with hover/focus submenus on wide screens and a toggle on narrow ones that opens a native popover drawer or a <details> dropdown.
|
|
4
|
+
"description": "Site navigation: an inline list with hover/focus submenus on wide screens and a toggle on narrow ones that opens a native popover drawer or a <details> dropdown. Submenus nest to any depth; the mega variant draws them as a column panel. Works without JavaScript; a small script (only when a menu has submenus) keeps aria-expanded true and lets Escape close a panel.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": [
|
|
8
|
+
"safe-url.ts",
|
|
8
9
|
"types.ts"
|
|
9
10
|
],
|
|
10
11
|
"variants": {
|
|
@@ -16,6 +17,14 @@
|
|
|
16
17
|
"default": "popover",
|
|
17
18
|
"description": "Narrow-screen menu: a side drawer (Popover API) or a dropdown under the toggle."
|
|
18
19
|
},
|
|
20
|
+
"menu": {
|
|
21
|
+
"options": [
|
|
22
|
+
"flyout",
|
|
23
|
+
"mega"
|
|
24
|
+
],
|
|
25
|
+
"default": "flyout",
|
|
26
|
+
"description": "How a submenu opens on wide screens: a list under the item (flyout) or a wide panel with one column per child and the grandchildren listed under it (mega)."
|
|
27
|
+
},
|
|
19
28
|
"align": {
|
|
20
29
|
"options": [
|
|
21
30
|
"start",
|
|
@@ -107,5 +116,5 @@
|
|
|
107
116
|
"classic/nav-menu"
|
|
108
117
|
]
|
|
109
118
|
},
|
|
110
|
-
"a11y": "A <nav> landmark with a label; the current page carries aria-current.
|
|
119
|
+
"a11y": "A <nav> landmark with a label; the current page carries aria-current. Each parent has a button (aria-controls its panel, labelled by the item) that opens the panel on hover and :focus-within; with the small script aria-expanded follows what is open and Escape closes the panel and returns focus to the button, as in the WAI-ARIA disclosure navigation pattern. Tab leaves a panel and closes it; nothing is hidden from crawlers (the panels are in the HTML). The drawer is a native popover: Escape and light dismiss close it and focus returns to the toggle."
|
|
111
120
|
}
|
|
@@ -12,6 +12,7 @@ import { tv } from 'tailwind-variants'
|
|
|
12
12
|
import Section from '../_shared/Section.astro'
|
|
13
13
|
import type { MeasuredStyle } from '../_shared/types'
|
|
14
14
|
import { newsletterForm } from './newsletter'
|
|
15
|
+
import { safeUrl } from '../_shared/safe-url'
|
|
15
16
|
|
|
16
17
|
const newsletter = tv({
|
|
17
18
|
slots: {
|
|
@@ -91,7 +92,7 @@ const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' }
|
|
|
91
92
|
</div>
|
|
92
93
|
)}
|
|
93
94
|
<div class="flex flex-col gap-2">
|
|
94
|
-
<form method="post" action={form.action} target={newTab ? '_blank' : undefined} {...formRel} class={s.form()} aria-labelledby={headingId} aria-label={headingId ? undefined : submitLabel}>
|
|
95
|
+
<form method="post" action={safeUrl(form.action)} target={newTab ? '_blank' : undefined} {...formRel} class={s.form()} aria-labelledby={headingId} aria-label={headingId ? undefined : submitLabel}>
|
|
95
96
|
{askName && form.name && (
|
|
96
97
|
<div class={s.field()}>
|
|
97
98
|
<label for={`${id}-name`} class={s.label()}>{nameLabel}</label>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// itself for page 1, `<base>page/<n>/` after — so a migrated archive keeps
|
|
4
4
|
// its page URLs.
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
|
+
import { safeHref } from '../_shared/safe-url'
|
|
6
7
|
|
|
7
8
|
const pagination = tv({
|
|
8
9
|
slots: {
|
|
@@ -52,7 +53,7 @@ const shown = [...new Set([1, current - 2, current - 1, current, current + 1, cu
|
|
|
52
53
|
{total > 1 && (
|
|
53
54
|
<nav aria-label={label} data-cr-part="pagination" class={slots.root()}>
|
|
54
55
|
{current > 1
|
|
55
|
-
? <a href={href(current - 1)} rel="prev" class:list={[slots.item(), slots.link()]}><span aria-hidden="true" class="inline-block rtl:-scale-x-100">←</span> <span class="max-sm:sr-only">{prevLabel}</span></a>
|
|
56
|
+
? <a href={safeHref(href(current - 1))} rel="prev" class:list={[slots.item(), slots.link()]}><span aria-hidden="true" class="inline-block rtl:-scale-x-100">←</span> <span class="max-sm:sr-only">{prevLabel}</span></a>
|
|
56
57
|
: style === 'simple' && <span />}
|
|
57
58
|
{style === 'numbered' && shown.map((page, index) => (
|
|
58
59
|
<>
|
|
@@ -60,10 +61,10 @@ const shown = [...new Set([1, current - 2, current - 1, current, current + 1, cu
|
|
|
60
61
|
{/* Two away from the current page: dropped on phones so the row fits one line. */}
|
|
61
62
|
{page === current
|
|
62
63
|
? <span aria-current="page" class:list={[slots.item(), slots.current()]}><span class="sr-only">{pageLabel} </span>{page}</span>
|
|
63
|
-
: <a href={href(page)} class:list={[slots.item(), slots.link(), Math.abs(page - current) === 2 && page !== 1 && page !== total && 'max-md:hidden']}><span class="sr-only">{pageLabel} </span>{page}</a>}
|
|
64
|
+
: <a href={safeHref(href(page))} class:list={[slots.item(), slots.link(), Math.abs(page - current) === 2 && page !== 1 && page !== total && 'max-md:hidden']}><span class="sr-only">{pageLabel} </span>{page}</a>}
|
|
64
65
|
</>
|
|
65
66
|
))}
|
|
66
67
|
{style === 'simple' && <span class="text-sm text-ink-muted">{pageLabel} {current} / {total}</span>}
|
|
67
|
-
{current < total && <a href={href(current + 1)} rel="next" class:list={[slots.item(), slots.link()]}><span class="max-sm:sr-only">{nextLabel}</span> <span aria-hidden="true" class="inline-block rtl:-scale-x-100">→</span></a>}
|
|
68
|
+
{current < total && <a href={safeHref(href(current + 1))} rel="next" class:list={[slots.item(), slots.link()]}><span class="max-sm:sr-only">{nextLabel}</span> <span aria-hidden="true" class="inline-block rtl:-scale-x-100">→</span></a>}
|
|
68
69
|
</nav>
|
|
69
70
|
)}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Links between the pages of a post list, at WordPress addresses (<base>page/<n>/).",
|
|
5
5
|
"category": "content",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": [],
|
|
7
|
+
"shared": ["safe-url.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"style": { "options": ["numbered", "simple"], "default": "numbered", "description": "Numbered pages with ellipses, or previous/next with 'Page n / m'." },
|
|
10
10
|
"align": { "options": ["start", "center", "end"], "default": "center", "description": "Alignment of the numbered style." }
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { tv } from 'tailwind-variants'
|
|
8
8
|
import KitImage from '../_shared/KitImage.astro'
|
|
9
9
|
import type { ImageInput, LinkInput } from '../_shared/types'
|
|
10
|
+
import { safeHref } from '../_shared/safe-url'
|
|
10
11
|
|
|
11
12
|
const card = tv({
|
|
12
13
|
slots: {
|
|
@@ -50,6 +51,8 @@ interface Props {
|
|
|
50
51
|
layout?: 'vertical' | 'horizontal' | 'minimal' | undefined
|
|
51
52
|
surface?: 'plain' | 'card' | undefined
|
|
52
53
|
byline?: 'inline' | 'below' | undefined
|
|
54
|
+
/** Clamp the excerpt to this many lines; absent, the layout's own (3 vertical, 2 otherwise). */
|
|
55
|
+
excerptLines?: 2 | 3 | 4 | undefined
|
|
53
56
|
/** The title in the site's link colour, as themes that colour every link do (Hello Elementor). */
|
|
54
57
|
linkTitle?: boolean | undefined
|
|
55
58
|
/** The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give. */
|
|
@@ -60,7 +63,7 @@ interface Props {
|
|
|
60
63
|
imageAspect?: string | undefined
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, sizes, imageHeight, imageAspect } = Astro.props
|
|
66
|
+
const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, excerptLines, sizes, imageHeight, imageAspect } = Astro.props
|
|
64
67
|
const date = rawDate === undefined ? undefined : new Date(rawDate)
|
|
65
68
|
const slots = card({ layout, surface, byline })
|
|
66
69
|
const below = byline === 'below'
|
|
@@ -77,18 +80,18 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
|
77
80
|
<div class={slots.body()}>
|
|
78
81
|
{below && category && (
|
|
79
82
|
<p class={slots.meta()}>
|
|
80
|
-
<a href={category.href} class="relative z-10 no-underline hover:underline">{category.label}</a>
|
|
83
|
+
<a href={safeHref(category.href)} class="relative z-10 no-underline hover:underline">{category.label}</a>
|
|
81
84
|
</p>
|
|
82
85
|
)}
|
|
83
86
|
{!below && (category || date) && (
|
|
84
87
|
<p class={slots.meta()}>
|
|
85
|
-
{category && <a href={category.href} class="relative z-10 font-medium text-accent-text no-underline hover:underline">{category.label}</a>}
|
|
88
|
+
{category && <a href={safeHref(category.href)} class="relative z-10 font-medium text-accent-text no-underline hover:underline">{category.label}</a>}
|
|
86
89
|
{category && date && <span aria-hidden="true">·</span>}
|
|
87
90
|
{date && <time datetime={date.toISOString()}>{shown}</time>}
|
|
88
91
|
</p>
|
|
89
92
|
)}
|
|
90
93
|
<Heading class={slots.title({ class: linkTitle ? 'text-[color:var(--color-link,var(--color-accent))]' : undefined })}>
|
|
91
|
-
<a href={href} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent-text">{title}</a>
|
|
94
|
+
<a href={safeHref(href)} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent-text">{title}</a>
|
|
92
95
|
</Heading>
|
|
93
96
|
{below && (author || date) && (
|
|
94
97
|
<p class={slots.meta()}>
|
|
@@ -97,7 +100,7 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
|
97
100
|
{date && <time datetime={date.toISOString()}>{shown}</time>}
|
|
98
101
|
</p>
|
|
99
102
|
)}
|
|
100
|
-
{excerpt && <p class={slots.excerpt()}>{excerpt}</p>}
|
|
103
|
+
{excerpt && <p class={slots.excerpt({ class: excerptLines ? { 2: 'line-clamp-2', 3: 'line-clamp-3', 4: 'line-clamp-4' }[excerptLines] : undefined })}>{excerpt}</p>}
|
|
101
104
|
{!below && author && <p class="text-sm text-ink-muted">{author}</p>}
|
|
102
105
|
</div>
|
|
103
106
|
</article>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "One post in a list (index, archive, related posts): image, category, date, title, excerpt, author. The whole card links to the post.",
|
|
5
5
|
"category": "content",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["KitImage.astro", "types.ts"],
|
|
7
|
+
"shared": ["KitImage.astro", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["vertical", "horizontal", "minimal"], "default": "vertical", "description": "Image on top, image beside the text, or text only with a divider." },
|
|
10
10
|
"surface": { "options": ["plain", "card"], "default": "plain", "description": "plain: no surface. card: its own background (--color-card), radius and shadow (--shadow-card), the text padded (--spacing-card) and the image flush at the edge, as theme cards are drawn on a tinted page." },
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"linkTitle": { "type": "boolean", "default": false, "description": "The title in the site's link colour (--color-link), as themes that colour every link do." },
|
|
24
24
|
"sizes": { "type": "string", "description": "The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give." },
|
|
25
25
|
"imageHeight": { "type": "string", "description": "The image block's height as the source sets it (a CSS length or clamp()), cropped to fit; wins over imageAspect." },
|
|
26
|
-
"imageAspect": { "type": "string", "description": "The image block's aspect ratio (16 / 9) where the source sets an aspect ratio; absent, 16 / 9." }
|
|
26
|
+
"imageAspect": { "type": "string", "description": "The image block's aspect ratio (16 / 9) where the source sets an aspect ratio; absent, 16 / 9." },
|
|
27
|
+
"excerptLines": { "type": "integer", "min": 2, "max": 4, "content": false, "description": "Clamp the excerpt to this many lines; absent, the layout's own (3 vertical, 2 otherwise)." }
|
|
27
28
|
},
|
|
28
29
|
"slots": {},
|
|
29
30
|
"js": "none",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A list of posts as cards: a grid, stacked rows, a featured first post over a
|
|
3
|
+
// grid, or a compact title-and-date list. The posts are the site's own — fed
|
|
4
|
+
// by the Contentrain query SDK, never copied from a page — and the `show*`
|
|
5
|
+
// switches decide which parts of each card the design displays.
|
|
6
|
+
import { tv } from 'tailwind-variants'
|
|
7
|
+
import Section from '../_shared/Section.astro'
|
|
8
|
+
import { measuredGrid } from '../_shared/measured'
|
|
9
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
10
|
+
import PostCard from '../post-card/PostCard.astro'
|
|
11
|
+
import type { ImageInput, LinkInput } from '../_shared/types'
|
|
12
|
+
import { safeHref } from '../_shared/safe-url'
|
|
13
|
+
|
|
14
|
+
export interface PostItem {
|
|
15
|
+
title: string
|
|
16
|
+
href: string
|
|
17
|
+
excerpt?: string | undefined
|
|
18
|
+
date?: Date | string | undefined
|
|
19
|
+
image?: ImageInput | undefined
|
|
20
|
+
category?: LinkInput | undefined
|
|
21
|
+
author?: string | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const list = tv({
|
|
25
|
+
slots: {
|
|
26
|
+
header: 'mb-10 flex max-w-2xl flex-col gap-3',
|
|
27
|
+
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
28
|
+
intro: 'text-lg opacity-80',
|
|
29
|
+
items: 'grid gap-8',
|
|
30
|
+
more: 'mt-10',
|
|
31
|
+
},
|
|
32
|
+
variants: {
|
|
33
|
+
layout: {
|
|
34
|
+
grid: { items: 'sm:grid-cols-2' },
|
|
35
|
+
list: { items: 'gap-6' },
|
|
36
|
+
// The first post spans the whole row, image beside text; the others fill the grid under it.
|
|
37
|
+
featured: { items: 'sm:grid-cols-2 [&>li:first-child]:sm:col-span-2' },
|
|
38
|
+
compact: { items: 'gap-4' },
|
|
39
|
+
},
|
|
40
|
+
columns: { '2': {}, '3': {}, '4': {} },
|
|
41
|
+
},
|
|
42
|
+
compoundVariants: [
|
|
43
|
+
{ layout: ['grid', 'featured'], columns: '3', class: { items: 'lg:grid-cols-3 [&>li:first-child]:lg:col-span-3' } },
|
|
44
|
+
{ layout: ['grid', 'featured'], columns: '4', class: { items: 'md:grid-cols-2 lg:grid-cols-4 [&>li:first-child]:lg:col-span-4' } },
|
|
45
|
+
],
|
|
46
|
+
defaultVariants: { layout: 'grid', columns: '3' },
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
heading?: string | undefined
|
|
51
|
+
intro?: string | undefined
|
|
52
|
+
posts: PostItem[]
|
|
53
|
+
layout?: 'grid' | 'list' | 'featured' | 'compact' | undefined
|
|
54
|
+
/** Cards per row on wide screens (grid and featured). */
|
|
55
|
+
columns?: '2' | '3' | '4' | undefined
|
|
56
|
+
/** Which parts of a card the design shows. */
|
|
57
|
+
showImage?: boolean | undefined
|
|
58
|
+
showExcerpt?: boolean | undefined
|
|
59
|
+
showAuthor?: boolean | undefined
|
|
60
|
+
showCategory?: boolean | undefined
|
|
61
|
+
excerptLines?: 2 | 3 | 4 | undefined
|
|
62
|
+
/** BCP 47 locale the dates are written in. */
|
|
63
|
+
locale?: string | undefined
|
|
64
|
+
/** Card surface: none, or each post its own card. */
|
|
65
|
+
surface?: 'plain' | 'card' | undefined
|
|
66
|
+
/** Where the full archive lives; with `moreLabel` a link follows the list. */
|
|
67
|
+
moreHref?: string | undefined
|
|
68
|
+
moreLabel?: string | undefined
|
|
69
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
70
|
+
/** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
|
|
71
|
+
measured?: MeasuredStyle | undefined
|
|
72
|
+
id?: string | undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { heading, intro, posts, layout = 'grid', columns, showImage = true, showExcerpt = true, showAuthor = true, showCategory = true, excerptLines, locale, surface, moreHref, moreLabel, tone, measured, id = 'posts' } = Astro.props
|
|
76
|
+
const s = list({ layout, columns })
|
|
77
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
78
|
+
// Cards sit one heading level under the section's; with no section heading they are the page's h2s.
|
|
79
|
+
const headingLevel = heading ? 3 : 2
|
|
80
|
+
const sizes = layout === 'list' || layout === 'compact' ? '256px' : columns === '4' ? '(min-width: 1024px) 25vw, 50vw' : columns === '2' ? '(min-width: 640px) 50vw, 100vw' : '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw'
|
|
81
|
+
// A featured first post is read as a horizontal card; the rest take the grid's vertical one.
|
|
82
|
+
const cardLayout = (i: number) => layout === 'compact' ? 'minimal' : layout === 'list' || (layout === 'featured' && i === 0) ? 'horizontal' : 'vertical'
|
|
83
|
+
---
|
|
84
|
+
{posts.length > 0 && (
|
|
85
|
+
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
86
|
+
{(heading || intro) && (
|
|
87
|
+
<header class={s.header()}>
|
|
88
|
+
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
89
|
+
{intro && <p class={s.intro()} data-kit-text>{intro}</p>}
|
|
90
|
+
</header>
|
|
91
|
+
)}
|
|
92
|
+
<ul class={s.items({ class: layout === 'grid' || layout === 'featured' ? measuredGrid(measured) : undefined })}>
|
|
93
|
+
{posts.map((post, i) => (
|
|
94
|
+
<li>
|
|
95
|
+
<PostCard
|
|
96
|
+
title={post.title}
|
|
97
|
+
href={safeHref(post.href)}
|
|
98
|
+
excerpt={showExcerpt && layout !== 'compact' ? post.excerpt : undefined}
|
|
99
|
+
date={post.date}
|
|
100
|
+
locale={locale}
|
|
101
|
+
image={showImage && layout !== 'compact' ? post.image : undefined}
|
|
102
|
+
category={showCategory && layout !== 'compact' ? post.category : undefined}
|
|
103
|
+
author={showAuthor && layout !== 'compact' ? post.author : undefined}
|
|
104
|
+
headingLevel={headingLevel}
|
|
105
|
+
layout={cardLayout(i)}
|
|
106
|
+
surface={surface}
|
|
107
|
+
excerptLines={excerptLines}
|
|
108
|
+
sizes={sizes}
|
|
109
|
+
/>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
{moreHref && moreLabel && <p class={s.more()}><a href={safeHref(moreHref)} class="font-medium text-accent-text underline underline-offset-4">{moreLabel}</a></p>}
|
|
114
|
+
</Section>
|
|
115
|
+
)}
|