uix(blocks): los ocho encabezados de sección eran el mismo, y uno ya había derivado

feature-grid, feature-split, pricing, testimonials, faq, article-grid, team y
contact declaraban cada uno su `.Header` con las mismas tres líneas: un Box con
maxWidth 48rem, un Motion trigger="viewport" y un Stack gap 3. Ocho ficheros,
una sola idea.

Y la copia se pagó donde siempre: article-grid había perdido su Motion. Su
encabezado aparecía sin el reveal que tenían los siete hermanos, y nadie lo
notó porque no hay dos secciones iguales delante a la vez. Componer el canon lo
restaura — medido: su primer hijo estampa ya `data-animation-trigger`, igual
que el de pricing.

Los ocho pasan a componer `Section.Header`, que gana `align` para la única
variación real: feature-grid hace llegar el eje de la sección al contenido del
encabezado por contexto (A-84) y article-grid alinea a start. faq conserva su
`marginX="auto"` en el call site. Ninguna parte pública desaparece:
`Pricing.Header` sigue siendo `Pricing.Header`, sólo que ahora hay una
implementación en vez de ocho.

Medido en las ocho previews: 768px de medida (48rem) donde tocaba, faq en sus
736 por su propio centrado, y el alignItems de cada Stack en lo que su block
pide — center en feature-grid, start en article-grid, stretch en el resto.

Gates: svelte-check 72/62 antes y después · vitest src/uix/blocks 36/36 ·
blocks:check 0/19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 68eb8af260
commit ecb23d6a48

@ -1,15 +1,17 @@
<script lang="ts">
/** ArticleGrid.Header — the section's own heading and lead. The app owns the
* words and the level; the block only caps the measure. */
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
/**
* ArticleGrid.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted. ⚠️ This one WAS the drifted copy: it had lost the `Motion`, so its heading
* appeared without the reveal every sibling section had. Composing the canon
* restores it.
*/
import { Section } from '$uix/eidos/components/section';
import type { ArticleGridHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: ArticleGridHeaderProps = $props();
let { children, ...rest }: ArticleGridHeaderProps = $props();
</script>
<Box {maxWidth} {...rest}>
<Stack gap={3} align="start">
{@render children?.()}
</Stack>
</Box>
<Section.Header align="start" {...rest}>
{@render children?.()}
</Section.Header>

@ -1,20 +1,15 @@
<script lang="ts">
/**
* Contact.Header — the section title + description, measure-capped. The app
* supplies a `Heading` (level 2) and a `Text`; their alignment is the app's.
* Contact.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted.
*/
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
import { Section } from '$uix/eidos/components/section';
import type { ContactHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: ContactHeaderProps = $props();
let { children, ...rest }: ContactHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header {...rest}>
{@render children?.()}
</Section.Header>

@ -1,28 +1,15 @@
<script lang="ts">
/** Faq.Header — the section title + description. The app supplies a `Heading`
* (level 2) and a `Text`. */
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
/**
* Faq.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted. The block centres it in its measure.
*/
import { Section } from '$uix/eidos/components/section';
import type { FaqHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: FaqHeaderProps = $props();
let { children, ...rest }: FaqHeaderProps = $props();
</script>
<!--
`marginX="auto"` is what makes the typedoc's «centered» true (A-19). `Box` does
not centre on its own — `maxWidth` rides `--box-max-width` and the margin is a
separate channel — and the root stacks with `align="stretch"`, so once the cap
bites the header anchored at cross-start instead. Same idiom the sibling block
already uses (`content-section-media.svelte`). With this block's own defaults
the cap never bites (a `md` container is narrower than `48rem`), so nothing
moves until a consumer widens the section — which is exactly the case the
promise was written for.
-->
<Box {maxWidth} width="100%" marginX="auto" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header marginX="auto" {...rest}>
{@render children?.()}
</Section.Header>

@ -1,30 +1,21 @@
<script lang="ts">
/**
* FeatureGrid.Header — the section title + description, measure-capped and
* centered by the parent. The app supplies a `Heading` (level 2) and a `Text`;
* their alignment is the app's to choose.
* FeatureGrid.Header — the section's name and its supporting line. It composes
* the canon `Section.Header` (2026-08-19) and adds the one thing that is this
* block's: the section's `align` axis reaches the header's own content through
* context (A-84), so it is set once on the root instead of being repeated on
* the `Heading` and the `Text` the app drops in here.
*/
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
import { Section } from '$uix/eidos/components/section';
import { getFeatureGridContext } from './context';
import type { FeatureGridHeaderProps } from './types';
let { align, maxWidth = '48rem', children, ...rest }: FeatureGridHeaderProps = $props();
let { align, children, ...rest }: FeatureGridHeaderProps = $props();
const grid = getFeatureGridContext();
const resolvedAlign = $derived(align ?? grid?.align ?? 'center');
</script>
<!--
The section's axis reaches the header's own content (A-84): it used to align
only the header BOX, and the type told the app to repeat the alignment on the
`Heading` and the `Text` it drops in here.
-->
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3} align={resolvedAlign}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header align={resolvedAlign} {...rest}>
{@render children?.()}
</Section.Header>

@ -1,26 +1,15 @@
<script lang="ts">
/**
* FeatureSplit.Header — the section title + description, measure-capped. The
* app supplies a `Heading` (level 2) and a `Text`; their alignment is the
* app's to choose, as in `Testimonials.Header` (this block has no `align`
* axis of its own, so there is nothing to inherit).
*
* It exists because a section names itself through its header (A-110): this
* was the only section block of the tier without one, so a three-row section
* contributed three sibling `h2`s and no name of its own.
* FeatureSplit.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted.
*/
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
import { Section } from '$uix/eidos/components/section';
import type { FeatureSplitHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: FeatureSplitHeaderProps = $props();
let { children, ...rest }: FeatureSplitHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header {...rest}>
{@render children?.()}
</Section.Header>

@ -1,18 +1,15 @@
<script lang="ts">
/** Pricing.Header — the section title + description, measure-capped and centered
* by the parent. The app supplies a `Heading` (level 2) and a `Text`. */
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
/**
* Pricing.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted.
*/
import { Section } from '$uix/eidos/components/section';
import type { PricingHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: PricingHeaderProps = $props();
let { children, ...rest }: PricingHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header {...rest}>
{@render children?.()}
</Section.Header>

@ -1,21 +1,15 @@
<script lang="ts">
/**
* Team.Header — the section title + description, measure-capped and placed by
* the parent. The app supplies a `Heading` (level 2) and a `Text`; their own
* alignment is the app's to choose.
* Team.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted.
*/
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
import { Section } from '$uix/eidos/components/section';
import type { TeamHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: TeamHeaderProps = $props();
let { children, ...rest }: TeamHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header {...rest}>
{@render children?.()}
</Section.Header>

@ -1,18 +1,15 @@
<script lang="ts">
/** Testimonials.Header — the section title + description, measure-capped and
* centered by the parent. The app supplies a `Heading` (level 2) and a `Text`. */
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion';
/**
* Testimonials.Header — the section's name and its supporting line. It composes the
* canon `Section.Header` (2026-08-19): eight blocks of the tier shipped the
* same three lines, and one copy had already drifted.
*/
import { Section } from '$uix/eidos/components/section';
import type { TestimonialsHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: TestimonialsHeaderProps = $props();
let { children, ...rest }: TestimonialsHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest}>
<Motion trigger="viewport">
<Stack gap={3}>
{@render children?.()}
</Stack>
</Motion>
</Box>
<Section.Header {...rest}>
{@render children?.()}
</Section.Header>

@ -14,12 +14,12 @@
import Motion from '../motion/motion.svelte';
import type { SectionHeaderProps } from './types';
let { maxWidth = '48rem', children, ...rest }: SectionHeaderProps = $props();
let { maxWidth = '48rem', align, children, ...rest }: SectionHeaderProps = $props();
</script>
<Box {maxWidth} width="100%" {...rest} data-section-header="">
<Motion trigger="viewport">
<Stack gap={3}>
<Stack gap={3} {align}>
{@render children?.()}
</Stack>
</Motion>

@ -3,6 +3,7 @@ import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
import type { ContainerSize } from '../container/types';
import type { BackgroundPatternKind } from '../background/types';
import type { StackProps } from '../stack/types';
export type SectionSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl'>;
@ -43,4 +44,10 @@ export type SectionProps = BoxProps & {
export type SectionHeaderProps = BoxProps & {
/** Measure cap for the header's own copy. @default '48rem' */
maxWidth?: BoxProps['maxWidth'];
/**
* Cross-axis alignment of the header's own content. Unset leaves the stack's
* natural stretch, which is what most sections want; a section whose items
* are centred passes `center` so the header follows them.
*/
align?: StackProps['align'];
};

Loading…
Cancel
Save

Powered by TurnKey Linux.