From b243396f096094d7d3d87d758dcbb8d2a0d02532 Mon Sep 17 00:00:00 2001 From: Gab Date: Thu, 24 Sep 2026 22:04:45 -0300 Subject: [PATCH 1/2] feat(webkit): add the section-module marketing component --- .specs/section-module.md | 149 +++++++++++++++ .../section-module/SectionModule.stories.js | 178 ++++++++++++++++++ packages/webkit/.size-limit.json | 5 + packages/webkit/catalog.json | 94 +++++++++ packages/webkit/package.json | 3 +- .../section-module/section-module.test.ts | 83 ++++++++ .../section-module/section-module.vue | 87 +++++++++ 7 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 .specs/section-module.md create mode 100644 apps/storybook/src/stories/components/marketing/section-module/SectionModule.stories.js create mode 100644 packages/webkit/src/components/marketing/section-module/section-module.test.ts create mode 100644 packages/webkit/src/components/marketing/section-module/section-module.vue diff --git a/.specs/section-module.md b/.specs/section-module.md new file mode 100644 index 000000000..7a9a79d85 --- /dev/null +++ b/.specs/section-module.md @@ -0,0 +1,149 @@ +--- +name: section-module +category: marketing +structure: monolithic +status: approved +spec_version: 1 +checksum: 9d1d5387f6f8ac5c222b7c2a1bc1ff36fe7e502e93c63dabab895ebc0b342c43 +style_seam: true +created: 2026-09-23 +last_updated: 2026-09-23 +--- + +# Section Module — Component Spec + +## Purpose + +The brick of the page column: a section whose header row is divided from its body by a hairline and which is divided from the module above it by another. Stacked inside a `section-container`, a run of modules reads as one continuous frame, because each module draws only its own top rule and hands its sides to the column. It is where a band's header and a band's content are joined, so no page has to re-draw that join. + +## When to use + +- For every band stacked inside a `section-container`. +- Whenever a band needs a header row divided from its content by a rule. +- For a body-only band, by passing no title and no header slot. + +## When NOT to use + +- For the full-bleed band at the top of a page → use `hero`. +- For the column the modules stack inside → use `section-container`. +- For the header alone, with no body → use `section-title`, which this module composes. +- For a hairline grid of cells → use `card-grid` inside this module's body. + +## Related + +- `section-container` — the framed column this module stacks inside. +- `section-title` — the header row this module renders by default. +- `card-grid` — the edge-to-edge cell grid a module's body commonly holds. +- `frame-box` — the registration frame a body wraps itself in. + +## Best practices + +- Pass `divided` as `false` on the first module in a column: its top edge is already the hero's bottom rule, and drawing its own would put two hairlines on one pixel. +- Pass `padded` as `false` when the body is an edge-to-edge `card-grid`, so the grid's rules meet the frame with no gutter. +- Use `title` and `eyebrow` for the ordinary header, and reach for the `header` slot only when the row needs markup the default header cannot express. + +## Usage + +```vue + + + +``` + +## Props + +| Prop | Type | Default | Required | JSDoc | +|---|---|---|---|---| +| `title` | `string` | `''` | false | Headline of the module's header row, rendered as its `h2`. | +| `description` | `string` | `''` | false | Supporting sentence under the headline. | +| `eyebrow` | `string` | `''` | false | Short uppercase overline rendered above the headline. | +| `kind` | `SectionModuleKind` | `'left'` | false | Layout of the default header row. | +| `divided` | `boolean` | `true` | false | Draw the top rule that divides this module from the one above it. | +| `padded` | `boolean` | `true` | false | Pad the module's body. Leave off for an edge-to-edge grid that owns its cell padding. | + +## Events + +| _none_ | — | — | + +## Slots + +| Slot | Scope | Notes | +|---|---|---| +| `default` | — | The module's body. | +| `header` | — | Replaces the default header row entirely. | +| `actions` | — | Trailing controls inside the default header row. | + +## States + +- Visual states: `default` +- `data-kind` carries the header layout +- `data-divided` present when the top rule is drawn +- `data-padded` present when the body is padded + +## Motion & Animations + +_none_ + +## Tokens + +| Region | Token (DESIGN.md) | +|---|---| +| module rule | `var(--border-default)` | +| body padding | `var(--spacing-xl)` | + +## Theme gaps + +| Figma variable | Temporary primitive | Follow-up | +|---|---|---| +| _none_ | — | — | + +## Accessibility (WCAG 2.1 AA) + +- Visible focus: not applicable — the module is a container; controls composed into it keep their own `focus-visible:ring-2 focus-visible:ring-(--ring-color)` ring. +- Keyboard map: none — the module is not focusable; `Tab` order is decided entirely by the slotted content. +- ARIA: the root renders a `section` and adds no role; the header's heading level comes from `section-title`, so a page's headings stay in order. +- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons) — the rules are non-informational decoration and the header and body own their own contrast. +- `motion-reduce:transition-none motion-reduce:transform-none` — not applicable, the component is static. +- Touch target ≥40×40 px — not applicable, no interactive control of its own. + +## Stories (Storybook) + +- Default +- Kinds — composite story rendering the header layouts side-by-side (justified: the layouts differ only in where the copy sits, which is legible only in comparison) +- Divided — a stack of two modules with the first undivided (justified: a shared edge is invisible on a single module, and the story is what proves the rule is drawn once) +- Padded — the module with an edge-to-edge body (mutually-exclusive boolean state of the `padded` prop) + +## Constraints — DO NOT + + + +- Do not add props beyond the Props table above. If you need a prop that is not listed, emit `BLOCKED: missing prop ` and stop — do not invent. +- Do not add events beyond the Events table above. Same rule for slots and sub-components. +- Do not invent imports. Every `@aziontech/webkit/*` path must exist in `packages/webkit/package.json#exports`. Every relative import must resolve to a real file. Every npm package must be installed. +- Do not use HEX/RGB/HSL colors, Tailwind palette names (e.g. `bg-blue-500`), raw typography classes (e.g. `text-sm`), `any`, `@ts-ignore`, or `class` inside `defineProps`. +- Do not install or import positioning/animation libraries (`@floating-ui/*`, `popper.js`, `tippy.js`, `gsap`, `framer-motion`, `motion`, `@vueuse/motion`, `@formkit/auto-animate`, drag-drop runtimes, scroll virtualization libs). Use CSS + Vue primitives (``, ``). See `.claude/rules/dependencies.md`. +- Do not improvise animations. Every `animate-*` / `transition-*` class must come from `packages/theme/src/tokens/semantic/animations.js`; every motion-bearing class pairs with `motion-reduce:*` on the same class string; no component-local `@keyframes`. +- Do not create class presets in JavaScript (`const kindClasses = {...}`, `const sharedClasses = [...]`, `const sizeClasses = {...}`, `const rootClasses = computed(...)`). Variants live on `data-*` attributes consumed by Tailwind `data-[attr=value]:`. All utilities live inline on the root element's `class` attribute. No `