@nexxtmove/ui 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +39 -8
- package/dist/index.js +1193 -958
- package/dist/nuxt.js +28 -25
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.stories.ts +28 -0
- package/src/components/Avatar/Avatar.test.ts +55 -0
- package/src/components/Avatar/Avatar.vue +49 -0
- package/src/components/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/Checkbox/Checkbox.vue +46 -0
- package/src/components/Table/Table.stories.ts +283 -0
- package/src/components/Table/Table.test.ts +220 -0
- package/src/components/Table/Table.vue +227 -0
- package/src/components.json +3 -0
- package/src/index.ts +3 -0
package/dist/nuxt.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { defineNuxtModule as
|
|
2
|
-
const
|
|
3
|
-
NexxtAnimatedNumber:
|
|
4
|
-
|
|
1
|
+
import { defineNuxtModule as c, addComponent as s, addTemplate as a } from "@nuxt/kit";
|
|
2
|
+
const i = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Avatar/Avatar.vue", x = "components/Button/Button.vue", u = "components/Calendar/Calendar.vue", p = "components/Checkbox/Checkbox.vue", l = "components/Chip/Chip.vue", r = "components/CustomerJourneyTile/CustomerJourneyTile.vue", d = "components/DatePicker/DatePicker.vue", v = "components/DropdownButton/DropdownButton.vue", N = "components/Header/Header.vue", T = "components/Icon/Icon.vue", C = "components/InfoBlock/InfoBlock.vue", S = "components/ProgressBar/ProgressBar.vue", b = "components/SocialIcons/SocialIcons.vue", f = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", h = "components/SocialMediaTemplate/SocialMediaTemplate.vue", k = "components/SocialMediaType/SocialMediaType.vue", B = "components/StepperHeader/StepperHeader.vue", M = "components/Table/Table.vue", P = "components/TimelineEvent/TimelineEvent.vue", I = "components/TimelinePhaseblock/TimelinePhaseblock.vue", y = "components/Tooltip/Tooltip.vue", A = {
|
|
3
|
+
NexxtAnimatedNumber: i,
|
|
4
|
+
NexxtAvatar: m,
|
|
5
|
+
NexxtButton: x,
|
|
5
6
|
NexxtCalendar: u,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
NexxtCheckbox: p,
|
|
8
|
+
NexxtChip: l,
|
|
9
|
+
NexxtCustomerJourneyTile: r,
|
|
10
|
+
NexxtDatePicker: d,
|
|
11
|
+
NexxtDropdownButton: v,
|
|
12
|
+
NexxtHeader: N,
|
|
13
|
+
NexxtIcon: T,
|
|
14
|
+
NexxtInfoBlock: C,
|
|
15
|
+
NexxtProgressBar: S,
|
|
16
|
+
NexxtSocialIcons: b,
|
|
17
|
+
NexxtSocialMediaCustomTemplate: f,
|
|
18
|
+
NexxtSocialMediaTemplate: h,
|
|
19
|
+
NexxtSocialMediaType: k,
|
|
20
|
+
NexxtStepperHeader: B,
|
|
21
|
+
NexxtTable: M,
|
|
22
|
+
NexxtTimelineEvent: P,
|
|
23
|
+
NexxtTimelinePhaseblock: I,
|
|
24
|
+
NexxtTooltip: y
|
|
25
|
+
}, H = c({
|
|
23
26
|
meta: {
|
|
24
27
|
name: "@nexxtmove/ui",
|
|
25
28
|
configKey: "nexxtmoveUi",
|
|
@@ -31,13 +34,13 @@ const a = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
|
|
|
31
34
|
addCSS: !0
|
|
32
35
|
},
|
|
33
36
|
setup(e, o) {
|
|
34
|
-
for (const [t, n] of Object.entries(
|
|
35
|
-
|
|
37
|
+
for (const [t, n] of Object.entries(A))
|
|
38
|
+
s({
|
|
36
39
|
name: t,
|
|
37
40
|
export: "default",
|
|
38
41
|
filePath: `@nexxtmove/ui/${n}`
|
|
39
42
|
});
|
|
40
|
-
e.addCSS && (
|
|
43
|
+
e.addCSS && (a({
|
|
41
44
|
filename: "nexxtmove-ui.css",
|
|
42
45
|
getContents: () => `
|
|
43
46
|
@import "tailwindcss";
|
|
@@ -50,5 +53,5 @@ const a = "components/AnimatedNumber/AnimatedNumber.vue", m = "components/Button
|
|
|
50
53
|
}
|
|
51
54
|
});
|
|
52
55
|
export {
|
|
53
|
-
|
|
56
|
+
H as default
|
|
54
57
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import Avatar from './Avatar.vue'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Atoms/Avatar',
|
|
6
|
+
component: Avatar,
|
|
7
|
+
parameters: {
|
|
8
|
+
design: {
|
|
9
|
+
type: 'figma',
|
|
10
|
+
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
size: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof Avatar>
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof Avatar>
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
name: 'Peter van der Sloot',
|
|
26
|
+
size: 'sm',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Avatar from './Avatar.vue'
|
|
4
|
+
|
|
5
|
+
describe('Avatar', () => {
|
|
6
|
+
it('generates 2 character initials from name with multiple words', () => {
|
|
7
|
+
const wrapper = mount(Avatar, {
|
|
8
|
+
props: {
|
|
9
|
+
name: 'Peter Vink',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(wrapper.text()).toContain('PV')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('generates 2 character initials from name with single word', () => {
|
|
17
|
+
const wrapper = mount(Avatar, {
|
|
18
|
+
props: {
|
|
19
|
+
name: 'Peter',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(wrapper.text()).toContain('PE')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('generates 2 character initials picking first and last words for names with tussenvoegsels', () => {
|
|
27
|
+
const wrapper = mount(Avatar, {
|
|
28
|
+
props: {
|
|
29
|
+
name: 'Peter van der Sloot',
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(wrapper.text()).toContain('PS')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('handles empty strings gracefully by returning no text', () => {
|
|
37
|
+
const wrapper = mount(Avatar, {
|
|
38
|
+
props: {
|
|
39
|
+
name: '',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(wrapper.text()).toBe('')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('handles whitespace-only strings gracefully by returning no text', () => {
|
|
47
|
+
const wrapper = mount(Avatar, {
|
|
48
|
+
props: {
|
|
49
|
+
name: ' ',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(wrapper.text()).toBe('')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface NexxtAvatarProps {
|
|
5
|
+
name: string
|
|
6
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'NexxtAvatar',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<NexxtAvatarProps>(), {
|
|
14
|
+
size: 'sm',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const computedInitials = computed(() => {
|
|
18
|
+
if (!props.name?.trim()) return ''
|
|
19
|
+
|
|
20
|
+
const parts = props.name.trim().split(/\s+/)
|
|
21
|
+
if (parts.length >= 2) {
|
|
22
|
+
const firstInitial = parts[0][0]
|
|
23
|
+
const lastInitial = parts[parts.length - 1][0]
|
|
24
|
+
|
|
25
|
+
return (firstInitial + lastInitial).toUpperCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return parts[0].substring(0, 2).toUpperCase()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const sizeClasses = {
|
|
32
|
+
xs: 'h-6 w-6 text-[10px]',
|
|
33
|
+
sm: 'h-8 w-8 text-sm',
|
|
34
|
+
md: 'h-10 w-10 text-base',
|
|
35
|
+
lg: 'h-12 w-12 text-lg',
|
|
36
|
+
xl: 'h-16 w-16 text-xl',
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div
|
|
42
|
+
:class="[
|
|
43
|
+
'relative flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cornflower-blue-50 font-semibold text-cornflower-blue-500',
|
|
44
|
+
sizeClasses[size],
|
|
45
|
+
]"
|
|
46
|
+
>
|
|
47
|
+
{{ computedInitials }}
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import Checkbox from './Checkbox.vue'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Atoms/Checkbox',
|
|
6
|
+
component: Checkbox,
|
|
7
|
+
} satisfies Meta<typeof Checkbox>
|
|
8
|
+
|
|
9
|
+
type Story = StoryObj<typeof Checkbox>
|
|
10
|
+
|
|
11
|
+
export const Default: Story = {
|
|
12
|
+
args: {
|
|
13
|
+
modelValue: false,
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Checked: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
modelValue: true,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Disabled: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
modelValue: false,
|
|
26
|
+
disabled: true,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DisabledChecked: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
modelValue: true,
|
|
33
|
+
disabled: true,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Checkbox from './Checkbox.vue'
|
|
4
|
+
|
|
5
|
+
describe('Checkbox', () => {
|
|
6
|
+
it('renders a checkbox input', () => {
|
|
7
|
+
const wrapper = mount(Checkbox)
|
|
8
|
+
|
|
9
|
+
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('is unchecked by default', () => {
|
|
13
|
+
const wrapper = mount(Checkbox)
|
|
14
|
+
|
|
15
|
+
expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('is checked when modelValue is true', () => {
|
|
19
|
+
const wrapper = mount(Checkbox, { props: { modelValue: true } })
|
|
20
|
+
|
|
21
|
+
expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('emits update:modelValue when toggled', async () => {
|
|
25
|
+
const wrapper = mount(Checkbox, { props: { modelValue: false } })
|
|
26
|
+
|
|
27
|
+
await wrapper.find('input').setValue(true)
|
|
28
|
+
|
|
29
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('disables the input when disabled prop is true', () => {
|
|
33
|
+
const wrapper = mount(Checkbox, { props: { disabled: true } })
|
|
34
|
+
|
|
35
|
+
expect((wrapper.find('input').element as HTMLInputElement).disabled).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('applies opacity class when disabled', () => {
|
|
39
|
+
const wrapper = mount(Checkbox, { props: { disabled: true } })
|
|
40
|
+
|
|
41
|
+
expect(wrapper.find('label').classes()).toContain('opacity-50')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('forwards attributes to the input', () => {
|
|
45
|
+
const wrapper = mount(Checkbox, { attrs: { 'aria-label': 'Accept terms' } })
|
|
46
|
+
|
|
47
|
+
expect(wrapper.find('input').attributes('aria-label')).toBe('Accept terms')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders the check icon', () => {
|
|
51
|
+
const wrapper = mount(Checkbox)
|
|
52
|
+
|
|
53
|
+
expect(wrapper.find('i.fa-check').exists()).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watchEffect } from 'vue'
|
|
3
|
+
import Icon from '../Icon/Icon.vue'
|
|
4
|
+
|
|
5
|
+
interface NexxtCheckboxProps {
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
indeterminate?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
defineOptions({
|
|
11
|
+
name: 'NexxtCheckbox',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const model = defineModel<boolean>()
|
|
15
|
+
const { indeterminate } = defineProps<NexxtCheckboxProps>()
|
|
16
|
+
|
|
17
|
+
const inputRef = ref<HTMLInputElement>()
|
|
18
|
+
watchEffect(() => {
|
|
19
|
+
if (inputRef.value) inputRef.value.indeterminate = indeterminate ?? false
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<label :class="['inline-flex cursor-pointer', disabled ? 'cursor-not-allowed opacity-50' : '']">
|
|
25
|
+
<input
|
|
26
|
+
v-bind="$attrs"
|
|
27
|
+
ref="inputRef"
|
|
28
|
+
type="checkbox"
|
|
29
|
+
class="peer sr-only"
|
|
30
|
+
v-model="model"
|
|
31
|
+
:disabled="disabled"
|
|
32
|
+
/>
|
|
33
|
+
<span
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
:class="[
|
|
36
|
+
'flex h-5 w-5 shrink-0 items-center justify-center rounded border transition-colors duration-200',
|
|
37
|
+
'peer-focus-visible:ring-primary peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2',
|
|
38
|
+
model || indeterminate
|
|
39
|
+
? 'border-primary text-cornflower-blue-600'
|
|
40
|
+
: 'border-gray-300 text-transparent',
|
|
41
|
+
]"
|
|
42
|
+
>
|
|
43
|
+
<Icon :name="indeterminate ? 'minus' : 'check'" type="solid" class="text-xs" />
|
|
44
|
+
</span>
|
|
45
|
+
</label>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { computed, ref, type ConcreteComponent } from 'vue'
|
|
3
|
+
import Table, { type TableColumn } from './Table.vue'
|
|
4
|
+
|
|
5
|
+
type RowKey = 'name' | 'email' | 'role' | 'status' | 'department' | 'lastLogin'
|
|
6
|
+
|
|
7
|
+
interface TableStoryProps {
|
|
8
|
+
columns: TableColumn<RowKey>[]
|
|
9
|
+
rows: Record<RowKey, string>[]
|
|
10
|
+
selectable?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta<TableStoryProps> = {
|
|
14
|
+
title: 'Components/Atoms/Table',
|
|
15
|
+
component: Table as ConcreteComponent<TableStoryProps>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj<TableStoryProps>
|
|
20
|
+
|
|
21
|
+
const columns: TableColumn<RowKey>[] = [
|
|
22
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
23
|
+
{ key: 'email', label: 'Email', sortable: true },
|
|
24
|
+
{ key: 'department', label: 'Department' },
|
|
25
|
+
{ key: 'role', label: 'Role', sortable: true, width: '0.5fr' },
|
|
26
|
+
{ key: 'lastLogin', label: 'lastLogin' },
|
|
27
|
+
{ key: 'status', label: 'Status', width: '0.5fr' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const allRows = [
|
|
31
|
+
{
|
|
32
|
+
name: 'Alice Johnson',
|
|
33
|
+
email: 'alice@example.com',
|
|
34
|
+
role: 'Admin',
|
|
35
|
+
status: 'Active',
|
|
36
|
+
department: 'HR',
|
|
37
|
+
lastLogin: '2026-03-25',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Bob Smith',
|
|
41
|
+
email: 'bob@example.com',
|
|
42
|
+
role: 'Editor',
|
|
43
|
+
status: 'Active',
|
|
44
|
+
department: 'Marketing',
|
|
45
|
+
lastLogin: '2026-03-27',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Carol White',
|
|
49
|
+
email: 'carol@example.com',
|
|
50
|
+
role: 'Viewer',
|
|
51
|
+
status: 'Inactive',
|
|
52
|
+
department: 'Finance',
|
|
53
|
+
lastLogin: '2026-02-12',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'David Brown',
|
|
57
|
+
email: 'david@example.com',
|
|
58
|
+
role: 'Editor',
|
|
59
|
+
status: 'Active',
|
|
60
|
+
department: 'IT',
|
|
61
|
+
lastLogin: '2026-03-30',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Eve Turner',
|
|
65
|
+
email: 'eve@example.com',
|
|
66
|
+
role: 'Admin',
|
|
67
|
+
status: 'Active',
|
|
68
|
+
department: 'HR',
|
|
69
|
+
lastLogin: '2026-03-28',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Frank Miller',
|
|
73
|
+
email: 'frank@example.com',
|
|
74
|
+
role: 'Viewer',
|
|
75
|
+
status: 'Active',
|
|
76
|
+
department: 'Support',
|
|
77
|
+
lastLogin: '2026-03-26',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Grace Lee',
|
|
81
|
+
email: 'grace@example.com',
|
|
82
|
+
role: 'Editor',
|
|
83
|
+
status: 'Active',
|
|
84
|
+
department: 'Product',
|
|
85
|
+
lastLogin: '2026-03-29',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Henry Wilson',
|
|
89
|
+
email: 'henry@example.com',
|
|
90
|
+
role: 'Viewer',
|
|
91
|
+
status: 'Inactive',
|
|
92
|
+
department: 'Finance',
|
|
93
|
+
lastLogin: '2026-01-10',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'Ivy Clark',
|
|
97
|
+
email: 'ivy@example.com',
|
|
98
|
+
role: 'Admin',
|
|
99
|
+
status: 'Active',
|
|
100
|
+
department: 'IT',
|
|
101
|
+
lastLogin: '2026-03-20',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'Jack Davis',
|
|
105
|
+
email: 'jack@example.com',
|
|
106
|
+
role: 'Editor',
|
|
107
|
+
status: 'Active',
|
|
108
|
+
department: 'Marketing',
|
|
109
|
+
lastLogin: '2026-03-23',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Karen Harris',
|
|
113
|
+
email: 'karen@example.com',
|
|
114
|
+
role: 'Viewer',
|
|
115
|
+
status: 'Active',
|
|
116
|
+
department: 'Support',
|
|
117
|
+
lastLogin: '2026-03-19',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'Leo King',
|
|
121
|
+
email: 'leo@example.com',
|
|
122
|
+
role: 'Editor',
|
|
123
|
+
status: 'Active',
|
|
124
|
+
department: 'Product',
|
|
125
|
+
lastLogin: '2026-03-28',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'Mia Scott',
|
|
129
|
+
email: 'mia@example.com',
|
|
130
|
+
role: 'Admin',
|
|
131
|
+
status: 'Active',
|
|
132
|
+
department: 'HR',
|
|
133
|
+
lastLogin: '2026-03-30',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'Nick Evans',
|
|
137
|
+
email: 'nick@example.com',
|
|
138
|
+
role: 'Viewer',
|
|
139
|
+
status: 'Inactive',
|
|
140
|
+
department: 'Finance',
|
|
141
|
+
lastLogin: '2026-02-05',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'Olivia Foster',
|
|
145
|
+
email: 'olivia@example.com',
|
|
146
|
+
role: 'Editor',
|
|
147
|
+
status: 'Active',
|
|
148
|
+
department: 'Product',
|
|
149
|
+
lastLogin: '2026-03-27',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'Paul Green',
|
|
153
|
+
email: 'paul@example.com',
|
|
154
|
+
role: 'Viewer',
|
|
155
|
+
status: 'Active',
|
|
156
|
+
department: 'Support',
|
|
157
|
+
lastLogin: '2026-03-18',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Quincy Adams',
|
|
161
|
+
email: 'quincy@example.com',
|
|
162
|
+
role: 'Editor',
|
|
163
|
+
status: 'Active',
|
|
164
|
+
department: 'Marketing',
|
|
165
|
+
lastLogin: '2026-03-25',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'Rachel Lewis',
|
|
169
|
+
email: 'rachel@example.com',
|
|
170
|
+
role: 'Admin',
|
|
171
|
+
status: 'Active',
|
|
172
|
+
department: 'HR',
|
|
173
|
+
lastLogin: '2026-03-29',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'Sam Nelson',
|
|
177
|
+
email: 'sam@example.com',
|
|
178
|
+
role: 'Viewer',
|
|
179
|
+
status: 'Inactive',
|
|
180
|
+
department: 'Finance',
|
|
181
|
+
lastLogin: '2026-01-15',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'Tina Brooks',
|
|
185
|
+
email: 'tina@example.com',
|
|
186
|
+
role: 'Editor',
|
|
187
|
+
status: 'Active',
|
|
188
|
+
department: 'IT',
|
|
189
|
+
lastLogin: '2026-03-30',
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
const sortRows = (rows: typeof allRows, key: string | undefined, direction: 'asc' | 'desc') => {
|
|
194
|
+
if (!key) return rows
|
|
195
|
+
return [...rows].sort((a, b) => {
|
|
196
|
+
const aVal = String(a[key as keyof typeof a] ?? '')
|
|
197
|
+
const bVal = String(b[key as keyof typeof b] ?? '')
|
|
198
|
+
const result = aVal.localeCompare(bVal, undefined, { numeric: true, sensitivity: 'base' })
|
|
199
|
+
return direction === 'asc' ? result : -result
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const Default: Story = {
|
|
204
|
+
args: { columns, rows: allRows },
|
|
205
|
+
render: (args) => ({
|
|
206
|
+
components: { Table },
|
|
207
|
+
setup() {
|
|
208
|
+
const sortKey = ref<string | undefined>(undefined)
|
|
209
|
+
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
210
|
+
const rows = computed(() =>
|
|
211
|
+
sortRows(args.rows as typeof allRows, sortKey.value, sortDirection.value),
|
|
212
|
+
)
|
|
213
|
+
const onSort = (key: string, direction: 'asc' | 'desc') => {
|
|
214
|
+
sortKey.value = key
|
|
215
|
+
sortDirection.value = direction
|
|
216
|
+
}
|
|
217
|
+
return { args, rows, sortKey, sortDirection, onSort }
|
|
218
|
+
},
|
|
219
|
+
template: `<Table :columns="args.columns" :rows="rows" :sort-key="sortKey" :sort-direction="sortDirection" @sort="onSort" />`,
|
|
220
|
+
}),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const WithSelection: Story = {
|
|
224
|
+
args: { columns, rows: allRows, selectable: true },
|
|
225
|
+
render: (args) => ({
|
|
226
|
+
components: { Table },
|
|
227
|
+
setup() {
|
|
228
|
+
const sortKey = ref<string | undefined>(undefined)
|
|
229
|
+
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
230
|
+
const selected = ref<number[]>([])
|
|
231
|
+
const rows = computed(() =>
|
|
232
|
+
sortRows(args.rows as typeof allRows, sortKey.value, sortDirection.value),
|
|
233
|
+
)
|
|
234
|
+
const onSort = (key: string, direction: 'asc' | 'desc') => {
|
|
235
|
+
sortKey.value = key
|
|
236
|
+
sortDirection.value = direction
|
|
237
|
+
}
|
|
238
|
+
return { args, rows, sortKey, sortDirection, onSort, selected }
|
|
239
|
+
},
|
|
240
|
+
template: `<Table :columns="args.columns" :rows="rows" :sort-key="sortKey" :sort-direction="sortDirection" :selectable="args.selectable" v-model="selected" @sort="onSort" />`,
|
|
241
|
+
}),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const WithSlots: Story = {
|
|
245
|
+
args: { columns, rows: allRows },
|
|
246
|
+
render: (args) => ({
|
|
247
|
+
components: { Table },
|
|
248
|
+
setup() {
|
|
249
|
+
const sortKey = ref<string | undefined>(undefined)
|
|
250
|
+
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
251
|
+
const rows = computed(() =>
|
|
252
|
+
sortRows(args.rows as typeof allRows, sortKey.value, sortDirection.value),
|
|
253
|
+
)
|
|
254
|
+
const onSort = (key: string, direction: 'asc' | 'desc') => {
|
|
255
|
+
sortKey.value = key
|
|
256
|
+
sortDirection.value = direction
|
|
257
|
+
}
|
|
258
|
+
return { args, rows, sortKey, sortDirection, onSort }
|
|
259
|
+
},
|
|
260
|
+
template: `
|
|
261
|
+
<Table :columns="args.columns" :rows="rows" :sort-key="sortKey" :sort-direction="sortDirection" @sort="onSort">
|
|
262
|
+
<template #cell-name="{ value, row }">
|
|
263
|
+
<div class="flex flex-col">
|
|
264
|
+
<span class="font-medium">{{ value }}</span>
|
|
265
|
+
<span class="text-xs text-slate-400">{{ row.email }}</span>
|
|
266
|
+
</div>
|
|
267
|
+
</template>
|
|
268
|
+
<template #cell-status="{ value }">
|
|
269
|
+
<span
|
|
270
|
+
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
|
271
|
+
:class="value === 'Active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'"
|
|
272
|
+
>
|
|
273
|
+
{{ value }}
|
|
274
|
+
</span>
|
|
275
|
+
</template>
|
|
276
|
+
</Table>
|
|
277
|
+
`,
|
|
278
|
+
}),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const Empty: Story = {
|
|
282
|
+
args: { columns, rows: [] },
|
|
283
|
+
}
|