Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions .specs/empty-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ name: empty-state
category: feedback
structure: monolithic
status: approved
spec_version: 1
spec_version: 2
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=446-864
node_id: 446:864
checksum: 29f1b8b4fe22c0f15d54fbc3611a02685f3d3cf8133ec27d7be66ce12549d023
checksum: 4ee0f68c74c6046d625e69c006af16abf34e63dfac4b3d569eb229ac0207cd20
created: 2026-06-25
last_updated: 2026-06-25
last_updated: 2026-07-22
---

# Empty State — Component Spec

## Purpose

Centered placeholder shown when a resource list or section has no content yet. Stacks a decorative illustration, a title, an optional description, and a consumer-composed actions area (buttons + documentation link) to guide the user toward a first action. Unlike `Message`, which is an inline severity banner, `EmptyState` is a full-region zero-data state and renders either inside a bordered surface card or on a plain transparent background.
Centered placeholder shown when a resource list or section has no content yet. Stacks an optional standardized adornment, a title, an optional description, and a consumer-composed actions area (buttons + documentation link) to guide the user toward a first action. The adornment is a standardized, size-scaled featured-icon tile driven by the `icon` prop; the consumer may instead supply custom content via the `icon` slot, or omit both for no adornment. Unlike `Message`, which is an inline severity banner, `EmptyState` is a full-region zero-data state and renders either inside a bordered surface card or on a plain transparent background.

## Usage

Expand Down Expand Up @@ -50,8 +50,9 @@ import MiniButton from '@aziontech/webkit/mini-button'
|---|---|---|---|---|
| `title` | `string` | `—` | yes | Primary heading announcing the empty resource. |
| `description` | `string` | `''` | no | Supporting body copy below the title. |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | no | Size token; affects the illustration size, the surrounding padding, and the gaps between illustration, text, and actions. |
| `bordered` | `boolean` | `false` | no | When true, wraps the content in a bordered surface card; otherwise renders on a transparent background. |
| `icon` | `string` | `''` | no | PrimeIcons/Azion icon class for the standardized adornment (e.g. `pi pi-inbox`). When set, renders a size-scaled featured-icon tile; when empty and the `icon` slot is unused, no adornment renders. |
| `size` | `'small' \| 'medium'` | `'medium'` | no | Size token (subset — only `small` and `medium`; no `large`). Scales the whole block on one harmonic ramp — the adornment size, the title and description typography, the surrounding padding, and the gaps between adornment, text, and actions all step up together. |
| `bordered` | `boolean` | `false` | no | When true, wraps the content in a dashed-bordered surface card; otherwise renders on a transparent background. |

## Events

Expand All @@ -61,7 +62,7 @@ import MiniButton from '@aziontech/webkit/mini-button'

| Slot | Scope | Notes |
|---|---|---|
| `icon` | — | Decorative illustration centered above the title; rendered inside an `aria-hidden` container. Defaults to the bundled `EmptyStateIllustration` sub-component when not provided. |
| `icon` | — | Custom adornment centered above the title, rendered inside an `aria-hidden` container. Overrides the `icon` prop's featured-icon tile. When neither this slot nor the `icon` prop is provided, no adornment renders. |
| `actions` | — | Action area below the description; consumer composes buttons and the documentation link. |

## States
Expand All @@ -78,23 +79,36 @@ _none_

| Region | Token (DESIGN.md) |
|---|---|
| title typography | `.text-body-sm` |
| description typography | `.text-body-xs` |
| title typography | per-`size` — see harmonic scale below |
| description typography | per-`size` — see harmonic scale below |
| surface (bordered) | `var(--bg-surface)` |
| border (bordered) | `var(--border-default)` |
| border (bordered) | `var(--border-default)` (dashed, `var(--border-width-default)`) |
| title text | `var(--text-default)` |
| description text | `var(--text-muted)` |
| adornment tile surface | `var(--bg-surface)` |
| adornment tile border | `var(--border-default)` |
| adornment decorative layers | `var(--bg-canvas)` · `var(--border-strong)` |
| adornment shape | `var(--shape-card)` · `var(--shape-elements)` |
| adornment icon | `var(--text-default)` |
| shape (bordered) | `var(--shape-button)` |
| spacing (padding, medium) | `var(--spacing-8)` (x) · `var(--spacing-12)` (y) |
| spacing (stack gap, medium) | `var(--spacing-6)` |
| spacing (title gap) | `var(--spacing-xs)` |
| spacing (actions gap) | `var(--spacing-xs)` |

## Theme gaps
### Harmonic scale (per `size`)

Every axis steps up one rung per size on a single harmonic ramp. The title climbs the **heading** scale (`heading` tokens only — never `body`); the description climbs the **body** scale one rung below it, so the pair stays in proportion at every size.

| Figma variable | Temporary primitive | Follow-up |
| Axis | `small` | `medium` |
|---|---|---|
| `Typography/Heading/xss` (title, 14px regular) | `.text-body-sm` (14px regular — size/weight match) | `TODO: catalogar` — naming-only gap; no heading-named token exists at 14px. |
| title typography | `.text-heading-xxs` (14px) | `.text-heading-sm` (14 → 18px) |
| description typography | `.text-body-xs` (12px) | `.text-body-sm` (14px) |
| adornment tile | `size-8` (32px) | `size-10` (40px) |
| padding (x · y) | `var(--spacing-6)` · `var(--spacing-8)` | `var(--spacing-8)` · `var(--spacing-12)` |
| stack gap | `var(--spacing-4)` | `var(--spacing-6)` |

## Theme gaps

_none_ — the title now uses `heading` tokens at every size (`.text-heading-xxs` is 14px regular, matching the Figma `Typography/Heading/xss` title), so the previous body-token workaround is retired.

## Accessibility (WCAG 2.1 AA)

Expand All @@ -107,8 +121,10 @@ _none_

## Stories (Storybook)

- Default
- Sizes — composite story rendering every `size` value side by side.
- Default — renders the standardized featured-icon tile via the `icon` prop.
- Sizes — composite story rendering every `size` value side by side (adornment scales with `size`).
- Icon — the standardized featured-icon tile adornment driven by the `icon` prop; justified because the tile is the primary adornment path and must be documented distinctly from custom slot content.
- NoAdornment — omits both the `icon` prop and the `icon` slot; justified because "no adornment" is a distinct rendered state (the adornment container does not render) that the default illustration previously masked.
- Bordered — state story (args delta `bordered: true`); justified because the bordered surface card is a distinct top-level Figma frame (446:864) that materially changes the rendered container.

## Constraints — DO NOT
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const meta = {
docs: {
description: {
component:
'A full-region placeholder shown when a list or section has no content yet. It stacks a decorative illustration (the bundled `EmptyStateIllustration` by default), a title, an optional description, and a consumer-composed actions area, and renders on a plain background or inside a bordered surface card (`bordered`).'
'A full-region placeholder shown when a list or section has no content yet. It stacks an optional standardized featured-icon tile (driven by the `icon` prop), a title, an optional description, and a consumer-composed actions area, and renders on a plain background or inside a bordered surface card (`bordered`). Supply custom adornment content via the `icon` slot, or omit both `icon` prop and slot for no adornment.'
},
canvas: { sourceState: 'shown' }
}
Expand All @@ -57,27 +57,27 @@ const meta = {
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
options: ['small', 'medium'],
description:
'Size token; affects the illustration size, the surrounding padding, and the gaps between illustration, text, and actions.',
'Size token (small and medium only); scales the adornment, the title and description typography, the surrounding padding, and the gaps on one harmonic ramp.',
table: {
category: 'props',
type: { summary: "'small' | 'medium' | 'large'" },
type: { summary: "'small' | 'medium'" },
defaultValue: { summary: "'medium'" }
}
},
icon: {
control: 'text',
description:
'PrimeIcons/Azion icon class for the standardized adornment; renders a size-scaled featured-icon tile. The `icon` slot overrides it; omit both for no adornment.',
table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } }
},
bordered: {
control: 'boolean',
description:
'When true, wraps the content in a bordered surface card; otherwise renders on a transparent background.',
table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
icon: {
control: false,
description:
'Decorative illustration centered above the title; rendered inside an aria-hidden container. Defaults to the bundled EmptyStateIllustration.',
table: { category: 'slots', type: { summary: 'slot' } }
},
actions: {
control: false,
description:
Expand All @@ -88,6 +88,7 @@ const meta = {
args: {
title: 'No resource yet',
description: 'Get started by creating your first resource.',
icon: 'pi pi-inbox',
size: 'medium',
bordered: false
}
Expand All @@ -108,6 +109,7 @@ ${ACTIONS_MARKUP}
const DEFAULT_MARKUP = `<EmptyState
title="No resource yet"
description="Get started by creating your first resource."
icon="pi pi-inbox"
>
${ACTIONS_MARKUP}
</EmptyState>`
Expand All @@ -118,7 +120,7 @@ export const Default = {
parameters: {
docs: {
description: {
story: 'Default empty state: the illustration tile, title, description, and actions.'
story: 'Default empty state: the standardized featured-icon tile, title, description, and actions.'
},
source: { code: toSfc(IMPORTS, DEFAULT_MARKUP) }
}
Expand All @@ -127,11 +129,12 @@ export const Default = {

const SIZES_TEMPLATE = `<div class="flex w-full flex-col gap-[var(--spacing-xl)]">
<EmptyState
v-for="size in ['small', 'medium', 'large']"
v-for="size in ['small', 'medium']"
:key="size"
:size="size"
title="No resource yet"
description="Get started by creating your first resource."
icon="pi pi-inbox"
>
${ACTIONS_MARKUP}
</EmptyState>
Expand All @@ -143,15 +146,59 @@ export const Sizes = {
parameters: {
docs: {
controls: { disable: true },
description: { story: 'The three size variants — small, medium, large — stacked.' },
description: { story: 'The size variants — small and medium — stacked.' },
source: { code: toSfc(IMPORTS, SIZES_TEMPLATE) }
}
}
}

const ICON_MARKUP = `<EmptyState
title="No resource yet"
description="Get started by creating your first resource."
icon="pi pi-inbox"
>
${ACTIONS_MARKUP}
</EmptyState>`

/** @type {import('@storybook/vue3').StoryObj<typeof EmptyState>} */
export const Icon = {
args: { icon: 'pi pi-inbox' },
render: Template,
parameters: {
docs: {
description: {
story: 'The standardized featured-icon tile adornment, driven by the `icon` prop.'
},
source: { code: toSfc(IMPORTS, ICON_MARKUP) }
}
}
}

const NO_ADORNMENT_MARKUP = `<EmptyState
title="No resource yet"
description="Get started by creating your first resource."
>
${ACTIONS_MARKUP}
</EmptyState>`

/** @type {import('@storybook/vue3').StoryObj<typeof EmptyState>} */
export const NoAdornment = {
args: { icon: '' },
render: Template,
parameters: {
docs: {
description: {
story: 'No adornment: neither the `icon` prop nor the `icon` slot is provided, so the adornment container does not render.'
},
source: { code: toSfc(IMPORTS, NO_ADORNMENT_MARKUP) }
}
}
}

const BORDERED_MARKUP = `<EmptyState
title="No resource yet"
description="Get started by creating your first resource."
icon="pi pi-inbox"
bordered
>
${ACTIONS_MARKUP}
Expand Down
17 changes: 12 additions & 5 deletions packages/webkit/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -4242,34 +4242,41 @@
"required": "no",
"doc": "Supporting body copy below the title."
},
{
"name": "icon",
"type": "string",
"default": "''",
"required": "no",
"doc": "PrimeIcons/Azion icon class for the standardized adornment (e.g. `pi pi-inbox`). When set, renders a size-scaled featured-icon tile; when empty and the `icon` slot is unused, no adornment renders."
},
{
"name": "size",
"type": "'small' | 'medium' | 'large'",
"type": "'small' | 'medium'",
"default": "'medium'",
"required": "no",
"doc": "Size token; affects the illustration size, the surrounding padding, and the gaps between illustration, text, and actions."
"doc": "Size token (subset — only `small` and `medium`; no `large`). Scales the whole block on one harmonic ramp — the adornment size, the title and description typography, the surrounding padding, and the gaps between adornment, text, and actions all step up together."
},
{
"name": "bordered",
"type": "boolean",
"default": "false",
"required": "no",
"doc": "When true, wraps the content in a bordered surface card; otherwise renders on a transparent background."
"doc": "When true, wraps the content in a dashed-bordered surface card; otherwise renders on a transparent background."
}
],
"slots": [
{
"name": "icon",
"scope": "—",
"notes": "Decorative illustration centered above the title; rendered inside an `aria-hidden` container. Defaults to the bundled `EmptyStateIllustration` sub-component when not provided."
"notes": "Custom adornment centered above the title, rendered inside an `aria-hidden` container. Overrides the `icon` prop's featured-icon tile. When neither this slot nor the `icon` prop is provided, no adornment renders."
},
{
"name": "actions",
"scope": "—",
"notes": "Action area below the description; consumer composes buttons and the documentation link."
}
],
"purpose": "Centered placeholder shown when a resource list or section has no content yet. Stacks a decorative illustration, a title, an optional description, and a consumer-composed actions area (buttons + documentation link) to guide the user toward a first action. Unlike `Message`, which is an inline severity banner, `EmptyState` is a full-region zero-data state and renders either inside a bordered surface card or on a plain transparent background."
"purpose": "Centered placeholder shown when a resource list or section has no content yet. Stacks an optional standardized adornment, a title, an optional description, and a consumer-composed actions area (buttons + documentation link) to guide the user toward a first action. The adornment is a standardized, size-scaled featured-icon tile driven by the `icon` prop; the consumer may instead supply custom content via the `icon` slot, or omit both for no adornment. Unlike `Message`, which is an inline severity banner, `EmptyState` is a full-region zero-data state and renders either inside a bordered surface card or on a plain transparent background."
},
"status-indicator": {
"import": "@aziontech/webkit/status-indicator",
Expand Down
17 changes: 8 additions & 9 deletions packages/webkit/src/components/data/table/table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import { useDebounceFn } from '@vueuse/core'
import { computed, getCurrentInstance, provide, ref, useAttrs, useSlots, watch } from 'vue'

import EmptyIllustration from '../../../svg/illustration-layers/illustration-layers.vue'
import { downloadCsv, toCsv } from '../../../utils/csv'
import EmptyState from '../../feedback/empty-state/empty-state.vue'
import Skeleton from '../../feedback/skeleton/skeleton.vue'
import Checkbox from '../../inputs/checkbox/checkbox.vue'
import ScrollArea from '../../layout/scroll-area/scroll-area.vue'
Expand Down Expand Up @@ -1128,16 +1128,15 @@
v-if="!loading && rows.length === 0"
role="presentation"
:data-testid="`${testId}__empty`"
class="flex w-full flex-col items-center justify-center gap-[var(--spacing-6)] px-[var(--spacing-8)] py-[var(--spacing-12)] text-center"
class="w-full"
>
<slot name="empty">
<EmptyIllustration class="mb-0!" />
<div class="flex flex-col gap-[var(--spacing-2)]">
<p class="text-heading-sm text-[var(--text-default)]">No results yet</p>
<p class="text-body-sm text-[var(--text-muted)]">
Get started by creating your first resource.
</p>
</div>
<EmptyState
size="small"
icon="pi pi-table"
title="No results yet"
description="Get started by creating your first resource."
/>
</slot>
</div>
</Transition>
Expand Down
Loading
Loading