import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { action } from 'storybook/actions'
import { computed, ref } from 'vue'
import CpTable from '@/components/CpTable.vue'
const meta = {
title: 'Organisms/CpTable',
component: CpTable,
parameters: {
backgrounds: {
default: 'dark',
},
layout: 'padded',
},
decorators: [
() => ({ template: '
' }),
],
} satisfies Meta
export default meta
type Story = StoryObj
type TableStoryArgs = NonNullable
const sampleColumns = ['name', 'age', 'email', 'status']
const sampleData = [
{ name: 'John Doe', age: 30, email: 'john@example.com', status: 'Active', date: '2026-01-01' },
{ name: 'Jane Smith', age: 25, email: 'jane@example.com', status: 'Inactive', date: '2026-01-02' },
{ name: 'Bob Johnson', age: 35, email: 'bob@example.com', status: 'Active', date: '2026-01-03' },
{ name: 'Alice Brown', age: 28, email: 'alice@example.com', status: 'Active', date: '2026-01-04' },
{ name: 'Charlie Wilson', age: 42, email: 'charlie@example.com', status: 'Inactive', date: '2026-01-05' },
]
/**
* Default table with sample columns and rows. Use the controls to
* experiment with each prop in isolation.
*/
export const Default: Story = {
args: {
columns: sampleColumns,
data: sampleData,
caption: 'User Management Table',
areRowsClickable: false,
emptyCellPlaceholder: 'n/a',
noResultPlaceholder: 'No results found',
isLoading: false,
enableRowOptions: false,
quickOptionsLimit: 3,
pagination: {
enabled: true,
},
onOnRowClick: action('on-row-click'),
onOnPreviousClick: action('on-previous-click'),
onOnNextClick: action('on-next-click'),
onOnColumnsChanged: action('on-columns-changed'),
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
`,
}),
}
/**
* Enable the column manager so users can show/hide, pin and reorder
* columns. Individual columns expose `isHidden`, `isProtected` and
* `isFull` flags to control the initial layout.
*/
export const EnableColumnEdition: Story = {
args: {
...Default.args,
columns: [
{ id: 'name', name: 'Name', isProtected: false },
{ id: 'age', name: 'Age', isHidden: false, isProtected: false, isFull: true },
{ id: 'email', name: 'Email', isHidden: false },
{ id: 'status', name: 'Status', isFull: false },
{ id: 'date', name: 'Date', isHidden: false, isProtected: true },
],
enableColumnEdition: true,
},
}
/**
* Rows become interactive — hover highlights and the `onRowClick` event
* fires on click.
*/
export const ClickableRows: Story = {
args: {
...Default.args,
areRowsClickable: true,
},
}
/**
* Loading state — skeleton rows are displayed while the data is fetched.
*/
export const Loading: Story = {
args: {
...Default.args,
isLoading: true,
},
}
/**
* Empty dataset — the `noResultPlaceholder` copy replaces the rows.
*/
export const Empty: Story = {
args: {
...Default.args,
data: [],
},
}
/**
* Configure pagination through the `pagination` prop. `format` switches
* between `PAGES` and alternative formats.
*/
export const WithPagination: Story = {
args: {
...Default.args,
data: [...sampleData, ...sampleData, ...sampleData],
pagination: {
enabled: true,
limit: 12,
},
},
}
/**
* Override the default footer details through the `#footer-details` slot.
*/
export const WithCustomFooterDetails: Story = {
args: {
...WithPagination.args,
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
Custom footer: showing paginated users list.
`,
}),
}
/**
* Group rows by providing an array of `{ groupBy, rows }` objects. A
* custom `#groupBy` slot renders the section header.
*/
export const WithGroupBy: Story = {
args: {
...Default.args,
data: [
{
groupBy: 'Active Users',
rows: [
{ name: 'John Doe', age: 30, email: 'john@example.com', status: 'Active' },
{ name: 'Bob Johnson', age: 35, email: 'bob@example.com', status: 'Active' },
],
},
{
groupBy: 'Inactive Users',
rows: [{ name: 'Jane Smith', age: 25, email: 'jane@example.com', status: 'Inactive' }],
},
],
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
{{ cell }}
`,
}),
}
/**
* Attach per-row actions through `rowOptions`. `quickOptionsLimit`
* controls how many actions appear inline before the overflow menu; each
* action can be async (`isAsync` + `isLoading`) or critical
* (`isCritical`).
*/
export const WithCustomRowOptions: Story = {
args: {
...Default.args,
enableRowOptions: true,
enableColumnEdition: true,
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
const isEditLoading = ref(false)
const rowOptions = computed(() => [
{
id: 'see',
label: 'See',
icon: 'eye',
action: () => console.log('See'),
},
{
id: 'edit',
label: 'Edit',
icon: 'edit-2',
isAsync: true,
isLoading: isEditLoading.value,
action: async (payload: Record) => {
isEditLoading.value = true
console.log('Edit', payload)
await new Promise((resolve) => setTimeout(resolve, 2000))
isEditLoading.value = false
},
},
{
id: 'disable',
label: 'Disable',
icon: 'history',
isDisabled: true,
action: (payload: Record) => console.log('History', payload),
},
{
id: 'delete',
label: 'Delete',
icon: 'trash',
isCritical: true,
action: () => console.log('Delete'),
},
])
return { args, isEditLoading, rowOptions }
},
template: `
{{ cell }}
`,
}),
}
/**
* Rows without a "primary" action — every option lives in the overflow
* menu. Useful when actions are secondary to the row click.
*/
export const WithoutDefaultAction: Story = {
args: {
...Default.args,
enableRowOptions: true,
quickOptionsLimit: 2,
enableColumnEdition: true,
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
const isEditLoading = ref(false)
const rowOptions = computed(() => [
{
id: 'see',
label: 'See',
icon: 'eye',
action: () => console.log('See'),
},
{
id: 'delete',
label: 'Delete',
icon: 'trash',
isCritical: true,
action: () => console.log('Delete'),
},
])
return { args, isEditLoading, rowOptions }
},
template: `
{{ cell }}
`,
}),
}
/**
* Setting `quickOptionsLimit: 0` forces the primary action to appear
* inline without any overflow menu.
*/
export const WithOnlyDefaultAction: Story = {
args: {
...Default.args,
enableRowOptions: true,
enableColumnEdition: true,
quickOptionsLimit: 0,
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
const isEditLoading = ref(false)
const rowOptions = computed(() => [
{
id: 'see',
label: 'See',
icon: 'eye',
action: () => console.log('See'),
},
{
id: 'edit',
label: 'Edit',
icon: 'edit-2',
isAsync: true,
isLoading: isEditLoading.value,
action: async (payload: Record) => {
isEditLoading.value = true
console.log('Edit', payload)
await new Promise((resolve) => setTimeout(resolve, 2000))
isEditLoading.value = false
},
},
])
return { args, isEditLoading, rowOptions }
},
template: `
{{ cell }}
`,
}),
}
/**
* When the table overflows horizontally, a subtle scroll cue appears on
* the right edge to hint at the extra content.
*/
export const ScrollCue: Story = {
args: {
...Default.args,
columns: ['name', 'age', 'email', 'status', 'company', 'role', 'city', 'country'],
data: [
{
name: 'John Doe',
age: 30,
email: 'john@example.com',
status: 'Active',
company: 'CitizenPlane',
role: 'Analyst',
city: 'Paris',
country: 'France',
},
{
name: 'Jane Smith',
age: 25,
email: 'jane@example.com',
status: 'Inactive',
company: 'Kiwi',
role: 'Manager',
city: 'Prague',
country: 'Czechia',
},
{
name: 'Bob Johnson',
age: 35,
email: 'bob@example.com',
status: 'Active',
company: 'Atlas',
role: 'Engineer',
city: 'Berlin',
country: 'Germany',
},
],
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
`,
}),
}
/**
* A row can override the column layout with `{ fullWidth: '...' }` to
* spread a single cell across the whole table (e.g. summaries or notes).
*/
export const FullWidthRow: Story = {
args: {
...Default.args,
data: [
{ name: 'John Doe', age: 30, email: 'john@example.com', status: 'Active' },
{ fullWidth: 'Full-width row content (e.g. summary or note)' },
{ name: 'Jane Smith', age: 25, email: 'jane@example.com', status: 'Inactive' },
],
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
`,
}),
}
/**
* Mark a row as selected by adding `isSelected: true`. The row adopts a
* highlighted background.
*/
export const SelectedRow: Story = {
args: {
...Default.args,
data: [
{ name: 'John Doe', age: 30, email: 'john@example.com', status: 'Active' },
{ name: 'Jane Smith', age: 25, email: 'jane@example.com', status: 'Inactive', isSelected: true },
{ name: 'Bob Johnson', age: 35, email: 'bob@example.com', status: 'Active' },
],
},
render: (args: TableStoryArgs) => ({
components: { CpTable },
setup() {
return { args }
},
template: `
`,
}),
}