astroidjs 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +67 -0
- package/dist/commerce/mirror.js +203 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +174 -12
- package/dist/config.js +43 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +50 -0
- package/dist/portal/guard.d.ts +48 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +106 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +140 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +41 -7
- package/dist/schema/collections.js +101 -12
- package/dist/schema/generate.js +10 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +107 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +325 -37
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -4
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A single full-width image, with an optional heading and caption.
|
|
3
|
+
//
|
|
4
|
+
// The alt/caption resolution is the point of `<Sections>`' one-query media
|
|
5
|
+
// lookup: the section's own `alt` is a per-usage override, and an empty one
|
|
6
|
+
// falls back to what the editor typed against the asset in the media library.
|
|
7
|
+
// So fixing alt text is one edit that reaches every page showing that image.
|
|
8
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
9
|
+
import { colorwayClass, field, mediaAlt, mediaCaption, setting } from "../sections.js";
|
|
10
|
+
import Editable from "../Editable.astro";
|
|
11
|
+
import MediaSlot from "../MediaSlot.astro";
|
|
12
|
+
|
|
13
|
+
type Props = SectionRenderProps;
|
|
14
|
+
|
|
15
|
+
const { item, base, edit, mediaMeta } = Astro.props;
|
|
16
|
+
|
|
17
|
+
const heading = field(item, "heading");
|
|
18
|
+
const image = field(item, "image");
|
|
19
|
+
const caption = mediaCaption(mediaMeta, image, field(item, "caption"));
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
23
|
+
<div class="mx-auto max-w-5xl px-6">
|
|
24
|
+
{heading && (
|
|
25
|
+
<Editable as="h2" base={base} field="heading" edit={edit} class="mb-6 text-3xl font-bold">
|
|
26
|
+
{heading}
|
|
27
|
+
</Editable>
|
|
28
|
+
)}
|
|
29
|
+
{image && (
|
|
30
|
+
<MediaSlot
|
|
31
|
+
src={image}
|
|
32
|
+
alt={mediaAlt(mediaMeta, image, field(item, "alt"))}
|
|
33
|
+
width={1600}
|
|
34
|
+
sizes="(max-width: 64rem) 100vw, 64rem"
|
|
35
|
+
ratio="16/9"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
{caption && (
|
|
39
|
+
<Editable as="p" base={base} field="caption" edit={edit} class="mt-3 text-sm opacity-70">
|
|
40
|
+
{caption}
|
|
41
|
+
</Editable>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Pricing tiers. Each tier's feature list is a nested array, so a feature's
|
|
3
|
+
// marker path is `<base>.items.<i>.features.<j>.text` — two levels of position,
|
|
4
|
+
// which the client's path parser handles because it splits on `.` and coerces
|
|
5
|
+
// every numeric segment.
|
|
6
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
7
|
+
import { colorwayClass, field, itemField, list, setting } from "../sections.js";
|
|
8
|
+
import Editable from "../Editable.astro";
|
|
9
|
+
|
|
10
|
+
type Props = SectionRenderProps;
|
|
11
|
+
|
|
12
|
+
const { item, base, edit } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const heading = field(item, "heading");
|
|
15
|
+
const tiers = list(item, "items");
|
|
16
|
+
|
|
17
|
+
/** A tier's nested feature rows. Untrusted like any stored array. */
|
|
18
|
+
const featuresOf = (row: Record<string, unknown>) =>
|
|
19
|
+
Array.isArray(row.features)
|
|
20
|
+
? row.features.filter(
|
|
21
|
+
(f): f is Record<string, unknown> =>
|
|
22
|
+
typeof f === "object" && f !== null && !Array.isArray(f),
|
|
23
|
+
)
|
|
24
|
+
: [];
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
28
|
+
<div class="mx-auto max-w-6xl px-6">
|
|
29
|
+
{heading && (
|
|
30
|
+
<Editable as="h2" base={base} field="heading" edit={edit} class="mb-10 text-3xl font-bold">
|
|
31
|
+
{heading}
|
|
32
|
+
</Editable>
|
|
33
|
+
)}
|
|
34
|
+
<div class="grid gap-6 md:grid-cols-3">
|
|
35
|
+
{tiers.map((row, i) => (
|
|
36
|
+
<div
|
|
37
|
+
class:list={[
|
|
38
|
+
"card bg-base-100 text-base-content shadow-sm",
|
|
39
|
+
itemField(row, "featured") === "yes" && "ring-2 ring-primary",
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<div class="card-body">
|
|
43
|
+
<Editable as="h3" sfield={`${base}.items.${i}.name`} edit={edit} class="card-title">
|
|
44
|
+
{itemField(row, "name")}
|
|
45
|
+
</Editable>
|
|
46
|
+
<p class="mt-2">
|
|
47
|
+
<Editable as="span" sfield={`${base}.items.${i}.price`} edit={edit} class="text-3xl font-bold">
|
|
48
|
+
{itemField(row, "price")}
|
|
49
|
+
</Editable>
|
|
50
|
+
<Editable as="span" sfield={`${base}.items.${i}.period`} edit={edit} class="opacity-70">
|
|
51
|
+
{itemField(row, "period")}
|
|
52
|
+
</Editable>
|
|
53
|
+
</p>
|
|
54
|
+
<ul class="mt-4 grid gap-2">
|
|
55
|
+
{featuresOf(row).map((feature, j) => (
|
|
56
|
+
<li>
|
|
57
|
+
<Editable
|
|
58
|
+
as="span"
|
|
59
|
+
sfield={`${base}.items.${i}.features.${j}.text`}
|
|
60
|
+
edit={edit}
|
|
61
|
+
>
|
|
62
|
+
{itemField(feature, "text")}
|
|
63
|
+
</Editable>
|
|
64
|
+
</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
{itemField(row, "ctaLabel") && (
|
|
68
|
+
<a class="btn btn-primary mt-6" href={itemField(row, "ctaHref") || "#"}>
|
|
69
|
+
<Editable as="span" sfield={`${base}.items.${i}.ctaLabel`} edit={edit}>
|
|
70
|
+
{itemField(row, "ctaLabel")}
|
|
71
|
+
</Editable>
|
|
72
|
+
</a>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A grid of products as authored content.
|
|
3
|
+
//
|
|
4
|
+
// Deliberately NOT a live catalog read. A section is stored content that an
|
|
5
|
+
// editor curates; the commerce mirror has its own loader (`readCatalog`) with
|
|
6
|
+
// its own freshness story. A site that wants the live catalog renders it in a
|
|
7
|
+
// page, not through the page-builder — mixing the two would make a section's
|
|
8
|
+
// stored value and what it displays disagree, and the editor would be editing
|
|
9
|
+
// something that isn't there.
|
|
10
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
11
|
+
import { colorwayClass, field, itemField, list, mediaAlt, setting } from "../sections.js";
|
|
12
|
+
import Editable from "../Editable.astro";
|
|
13
|
+
import MediaSlot from "../MediaSlot.astro";
|
|
14
|
+
|
|
15
|
+
type Props = SectionRenderProps;
|
|
16
|
+
|
|
17
|
+
const { item, base, edit, mediaMeta } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const heading = field(item, "heading");
|
|
20
|
+
const items = list(item, "items");
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
24
|
+
<div class="mx-auto max-w-6xl px-6">
|
|
25
|
+
{heading && (
|
|
26
|
+
<Editable as="h2" base={base} field="heading" edit={edit} class="mb-8 text-3xl font-bold">
|
|
27
|
+
{heading}
|
|
28
|
+
</Editable>
|
|
29
|
+
)}
|
|
30
|
+
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
31
|
+
{items.map((row, i) => {
|
|
32
|
+
const src = itemField(row, "image");
|
|
33
|
+
const href = itemField(row, "href");
|
|
34
|
+
return (
|
|
35
|
+
<article class="card bg-base-100 text-base-content shadow-sm">
|
|
36
|
+
{/* The link wraps the media only; the name and price stay outside it
|
|
37
|
+
so they remain in-place editable rather than being swallowed by
|
|
38
|
+
a navigation target. */}
|
|
39
|
+
{src && (
|
|
40
|
+
href ? (
|
|
41
|
+
<a href={href}>
|
|
42
|
+
<MediaSlot
|
|
43
|
+
src={src}
|
|
44
|
+
alt={mediaAlt(mediaMeta, src, itemField(row, "alt"))}
|
|
45
|
+
width={640}
|
|
46
|
+
sizes="(max-width: 40rem) 100vw, 20rem"
|
|
47
|
+
ratio="1/1"
|
|
48
|
+
/>
|
|
49
|
+
</a>
|
|
50
|
+
) : (
|
|
51
|
+
<MediaSlot
|
|
52
|
+
src={src}
|
|
53
|
+
alt={mediaAlt(mediaMeta, src, itemField(row, "alt"))}
|
|
54
|
+
width={640}
|
|
55
|
+
sizes="(max-width: 40rem) 100vw, 20rem"
|
|
56
|
+
ratio="1/1"
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
)}
|
|
60
|
+
<div class="card-body">
|
|
61
|
+
<Editable as="h3" sfield={`${base}.items.${i}.name`} edit={edit} class="card-title text-base">
|
|
62
|
+
{itemField(row, "name")}
|
|
63
|
+
</Editable>
|
|
64
|
+
<Editable as="p" sfield={`${base}.items.${i}.price`} edit={edit} class="opacity-80">
|
|
65
|
+
{itemField(row, "price")}
|
|
66
|
+
</Editable>
|
|
67
|
+
</div>
|
|
68
|
+
</article>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Image beside copy. Which side the image sits on is a `_layout` token, so an
|
|
3
|
+
// editor flips it from the inspector and Louise stores `"imageEnd"` — never a
|
|
4
|
+
// class name. The mapping from token to CSS is here, where it belongs.
|
|
5
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
6
|
+
import { colorwayClass, field, mediaAlt, setting } from "../sections.js";
|
|
7
|
+
import Editable from "../Editable.astro";
|
|
8
|
+
import MediaSlot from "../MediaSlot.astro";
|
|
9
|
+
|
|
10
|
+
type Props = SectionRenderProps;
|
|
11
|
+
|
|
12
|
+
const { item, base, edit, mediaMeta } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const heading = field(item, "heading");
|
|
15
|
+
const body = field(item, "body");
|
|
16
|
+
const image = field(item, "image");
|
|
17
|
+
const ctaLabel = field(item, "ctaLabel");
|
|
18
|
+
const ctaHref = field(item, "ctaHref");
|
|
19
|
+
|
|
20
|
+
// `imageStart` is the default. An unknown/absent token lands here too, so a
|
|
21
|
+
// hand-edited layout can't produce an unstyled row.
|
|
22
|
+
const imageEnd = item._layout === "imageEnd";
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
26
|
+
<div class="mx-auto grid max-w-6xl items-center gap-10 px-6 md:grid-cols-2">
|
|
27
|
+
{image && (
|
|
28
|
+
<div class:list={[imageEnd && "md:order-2"]}>
|
|
29
|
+
<MediaSlot
|
|
30
|
+
src={image}
|
|
31
|
+
alt={mediaAlt(mediaMeta, image, field(item, "alt"))}
|
|
32
|
+
width={800}
|
|
33
|
+
sizes="(max-width: 48rem) 100vw, 32rem"
|
|
34
|
+
ratio="4/3"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
<div>
|
|
39
|
+
<Editable as="h2" base={base} field="heading" edit={edit} class="text-3xl font-bold">
|
|
40
|
+
{heading}
|
|
41
|
+
</Editable>
|
|
42
|
+
{body && (
|
|
43
|
+
<Editable
|
|
44
|
+
as="div"
|
|
45
|
+
base={base}
|
|
46
|
+
field="body"
|
|
47
|
+
type="richtext"
|
|
48
|
+
edit={edit}
|
|
49
|
+
class="prose mt-4 max-w-none"
|
|
50
|
+
>
|
|
51
|
+
<Fragment set:html={body} />
|
|
52
|
+
</Editable>
|
|
53
|
+
)}
|
|
54
|
+
{ctaLabel && (
|
|
55
|
+
<a class="btn btn-primary mt-6" href={ctaHref || "#"}>
|
|
56
|
+
<Editable as="span" base={base} field="ctaLabel" edit={edit}>{ctaLabel}</Editable>
|
|
57
|
+
</a>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A numbered sequence — "how it works", a commission process, an onboarding
|
|
3
|
+
// flow. The number is generated from position rather than stored, so reordering
|
|
4
|
+
// steps in the inspector renumbers them with no content edit.
|
|
5
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
6
|
+
import { colorwayClass, field, itemField, list, setting } from "../sections.js";
|
|
7
|
+
import Editable from "../Editable.astro";
|
|
8
|
+
|
|
9
|
+
type Props = SectionRenderProps;
|
|
10
|
+
|
|
11
|
+
const { item, base, edit } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const heading = field(item, "heading");
|
|
14
|
+
const items = list(item, "items");
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
18
|
+
<div class="mx-auto max-w-4xl px-6">
|
|
19
|
+
{heading && (
|
|
20
|
+
<Editable as="h2" base={base} field="heading" edit={edit} class="mb-10 text-3xl font-bold">
|
|
21
|
+
{heading}
|
|
22
|
+
</Editable>
|
|
23
|
+
)}
|
|
24
|
+
<ol class="grid gap-8">
|
|
25
|
+
{items.map((row, i) => (
|
|
26
|
+
<li class="flex gap-5">
|
|
27
|
+
{/* aria-hidden: the marker duplicates the <ol> semantics a screen
|
|
28
|
+
reader already conveys, so announcing "1" twice is noise. */}
|
|
29
|
+
<span
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary text-primary-content font-bold"
|
|
32
|
+
>
|
|
33
|
+
{i + 1}
|
|
34
|
+
</span>
|
|
35
|
+
<div>
|
|
36
|
+
<Editable
|
|
37
|
+
as="h3"
|
|
38
|
+
sfield={`${base}.items.${i}.title`}
|
|
39
|
+
edit={edit}
|
|
40
|
+
class="text-lg font-semibold"
|
|
41
|
+
>
|
|
42
|
+
{itemField(row, "title")}
|
|
43
|
+
</Editable>
|
|
44
|
+
<Editable
|
|
45
|
+
as="p"
|
|
46
|
+
sfield={`${base}.items.${i}.body`}
|
|
47
|
+
edit={edit}
|
|
48
|
+
multiline
|
|
49
|
+
class="mt-1 opacity-80"
|
|
50
|
+
>
|
|
51
|
+
{itemField(row, "body")}
|
|
52
|
+
</Editable>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ol>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A single quote with optional portrait.
|
|
3
|
+
//
|
|
4
|
+
// <blockquote> + <figcaption> rather than styled divs: the attribution is
|
|
5
|
+
// semantically tied to the quote, which is what a screen reader needs to say
|
|
6
|
+
// who said it.
|
|
7
|
+
import type { SectionRenderProps } from "../sections.js";
|
|
8
|
+
import { alignClass, colorwayClass, field, mediaAlt, setting } from "../sections.js";
|
|
9
|
+
import Editable from "../Editable.astro";
|
|
10
|
+
import MediaSlot from "../MediaSlot.astro";
|
|
11
|
+
|
|
12
|
+
type Props = SectionRenderProps;
|
|
13
|
+
|
|
14
|
+
const { item, base, edit, mediaMeta } = Astro.props;
|
|
15
|
+
|
|
16
|
+
const quote = field(item, "quote");
|
|
17
|
+
const attribution = field(item, "attribution");
|
|
18
|
+
const role = field(item, "role");
|
|
19
|
+
const image = field(item, "image");
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<section class:list={["py-16", colorwayClass(setting(item, "colorway", "base"))]}>
|
|
23
|
+
<figure
|
|
24
|
+
class:list={[
|
|
25
|
+
"mx-auto flex max-w-3xl flex-col gap-6 px-6",
|
|
26
|
+
alignClass(setting(item, "align", "center")),
|
|
27
|
+
]}
|
|
28
|
+
>
|
|
29
|
+
{image && (
|
|
30
|
+
<MediaSlot src={image} alt={mediaAlt(mediaMeta, image, field(item, "alt"))} shape="circle" size={80} />
|
|
31
|
+
)}
|
|
32
|
+
<blockquote>
|
|
33
|
+
<Editable as="p" base={base} field="quote" edit={edit} multiline class="text-2xl italic">
|
|
34
|
+
{quote}
|
|
35
|
+
</Editable>
|
|
36
|
+
</blockquote>
|
|
37
|
+
{(attribution || role) && (
|
|
38
|
+
<figcaption class="opacity-80">
|
|
39
|
+
<Editable as="span" base={base} field="attribution" edit={edit} class="font-semibold">
|
|
40
|
+
{attribution}
|
|
41
|
+
</Editable>
|
|
42
|
+
{role && (
|
|
43
|
+
<>
|
|
44
|
+
{attribution && <span aria-hidden="true"> — </span>}
|
|
45
|
+
<Editable as="span" base={base} field="role" edit={edit}>{role}</Editable>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
</figcaption>
|
|
49
|
+
)}
|
|
50
|
+
</figure>
|
|
51
|
+
</section>
|