Prosepopular

apps/www/src/sections/Prose.astro · 96 lines · group Text & prose

Prose — a text section: kicker + title + lead, then markdown children in a prose container. Covers "mission", "about", certification texts, any block that is words rather than a grid. align="center" (default) | "left" image optional picture under the text (certificate scan, schema) Buttons and a link are optional; `aside` slot renders a card to the right.

In the fleet

Used on 3 sites built with this engine.

Samples 2

sample

WHY WE BUILT IT

A thermometer is not a decision

Growers used to choose between a thermometer that told them nothing and a research mast nobody could afford.

Nordvind sits in between: the sensors that change a decision, and none of the ones that only look good on a datasheet.

  • Frost, wind, rain and leaf wetness — inside the crop
  • One battery per season, one bolt per station
  • Readings exported as CSV, PDF or through the API
MDX of this sample
<Prose kicker="WHY WE BUILT IT" title="A thermometer is not a decision">
Growers used to choose between a thermometer that told them nothing and a research mast nobody could afford.

Nordvind sits in between: the sensors that change a decision, and none of the ones that only look good on a datasheet.

* Frost, wind, rain and leaf wetness — inside the crop
* One battery per season, one bolt per station
* Readings exported as CSV, PDF or through the API
</Prose>
sample

What we promise, and what we do not

We promise that a station keeps measuring when the network is down, that a sensor can be replaced by the person who found the problem, and that the data leaves with you if you leave us.

We do not promise a forecast that is right every time. Nobody can. What we can do is tell you what is happening in your field right now, minutes after it starts happening.

MDX of this sample
<Prose width="wide" title="What we promise, and what we do not" heading="left" decor="dots">
We promise that a station keeps measuring when the network is down, that a sensor can be replaced by the person who found the problem, and that the data leaves with you if you leave us.

We do not promise a forecast that is right every time. Nobody can. What we can do is tell you what is happening in your field right now, minutes after it starts happening.

<ProseAside title="In one line" quote="Measure where the crop is, alert the person on duty, keep the history." name="Anna Lindqvist" role="Head of agronomy, Almara Farms" avatar="avatar-01.jpg" />
</Prose>

Props

NameTypeRequiredDefaultDescription
idstring
kickerstring
titlestring
descriptionstring
align'center' | 'left''center'
width'text' | 'wide''text''text' keeps the comfortable reading measure; 'wide' uses the whole print zone — that is what a section with a <ProseAside> wants
heading'center' | 'left'header alignment; defaults to `align` (a centered heading over a left-aligned body is the certification-page shape)
themeTheme'light'
decor'none' | 'dots' | 'dots-left''none'decorative dot grid behind the content
imagestring
linkTextstring
hrefstring
primaryTextstring
primaryHrefstring'#'
secondaryTextstring
secondaryHrefstring'#'
classstringescape hatch for compositions: extra classes for the section shell

Source apps/www/src/sections/Prose.astro @ gitt.one

---
/**
 * Prose — a text section: kicker + title + lead, then markdown children in a
 * prose container. Covers "mission", "about", certification texts, any
 * block that is words rather than a grid.
 *
 * align="center" (default) | "left"
 * image   optional picture under the text (certificate scan, schema)
 * Buttons and a link are optional; `aside` slot renders a card to the right.
 */
import { Image } from 'astro:assets';
import { resolveImage } from '../lib/images';
import Section from './_Section.astro';
import type { Theme } from './_Section.astro';
import Button from './_Button.astro';

interface Props {
  id?: string;
  kicker?: string;
  title?: string;
  description?: string;
  align?: 'center' | 'left';
  /** 'text' keeps the comfortable reading measure; 'wide' uses the whole
      print zone — that is what a section with a <ProseAside> wants */
  width?: 'text' | 'wide';
  /** header alignment; defaults to `align` (a centered heading over a
      left-aligned body is the certification-page shape) */
  heading?: 'center' | 'left';
  theme?: Theme;
  /** decorative dot grid behind the content */
  decor?: 'none' | 'dots' | 'dots-left';
  image?: string;
  linkText?: string;
  href?: string;
  primaryText?: string;
  primaryHref?: string;
  secondaryText?: string;
  secondaryHref?: string;
  /** escape hatch for compositions: extra classes for the section shell */
  class?: string;
}

const {
  id, kicker, title, description, align = 'center', heading, width = 'text', theme = 'light', decor = 'none', image,
  linkText, href, primaryText, primaryHref = '#', secondaryText, secondaryHref = '#', class: extra = '',
} = Astro.props;
const img = image ? await resolveImage(image) : undefined;
const hasAside = Astro.slots.has('aside');
const center = align === 'center' && !hasAside;
const headerCenter = (heading ?? align) === 'center' && !hasAside;
// with an aside the text column is narrow, so a centered heading belongs
// above the whole grid (the product-quality shape)
const headerAbove = hasAside && (heading ?? align) === 'center' && Boolean(title || kicker || description);
---

<Section id={id} theme={theme} class={extra} decor={decor}>
  {headerAbove && (
    <div class="mx-auto mb-12 max-w-3xl text-center">
      {kicker && (
        <p class="text-sm font-semibold uppercase tracking-wide">
          <span class="bg-gradient-to-r from-primary-500 to-primary-alt-600 bg-clip-text text-transparent">{kicker}</span>
        </p>
      )}
      {title && <h2 class="mt-3 text-3xl font-extrabold tracking-tight sm:text-4xl">{title}</h2>}
      {description && <p class="mt-6 text-xl leading-relaxed text-gray-500 [.bg-stone-800_&]:text-primary-100">{description}</p>}
    </div>
  )}
  <div class:list={[hasAside && 'lg:grid lg:grid-cols-2 lg:gap-12 lg:items-start']}>
    <div class:list={[width === 'wide' ? 'max-w-none' : 'max-w-3xl', (center || headerCenter) && 'mx-auto', center && 'text-center']}>
      {!headerAbove && (
      <div class:list={[headerCenter && !center && 'text-center']}>
        {kicker && (
          <p class="text-sm font-semibold uppercase tracking-wide">
            <span class="bg-gradient-to-r from-primary-500 to-primary-alt-600 bg-clip-text text-transparent">{kicker}</span>
          </p>
        )}
        {title && <h2 class="mt-3 text-3xl font-extrabold tracking-tight sm:text-4xl">{title}</h2>}
        {description && <p class="mt-6 text-xl leading-relaxed text-gray-500 [.bg-stone-800_&]:text-primary-100">{description}</p>}
      </div>
      )}
      <div class:list={['prose prose-gray mt-6 max-w-none text-gray-500 after:block after:clear-both after:content-[""] [&_h2_a]:no-underline [&_h3_a]:no-underline [&_h2]:mt-0 [&_h3]:mt-0', center && 'mx-auto']}>
        <slot />
      </div>
      {linkText && href && <p class="mt-6"><Button href={href} kind="link">{linkText}</Button></p>}
      {(primaryText || secondaryText) && (
        <div class:list={['mt-8 flex flex-wrap gap-4', center && 'justify-center']}>
          {primaryText && <Button href={primaryHref}>{primaryText}</Button>}
          {secondaryText && <Button href={secondaryHref} kind="secondary">{secondaryText}</Button>}
        </div>
      )}
      {img && <Image src={img} alt="" widths={[400, 840]} class:list={['mt-8 w-full max-w-2xl rounded-lg shadow-lg', center && 'mx-auto']} />}
    </div>
    {hasAside && <div class="mt-12 lg:mt-0"><slot name="aside" /></div>}
  </div>
</Section>

Source links point to engine-astro@38c294d9. Samples are demo content: the components are the real ones, the copy and the pictures are made up.