From 990fc022879260625d077e412b9fed5cfe9c3460 Mon Sep 17 00:00:00 2001 From: Gab Date: Thu, 24 Sep 2026 22:04:46 -0300 Subject: [PATCH 1/3] feat(webkit): move section-title into the marketing category --- .specs/section-title.md | 18 ++++---- .../section-title/SectionTitle.stories.js | 3 +- packages/webkit/.size-limit.json | 4 +- packages/webkit/catalog.json | 16 ++++++-- packages/webkit/package.json | 2 +- .../section-title/section-title.test.ts | 41 ++++++++++++++++++- .../section-title/section-title.vue | 27 ++++++++---- 7 files changed, 84 insertions(+), 27 deletions(-) rename apps/storybook/src/stories/components/{content => marketing}/section-title/SectionTitle.stories.js (99%) rename packages/webkit/src/components/{content => marketing}/section-title/section-title.test.ts (80%) rename packages/webkit/src/components/{content => marketing}/section-title/section-title.vue (83%) diff --git a/.specs/section-title.md b/.specs/section-title.md index 3d6189a2f..09c5547c0 100644 --- a/.specs/section-title.md +++ b/.specs/section-title.md @@ -1,15 +1,15 @@ --- name: section-title -category: content +category: marketing structure: monolithic status: approved -spec_version: 2 -checksum: 3a4b15c27d3dda6af9bda55a10bb9e9db13df6865900f9ef805ae97589696e55 +spec_version: 3 +checksum: 5090beab7c8add1bbd4b5109b284e28671723fb8b34b80fa25a2c2d29c4452ee figma: url: https://www.figma.com/design/QEbHSTFDWfh4VHkBp6NWN3/Azion.com?node-id=7495-24338 node_id: 7495:24338 created: 2026-08-11 -last_updated: 2026-08-11 +last_updated: 2026-09-23 --- # Section Title — Component Spec @@ -26,19 +26,21 @@ The framed header row that opens a page section: an optional overline, the secti ## When NOT to use -- For the page's leading band and its `h1` → use `hero-title` instead. +- For the page's leading band and its `h1` → use `hero` instead. - For the top bar of an application shell → use `global-header` instead. - For a plain framed container with arbitrary content → use `frame-box` instead. ## Related -- `hero-title` — the hero counterpart; renders the page's `h1` at hero scale. +- `hero` — the opening band; its `Hero.Title` renders the page's `h1` at hero scale. - `frame-box` — the frame this component composes. - `section-gap` — the empty frame that sets the air before and after a section header; this component holds no vertical air of its own beyond its padding. - `overline` — the eyebrow treatment rendered above the headline. ## Best practices +- Leave `framed` on when the header is a brick of its own in a page column; turn it off when a band composes the header inside a frame it already draws, so the rule and the padding are not drawn twice. + - Keep one `section-title` per section, and let it own the section's `h2` so the page keeps one document outline. - Write the eyebrow as one or two words: it is set uppercase and prefixed with `//`, so a sentence in it reads as noise. - Reach for `horizontal` when the description is long enough to earn its own column — on a narrow viewport it stacks back under the headline. @@ -74,6 +76,7 @@ import Button from '@aziontech/webkit/button' | `description` | `string` | `''` | false | Supporting sentence under the headline; overridden by the default slot. | | `eyebrow` | `string` | `''` | false | Short uppercase overline rendered above the headline. | | `kind` | `'centered' \| 'left' \| 'horizontal'` | `'centered'` | false | Layout of the header: `centered` stacks and centers the copy, `left` stacks it at the start edge, `horizontal` sets the headline and its description in two columns. | +| `framed` | `boolean` | `true` | false | Draw the header's own frame and padding. Turn it off when the header is composed inside a band that already owns both. | ## Events @@ -90,6 +93,7 @@ import Button from '@aziontech/webkit/button' - Visual states: `default` - `data-kind` mirrors the `kind` prop: `centered` | `left` | `horizontal` +- `data-framed` present when the header draws its own frame and padding ## Motion & Animations @@ -120,7 +124,7 @@ _none_ - Visible focus: not applicable to the header itself; controls composed into the `actions` slot keep their own `focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-canvas)` ring. - Keyboard map: none of its own — `Tab` reaches only the controls placed in the `actions` slot, in DOM order. -- ARIA: the headline is a real `h2` (the page's `h1` belongs to `hero-title`), so no `role` or `aria-label` is added; the frame's rules and corner marks stay `aria-hidden="true"`. In `horizontal` the headline precedes its description in DOM order, so the reading order matches the visual one. +- ARIA: the headline is a real `h2` (the page's `h1` belongs to `Hero.Title`), so no `role` or `aria-label` is added; the frame's rules and corner marks stay `aria-hidden="true"`. In `horizontal` the headline precedes its description in DOM order, so the reading order matches the visual one. - Contrast ≥4.5:1 (text) / ≥3:1 (large + icons): headline on `var(--text-default)`, description on `var(--text-muted)`, both over the page canvas. - `motion-reduce:transition-none motion-reduce:transform-none` — not applicable, the component is static. - Touch target ≥40×40 px — the `actions` row stretches its children to full width below `sm`, so slotted buttons keep their own target size. diff --git a/apps/storybook/src/stories/components/content/section-title/SectionTitle.stories.js b/apps/storybook/src/stories/components/marketing/section-title/SectionTitle.stories.js similarity index 99% rename from apps/storybook/src/stories/components/content/section-title/SectionTitle.stories.js rename to apps/storybook/src/stories/components/marketing/section-title/SectionTitle.stories.js index 6c72b8230..22f25e79e 100644 --- a/apps/storybook/src/stories/components/content/section-title/SectionTitle.stories.js +++ b/apps/storybook/src/stories/components/marketing/section-title/SectionTitle.stories.js @@ -8,7 +8,7 @@ const IMPORT_WITH_BUTTON = [IMPORT, "import Button from '@aziontech/webkit/butto /** @type {import('@storybook/vue3').Meta} */ const meta = { - title: 'Components/Content/SectionTitle', + title: 'Components/Marketing/SectionTitle', component: SectionTitle, tags: ['autodocs'], parameters: { @@ -172,4 +172,3 @@ export const WithActions = { } } } - diff --git a/packages/webkit/.size-limit.json b/packages/webkit/.size-limit.json index dbcbf8e11..05f4dde7e 100644 --- a/packages/webkit/.size-limit.json +++ b/packages/webkit/.size-limit.json @@ -36,8 +36,8 @@ }, { "name": "section-title", - "path": "src/components/content/section-title/section-title.vue", - "limit": "4 KB" + "path": "src/components/marketing/section-title/section-title.vue", + "limit": "3 KB" }, { "name": "hero-title", diff --git a/packages/webkit/catalog.json b/packages/webkit/catalog.json index 039f4a19f..735b9f3ff 100644 --- a/packages/webkit/catalog.json +++ b/packages/webkit/catalog.json @@ -2974,10 +2974,10 @@ }, "section-title": { "import": "@aziontech/webkit/section-title", - "target": "./src/components/content/section-title/section-title.vue", + "target": "./src/components/marketing/section-title/section-title.vue", "kind": "component", "treeShakeableImport": "@aziontech/webkit/section-title", - "category": "content", + "category": "marketing", "structure": "monolithic", "status": "approved", "props": [ @@ -3008,6 +3008,13 @@ "default": "'centered'", "required": "false", "doc": "Layout of the header: `centered` stacks and centers the copy, `left` stacks it at the start edge, `horizontal` sets the headline and its description in two columns." + }, + { + "name": "framed", + "type": "boolean", + "default": "true", + "required": "false", + "doc": "Draw the header's own frame and padding. Turn it off when the header is composed inside a band that already owns both." } ], "slots": [ @@ -3029,17 +3036,18 @@ "For a wide band whose headline and supporting sentence should sit side by side rather than stacked — `kind=\"horizontal\"`." ], "avoidWhen": [ - "For the page's leading band and its `h1` → use `hero-title` instead.", + "For the page's leading band and its `h1` → use `hero` instead.", "For the top bar of an application shell → use `global-header` instead.", "For a plain framed container with arbitrary content → use `frame-box` instead." ], "related": [ - "hero-title` — the hero counterpart; renders the page's `h1` at hero scale.", + "hero` — the opening band; its `Hero.Title` renders the page's `h1` at hero scale.", "frame-box` — the frame this component composes.", "section-gap` — the empty frame that sets the air before and after a section header; this component holds no vertical air of its own beyond its padding.", "overline` — the eyebrow treatment rendered above the headline." ], "bestPractices": [ + "Leave `framed` on when the header is a brick of its own in a page column; turn it off when a band composes the header inside a frame it already draws, so the rule and the padding are not drawn twice.", "Keep one `section-title` per section, and let it own the section's `h2` so the page keeps one document outline.", "Write the eyebrow as one or two words: it is set uppercase and prefixed with `//`, so a sentence in it reads as noise.", "Reach for `horizontal` when the description is long enough to earn its own column — on a narrow viewport it stacks back under the headline.", diff --git a/packages/webkit/package.json b/packages/webkit/package.json index d69e67e99..8eb2fc065 100644 --- a/packages/webkit/package.json +++ b/packages/webkit/package.json @@ -132,7 +132,7 @@ "./card-box": "./src/components/content/card-box/card-box.vue", "./card-pricing": "./src/components/content/card-pricing/card-pricing.vue", "./hero-title": "./src/components/content/hero-title/hero-title.vue", - "./section-title": "./src/components/content/section-title/section-title.vue", + "./section-title": "./src/components/marketing/section-title/section-title.vue", "./item": "./src/components/content/item/index.ts", "./item-root": "./src/components/content/item/item.vue", "./item-group": "./src/components/content/item/item-group.vue", diff --git a/packages/webkit/src/components/content/section-title/section-title.test.ts b/packages/webkit/src/components/marketing/section-title/section-title.test.ts similarity index 80% rename from packages/webkit/src/components/content/section-title/section-title.test.ts rename to packages/webkit/src/components/marketing/section-title/section-title.test.ts index 177937a09..3efaa417c 100644 --- a/packages/webkit/src/components/content/section-title/section-title.test.ts +++ b/packages/webkit/src/components/marketing/section-title/section-title.test.ts @@ -2,13 +2,13 @@ import { composeStories } from '@storybook/vue3' import { render } from '@testing-library/vue' import { describe, expect, it } from 'vitest' -import * as stories from '../../../../../../apps/storybook/src/stories/components/content/section-title/SectionTitle.stories' +import * as stories from '../../../../../../apps/storybook/src/stories/components/marketing/section-title/SectionTitle.stories' import { expectNoA11yViolations } from '../../../test/axe' import SectionTitle from './section-title.vue' const { Default, Kinds, WithActions } = composeStories(stories) -const TESTID = 'content-section-title' +const TESTID = 'marketing-section-title' const props = { title: 'Everything runs at the edge' } @@ -157,4 +157,41 @@ describe('SectionTitle', () => { ]) }) }) + + describe('framed', () => { + it('draws its own frame by default', () => { + const { getByTestId } = render(SectionTitle, { props: { title: 'Platform' } }) + const root = getByTestId(TESTID) + + expect(root).toHaveAttribute('data-framed') + // the frame resolves to FrameBox, which carries the resolved side list + expect(root).toHaveAttribute('data-borders') + }) + + it('renders as a plain block when framed is false', () => { + const { getByTestId } = render(SectionTitle, { + props: { title: 'Platform', framed: false } + }) + const root = getByTestId(TESTID) + + expect(root).not.toHaveAttribute('data-framed') + expect(root).not.toHaveAttribute('data-borders') + expect(root).not.toHaveAttribute('data-marks') + }) + + it('keeps its copy and its heading level unframed', () => { + const { getByRole, getByText } = render(SectionTitle, { + props: { + title: 'Platform', + eyebrow: 'Build', + description: 'One sentence.', + framed: false + } + }) + + expect(getByRole('heading', { level: 2, name: 'Platform' })).toBeInTheDocument() + expect(getByText('Build')).toBeInTheDocument() + expect(getByText('One sentence.')).toBeInTheDocument() + }) + }) }) diff --git a/packages/webkit/src/components/content/section-title/section-title.vue b/packages/webkit/src/components/marketing/section-title/section-title.vue similarity index 83% rename from packages/webkit/src/components/content/section-title/section-title.vue rename to packages/webkit/src/components/marketing/section-title/section-title.vue index 97030a05f..54e63777e 100644 --- a/packages/webkit/src/components/content/section-title/section-title.vue +++ b/packages/webkit/src/components/marketing/section-title/section-title.vue @@ -21,12 +21,15 @@ eyebrow?: string /** Layout of the header: `centered` stacks and centers the copy, `left` stacks it at the start edge, `horizontal` sets the headline and its description in two columns. */ kind?: SectionTitleKind + /** Draw the header's own frame and padding. Turn it off when the header is composed inside a band that already owns both. */ + framed?: boolean } const props = withDefaults(defineProps(), { description: '', eyebrow: '', - kind: 'centered' + kind: 'centered', + framed: true }) const slots = defineSlots<{ @@ -37,12 +40,19 @@ const attrs = useAttrs() const testId = computed( - () => (attrs['data-testid'] as string | undefined) ?? 'content-section-title' + () => (attrs['data-testid'] as string | undefined) ?? 'marketing-section-title' ) const hasDescription = computed( () => Boolean(slots.default) || props.description.length > 0 ) + + // Unframed, the header is a plain block: the band composing it owns the rule and the + // padding, so drawing either here would draw it twice. + const frame = computed(() => (props.framed ? FrameBox : 'div')) + const frameProps = computed(() => + props.framed ? { flush: true, borders: 'y', marks: 'bottom' } : {} + )