From 1e4fa569981815f8fd268492b2b512ba5482e869 Mon Sep 17 00:00:00 2001 From: gablisboa Date: Wed, 27 May 2026 17:59:10 -0300 Subject: [PATCH 01/11] feat(navigation): refine menu-item interaction and docs story Align menu-item focus and icon state behavior with selected/default semantics and simplify story coverage to the essential states. Co-authored-by: Cursor --- .specs/menu-item.md | 4 +- .../webkit/navigation/MenuItem.stories.js | 136 +++++------------- .../navigation/menu-item/menu-item.vue | 67 ++++++--- 3 files changed, 86 insertions(+), 121 deletions(-) diff --git a/.specs/menu-item.md b/.specs/menu-item.md index 3a596b462..2d9bee1fb 100644 --- a/.specs/menu-item.md +++ b/.specs/menu-item.md @@ -7,9 +7,9 @@ spec_version: 1 figma: url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=3601-2693 node_id: 3601:2693 -checksum: 9142b6a3366d26151037777ebb01398851c84803f92a1b4c79631c75b0265043 +checksum: d084e89a360fad2914c4137f7e020e00588d18792b843d811637ad01edb1d2b6 created: 2026-05-22 -last_updated: 2026-05-22 +last_updated: 2026-05-27 --- # Menu Item — Component Spec diff --git a/apps/storybook/src/stories/webkit/navigation/MenuItem.stories.js b/apps/storybook/src/stories/webkit/navigation/MenuItem.stories.js index 133266f9c..fe0622bae 100644 --- a/apps/storybook/src/stories/webkit/navigation/MenuItem.stories.js +++ b/apps/storybook/src/stories/webkit/navigation/MenuItem.stories.js @@ -1,5 +1,4 @@ import MenuItem from '@aziontech/webkit/navigation/menu-item' -import { expect, userEvent, within } from '@storybook/test' /** @type {import('@storybook/vue3').Meta} */ const meta = { @@ -7,7 +6,7 @@ const meta = { component: MenuItem, tags: ['autodocs'], parameters: { - layout: 'padded', + layout: 'centered', backgrounds: { default: 'dark' }, a11y: { config: { @@ -20,7 +19,7 @@ const meta = { docs: { description: { component: - 'Sidebar navigation row (option) or section overline (group). Maps to Figma Webkit MenuItem (node 3601:2693). Use inside `SidebarGroup` lists or standalone for compact nav patterns.' + 'Sidebar navigation row (`option`) or section overline (`group`). Supports optional icon, optional link rendering when `href` is set, and optional trailing tag.' } } }, @@ -31,7 +30,7 @@ const meta = { description: 'Structural variant: navigable row or section overline label.', table: { type: { summary: 'MenuItemKind' }, - defaultValue: { summary: 'option' }, + defaultValue: { summary: "'option'" }, category: 'props' } }, @@ -40,7 +39,7 @@ const meta = { description: 'Visible label for the row or group header.', table: { type: { summary: 'string' }, - defaultValue: { summary: 'Option 1' }, + defaultValue: { summary: "'Option 1'" }, category: 'props' } }, @@ -49,7 +48,7 @@ const meta = { description: 'When true, applies the selected surface on option rows.', table: { type: { summary: 'boolean' }, - defaultValue: { summary: false }, + defaultValue: { summary: 'false' }, category: 'props' } }, @@ -58,7 +57,7 @@ const meta = { description: 'Disables interaction on option rows.', table: { type: { summary: 'boolean' }, - defaultValue: { summary: false }, + defaultValue: { summary: 'false' }, category: 'props' } }, @@ -67,7 +66,7 @@ const meta = { description: 'PrimeIcons class for the leading icon (option kind only).', table: { type: { summary: 'string' }, - defaultValue: { summary: 'pi pi-home' }, + defaultValue: { summary: "'pi pi-home'" }, category: 'props' } }, @@ -76,7 +75,7 @@ const meta = { description: 'Destination URL; renders an anchor when set on option rows.', table: { type: { summary: 'string' }, - defaultValue: { summary: '' }, + defaultValue: { summary: "''" }, category: 'props' } }, @@ -86,7 +85,7 @@ const meta = { description: 'Link target when `href` is set.', table: { type: { summary: "'_self' | '_blank'" }, - defaultValue: { summary: '_self' }, + defaultValue: { summary: "'_self'" }, category: 'props' } }, @@ -104,7 +103,7 @@ const meta = { description: 'Severity token for the optional trailing Tag.', table: { type: { summary: 'MenuItemTagSeverity' }, - defaultValue: { summary: 'info' }, + defaultValue: { summary: "'info'" }, category: 'props' } }, @@ -140,8 +139,7 @@ const meta = { }, decorators: [ () => ({ - template: - '
' + template: '
' }) ] } @@ -153,25 +151,35 @@ const Template = (args) => ({ setup() { return { args } }, - template: '' + template: '' }) /** @type {import('@storybook/vue3').StoryObj} */ export const Default = { render: Template, parameters: { - docs: { description: { story: 'Default option row (Figma Type=Option, State=Default).' } } + docs: { description: { story: 'Default option row.' } } } } -export const Selected = { - args: { selected: true }, - render: Template, +/** @type {import('@storybook/vue3').StoryObj} */ +export const States = { + render: () => ({ + components: { MenuItem }, + template: ` +
    + + + +
+ ` + }), parameters: { - docs: { description: { story: 'Selected option with raised surface (Figma Selected=true).' } } + docs: { description: { story: 'Default, selected, and disabled option states.' } } } } +/** @type {import('@storybook/vue3').StoryObj} */ export const WithTag = { args: { tagValue: 'Label', tagSeverity: 'info' }, render: Template, @@ -180,90 +188,22 @@ export const WithTag = { } } +/** @type {import('@storybook/vue3').StoryObj} */ export const Group = { - args: { kind: 'group', label: 'Label Group' }, - decorators: [ - () => ({ - template: '
' - }) - ], - render: Template, - parameters: { - docs: { description: { story: 'Section overline label (Figma Type=Group).' } } - } -} - -export const Disabled = { - args: { disabled: true }, - render: Template, - parameters: { - docs: { description: { story: 'Disabled option — no interaction, muted tokens.' } } - } -} - -export const LightDark = { - parameters: { - layout: 'fullscreen', - docs: { - description: { - story: 'MenuItem option and group on light and dark canvas backgrounds.' - } - } - }, render: () => ({ components: { MenuItem }, template: ` -
-
-
    - - - -
-
-
-
    - - - -
-
-
+
    + + + + + + +
` - }) -} - -export const Accessibility = { - args: { href: '#home' }, - render: Template, + }), parameters: { - docs: { - description: { - story: - 'Keyboard: Tab focuses the link; Enter activates. Screen reader: current page announced when selected.' - } - } - }, - play: async ({ canvasElement, args, step }) => { - const canvas = within(canvasElement) - await step('Tab focuses the menu link', async () => { - await userEvent.tab() - expect(canvas.getByRole('link')).toHaveFocus() - }) - await step('Enter triggers click', async () => { - await userEvent.keyboard('{Enter}') - expect(args.onClick).toHaveBeenCalled() - }) - } -} - -export const Playground = { - render: Template, - parameters: { - docs: { - description: { - story: 'Drive every prop from the Controls panel.' - } - } + docs: { description: { story: 'Group header with five option rows.' } } } } diff --git a/packages/webkit/src/components/navigation/menu-item/menu-item.vue b/packages/webkit/src/components/navigation/menu-item/menu-item.vue index 2e522fc31..34330c062 100644 --- a/packages/webkit/src/components/navigation/menu-item/menu-item.vue +++ b/packages/webkit/src/components/navigation/menu-item/menu-item.vue @@ -64,6 +64,13 @@ const attrs = useAttrs() const slots = useSlots() + const forwardedAttrs = computed(() => { + const rest = { ...attrs } + delete rest.class + delete rest['data-testid'] + return rest + }) + const testId = computed( () => (attrs['data-testid'] as string | undefined) ?? 'navigation-menu-item' ) @@ -75,33 +82,49 @@ () => isOption.value && (Boolean(props.tagValue) || Boolean(slots['tag'])) ) - const sharedRowClasses = [ - 'relative flex w-full shrink-0 items-center', - 'rounded-[var(--shape-button)] transition-colors motion-reduce:transition-none', - 'pl-[var(--spacing-xxs)] pr-[var(--spacing-xs)] py-[var(--spacing-xxs)]', + const listItemClasses = computed(() => cn('relative w-full shrink-0', attrs.class)) + + const focusRingClasses = [ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring-color)]', - 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-canvas)]' + 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--menu-item-ring-offset,var(--bg-canvas))]' ] - const optionRowClasses = computed(() => + const interactiveClasses = computed(() => cn( - sharedRowClasses, - 'h-9 gap-[var(--spacing-xs)]', + 'group relative flex h-8 w-full shrink-0 items-center', + 'gap-[var(--spacing-xs)] rounded-[var(--shape-elements)]', + 'pl-[var(--spacing-xxs)] pr-[var(--spacing-xs)] py-[var(--spacing-xxs)]', + 'transition-colors motion-reduce:transition-none', + focusRingClasses, props.selected - ? 'bg-[var(--bg-surface-raised)] text-[var(--text-default)]' - : 'text-[var(--text-default)] hover:bg-[var(--bg-hover)]', - props.disabled && 'pointer-events-none text-[var(--text-disabled)] hover:bg-transparent', - attrs.class + ? 'bg-[var(--bg-selected)] text-[var(--text-default)]' + : 'text-[var(--text-default)] hover:bg-[var(--bg-hover)] focus-visible:bg-[var(--bg-hover)]', + props.disabled && + 'pointer-events-none text-[var(--text-disabled)] hover:bg-transparent focus-visible:bg-transparent' ) ) const groupRowClasses = computed(() => - cn(sharedRowClasses, 'h-7 text-[var(--text-muted)]', attrs.class) + cn( + 'relative flex h-7 w-full shrink-0 items-center', + 'rounded-[var(--shape-elements)] pl-[var(--spacing-xxs)] pr-[var(--spacing-xs)] py-[var(--spacing-xxs)]', + 'text-[var(--text-muted)]', + attrs.class + ) ) const iconBoxClasses = 'flex size-8 shrink-0 items-center justify-center overflow-hidden' - const iconClasses = 'size-4 shrink-0 leading-none text-[length:inherit]' + const iconClasses = computed(() => + cn( + 'size-4 shrink-0 leading-none text-[length:inherit]', + props.disabled + ? 'text-[var(--text-disabled)]' + : props.selected + ? 'text-[var(--text-default)]' + : 'text-[var(--text-muted)] group-hover:text-[var(--text-default)] group-focus-visible:text-[var(--text-default)]' + ) + ) const handleClick = (event: MouseEvent) => { if (props.disabled || isGroup.value) { @@ -116,7 +139,8 @@ From 201badad138d95bb676efc2bcb70d415404b102c Mon Sep 17 00:00:00 2001 From: gablisboa Date: Wed, 27 May 2026 17:59:19 -0300 Subject: [PATCH 02/11] feat(layout): update sidebar slot regions and profile-footer stories Move sidebar spacing ownership to slot content and refresh Storybook variants to cover content-only, header search, and profile footer layouts from the latest design. Co-authored-by: Cursor --- .specs/sidebar.md | 4 +- .../stories/webkit/layout/Sidebar.stories.js | 295 ++++++------------ .../webkit/templates/PlatformShell.stories.js | 6 +- .../src/components/layout/sidebar/sidebar.vue | 9 +- 4 files changed, 106 insertions(+), 208 deletions(-) diff --git a/.specs/sidebar.md b/.specs/sidebar.md index 28cae16d1..c089afd08 100644 --- a/.specs/sidebar.md +++ b/.specs/sidebar.md @@ -7,9 +7,9 @@ spec_version: 1 figma: url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=3735-14866 node_id: 3735:14866 -checksum: 3e8dacf24608d93e15664ef7d188cf85d3212c3118e11979ad6439f3800949c8 +checksum: 8c3214deb0f77fbc7d4e5ec6888f0cee4054b866cee999ce6b048f00a251be30 created: 2026-05-22 -last_updated: 2026-05-22 +last_updated: 2026-05-27 --- # Sidebar — Component Spec diff --git a/apps/storybook/src/stories/webkit/layout/Sidebar.stories.js b/apps/storybook/src/stories/webkit/layout/Sidebar.stories.js index 290b9b734..526a55cc9 100644 --- a/apps/storybook/src/stories/webkit/layout/Sidebar.stories.js +++ b/apps/storybook/src/stories/webkit/layout/Sidebar.stories.js @@ -1,31 +1,23 @@ +import Avatar from '@aziontech/webkit/content/avatar' import InputText from '@aziontech/webkit/inputs/input-text' -import MenuItem from '@aziontech/webkit/navigation/menu-item' import Sidebar from '@aziontech/webkit/layout/sidebar' import SidebarFooter from '@aziontech/webkit/layout/sidebar-footer' import SidebarGroup from '@aziontech/webkit/layout/sidebar-group' -import { expect, userEvent, within } from '@storybook/test' +import MenuItem from '@aziontech/webkit/navigation/menu-item' /** @type {import('@storybook/vue3').Meta} */ const meta = { title: 'Webkit/Layout/Sidebar', component: Sidebar, - subcomponents: { SidebarGroup, SidebarFooter, MenuItem }, + subcomponents: { SidebarGroup, SidebarFooter, MenuItem, Avatar, InputText }, tags: ['autodocs'], parameters: { layout: 'fullscreen', backgrounds: { default: 'dark' }, - a11y: { - config: { - rules: [ - { id: 'color-contrast', enabled: true }, - { id: 'focus-order-semantics', enabled: true } - ] - } - }, docs: { description: { component: - 'Composable application sidebar: `header` (search), scrollable groups (`SidebarGroup scroll`), optional `footer` (hidden when empty). Default story mirrors Console `getMenuItens()`.' + 'Composable application sidebar with slot-driven regions. By default, only navigation content is rendered; spacing is controlled by region content.' } } }, @@ -38,21 +30,6 @@ const meta = { defaultValue: { summary: 'Sidebar' }, category: 'props' } - }, - header: { - description: 'Top region — typically search (`InputText`).', - control: false, - table: { type: { summary: 'VNode' }, category: 'slots' } - }, - default: { - description: 'Navigation region — wrap sections in ``.', - control: false, - table: { type: { summary: 'VNode' }, category: 'slots' } - }, - footer: { - description: 'Bottom region (pinned below scroll). Hidden when the slot is empty.', - control: false, - table: { type: { summary: 'VNode' }, category: 'slots' } } }, args: { @@ -62,203 +39,119 @@ const meta = { export default meta -const sidebarTemplate = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const defaultMenu = ` + + + + + + + + + ` -const Template = (args) => ({ - components: { Sidebar, SidebarGroup, MenuItem, InputText }, +const ContentOnlyTemplate = (args) => ({ + components: { Sidebar, SidebarGroup, MenuItem }, setup() { return { args } }, - template: sidebarTemplate + template: ` + + + ${defaultMenu} + + + ` }) -/** @type {import('@storybook/vue3').StoryObj} */ -export const Default = { - render: Template, - parameters: { - docs: { - description: { - story: - 'Application menu template aligned with Console `getMenuItens()` (Home, Marketplace, Domains, Build, Secure, Store, Deploy, Observe, Tools, Edge Libraries, Marketplace Products).' - } - } - } -} +const WithHeaderTemplate = (args) => ({ + components: { Sidebar, SidebarGroup, MenuItem, InputText }, + setup() { + return { args } + }, + template: ` + + + + ${defaultMenu} + + + ` +}) -export const WithFooter = { - render: (args) => ({ - components: { Sidebar, SidebarGroup, SidebarFooter, MenuItem, InputText }, - setup() { - return { args } - }, - template: ` - - + + ` +}) -export const WithoutSearch = { - render: (args) => ({ - components: { Sidebar, SidebarGroup, MenuItem }, - setup() { - return { args } - }, - template: ` - - - - - - - - - ` - }), +/** @type {import('@storybook/vue3').StoryObj} */ +export const Default = { + render: ContentOnlyTemplate, parameters: { - docs: { description: { story: 'Sidebar without the header search slot.' } } + docs: { description: { story: 'Content-only sidebar (no header and no footer).' } } } } -export const LightDark = { - parameters: { - layout: 'fullscreen', - docs: { - description: { - story: 'Sidebar template on light and dark canvas backgrounds.' - } - } - }, - render: () => ({ - components: { Sidebar, SidebarGroup, MenuItem, InputText }, - template: ` -
-
- ${sidebarTemplate.replace('v-bind="args"', 'aria-label="Application light" class="h-screen w-[280px]"')} -
-
- ${sidebarTemplate.replace('v-bind="args"', 'aria-label="Application dark" class="h-screen w-[280px]"')} -
-
- ` - }) -} - -export const Accessibility = { - render: Template, +/** @type {import('@storybook/vue3').StoryObj} */ +export const WithHeaderSearch = { + render: WithHeaderTemplate, parameters: { - docs: { - description: { - story: - 'Keyboard: Tab moves through search and menu links. Landmark: aside + nav structure for screen readers.' - } - } - }, - play: async ({ canvasElement, step }) => { - const canvas = within(canvasElement) - await step('Tab reaches the first menu link', async () => { - await userEvent.tab() - await userEvent.tab() - const home = canvas.getByRole('link', { name: 'Home' }) - expect(home).toHaveFocus() - }) + docs: { description: { story: 'Adds the header region with a search input.' } } } } -export const Playground = { - render: Template, +/** @type {import('@storybook/vue3').StoryObj} */ +export const WithHeaderAndProfileFooter = { + render: WithHeaderAndFooterTemplate, parameters: { docs: { description: { - story: 'Interactive sidebar template — adjust `ariaLabel` via controls.' + story: + 'Adds header search and a profile footer menu layout based on Figma node `4153:15282`.' } } } diff --git a/apps/storybook/src/stories/webkit/templates/PlatformShell.stories.js b/apps/storybook/src/stories/webkit/templates/PlatformShell.stories.js index 29cc142d4..8969c739b 100644 --- a/apps/storybook/src/stories/webkit/templates/PlatformShell.stories.js +++ b/apps/storybook/src/stories/webkit/templates/PlatformShell.stories.js @@ -98,9 +98,11 @@ export default meta const navigationTemplate = ` - + diff --git a/packages/webkit/src/components/layout/sidebar/sidebar.vue b/packages/webkit/src/components/layout/sidebar/sidebar.vue index fc1120db1..9aa4e51c5 100644 --- a/packages/webkit/src/components/layout/sidebar/sidebar.vue +++ b/packages/webkit/src/components/layout/sidebar/sidebar.vue @@ -34,11 +34,14 @@ const rootClasses = computed(() => cn( - 'flex h-full min-h-0 w-full min-w-0 flex-col gap-[var(--spacing-md)]', - 'border-r border-[var(--border-muted)] bg-[var(--bg-surface)] p-[var(--spacing-md)]', + 'flex h-full min-h-0 w-full min-w-0 flex-col', + 'border-r border-[var(--border-muted)] bg-[var(--bg-surface)]', attrs.class ) ) + + const navClasses = + 'flex h-full min-h-0 flex-1 flex-col overflow-hidden [--menu-item-ring-offset:var(--bg-surface)]'