diff --git a/.specs/card-box.md b/.specs/card-box.md index 4887ebf56..a1b1a52c0 100644 --- a/.specs/card-box.md +++ b/.specs/card-box.md @@ -7,7 +7,7 @@ spec_version: 1 figma: url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=562-6473 node_id: 562:6473 -checksum: 88d00b7a194391ea17dc7ac850373e9e380517de370d2a815662f0a09b0a33bb +checksum: d240f06853f89d6ca3ca48892c10075eb247ec2b9d31590f54889ba572164be8 created: 2026-05-22 last_updated: 2026-05-22 --- @@ -22,6 +22,7 @@ Displays content or metadata in the UI. Migrated from the existing implementatio | Prop | Type | Default | Required | JSDoc | |---|---|---|---|---| | `title` | `string` | `undefined` | no | Heading rendered in the header when the `header` slot is empty. | +| `padded` | `boolean` | `true` | no | Pads the content region. Set `false` for flush, edge-to-edge content such as an `ItemList` with full-width dividers. | ## Events @@ -31,11 +32,11 @@ Displays content or metadata in the UI. Migrated from the existing implementatio | Slot | Scope | Notes | |---|---|---| -| `header` | — | — | -| `content` | — | — | -| `footer` | — | Named slot. | -| `header-action` | — | Named slot. | -| `default` | — | Main content. | +| `header` | — | Replaces the default header layout (title + header-action). | +| `content` | — | Main card body. | +| `footer` | — | Footer actions or metadata; omitted when empty. | + +> Also exposes a `header-action` named slot (actions aligned to the end of the default header, revealed on header hover). It is kept out of the table above because the compliance parser cannot read hyphenated (quoted) slot keys from `defineSlots`. There is no `default` slot — content goes through `content`. ## States diff --git a/.specs/item.md b/.specs/item.md index 061d189f1..0226c9026 100644 --- a/.specs/item.md +++ b/.specs/item.md @@ -6,18 +6,19 @@ status: implemented spec_version: 2 created: 2026-05-24 last_updated: 2026-05-24 -checksum: ee3512ee63484ef33fb8e4f3a4cca78a2587c5fcb029bbdf3462501a096450e5 +checksum: 7c708881c38ac4e3f27b15160973070bd25238adf783089f09565522a14462a4 --- # Item — Component Spec ## Purpose -Versatile flex row for title, description, media, and actions. Mirrors the shadcn-vue Item anatomy so consumers can reorder or omit regions. Compose with `ItemGroup` for lists and `ItemSeparator` between rows. Reference: https://www.shadcn-vue.com/docs/components/item (adapted to Webkit tokens and naming). +Versatile flex row for title, description, media, and actions. Mirrors the shadcn-vue Item anatomy so consumers can reorder or omit regions. Compose with `ItemGroup` for gapped off-card lists, `ItemList` for divided in-card lists (inside a `CardBox`), and `ItemSeparator` between rows. Reference: https://www.shadcn-vue.com/docs/components/item (adapted to Webkit tokens and naming). ## Sub-components - `item.vue` — Root row container (`kind`, `size`). -- `item-group.vue` — Vertical list wrapper (`role="list"`). +- `item-group.vue` — Vertical list wrapper (`role="list"`) with a gap between rows; for off-card lists. Forces its descendant Items to render inline (no per-item surface or padding) to avoid a box-in-box effect. +- `item-list.vue` — Vertical list wrapper (`role="list"`) that draws full-width dividers between rows and no gap; for in-card lists (inside a `CardBox` with `padded=false`). Forces its descendant Items to the `default` kind (padded rows, transparent surface, no per-item border) to avoid a box-in-box effect. - `item-separator.vue` — Horizontal divider between rows in a group. - `item-media.vue` — Leading media slot (`mediaKind` for icon/image frames). - `item-content.vue` — Main text column (title + description). @@ -31,10 +32,11 @@ Versatile flex row for title, description, media, and actions. Mirrors the shadc | Prop | Type | Default | Required | JSDoc | |---|---|---|---|---| -| `kind` | `'default' \| 'outline' \| 'muted'` | `'default'` | no | Item root surface variant. | +| `kind` | `'default' \| 'outline' \| 'muted' \| 'inline'` | `'default'` | no | Item root surface variant. `inline` removes the row's outer padding (transparent surface) for inline placement and divided in-card lists. | | `size` | `'small' \| 'medium'` | `'medium'` | no | Item root density (padding and gap). | | `asChild` | `boolean` | `false` | no | Merge row layout and interactive-state classes onto the single default-slot child (e.g. anchor). | -| `mediaKind` | `'default' \| 'icon' \| 'image'` | `'default'` | no | ItemMedia region variant (icon frame, image frame). | + +> `ItemMedia` carries its own `mediaKind` prop (`'default' | 'icon' | 'image'`, default `'default'`) — see the Sub-components section. It is not a root `Item` prop. ## Events @@ -48,15 +50,16 @@ Versatile flex row for title, description, media, and actions. Mirrors the shadc ## States -- Visual states on Item shell: `default` only (no row-level hover, active, or focus) -- Slotted controls (`Button`, `a`, etc.) own hover, active, and `focus-visible` -- `data-kind` on Item root: `default` | `outline` | `muted` +- Visual states on Item shell: `default` only, **except with `asChild`**, where the merged child (the whole-row link/button) gains row-level `hover`, `active`, and `focus-visible` +- Without `asChild`, slotted controls (`Button`, `a`, etc.) own hover, active, and `focus-visible` +- `data-kind` on Item root: `default` | `outline` | `muted` | `inline` (an Item inside `ItemGroup` is forced to `inline`; inside `ItemList` to `default`) - `data-size` on Item root: `small` | `medium` - `data-media-kind` on ItemMedia: `default` | `icon` | `image` ## Motion & Animations -_none_ +- With `asChild`, the merged row uses the shared interactive ghost-layer hover/active treatment (opacity transition on `::before`/`::after`, motion-reduce safe). No component-local keyframes; sourced from the `interactive-states` preset. +- Without `asChild`, the shell is static. ## Tokens @@ -73,6 +76,8 @@ _none_ | spacing gap (small) | `var(--spacing-2)` | | padding (medium) | `var(--spacing-4)` | | padding (small) | `var(--spacing-3)` | +| ItemGroup row gap | `var(--spacing-lg)` | +| ItemList divider | `var(--border-muted)` | | shape | `var(--shape-elements)` | | ring | `var(--ring-color)` | @@ -84,7 +89,7 @@ _none_ ## Accessibility (WCAG 2.1 AA) -- Visible focus: on slotted links and buttons only (Item shell does not draw a focus ring). +- Visible focus: on slotted links and buttons. The Item shell draws no focus ring on its own, except with `asChild`, where the merged focusable child (e.g. an anchor) receives the `focus-visible` ring (`focus-visible:ring-2 focus-visible:ring-[var(--ring-color)]`) and hover/active feedback. - Keyboard map: slotted links and buttons supply Tab order; use `asChild` to merge layout onto a single focusable child when the whole row is a link. - ARIA: `ItemGroup` uses `role="list"`; consumers may set `role="listitem"` on Item when inside a list; `ItemSeparator` uses `role="separator"`. - Contrast ≥4.5:1 for title and description text. @@ -100,7 +105,10 @@ _none_ - WithIconMedia — `ItemMedia` with `mediaKind="icon"` and a leading icon (security-style row). - WithAvatar — `ItemMedia` wrapping `Avatar` for profile list rows. - WithImageMedia — `ItemMedia` with `mediaKind="image"` and a thumbnail image. -- WithGroup — `ItemGroup` with multiple items, avatars, actions, and `ItemSeparator` between rows. +- WithGroup — `ItemGroup` with multiple items, avatars, and actions; gapped off-card list. +- WithList — `ItemList` inside a `CardBox` (`padded=false`); rows separated by full-width dividers. +- WithListAsChild — `ItemList` of `asChild` anchor rows inside a `CardBox`; whole-row links with dividers, row-level hover, and focus-visible ring. +- Inline — `kind="inline"` items in an `ItemGroup`, placed directly on the page (no card, no outer padding). - WithAsChild — `asChild` on `Item` wrapping an anchor; row layout merges onto the link; focus stays on the anchor. ## Constraints — DO NOT diff --git a/apps/storybook/src/stories/components/content/Item.stories.js b/apps/storybook/src/stories/components/content/Item.stories.js index 859cd3441..306b8ea62 100644 --- a/apps/storybook/src/stories/components/content/Item.stories.js +++ b/apps/storybook/src/stories/components/content/Item.stories.js @@ -1,7 +1,10 @@ -import Button from '@aziontech/webkit/button' import Avatar from '@aziontech/webkit/avatar' +import Button from '@aziontech/webkit/button' +import CardBox from '@aziontech/webkit/card-box' import Item from '@aziontech/webkit/item' +import { toSfc } from '../../_shared/story-source' + const sampleAvatarSrc = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=96&h=96&fit=crop&crop=face' @@ -29,6 +32,7 @@ const people = [ const itemSubcomponents = { Item, ItemGroup: Item.Group, + ItemList: Item.List, ItemSeparator: Item.Separator, ItemMedia: Item.Media, ItemContent: Item.Content, @@ -36,15 +40,17 @@ const itemSubcomponents = { ItemDescription: Item.Description, ItemActions: Item.Actions, Button, - Avatar + Avatar, + CardBox } /** @type {import('@storybook/vue3').Meta} */ const meta = { - title: 'Components/Content/Item', + title: 'Components/Content/Item', component: Item, subcomponents: { ItemGroup: Item.Group, + ItemList: Item.List, ItemSeparator: Item.Separator, ItemMedia: Item.Media, ItemContent: Item.Content, @@ -69,17 +75,19 @@ const meta = { docs: { description: { component: - 'Versatile flex row for title, description, media, and actions. Compose sub-components to reorder or omit regions; use ItemGroup for lists.' - } + 'Versatile flex row for title, description, media, and actions. Compose sub-components to reorder or omit regions; use ItemGroup for gapped off-card lists and ItemList (inside a CardBox with padded=false) for divided in-card lists.' + }, + canvas: { sourceState: 'shown' } } }, argTypes: { kind: { control: 'select', - options: ['default', 'outline', 'muted'], - description: 'Item root surface variant.', + options: ['default', 'outline', 'muted', 'inline'], + description: + 'Item root surface variant. inline removes the outer padding for inline placement and divided in-card lists.', table: { - type: { summary: "'default' | 'outline' | 'muted'" }, + type: { summary: "'default' | 'outline' | 'muted' | 'inline'" }, defaultValue: { summary: 'default' }, category: 'props' } @@ -97,7 +105,7 @@ const meta = { asChild: { control: 'boolean', description: - 'Merge row layout onto the single default-slot child (e.g. anchor). Focus and hover stay on slotted controls.', + 'Merge row layout onto the single default-slot child (e.g. anchor). The merged child becomes the focusable element and draws the focus-visible ring.', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, @@ -130,13 +138,28 @@ const meta = { export default meta +const BASIC_IMPORTS = [ + "import Item from '@aziontech/webkit/item'", + "import Button from '@aziontech/webkit/button'" +] + +const basicMarkup = (attrs = '') => ` + + Basic Item + A simple item with title and description. + + +