@contentrain/astro-kit 0.32.0 → 0.33.1
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 +850 -19
- 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 +13 -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 +4 -3
- package/components/contact-form/meta.json +1 -0
- package/components/cta/meta.json +1 -1
- 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/fixtures.json +4 -0
- package/components/faq/meta.json +2 -2
- package/components/feature-list/meta.json +1 -0
- package/components/footer/Footer.astro +5 -4
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +2 -1
- package/components/gallery/meta.json +1 -0
- 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 +2 -1
- package/components/logo-cloud/meta.json +1 -0
- 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 +4 -3
- package/components/post-card/meta.json +1 -1
- package/components/post-list/PostList.astro +3 -2
- package/components/post-list/meta.json +1 -0
- package/components/pricing/meta.json +1 -0
- 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/meta.json +1 -0
- package/components/stats/Stats.astro +19 -8
- package/components/stats/fixtures.json +87 -0
- package/components/stats/meta.json +1 -0
- package/components/steps/meta.json +1 -0
- 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 +3 -2
- package/components/team/meta.json +2 -0
- package/components/team/team.ts +3 -8
- package/components/testimonial/meta.json +2 -3
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +117 -2
- package/mapping/divi.json +6 -5
- package/mapping/elementor.json +120 -7
- package/mapping/gutenberg.json +20 -8
- package/package.json +2 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A popup, as a dialog the visitor opens: a button in a section of the page and a modal <dialog> with a heading, text
|
|
3
|
+
// and buttons. Replaces Elementor popups and Popup Maker overlays. Never opens by itself (no timers, no exit intent):
|
|
4
|
+
// content that a source popup showed on load or on scroll is reachable from the button instead, and sits in the HTML
|
|
5
|
+
// for crawlers. The section is the kit's (Section: tone, spacing, measured style), like every other section.
|
|
6
|
+
//
|
|
7
|
+
// Modal behaviour is the browser's (the APG dialog pattern as `showModal()` implements it): focus moves into the
|
|
8
|
+
// dialog and is kept there, the page behind is inert, Escape closes it and focus returns to the button. The button
|
|
9
|
+
// opens it with no script where Invoker Commands exist (`command`/`commandfor`); a small script covers browsers
|
|
10
|
+
// without them and closes it on a click outside. With scripting off altogether, the dialog's content is laid out in
|
|
11
|
+
// the section itself, under where the button was: nothing is hidden and nothing blocks the page.
|
|
12
|
+
import { tv } from 'tailwind-variants'
|
|
13
|
+
import Button from '../_shared/Button.astro'
|
|
14
|
+
import Section from '../_shared/Section.astro'
|
|
15
|
+
import type { ActionInput, MeasuredStyle } from '../_shared/types'
|
|
16
|
+
import Prose from '../prose/Prose.astro'
|
|
17
|
+
|
|
18
|
+
const dialog = tv({
|
|
19
|
+
slots: {
|
|
20
|
+
root: 'flex flex-wrap gap-6',
|
|
21
|
+
trigger: 'inline-flex cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-control,var(--radius-md))] px-5 py-2.5 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2',
|
|
22
|
+
// A dialog is not positioned by the page: UA styles centre it; width and surface come from here. Open but not
|
|
23
|
+
// modal (the `open` preview prop) it is a block in the flow under the button, not a box over it. The inner box
|
|
24
|
+
// scrolls, not the dialog: a click on the scrollbar then never lands on the dialog itself (the backdrop test). The
|
|
25
|
+
// display is set only when open (`open:flex`): an author `display` would override the UA's `display: none` when closed.
|
|
26
|
+
box: 'm-auto w-[min(92vw,var(--dialog-width))] max-h-[85dvh] flex-col open:flex rounded-card border border-line bg-surface p-0 text-ink shadow-xl backdrop:bg-ink/50 [&[open]:not(:modal)]:relative [&[open]:not(:modal)]:m-0 [&[open]:not(:modal)]:basis-full',
|
|
27
|
+
inner: 'flex min-h-0 flex-col gap-4 overflow-y-auto p-6 md:p-8',
|
|
28
|
+
heading: 'pe-10 text-xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-2xl',
|
|
29
|
+
actions: 'flex flex-wrap gap-3',
|
|
30
|
+
close: 'absolute end-3 top-3 inline-flex size-9 cursor-pointer items-center justify-center rounded-md hover:bg-surface-muted focus-visible:outline-2',
|
|
31
|
+
},
|
|
32
|
+
variants: {
|
|
33
|
+
triggerStyle: {
|
|
34
|
+
primary: { trigger: 'bg-accent text-accent-ink hover:bg-ink hover:text-surface' },
|
|
35
|
+
secondary: { trigger: 'bg-ink text-surface hover:bg-accent hover:text-accent-ink' },
|
|
36
|
+
ghost: { trigger: 'border border-current bg-transparent text-current hover:bg-ink/5' },
|
|
37
|
+
link: { trigger: 'bg-transparent p-0 text-[color:var(--color-link,var(--color-accent))] underline underline-offset-4' },
|
|
38
|
+
},
|
|
39
|
+
size: { sm: { box: '[--dialog-width:28rem]' }, md: { box: '[--dialog-width:40rem]' }, lg: { box: '[--dialog-width:56rem]' } },
|
|
40
|
+
// Where the button sits in the section.
|
|
41
|
+
align: { start: { root: 'justify-start' }, center: { root: 'justify-center' }, end: { root: 'justify-end' } },
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: { triggerStyle: 'primary', size: 'md', align: 'start' },
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
interface Props {
|
|
47
|
+
/** Unique on the page; the button points at the dialog by it. */
|
|
48
|
+
id?: string | undefined
|
|
49
|
+
/** Text of the button that opens the dialog. */
|
|
50
|
+
triggerLabel: string
|
|
51
|
+
heading?: string | undefined
|
|
52
|
+
/** The dialog's text (rendered HTML). */
|
|
53
|
+
html?: string | undefined
|
|
54
|
+
actions?: ActionInput[] | undefined
|
|
55
|
+
/** Accessible name of the close button (ui-strings dialog.close). */
|
|
56
|
+
closeLabel?: string | undefined
|
|
57
|
+
triggerStyle?: 'primary' | 'secondary' | 'ghost' | 'link' | undefined
|
|
58
|
+
size?: 'sm' | 'md' | 'lg' | undefined
|
|
59
|
+
align?: 'start' | 'center' | 'end' | undefined
|
|
60
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
61
|
+
/** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image. Absent, the site's tokens; present, it wins over tone. */
|
|
62
|
+
measured?: MeasuredStyle | undefined
|
|
63
|
+
/** Rendered already open, in the flow of the section and not modal: for previews and visual baselines only, never for a page. */
|
|
64
|
+
open?: boolean | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { id: givenId = 'dialog', triggerLabel, heading, html, actions = [], closeLabel = 'Close', triggerStyle, size, align: given, tone, measured, open = false } = Astro.props
|
|
68
|
+
// The id goes into attributes and into the scripting-off style sheet, so it is a plain token or the default.
|
|
69
|
+
const id = /^[A-Za-z][\w-]*$/.test(givenId) ? givenId : 'dialog'
|
|
70
|
+
const align = given ?? measured?.align
|
|
71
|
+
// On the accent band the primary button (accent) would vanish: drawn as the secondary style there, as in Cta. The
|
|
72
|
+
// dialog's own actions sit on the dialog's surface, not the band, so they keep their styles.
|
|
73
|
+
const onAccent = tone === 'accent' && !measured?.bg?.image
|
|
74
|
+
const s = dialog({ triggerStyle: onAccent && (triggerStyle ?? 'primary') === 'primary' ? 'secondary' : triggerStyle, size, align })
|
|
75
|
+
const headingId = `${id}-title`
|
|
76
|
+
// Invoker Commands attributes are not in Astro's button types.
|
|
77
|
+
const invoker = { command: 'show-modal', commandfor: id }
|
|
78
|
+
// Scripting off: the content takes the button's place in the section, as a plain block, and the close button goes
|
|
79
|
+
// (it can only close an open dialog). Styles scoped by the dialog's id (a checked token, nothing from content); the selectors win over the UA sheet by specificity.
|
|
80
|
+
const noscript = `<style>[data-kit-dialog="${id}"]>[data-kit-dialog-open],#${id} form[method=dialog]{display:none}#${id}:not([open]){display:block;position:static;margin:0;width:100%;max-width:none;max-height:none;box-shadow:none}</style>`
|
|
81
|
+
---
|
|
82
|
+
<Section measured={measured} tone={tone} spacing="md" id={`${id}-section`}>
|
|
83
|
+
<div class={s.root()} data-kit-dialog={id}>
|
|
84
|
+
<noscript set:html={noscript} />
|
|
85
|
+
<button type="button" class={s.trigger()} data-kit-dialog-open aria-haspopup="dialog" {...invoker}>{triggerLabel}</button>
|
|
86
|
+
<dialog id={id} class={s.box()} open={open || undefined} aria-labelledby={heading ? headingId : undefined} aria-label={heading ? undefined : triggerLabel}>
|
|
87
|
+
<div class={s.inner()}>
|
|
88
|
+
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
89
|
+
{html && <Prose html={html} searchable={false} />}
|
|
90
|
+
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} />)}</div>}
|
|
91
|
+
</div>
|
|
92
|
+
<form method="dialog">
|
|
93
|
+
<button type="submit" class={s.close()} aria-label={closeLabel}>
|
|
94
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
95
|
+
</button>
|
|
96
|
+
</form>
|
|
97
|
+
</dialog>
|
|
98
|
+
</div>
|
|
99
|
+
</Section>
|
|
100
|
+
|
|
101
|
+
<script>
|
|
102
|
+
// Browsers without Invoker Commands: the button opens the dialog here. Everywhere, a click on the backdrop closes it.
|
|
103
|
+
const invokers = 'commandForElement' in HTMLButtonElement.prototype
|
|
104
|
+
for (const root of document.querySelectorAll<HTMLElement>('[data-kit-dialog]')) {
|
|
105
|
+
const dialog = root.querySelector('dialog')
|
|
106
|
+
const open = root.querySelector<HTMLButtonElement>('[data-kit-dialog-open]')
|
|
107
|
+
if (!dialog || !open || root.dataset.ready !== undefined) continue
|
|
108
|
+
root.dataset.ready = ''
|
|
109
|
+
if (!invokers) open.addEventListener('click', () => { if (!dialog.open) dialog.showModal() })
|
|
110
|
+
// The inner box fills the dialog and owns the scrollbar, so a click whose target is the dialog itself landed on the backdrop.
|
|
111
|
+
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close() })
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "default",
|
|
4
|
+
"props": {
|
|
5
|
+
"id": "fx-dialog-a",
|
|
6
|
+
"triggerLabel": "Open the opening hours",
|
|
7
|
+
"heading": "Opening hours",
|
|
8
|
+
"html": "<p>We are open Monday to Friday, <strong>9:00 to 17:00</strong>.</p><p>On public holidays we are closed.</p>",
|
|
9
|
+
"actions": [
|
|
10
|
+
{
|
|
11
|
+
"label": "Contact us",
|
|
12
|
+
"href": "/contact/"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"closeLabel": "Close"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "link-large",
|
|
20
|
+
"props": {
|
|
21
|
+
"id": "fx-dialog-b",
|
|
22
|
+
"triggerLabel": "Read the terms",
|
|
23
|
+
"triggerStyle": "link",
|
|
24
|
+
"size": "lg",
|
|
25
|
+
"heading": "Terms of the offer",
|
|
26
|
+
"html": "<p>The offer runs while stocks last.</p><ul><li>One per customer</li><li>Not combinable</li></ul>",
|
|
27
|
+
"align": "end"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "small-no-heading",
|
|
32
|
+
"props": {
|
|
33
|
+
"id": "fx-dialog-c",
|
|
34
|
+
"triggerLabel": "Subscribe",
|
|
35
|
+
"triggerStyle": "ghost",
|
|
36
|
+
"size": "sm",
|
|
37
|
+
"html": "<p>Thank you for your interest.</p>",
|
|
38
|
+
"tone": "inverse"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "secondary",
|
|
43
|
+
"props": {
|
|
44
|
+
"id": "fx-dialog-d",
|
|
45
|
+
"triggerLabel": "Subscribe to updates",
|
|
46
|
+
"triggerStyle": "secondary",
|
|
47
|
+
"heading": "Stay in the loop",
|
|
48
|
+
"html": "<p>One email a month, never more.</p>",
|
|
49
|
+
"actions": [
|
|
50
|
+
{
|
|
51
|
+
"label": "Subscribe",
|
|
52
|
+
"href": "/newsletter/",
|
|
53
|
+
"style": "secondary"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"align": "center",
|
|
57
|
+
"tone": "muted"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "open-preview",
|
|
62
|
+
"frame": "tall",
|
|
63
|
+
"props": {
|
|
64
|
+
"id": "fx-dialog-e",
|
|
65
|
+
"triggerLabel": "Open the opening hours",
|
|
66
|
+
"heading": "Opening hours",
|
|
67
|
+
"html": "<p>We are open Monday to Friday, <strong>9:00 to 17:00</strong>.</p><p>On public holidays we are closed.</p>",
|
|
68
|
+
"actions": [
|
|
69
|
+
{
|
|
70
|
+
"label": "Contact us",
|
|
71
|
+
"href": "/contact/"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"label": "Call",
|
|
75
|
+
"href": "tel:+10000000000",
|
|
76
|
+
"style": "ghost"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"closeLabel": "Close",
|
|
80
|
+
"open": true,
|
|
81
|
+
"tone": "accent"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
]
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "dialog",
|
|
3
|
+
"name": "Dialog",
|
|
4
|
+
"description": "A popup as a dialog the visitor opens: a button in a section of the page and a modal <dialog> with a heading, text and buttons. Never opens by itself. Focus is trapped inside and returns to the button, Escape closes it; the button opens it with no script where Invoker Commands exist, a small script covers other browsers and closes on a backdrop click, and with scripting off the content is laid out in the section itself.",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [
|
|
7
|
+
"prose"
|
|
8
|
+
],
|
|
9
|
+
"shared": [
|
|
10
|
+
"Button.astro",
|
|
11
|
+
"Section.astro",
|
|
12
|
+
"measured.ts",
|
|
13
|
+
"safe-url.ts",
|
|
14
|
+
"types.ts"
|
|
15
|
+
],
|
|
16
|
+
"variants": {
|
|
17
|
+
"triggerStyle": {
|
|
18
|
+
"options": [
|
|
19
|
+
"primary",
|
|
20
|
+
"secondary",
|
|
21
|
+
"ghost",
|
|
22
|
+
"link"
|
|
23
|
+
],
|
|
24
|
+
"default": "primary",
|
|
25
|
+
"description": "How the opening button looks: a filled or outlined button, or a text link."
|
|
26
|
+
},
|
|
27
|
+
"size": {
|
|
28
|
+
"options": [
|
|
29
|
+
"sm",
|
|
30
|
+
"md",
|
|
31
|
+
"lg"
|
|
32
|
+
],
|
|
33
|
+
"default": "md",
|
|
34
|
+
"description": "Width of the dialog: 28, 40 or 56 rem (never wider than the screen)."
|
|
35
|
+
},
|
|
36
|
+
"align": {
|
|
37
|
+
"options": [
|
|
38
|
+
"start",
|
|
39
|
+
"center",
|
|
40
|
+
"end"
|
|
41
|
+
],
|
|
42
|
+
"default": "start",
|
|
43
|
+
"description": "Where the button sits in the section; the measured align when not set."
|
|
44
|
+
},
|
|
45
|
+
"tone": {
|
|
46
|
+
"options": [
|
|
47
|
+
"default",
|
|
48
|
+
"muted",
|
|
49
|
+
"inverse",
|
|
50
|
+
"accent"
|
|
51
|
+
],
|
|
52
|
+
"default": "default",
|
|
53
|
+
"description": "Section background."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"props": {
|
|
57
|
+
"triggerLabel": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"required": true,
|
|
60
|
+
"label": "Button text",
|
|
61
|
+
"description": "Text of the button that opens the dialog (the popup's trigger link or button)."
|
|
62
|
+
},
|
|
63
|
+
"heading": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"label": "Heading",
|
|
66
|
+
"description": "The dialog's title; it names the dialog for screen readers."
|
|
67
|
+
},
|
|
68
|
+
"html": {
|
|
69
|
+
"type": "richtext",
|
|
70
|
+
"label": "Text",
|
|
71
|
+
"description": "The popup's content as rendered HTML (the popup template's text)."
|
|
72
|
+
},
|
|
73
|
+
"actions": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"label": "Buttons",
|
|
76
|
+
"required": false,
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"fields": {
|
|
80
|
+
"label": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"label": "Label",
|
|
83
|
+
"required": true,
|
|
84
|
+
"description": "The button text."
|
|
85
|
+
},
|
|
86
|
+
"href": {
|
|
87
|
+
"type": "url",
|
|
88
|
+
"label": "Link",
|
|
89
|
+
"required": true,
|
|
90
|
+
"description": "Where the button goes: a page on this site or a full address."
|
|
91
|
+
},
|
|
92
|
+
"newTab": {
|
|
93
|
+
"type": "boolean",
|
|
94
|
+
"label": "Open in a new tab",
|
|
95
|
+
"description": "Open the link in a new tab (an external site the reader should not leave for)."
|
|
96
|
+
},
|
|
97
|
+
"style": {
|
|
98
|
+
"type": "select",
|
|
99
|
+
"label": "Style",
|
|
100
|
+
"options": [
|
|
101
|
+
"primary",
|
|
102
|
+
"secondary",
|
|
103
|
+
"ghost"
|
|
104
|
+
],
|
|
105
|
+
"description": "primary for the main action, secondary or ghost for the ones beside it."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"description": "The popup's own buttons, under its text, in order; none by default."
|
|
110
|
+
},
|
|
111
|
+
"id": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"default": "dialog",
|
|
114
|
+
"content": false,
|
|
115
|
+
"description": "Unique on the page: the button points at the dialog by it."
|
|
116
|
+
},
|
|
117
|
+
"closeLabel": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"default": "Close",
|
|
120
|
+
"content": false,
|
|
121
|
+
"description": "Accessible name of the close button (ui-strings dialog.close)."
|
|
122
|
+
},
|
|
123
|
+
"measured": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"content": false,
|
|
126
|
+
"description": "The source section's measured style (MeasuredStyle): frame width, padding, fill, image, columns per width, gap, text tone, radius, minimum height — all in pixels. Absent, the site's tokens; present, it wins over tone and columns. Values that are not a number, a colour or a known keyword are dropped.",
|
|
127
|
+
"fields": {
|
|
128
|
+
"containerPx": {
|
|
129
|
+
"type": "integer",
|
|
130
|
+
"min": 0,
|
|
131
|
+
"max": 2560,
|
|
132
|
+
"description": "The frame's content width."
|
|
133
|
+
},
|
|
134
|
+
"padY": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"min": 0,
|
|
137
|
+
"max": 400,
|
|
138
|
+
"description": "Top and bottom padding."
|
|
139
|
+
},
|
|
140
|
+
"padX": {
|
|
141
|
+
"type": "integer",
|
|
142
|
+
"min": 0,
|
|
143
|
+
"max": 200,
|
|
144
|
+
"description": "The frame's side padding (gutter)."
|
|
145
|
+
},
|
|
146
|
+
"bg": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"description": "The section's background.",
|
|
149
|
+
"fields": {
|
|
150
|
+
"color": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
153
|
+
},
|
|
154
|
+
"image": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
157
|
+
},
|
|
158
|
+
"overlay": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
161
|
+
},
|
|
162
|
+
"size": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
165
|
+
},
|
|
166
|
+
"position": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"columns": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
175
|
+
"fields": {
|
|
176
|
+
"390": {
|
|
177
|
+
"type": "integer",
|
|
178
|
+
"min": 1,
|
|
179
|
+
"max": 6,
|
|
180
|
+
"description": "Phone."
|
|
181
|
+
},
|
|
182
|
+
"768": {
|
|
183
|
+
"type": "integer",
|
|
184
|
+
"min": 1,
|
|
185
|
+
"max": 6,
|
|
186
|
+
"description": "Tablet."
|
|
187
|
+
},
|
|
188
|
+
"1280": {
|
|
189
|
+
"type": "integer",
|
|
190
|
+
"min": 1,
|
|
191
|
+
"max": 6,
|
|
192
|
+
"description": "Desktop."
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"gap": {
|
|
197
|
+
"type": "integer",
|
|
198
|
+
"min": 0,
|
|
199
|
+
"max": 200,
|
|
200
|
+
"description": "Gap between items."
|
|
201
|
+
},
|
|
202
|
+
"tone": {
|
|
203
|
+
"type": "select",
|
|
204
|
+
"options": [
|
|
205
|
+
"light",
|
|
206
|
+
"dark"
|
|
207
|
+
],
|
|
208
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
209
|
+
},
|
|
210
|
+
"align": {
|
|
211
|
+
"type": "select",
|
|
212
|
+
"options": [
|
|
213
|
+
"start",
|
|
214
|
+
"center",
|
|
215
|
+
"end"
|
|
216
|
+
],
|
|
217
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
218
|
+
},
|
|
219
|
+
"radius": {
|
|
220
|
+
"type": "integer",
|
|
221
|
+
"min": 0,
|
|
222
|
+
"max": 200,
|
|
223
|
+
"description": "Card and item corner radius."
|
|
224
|
+
},
|
|
225
|
+
"minHeight": {
|
|
226
|
+
"type": "integer",
|
|
227
|
+
"min": 0,
|
|
228
|
+
"max": 2000,
|
|
229
|
+
"description": "The section's minimum height."
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"open": {
|
|
234
|
+
"type": "boolean",
|
|
235
|
+
"default": false,
|
|
236
|
+
"content": false,
|
|
237
|
+
"description": "Rendered already open, in the flow of the section and not modal: for previews and visual baselines only, never for a page."
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"slots": {},
|
|
241
|
+
"js": "vanilla",
|
|
242
|
+
"dependencies": {},
|
|
243
|
+
"sources": {
|
|
244
|
+
"elementor": [
|
|
245
|
+
"elementor/popup"
|
|
246
|
+
],
|
|
247
|
+
"classic": [
|
|
248
|
+
"classic/popup-maker"
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"a11y": "A native modal <dialog> (the APG dialog pattern as the platform implements it): focus moves in and stays in, the page behind is inert, Escape closes and focus returns to the button. The heading names it (aria-labelledby; the button text when there is no heading). The close button is named by closeLabel. Nothing opens on load, on scroll or on exit intent; the text is in the HTML, so crawlers read it, and with scripting off it is shown in the section as a plain block, so nothing is hidden or blocks the page."
|
|
252
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "A video, map or player from another site (YouTube, Vimeo, Google Maps, OpenStreetMap, Spotify, SoundCloud, Calendly, Typeform, Google Forms, Loom, Wistia), loaded only on click: until then a poster link to the provider's page, with no iframe, no third-party request and no cookie (YouTube through youtube-nocookie.com). Unknown providers are never framed; they stay a plain link.",
|
|
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
|
"aspect": { "options": ["auto", "video", "portrait", "square", "map"], "default": "auto", "description": "auto: 16:9 for videos, 9:16 for YouTube Shorts, 4:3 (16:9 wide) for maps, a fixed strip for audio players. The others force a ratio." },
|
|
10
10
|
"width": { "options": ["prose", "page"], "default": "prose", "description": "prose: as wide as the reading column. page: the full page frame." }
|
|
@@ -115,5 +115,9 @@
|
|
|
115
115
|
"tone": "dark"
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "six-measured-columns",
|
|
121
|
+
"props": { "heading": "Six questions", "id": "six", "style": "lines", "items": [{ "question": "Question 1?", "answer": "<p>Answer 1, in a sentence or two.</p>" }, { "question": "Question 2?", "answer": "<p>Answer 2, in a sentence or two.</p>" }, { "question": "Question 3?", "answer": "<p>Answer 3, in a sentence or two.</p>" }, { "question": "Question 4?", "answer": "<p>Answer 4, in a sentence or two.</p>" }, { "question": "Question 5?", "answer": "<p>Answer 5, in a sentence or two.</p>" }, { "question": "Question 6?", "answer": "<p>Answer 6, in a sentence or two.</p>" }], "measured": { "columns": { "390": 1, "768": 2, "1280": 3 }, "gap": 32, "padY": 48 } }
|
|
118
122
|
}
|
|
119
123
|
]
|
package/components/faq/meta.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Questions and answers as native <details> disclosures (no script), in lines or cards; optionally one open at a time, and optionally with FAQPage structured data for search engines and AI answers.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["Section.astro", "measured.ts", "types.ts"],
|
|
7
|
+
"shared": ["Section.astro", "measured.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["single", "split"], "default": "single", "description": "single: the heading above one column. split: the heading and introduction stay in a left column beside the list; stacks on a phone." },
|
|
10
10
|
"columns": { "options": ["1", "2"], "default": "1", "description": "Columns of questions on wide screens." },
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"gutenberg": ["core/details"],
|
|
32
32
|
"elementor": ["elementor/accordion", "elementor/toggle", "elementor/nested-accordion"],
|
|
33
33
|
"divi": ["divi/et_pb_accordion", "divi/et_pb_toggle"],
|
|
34
|
-
"classic": ["classic/faq"]
|
|
34
|
+
"classic": ["classic/faq", "classic/details"]
|
|
35
35
|
},
|
|
36
36
|
"a11y": "Native <details>/<summary>: keyboard operable and announced as expandable by every screen reader, with no script to fail. The plus icon is decorative. The JSON-LD answer text is the answer with its markup removed."
|
|
37
37
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { tv } from 'tailwind-variants'
|
|
5
5
|
import type { LinkInput } from '../_shared/types'
|
|
6
6
|
import { footerLinkSizeClasses, footerVars } from './footer'
|
|
7
|
+
import { safeHref } from '../_shared/safe-url'
|
|
7
8
|
|
|
8
9
|
const footer = tv({
|
|
9
10
|
slots: {
|
|
@@ -107,7 +108,7 @@ const hasBottom = Boolean(line || consentLabel)
|
|
|
107
108
|
<nav aria-label={column.label ?? column.title} data-cr-part="nav-footer">
|
|
108
109
|
{column.title && <p class="mb-3 font-semibold">{column.title}</p>}
|
|
109
110
|
<ul class="flex flex-col gap-2">
|
|
110
|
-
{column.links.map(item => <li><a href={item.href} class={link()}>{item.label}</a></li>)}
|
|
111
|
+
{column.links.map(item => <li><a href={safeHref(item.href)} class={link()}>{item.label}</a></li>)}
|
|
111
112
|
</ul>
|
|
112
113
|
</nav>
|
|
113
114
|
))
|
|
@@ -115,13 +116,13 @@ const hasBottom = Boolean(line || consentLabel)
|
|
|
115
116
|
<div class:list={['flex flex-col gap-4', align === 'center' ? 'items-center' : 'md:items-end']}>
|
|
116
117
|
{social.length > 0 && (
|
|
117
118
|
<ul class="flex flex-wrap gap-4">
|
|
118
|
-
{social.map(item => <li><a href={item.href} rel="me noopener" target="_blank" class={link()}>{item.label}</a></li>)}
|
|
119
|
+
{social.map(item => <li><a href={safeHref(item.href)} rel="me noopener" target="_blank" class={link()}>{item.label}</a></li>)}
|
|
119
120
|
</ul>
|
|
120
121
|
)}
|
|
121
122
|
{links.length > 0 && (
|
|
122
123
|
<nav aria-label={navLabel} data-cr-part="nav-footer">
|
|
123
124
|
<ul class="flex flex-wrap gap-x-6 gap-y-2">
|
|
124
|
-
{links.map(item => <li><a href={item.href} class={link()}>{item.label}</a></li>)}
|
|
125
|
+
{links.map(item => <li><a href={safeHref(item.href)} class={link()}>{item.label}</a></li>)}
|
|
125
126
|
</ul>
|
|
126
127
|
</nav>
|
|
127
128
|
)}
|
|
@@ -130,7 +131,7 @@ const hasBottom = Boolean(line || consentLabel)
|
|
|
130
131
|
)}
|
|
131
132
|
{layout === 'columns' && social.length > 0 && (
|
|
132
133
|
<ul class="flex flex-wrap gap-4 md:col-span-full">
|
|
133
|
-
{social.map(item => <li><a href={item.href} rel="me noopener" target="_blank" class={link()}>{item.label}</a></li>)}
|
|
134
|
+
{social.map(item => <li><a href={safeHref(item.href)} rel="me noopener" target="_blank" class={link()}>{item.label}</a></li>)}
|
|
134
135
|
</ul>
|
|
135
136
|
)}
|
|
136
137
|
</div>
|
|
@@ -11,6 +11,7 @@ import { measuredGrid } from '../_shared/measured'
|
|
|
11
11
|
import type { MeasuredStyle } from '../_shared/types'
|
|
12
12
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
13
13
|
import { type ImageInput } from '../_shared/types'
|
|
14
|
+
import { safeHref } from '../_shared/safe-url'
|
|
14
15
|
|
|
15
16
|
export interface GalleryItem {
|
|
16
17
|
/** The image's address (or, until 0.7, an `ImageInput`). */
|
|
@@ -104,7 +105,7 @@ const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 2
|
|
|
104
105
|
{open === 'popover'
|
|
105
106
|
? <button type="button" popovertarget={viewer} class={s.frame()} aria-label={`${enlargeLabel}: ${item.image.alt || item.caption || ''}`.trim()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></button>
|
|
106
107
|
: open === 'link'
|
|
107
|
-
? <a href={item.image.src} class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
108
|
+
? <a href={safeHref(item.image.src)} class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
108
109
|
: <div class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></div>}
|
|
109
110
|
{item.caption && <figcaption class={s.caption({ class: tone === 'accent' ? 'opacity-100' : undefined })}>{item.caption}</figcaption>}
|
|
110
111
|
</figure>
|
|
@@ -6,6 +6,8 @@ import Button from '../_shared/Button.astro'
|
|
|
6
6
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
7
|
import type { ActionInput, ImageInput, NavItem } from '../_shared/types'
|
|
8
8
|
import Nav from '../nav/Nav.astro'
|
|
9
|
+
import SearchBox from '../search-box/SearchBox.astro'
|
|
10
|
+
import { safeHref } from '../_shared/safe-url'
|
|
9
11
|
|
|
10
12
|
const header = tv({
|
|
11
13
|
slots: {
|
|
@@ -54,28 +56,33 @@ interface Props {
|
|
|
54
56
|
homeHref?: string | undefined
|
|
55
57
|
items: NavItem[]
|
|
56
58
|
action?: ActionInput | undefined
|
|
59
|
+
/** A search field in the bar (a GET form to the site's search page); below the bar until lg. */
|
|
60
|
+
search?: { label?: string | undefined, placeholder?: string | undefined, submitLabel?: string | undefined, action?: string | undefined } | undefined
|
|
57
61
|
navLabel?: string | undefined
|
|
58
62
|
menuLabel?: string | undefined
|
|
59
63
|
closeLabel?: string | undefined
|
|
60
64
|
layout?: 'split' | 'centered' | 'brand-left' | undefined
|
|
61
65
|
tone?: 'default' | 'inverse' | 'transparent' | undefined
|
|
62
66
|
position?: 'static' | 'sticky' | undefined
|
|
67
|
+
/** How submenus open on wide screens: a list under the item, or a wide panel of columns. */
|
|
68
|
+
menu?: 'flyout' | 'mega' | undefined
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
const { siteName, tagline, brandSize, brandWeight, brandTracking, bordered, offsetTop, padTop, padBottom, linkNav, logo, showName = true, homeHref = '/', items, action, navLabel, menuLabel, closeLabel, layout, tone, position } = Astro.props
|
|
71
|
+
const { siteName, tagline, brandSize, brandWeight, brandTracking, bordered, offsetTop, padTop, padBottom, linkNav, logo, showName = true, homeHref = '/', items, action, search, navLabel, menuLabel, closeLabel, layout, tone, position, menu } = Astro.props
|
|
66
72
|
const { root, inner, brand } = header({ layout, tone, position, bordered })
|
|
67
73
|
const rootStyle = offsetTop ? `margin-top: ${offsetTop}` : undefined
|
|
68
74
|
const innerStyle = [padTop && `padding-top: ${padTop}`, padBottom && `padding-bottom: ${padBottom}`].filter(Boolean).join('; ') || undefined
|
|
69
75
|
const brandStyle = [brandSize && `font-size: ${brandSize}`, brandWeight && `font-weight: ${brandWeight}`, brandTracking && `letter-spacing: ${brandTracking}`].filter(Boolean).join('; ') || undefined
|
|
70
76
|
// A tagline under the name keeps its line on a phone, as themes print it: the navigation wraps below.
|
|
71
|
-
|
|
77
|
+
// The search field sits in the bar from lg; at md the bar already holds the navigation, so the field keeps its own row.
|
|
78
|
+
const wrap = search ? 'flex-wrap lg:flex-nowrap' : tagline ? 'flex-wrap md:flex-nowrap' : undefined
|
|
72
79
|
---
|
|
73
80
|
<header class={root()} style={rootStyle}>
|
|
74
81
|
<div class={inner({ class: wrap })} style={innerStyle}>
|
|
75
82
|
{tagline
|
|
76
83
|
? (
|
|
77
84
|
<div class="flex min-w-0 basis-full flex-col gap-1 md:basis-auto">
|
|
78
|
-
<a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName} style={brandStyle} data-kit-brand>
|
|
85
|
+
<a href={safeHref(homeHref)} class={brand()} aria-label={showName ? undefined : siteName} style={brandStyle} data-kit-brand>
|
|
79
86
|
{logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
|
|
80
87
|
{showName && <span>{siteName}</span>}
|
|
81
88
|
</a>
|
|
@@ -83,13 +90,18 @@ const wrap = tagline ? 'flex-wrap md:flex-nowrap' : undefined
|
|
|
83
90
|
</div>
|
|
84
91
|
)
|
|
85
92
|
: (
|
|
86
|
-
<a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName} style={brandStyle} data-kit-brand>
|
|
93
|
+
<a href={safeHref(homeHref)} class={brand()} aria-label={showName ? undefined : siteName} style={brandStyle} data-kit-brand>
|
|
87
94
|
{logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
|
|
88
95
|
{showName && <span>{siteName}</span>}
|
|
89
96
|
</a>
|
|
90
97
|
)}
|
|
98
|
+
{search && (
|
|
99
|
+
<div class="order-last basis-full lg:order-none lg:ms-auto lg:basis-auto">
|
|
100
|
+
<SearchBox action={search.action} label={search.label} placeholder={search.placeholder} submitLabel={search.submitLabel} size="sm" id="header-search" />
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
91
103
|
<div class:list={['flex items-center gap-4', layout === 'centered' ? 'justify-center' : 'ms-auto']}>
|
|
92
|
-
<Nav items={items} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" color={linkNav ? 'link' : 'current'} align={layout === 'centered' ? 'center' : 'end'} />
|
|
104
|
+
<Nav items={items} menu={menu} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" color={linkNav ? 'link' : 'current'} align={layout === 'centered' ? 'center' : 'end'} />
|
|
93
105
|
{action && <Button action={action} size="sm" class="hidden md:inline-flex" />}
|
|
94
106
|
</div>
|
|
95
107
|
</div>
|