frappe-ui 0.1.5 → 0.1.7
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/package.json +2 -1
- package/src/components/Badge.vue +6 -2
- package/src/components/Breadcrumbs.story.vue +73 -0
- package/src/components/Breadcrumbs.vue +108 -0
- package/src/components/ListView/ListHeader.vue +28 -0
- package/src/components/ListView/ListHeaderItem.vue +22 -0
- package/src/components/ListView/ListRow.vue +66 -23
- package/src/components/ListView/ListRowItem.vue +39 -7
- package/src/components/ListView/ListRows.vue +19 -0
- package/src/components/ListView/ListSelectBanner.vue +82 -0
- package/src/components/ListView/ListView.vue +54 -100
- package/src/components/ListView/utils.js +23 -0
- package/src/components/ListView.story.md +123 -0
- package/src/components/ListView.story.vue +200 -0
- package/src/components/Tabs.story.vue +72 -0
- package/src/components/Tabs.vue +97 -0
- package/src/index.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@tiptap/starter-kit": "^2.0.3",
|
|
51
51
|
"@tiptap/suggestion": "^2.0.3",
|
|
52
52
|
"@tiptap/vue-3": "^2.0.3",
|
|
53
|
+
"@vueuse/core": "^10.4.1",
|
|
53
54
|
"feather-icons": "^4.28.0",
|
|
54
55
|
"idb-keyval": "^6.2.0",
|
|
55
56
|
"showdown": "^2.1.0",
|
package/src/components/Badge.vue
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
>
|
|
10
10
|
<slot name="prefix"></slot>
|
|
11
11
|
</div>
|
|
12
|
-
<slot>{{ props.label }}</slot>
|
|
12
|
+
<slot>{{ props.label?.toString() }}</slot>
|
|
13
13
|
<div
|
|
14
14
|
:class="[props.size == 'lg' ? 'max-h-6' : 'max-h-4']"
|
|
15
15
|
v-if="$slots.suffix"
|
|
@@ -22,11 +22,15 @@
|
|
|
22
22
|
<script lang="ts" setup>
|
|
23
23
|
import { computed } from 'vue'
|
|
24
24
|
|
|
25
|
+
interface Label {
|
|
26
|
+
toString(): string
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
interface BadgeProps {
|
|
26
30
|
theme?: 'gray' | 'blue' | 'green' | 'orange' | 'red'
|
|
27
31
|
size?: 'sm' | 'md' | 'lg'
|
|
28
32
|
variant?: 'solid' | 'subtle' | 'outline' | 'ghost'
|
|
29
|
-
label?: string
|
|
33
|
+
label?: Label | string | number
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const props = withDefaults(defineProps<BadgeProps>(), {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { logEvent } from 'histoire/client'
|
|
3
|
+
import Breadcrumbs from './Breadcrumbs.vue'
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<Story :layout="{ type: 'grid', width: 500 }">
|
|
8
|
+
<Variant title="With route option">
|
|
9
|
+
<Breadcrumbs
|
|
10
|
+
:items="[
|
|
11
|
+
{
|
|
12
|
+
label: 'Home',
|
|
13
|
+
route: { name: 'Home' },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'Views',
|
|
17
|
+
route: '/components',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'List',
|
|
21
|
+
route: '/components/breadcrumbs',
|
|
22
|
+
},
|
|
23
|
+
]"
|
|
24
|
+
/>
|
|
25
|
+
</Variant>
|
|
26
|
+
<Variant title="With onClick option">
|
|
27
|
+
<Breadcrumbs
|
|
28
|
+
:items="[
|
|
29
|
+
{
|
|
30
|
+
label: 'Home',
|
|
31
|
+
onClick: () => logEvent('onClick', 'Home'),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Views',
|
|
35
|
+
onClick: () => logEvent('onClick', 'Home'),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'Kanban',
|
|
39
|
+
onClick: () => logEvent('onClick', 'Home'),
|
|
40
|
+
},
|
|
41
|
+
]"
|
|
42
|
+
/>
|
|
43
|
+
</Variant>
|
|
44
|
+
|
|
45
|
+
<Variant title="With prefix slot">
|
|
46
|
+
<Breadcrumbs
|
|
47
|
+
:items="[
|
|
48
|
+
{
|
|
49
|
+
label: 'Home',
|
|
50
|
+
icon: '🏡',
|
|
51
|
+
route: { name: 'Home' },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'Views',
|
|
55
|
+
icon: '🏞️',
|
|
56
|
+
route: '/components',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'List',
|
|
60
|
+
icon: '📃',
|
|
61
|
+
route: '/components/breadcrumbs',
|
|
62
|
+
},
|
|
63
|
+
]"
|
|
64
|
+
>
|
|
65
|
+
<template #prefix="{ item }">
|
|
66
|
+
<span class="mr-1">
|
|
67
|
+
{{ item.icon }}
|
|
68
|
+
</span>
|
|
69
|
+
</template>
|
|
70
|
+
</Breadcrumbs>
|
|
71
|
+
</Variant>
|
|
72
|
+
</Story>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex min-w-0 items-center">
|
|
3
|
+
<template v-if="dropdownItems.length">
|
|
4
|
+
<Dropdown class="h-7" :options="dropdownItems">
|
|
5
|
+
<Button variant="ghost">
|
|
6
|
+
<template #icon>
|
|
7
|
+
<svg
|
|
8
|
+
class="w-4 text-gray-600"
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
width="24"
|
|
11
|
+
height="24"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
stroke-width="2"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
stroke-linejoin="round"
|
|
18
|
+
>
|
|
19
|
+
<circle cx="12" cy="12" r="1" />
|
|
20
|
+
<circle cx="19" cy="12" r="1" />
|
|
21
|
+
<circle cx="5" cy="12" r="1" />
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
24
|
+
</Button>
|
|
25
|
+
</Dropdown>
|
|
26
|
+
<span class="ml-1 mr-0.5 text-base text-gray-500" aria-hidden="true">
|
|
27
|
+
/
|
|
28
|
+
</span>
|
|
29
|
+
</template>
|
|
30
|
+
<div
|
|
31
|
+
class="flex min-w-0 items-center overflow-hidden text-ellipsis whitespace-nowrap"
|
|
32
|
+
>
|
|
33
|
+
<template v-for="(item, i) in crumbs" :key="item.label">
|
|
34
|
+
<component
|
|
35
|
+
:is="item.route ? 'router-link' : 'button'"
|
|
36
|
+
class="flex items-center rounded px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400"
|
|
37
|
+
:class="[
|
|
38
|
+
i == crumbs.length - 1
|
|
39
|
+
? 'text-gray-900'
|
|
40
|
+
: 'text-gray-600 hover:text-gray-700',
|
|
41
|
+
]"
|
|
42
|
+
v-bind="item.route ? { to: item.route } : { onClick: item.onClick }"
|
|
43
|
+
>
|
|
44
|
+
<slot name="prefix" :item="item" />
|
|
45
|
+
<span>
|
|
46
|
+
{{ item.label }}
|
|
47
|
+
</span>
|
|
48
|
+
</component>
|
|
49
|
+
<span
|
|
50
|
+
v-if="i != crumbs.length - 1"
|
|
51
|
+
class="mx-0.5 text-base text-gray-500"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
/
|
|
55
|
+
</span>
|
|
56
|
+
</template>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
<script setup lang="ts">
|
|
61
|
+
import { useWindowSize } from '@vueuse/core'
|
|
62
|
+
import { computed } from 'vue'
|
|
63
|
+
import { RouterLinkProps, useRouter } from 'vue-router'
|
|
64
|
+
import Dropdown from '../components/Dropdown.vue'
|
|
65
|
+
import Button from '../components/Button.vue'
|
|
66
|
+
|
|
67
|
+
interface BreadcrumbItem {
|
|
68
|
+
label: string
|
|
69
|
+
route?: RouterLinkProps['to']
|
|
70
|
+
onClick?: () => void
|
|
71
|
+
[key: string]: any
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface BreadcrumbsProps {
|
|
75
|
+
items: BreadcrumbItem[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const props = defineProps<BreadcrumbsProps>()
|
|
79
|
+
|
|
80
|
+
const router = useRouter()
|
|
81
|
+
const { width } = useWindowSize()
|
|
82
|
+
|
|
83
|
+
const items = computed(() => {
|
|
84
|
+
return (props.items || []).filter(Boolean)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const dropdownItems = computed(() => {
|
|
88
|
+
if (width.value > 640) return []
|
|
89
|
+
|
|
90
|
+
let allExceptLastTwo = items.value.slice(0, -2)
|
|
91
|
+
return allExceptLastTwo.map((item) => {
|
|
92
|
+
let onClick = item.onClick ? item.onClick : () => router.push(item.route)
|
|
93
|
+
return {
|
|
94
|
+
...item,
|
|
95
|
+
icon: null,
|
|
96
|
+
label: item.label,
|
|
97
|
+
onClick,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const crumbs = computed(() => {
|
|
103
|
+
if (width.value > 640) return items.value
|
|
104
|
+
|
|
105
|
+
let lastTwo = items.value.slice(-2)
|
|
106
|
+
return lastTwo
|
|
107
|
+
})
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="mx-5 mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
|
4
|
+
:style="{ gridTemplateColumns: getGridTemplateColumns(columns) }"
|
|
5
|
+
>
|
|
6
|
+
<Checkbox
|
|
7
|
+
class="cursor-pointer duration-300"
|
|
8
|
+
:modelValue="allRowsSelected"
|
|
9
|
+
@click.stop="toggleAllRows"
|
|
10
|
+
/>
|
|
11
|
+
<slot>
|
|
12
|
+
<ListHeaderItem
|
|
13
|
+
v-for="column in columns"
|
|
14
|
+
:key="column.key"
|
|
15
|
+
:column="column"
|
|
16
|
+
/>
|
|
17
|
+
</slot>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
import Checkbox from '../Checkbox.vue'
|
|
23
|
+
import ListHeaderItem from './ListHeaderItem.vue'
|
|
24
|
+
import { getGridTemplateColumns } from './utils'
|
|
25
|
+
import { inject } from 'vue'
|
|
26
|
+
|
|
27
|
+
const { columns, allRowsSelected, toggleAllRows } = inject('list')
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex items-center space-x-2 text-base text-gray-600"
|
|
4
|
+
:class="alignmentMap[column.align]"
|
|
5
|
+
>
|
|
6
|
+
<slot name="prefix" v-bind="{ column }" />
|
|
7
|
+
<div>
|
|
8
|
+
{{ column.label }}
|
|
9
|
+
</div>
|
|
10
|
+
<slot name="suffix" v-bind="{ column }" />
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import { alignmentMap } from './utils'
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
column: {
|
|
18
|
+
type: Object,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
@@ -1,29 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="row.route ? 'router-link' : 'div'"
|
|
4
|
+
class="mx-5 flex cursor-pointer flex-col transition-all duration-300 ease-in-out"
|
|
5
|
+
v-bind="row.route ? { to: row.route } : { onClick: row.onClick }"
|
|
6
|
+
>
|
|
7
|
+
<component
|
|
8
|
+
:is="row.route ? 'template' : 'button'"
|
|
9
|
+
class="[all:unset] hover:[all:unset]"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
class="grid items-center space-x-4 rounded px-2 py-2.5"
|
|
13
|
+
:class="
|
|
14
|
+
selections.has(row[rowKey])
|
|
15
|
+
? 'bg-gray-100 hover:bg-gray-200'
|
|
16
|
+
: 'hover:bg-gray-50'
|
|
17
|
+
"
|
|
18
|
+
:style="{ gridTemplateColumns: getGridTemplateColumns(columns) }"
|
|
19
|
+
>
|
|
20
|
+
<Checkbox
|
|
21
|
+
:modelValue="selections.has(row[rowKey])"
|
|
22
|
+
@click.stop="toggleRow(row[rowKey])"
|
|
23
|
+
class="cursor-pointer duration-300"
|
|
24
|
+
/>
|
|
25
|
+
<div
|
|
26
|
+
v-for="column in columns"
|
|
27
|
+
:key="column.key"
|
|
28
|
+
:class="alignmentMap[column.align]"
|
|
29
|
+
>
|
|
30
|
+
<slot v-bind="{ column, item: _row(row)[column.key] }">
|
|
31
|
+
<ListRowItem
|
|
32
|
+
:item="_row(row)[column.key]"
|
|
33
|
+
:type="column.type"
|
|
34
|
+
:align="column.align"
|
|
35
|
+
/>
|
|
36
|
+
</slot>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
v-if="idx < rows.length - 1"
|
|
41
|
+
class="mx-2 h-px border-t border-gray-200"
|
|
42
|
+
/>
|
|
43
|
+
</component>
|
|
44
|
+
</component>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
1
47
|
<script setup>
|
|
2
|
-
import { inject } from 'vue'
|
|
3
48
|
import Checkbox from '../Checkbox.vue'
|
|
49
|
+
import ListRowItem from './ListRowItem.vue'
|
|
50
|
+
import { alignmentMap, getGridTemplateColumns } from './utils'
|
|
51
|
+
import { inject } from 'vue'
|
|
4
52
|
|
|
5
|
-
const list = inject('list')
|
|
6
53
|
const props = defineProps({
|
|
7
|
-
|
|
8
|
-
|
|
54
|
+
row: {
|
|
55
|
+
type: Object,
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
idx: {
|
|
59
|
+
type: Number,
|
|
60
|
+
required: true,
|
|
61
|
+
},
|
|
9
62
|
})
|
|
10
|
-
</script>
|
|
11
63
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
>
|
|
22
|
-
<Checkbox
|
|
23
|
-
:modelValue="list.selections.has(row.name)"
|
|
24
|
-
@click.stop="list.toggleSelection(row.name)"
|
|
25
|
-
class="cursor-pointer duration-300"
|
|
26
|
-
/>
|
|
27
|
-
<slot></slot>
|
|
28
|
-
</component>
|
|
29
|
-
</template>
|
|
64
|
+
function _row(row) {
|
|
65
|
+
if (row.row && typeof row.row === 'object' && (row.onClick || row.route)) {
|
|
66
|
+
return row.row
|
|
67
|
+
}
|
|
68
|
+
return row
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { rows, columns, rowKey, selections, toggleRow } = inject('list')
|
|
72
|
+
</script>
|
|
@@ -1,10 +1,42 @@
|
|
|
1
|
-
<script setup></script>
|
|
2
|
-
|
|
3
1
|
<template>
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
<Tooltip
|
|
3
|
+
:text="label"
|
|
4
|
+
class="flex items-center space-x-2"
|
|
5
|
+
:class="alignmentMap[align]"
|
|
7
6
|
>
|
|
8
|
-
<slot
|
|
9
|
-
|
|
7
|
+
<slot name="prefix" />
|
|
8
|
+
<slot v-bind="{ label }">
|
|
9
|
+
<div class="truncate text-base">
|
|
10
|
+
{{ label }}
|
|
11
|
+
</div>
|
|
12
|
+
</slot>
|
|
13
|
+
<slot name="suffix" />
|
|
14
|
+
</Tooltip>
|
|
10
15
|
</template>
|
|
16
|
+
<script setup>
|
|
17
|
+
import { alignmentMap } from './utils'
|
|
18
|
+
import Tooltip from '../Tooltip.vue'
|
|
19
|
+
import { computed } from 'vue'
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
item: {
|
|
23
|
+
type: [String, Number, Object],
|
|
24
|
+
default: '',
|
|
25
|
+
},
|
|
26
|
+
align: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'left',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const label = computed(() => {
|
|
33
|
+
return getValue(props.item).label || ''
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function getValue(value) {
|
|
37
|
+
if (value && typeof value === 'object') {
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
return { label: value }
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-y-auto">
|
|
3
|
+
<slot>
|
|
4
|
+
<ListRow
|
|
5
|
+
v-for="(row, i) in rows"
|
|
6
|
+
:key="row[rowKey]"
|
|
7
|
+
:row="row"
|
|
8
|
+
:idx="i"
|
|
9
|
+
/>
|
|
10
|
+
</slot>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import ListRow from './ListRow.vue'
|
|
16
|
+
import { inject } from 'vue'
|
|
17
|
+
|
|
18
|
+
const { rows, rowKey } = inject('list')
|
|
19
|
+
</script>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition
|
|
3
|
+
enter-active-class="duration-300 ease-out"
|
|
4
|
+
enter-from-class="transform opacity-0"
|
|
5
|
+
enter-to-class="opacity-100"
|
|
6
|
+
leave-active-class="duration-300 ease-in"
|
|
7
|
+
leave-from-class="opacity-100"
|
|
8
|
+
leave-to-class="transform opacity-0"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
v-if="selections.size"
|
|
12
|
+
class="absolute inset-x-0 bottom-6 mx-auto w-max text-base"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
class="flex min-w-[596px] items-center space-x-3 rounded-lg bg-white px-4 py-2 shadow-2xl"
|
|
16
|
+
:class="$attrs.class"
|
|
17
|
+
>
|
|
18
|
+
<slot
|
|
19
|
+
v-bind="{
|
|
20
|
+
selections,
|
|
21
|
+
allRowsSelected,
|
|
22
|
+
selectAll: () => toggleAllRows(true),
|
|
23
|
+
unselectAll: () => toggleAllRows(false),
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
class="flex flex-1 justify-between border-r border-gray-300 text-gray-900"
|
|
28
|
+
>
|
|
29
|
+
<div class="flex items-center space-x-3">
|
|
30
|
+
<Checkbox
|
|
31
|
+
:modelValue="true"
|
|
32
|
+
:disabled="true"
|
|
33
|
+
class="text-gray-900"
|
|
34
|
+
/>
|
|
35
|
+
<div>{{ selectedText }}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="mr-3">
|
|
38
|
+
<slot
|
|
39
|
+
name="actions"
|
|
40
|
+
v-bind="{
|
|
41
|
+
selections,
|
|
42
|
+
allRowsSelected,
|
|
43
|
+
selectAll: () => toggleAllRows(true),
|
|
44
|
+
unselectAll: () => toggleAllRows(false),
|
|
45
|
+
}"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="flex items-center space-x-1">
|
|
50
|
+
<Button
|
|
51
|
+
class="w- text-gray-700"
|
|
52
|
+
:disabled="allRowsSelected"
|
|
53
|
+
:class="allRowsSelected ? 'cursor-not-allowed' : ''"
|
|
54
|
+
variant="ghost"
|
|
55
|
+
@click="toggleAllRows(true)"
|
|
56
|
+
>
|
|
57
|
+
Select all
|
|
58
|
+
</Button>
|
|
59
|
+
<Button icon="x" variant="ghost" @click="toggleAllRows(false)" />
|
|
60
|
+
</div>
|
|
61
|
+
</slot>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</transition>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup>
|
|
68
|
+
import Checkbox from '../Checkbox.vue'
|
|
69
|
+
import Button from '../Button.vue'
|
|
70
|
+
import { computed, inject } from 'vue'
|
|
71
|
+
|
|
72
|
+
defineOptions({
|
|
73
|
+
inheritAttrs: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
let selectedText = computed(() => {
|
|
77
|
+
let title = selections.size === 1 ? 'Row' : 'Rows'
|
|
78
|
+
return `${selections.size} ${title} selected`
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const { list, selections, allRowsSelected, toggleAllRows } = inject('list')
|
|
82
|
+
</script>
|
|
@@ -1,116 +1,70 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/>
|
|
13
|
-
<div
|
|
14
|
-
v-for="column in props.columns"
|
|
15
|
-
:key="column"
|
|
16
|
-
class="flex-1 text-sm text-gray-600"
|
|
17
|
-
:class="column.class"
|
|
18
|
-
>
|
|
19
|
-
{{ column.label }}
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
<div id="list-rows" class="h-full overflow-y-auto">
|
|
23
|
-
<template v-for="row in props.rows" :key="row.name">
|
|
24
|
-
<slot name="list-row" :row="row">
|
|
25
|
-
<ListRow as="div" :row="row">
|
|
26
|
-
<ListRowItem v-for="column in props.columns" :key="column.name">
|
|
27
|
-
{{ row[column.name] }}
|
|
28
|
-
</ListRowItem>
|
|
29
|
-
</ListRow>
|
|
30
|
-
</slot>
|
|
31
|
-
</template>
|
|
32
|
-
</div>
|
|
33
|
-
<transition
|
|
34
|
-
enter-active-class="duration-300 ease-out"
|
|
35
|
-
enter-from-class="transform opacity-0"
|
|
36
|
-
enter-to-class="opacity-100"
|
|
37
|
-
leave-active-class="duration-300 ease-in"
|
|
38
|
-
leave-from-class="opacity-100"
|
|
39
|
-
leave-to-class="transform opacity-0"
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
v-if="state.selections.size"
|
|
43
|
-
class="fixed inset-x-0 bottom-6 mx-auto w-max text-base"
|
|
44
|
-
>
|
|
45
|
-
<div
|
|
46
|
-
class="flex w-[596px] items-center space-x-3 rounded-lg bg-white px-4 py-2 shadow-2xl"
|
|
47
|
-
>
|
|
48
|
-
<div
|
|
49
|
-
class="flex flex-1 items-center space-x-3 border-r border-gray-300 text-gray-900"
|
|
50
|
-
>
|
|
51
|
-
<Checkbox
|
|
52
|
-
:modelValue="true"
|
|
53
|
-
:disabled="true"
|
|
54
|
-
class="text-gray-900"
|
|
55
|
-
/>
|
|
56
|
-
<div>{{ state.selectedText }}</div>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="flex items-center space-x-1">
|
|
59
|
-
<Button
|
|
60
|
-
class="text-gray-700"
|
|
61
|
-
:disabled="state.allRowsSelected"
|
|
62
|
-
:class="state.allRowsSelected ? 'cursor-not-allowed' : ''"
|
|
63
|
-
variant="ghost"
|
|
64
|
-
@click="state.toggleAllRows(true)"
|
|
65
|
-
>
|
|
66
|
-
Select all
|
|
67
|
-
</Button>
|
|
68
|
-
<Button
|
|
69
|
-
icon="x"
|
|
70
|
-
variant="ghost"
|
|
71
|
-
@click="state.toggleAllRows(false)"
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</transition>
|
|
2
|
+
<div class="relative flex w-full flex-1 flex-col overflow-x-auto">
|
|
3
|
+
<div
|
|
4
|
+
class="mt-3 flex w-max min-w-full flex-col overflow-y-hidden"
|
|
5
|
+
:class="$attrs.class"
|
|
6
|
+
>
|
|
7
|
+
<slot>
|
|
8
|
+
<ListHeader />
|
|
9
|
+
<ListRows />
|
|
10
|
+
<ListSelectBanner />
|
|
11
|
+
</slot>
|
|
77
12
|
</div>
|
|
78
13
|
</div>
|
|
79
14
|
</template>
|
|
80
15
|
<script setup>
|
|
81
|
-
import
|
|
82
|
-
import
|
|
83
|
-
import
|
|
84
|
-
import
|
|
85
|
-
|
|
16
|
+
import ListHeader from './ListHeader.vue'
|
|
17
|
+
import ListRows from './ListRows.vue'
|
|
18
|
+
import ListSelectBanner from './ListSelectBanner.vue'
|
|
19
|
+
import { reactive, computed, provide } from 'vue'
|
|
20
|
+
|
|
21
|
+
defineOptions({
|
|
22
|
+
inheritAttrs: false,
|
|
23
|
+
})
|
|
86
24
|
|
|
87
25
|
const props = defineProps({
|
|
88
|
-
columns: {
|
|
89
|
-
|
|
26
|
+
columns: {
|
|
27
|
+
type: Array,
|
|
28
|
+
default: [],
|
|
29
|
+
},
|
|
30
|
+
rows: {
|
|
31
|
+
type: Array,
|
|
32
|
+
default: [],
|
|
33
|
+
},
|
|
34
|
+
rowKey: {
|
|
35
|
+
type: String,
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
90
38
|
})
|
|
91
39
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
? `${state.selections.size} items selected`
|
|
98
|
-
: `${state.selections.size} item selected`
|
|
99
|
-
}),
|
|
40
|
+
let selections = reactive(new Set())
|
|
41
|
+
|
|
42
|
+
const allRowsSelected = computed(() => {
|
|
43
|
+
if (!props.rows.length) return false
|
|
44
|
+
return selections.size === props.rows.length
|
|
100
45
|
})
|
|
101
|
-
provide('list', state)
|
|
102
46
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
47
|
+
function toggleRow(row) {
|
|
48
|
+
if (!selections.delete(row)) {
|
|
49
|
+
selections.add(row)
|
|
50
|
+
}
|
|
107
51
|
}
|
|
108
52
|
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
state.selections.add(name)
|
|
53
|
+
function toggleAllRows(select) {
|
|
54
|
+
if (!select || allRowsSelected.value) {
|
|
55
|
+
selections.clear()
|
|
56
|
+
return
|
|
114
57
|
}
|
|
58
|
+
props.rows.forEach((row) => selections.add(row[props.rowKey]))
|
|
115
59
|
}
|
|
60
|
+
|
|
61
|
+
provide('list', {
|
|
62
|
+
rowKey: props.rowKey,
|
|
63
|
+
rows: props.rows,
|
|
64
|
+
columns: props.columns,
|
|
65
|
+
selections,
|
|
66
|
+
allRowsSelected,
|
|
67
|
+
toggleRow,
|
|
68
|
+
toggleAllRows,
|
|
69
|
+
})
|
|
116
70
|
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function getGridTemplateColumns(columns) {
|
|
2
|
+
return (
|
|
3
|
+
'14px ' +
|
|
4
|
+
columns
|
|
5
|
+
.map((col) => {
|
|
6
|
+
let width = col.width || 1
|
|
7
|
+
if (typeof width === 'number') {
|
|
8
|
+
return width + 'fr'
|
|
9
|
+
}
|
|
10
|
+
return width
|
|
11
|
+
})
|
|
12
|
+
.join(' ')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const alignmentMap = {
|
|
17
|
+
left: 'justify-start',
|
|
18
|
+
start: 'justify-start',
|
|
19
|
+
center: 'justify-center',
|
|
20
|
+
middle: 'justify-center',
|
|
21
|
+
right: 'justify-end',
|
|
22
|
+
end: 'justify-end',
|
|
23
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
**Column:**
|
|
2
|
+
|
|
3
|
+
1. `label` & `key` is required in column object.
|
|
4
|
+
|
|
5
|
+
2. `width` is optional and it is used to set column width in list
|
|
6
|
+
|
|
7
|
+
1. If you need a column to be `3` times a default column then add `3`. if
|
|
8
|
+
width is not mentioned default will be `1`
|
|
9
|
+
2. You can also add custom width in px and rem e.g `300px` or `12rem`
|
|
10
|
+
3. Combination of both can also be used.
|
|
11
|
+
|
|
12
|
+
3. `align` is also optional. You can change the alignment of the content in the
|
|
13
|
+
column by setting it as.
|
|
14
|
+
|
|
15
|
+
1. `start` or `left` (default)
|
|
16
|
+
2. `center` or `middle`
|
|
17
|
+
3. `end` or `right`
|
|
18
|
+
|
|
19
|
+
4. You can add more attributes which can be used to render custom column header
|
|
20
|
+
items.
|
|
21
|
+
|
|
22
|
+
**Row**
|
|
23
|
+
|
|
24
|
+
1. The row object must contain a unique_key which was mentioned in ListView
|
|
25
|
+
`row-key`
|
|
26
|
+
2. You can either add all row fields in a separate `row` object or just add them
|
|
27
|
+
in directly if the fieldnames doesn't conflict with `route` or `onClick` E.g.
|
|
28
|
+
1
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
{
|
|
32
|
+
// unique_key 'id'
|
|
33
|
+
id: 1,
|
|
34
|
+
|
|
35
|
+
// row fields
|
|
36
|
+
name: 'John Doe',
|
|
37
|
+
age: 25,
|
|
38
|
+
email: 'john@doe.com',
|
|
39
|
+
|
|
40
|
+
// if you need to route
|
|
41
|
+
route: { label: 'User', { params: { userId: 1 } }
|
|
42
|
+
|
|
43
|
+
// if you need to perform action
|
|
44
|
+
onClick: () => console.log('John Doe was clicked')
|
|
45
|
+
|
|
46
|
+
// you can add more options after this which you can use to render custom row items
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
E.g. 2
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
{
|
|
54
|
+
// unique_key 'id'
|
|
55
|
+
id: 1,
|
|
56
|
+
|
|
57
|
+
// row fields in separate row object
|
|
58
|
+
row: {
|
|
59
|
+
name: 'John Doe',
|
|
60
|
+
age: 25,
|
|
61
|
+
email: 'john@doe.com',
|
|
62
|
+
route: '', // used separate row to avoid this conflict
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// if you need to route
|
|
66
|
+
route: { label: 'User', { params: { userId: 1 } }
|
|
67
|
+
|
|
68
|
+
// if you need to perform action
|
|
69
|
+
onClick: () => console.log('John Doe was clicked')
|
|
70
|
+
|
|
71
|
+
// you can add more options after this which you can use to render custom row items
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. You can also add an object for the field value but make sure it has a `label`
|
|
76
|
+
attribute which holds the actual value to be shown
|
|
77
|
+
```
|
|
78
|
+
row: {
|
|
79
|
+
name: {
|
|
80
|
+
label: 'John Doe',
|
|
81
|
+
image: '/johndoe.jpg',
|
|
82
|
+
},
|
|
83
|
+
age: 25,
|
|
84
|
+
status: {
|
|
85
|
+
label: 'Active',
|
|
86
|
+
color: 'green'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
4. Click action: Add route or onClick event in row object
|
|
91
|
+
1. If you want to route using router-link just add a
|
|
92
|
+
`route: { name: 'User', params: { userId: 2 } }`
|
|
93
|
+
2. if you need to do some action or open a dialog add a click event instead
|
|
94
|
+
of a route `onClick: () => console.log('John Doe was clicked')`
|
|
95
|
+
|
|
96
|
+
**Selection Banner:**
|
|
97
|
+
|
|
98
|
+
**Without custom action buttons:**
|
|
99
|
+
<img width="1213" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/36fafcf5-45c6-43f0-acde-f64afe38b550">
|
|
100
|
+
|
|
101
|
+
**With custom action buttons:**
|
|
102
|
+
<img width="1212" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/55e751b2-df66-4ff0-b852-af463014463f">
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
<ListSelectBanner>
|
|
106
|
+
<template #actions>
|
|
107
|
+
<div class="flex gap-2">
|
|
108
|
+
<Button variant="ghost" label="Delete" />
|
|
109
|
+
<Button variant="ghost" label="Edit" />
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
</ListSelectBanner>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also make your own custom selection banner
|
|
116
|
+
|
|
117
|
+
<img width="629" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/38dfa834-96a2-4ac5-ad4b-30b3e6871d3f">
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
<ListSelectBanner>
|
|
121
|
+
<div>Custom Banner</div>
|
|
122
|
+
</ListSelectBanner>
|
|
123
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ListView from './ListView/ListView.vue'
|
|
3
|
+
import ListHeader from './ListView/ListHeader.vue'
|
|
4
|
+
import ListHeaderItem from './ListView/ListHeaderItem.vue'
|
|
5
|
+
import ListRows from './ListView/ListRows.vue'
|
|
6
|
+
import ListRow from './ListView/ListRow.vue'
|
|
7
|
+
import ListRowItem from './ListView/ListRowItem.vue'
|
|
8
|
+
import ListSelectBanner from './ListView/ListSelectBanner.vue'
|
|
9
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
10
|
+
import Badge from './Badge.vue'
|
|
11
|
+
import Button from './Button.vue'
|
|
12
|
+
import Avatar from './Avatar.vue'
|
|
13
|
+
|
|
14
|
+
const simple_columns = [
|
|
15
|
+
{
|
|
16
|
+
label: 'Name',
|
|
17
|
+
key: 'name',
|
|
18
|
+
width: 3,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: 'Email',
|
|
22
|
+
key: 'email',
|
|
23
|
+
width: '200px',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'Role',
|
|
27
|
+
key: 'role',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'Status',
|
|
31
|
+
key: 'status',
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const simple_rows = [
|
|
36
|
+
{
|
|
37
|
+
id: 1,
|
|
38
|
+
row: {
|
|
39
|
+
name: 'John Doe',
|
|
40
|
+
email: 'john@doe.com',
|
|
41
|
+
status: 'Active',
|
|
42
|
+
role: 'Developer',
|
|
43
|
+
},
|
|
44
|
+
onClick: () => console.log('John Doe was clicked'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 2,
|
|
48
|
+
row: {
|
|
49
|
+
name: 'Jane Doe',
|
|
50
|
+
email: 'jane@doe.com',
|
|
51
|
+
status: 'Inactive',
|
|
52
|
+
role: 'HR',
|
|
53
|
+
},
|
|
54
|
+
route: { name: 'User', params: { userId: 2 } },
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const custom_columns = [
|
|
59
|
+
{
|
|
60
|
+
label: 'Name',
|
|
61
|
+
key: 'name',
|
|
62
|
+
width: 3,
|
|
63
|
+
icon: 'user',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Email',
|
|
67
|
+
key: 'email',
|
|
68
|
+
width: '200px',
|
|
69
|
+
icon: 'at-sign',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'Role',
|
|
73
|
+
key: 'role',
|
|
74
|
+
icon: 'users',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'Status',
|
|
78
|
+
key: 'status',
|
|
79
|
+
icon: 'check-circle',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const custom_rows = [
|
|
84
|
+
{
|
|
85
|
+
id: 1,
|
|
86
|
+
row: {
|
|
87
|
+
name: {
|
|
88
|
+
label: 'John Doe',
|
|
89
|
+
image: 'https://avatars.githubusercontent.com/u/499550',
|
|
90
|
+
},
|
|
91
|
+
email: 'john@doe.com',
|
|
92
|
+
status: {
|
|
93
|
+
label: 'Active',
|
|
94
|
+
bg_color: 'bg-green-600',
|
|
95
|
+
},
|
|
96
|
+
role: {
|
|
97
|
+
label: 'Developer',
|
|
98
|
+
color: 'green',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
onClick: () => console.log('John Doe was clicked'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 2,
|
|
105
|
+
row: {
|
|
106
|
+
name: {
|
|
107
|
+
label: 'Jane Doe',
|
|
108
|
+
image: 'https://avatars.githubusercontent.com/u/499120',
|
|
109
|
+
},
|
|
110
|
+
email: 'jane@doe.com',
|
|
111
|
+
status: {
|
|
112
|
+
label: 'Inactive',
|
|
113
|
+
bg_color: 'bg-red-600',
|
|
114
|
+
},
|
|
115
|
+
role: {
|
|
116
|
+
label: 'HR',
|
|
117
|
+
color: 'red',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
route: { name: 'User', params: { userId: 2 } },
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<template>
|
|
126
|
+
<Story :layout="{ type: 'grid', width: '95%' }">
|
|
127
|
+
<Variant title="Simple List">
|
|
128
|
+
<ListView
|
|
129
|
+
class="h-[250px]"
|
|
130
|
+
:columns="simple_columns"
|
|
131
|
+
:rows="simple_rows"
|
|
132
|
+
row-key="id"
|
|
133
|
+
/>
|
|
134
|
+
</Variant>
|
|
135
|
+
<Variant title="Custom List">
|
|
136
|
+
<ListView
|
|
137
|
+
class="h-[250px]"
|
|
138
|
+
:columns="custom_columns"
|
|
139
|
+
:rows="custom_rows"
|
|
140
|
+
row-key="id"
|
|
141
|
+
>
|
|
142
|
+
<ListHeader>
|
|
143
|
+
<ListHeaderItem
|
|
144
|
+
v-for="column in custom_columns"
|
|
145
|
+
:key="column.key"
|
|
146
|
+
:column="column"
|
|
147
|
+
>
|
|
148
|
+
<template #prefix="{ column }">
|
|
149
|
+
<FeatherIcon :name="column.icon" class="h-4 w-4" />
|
|
150
|
+
</template>
|
|
151
|
+
</ListHeaderItem>
|
|
152
|
+
</ListHeader>
|
|
153
|
+
<ListRows>
|
|
154
|
+
<ListRow
|
|
155
|
+
v-for="(row, i) in custom_rows"
|
|
156
|
+
:key="i"
|
|
157
|
+
v-slot="{ column, item }"
|
|
158
|
+
:row="row"
|
|
159
|
+
:idx="i"
|
|
160
|
+
>
|
|
161
|
+
<ListRowItem :item="item" :align="column.align">
|
|
162
|
+
<template #prefix>
|
|
163
|
+
<div
|
|
164
|
+
v-if="column.key == 'status'"
|
|
165
|
+
class="h-3 w-3 rounded-full"
|
|
166
|
+
:class="item.bg_color"
|
|
167
|
+
/>
|
|
168
|
+
<Avatar
|
|
169
|
+
v-if="column.key == 'name'"
|
|
170
|
+
:shape="'circle'"
|
|
171
|
+
:image="item.image"
|
|
172
|
+
size="sm"
|
|
173
|
+
/>
|
|
174
|
+
</template>
|
|
175
|
+
<Badge
|
|
176
|
+
v-if="column.key == 'role'"
|
|
177
|
+
variant="subtle"
|
|
178
|
+
:theme="item.color"
|
|
179
|
+
size="md"
|
|
180
|
+
:label="item.label"
|
|
181
|
+
/>
|
|
182
|
+
</ListRowItem>
|
|
183
|
+
</ListRow>
|
|
184
|
+
</ListRows>
|
|
185
|
+
<ListSelectBanner>
|
|
186
|
+
<template #actions="{ unselectAll }">
|
|
187
|
+
<div class="flex gap-2">
|
|
188
|
+
<Button variant="ghost" label="Delete" />
|
|
189
|
+
<Button
|
|
190
|
+
variant="ghost"
|
|
191
|
+
label="Unselect all"
|
|
192
|
+
@click="unselectAll"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
</ListSelectBanner>
|
|
197
|
+
</ListView>
|
|
198
|
+
</Variant>
|
|
199
|
+
</Story>
|
|
200
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { h, reactive } from 'vue'
|
|
3
|
+
import Tabs from './Tabs.vue'
|
|
4
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
5
|
+
const state = reactive({
|
|
6
|
+
index: 0,
|
|
7
|
+
tabs_without_icon: [
|
|
8
|
+
{
|
|
9
|
+
label: 'Github',
|
|
10
|
+
content:
|
|
11
|
+
'Github is a code hosting platform for version control and collaboration. It lets you and others work together on projects from anywhere.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
label: 'Twitter',
|
|
15
|
+
content:
|
|
16
|
+
'Twitter is an American microblogging and social networking service on which users post and interact with messages known as "tweets".',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: 'Linkedin',
|
|
20
|
+
content:
|
|
21
|
+
'LinkedIn is an American business and employment-oriented online service that operates via websites and mobile apps.',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
tabs_with_icon: [
|
|
25
|
+
{
|
|
26
|
+
label: 'Github',
|
|
27
|
+
content:
|
|
28
|
+
'Github is a code hosting platform for version control and collaboration. It lets you and others work together on projects from anywhere.',
|
|
29
|
+
icon: h(FeatherIcon, { class: 'w-4 h-4', name: 'github' }),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Twitter',
|
|
33
|
+
content:
|
|
34
|
+
'Twitter is an American microblogging and social networking service on which users post and interact with messages known as "tweets".',
|
|
35
|
+
icon: h(FeatherIcon, { class: 'w-4 h-4', name: 'twitter' }),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'Linkedin',
|
|
39
|
+
content:
|
|
40
|
+
'LinkedIn is an American business and employment-oriented online service that operates via websites and mobile apps.',
|
|
41
|
+
icon: h(FeatherIcon, { class: 'w-4 h-4', name: 'linkedin' }),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<Story :layout="{ type: 'grid', width: '80%' }">
|
|
49
|
+
<Variant title="Without Icon">
|
|
50
|
+
<Tabs
|
|
51
|
+
v-slot="{ tab }"
|
|
52
|
+
v-model="state.index"
|
|
53
|
+
:tabs="state.tabs_without_icon"
|
|
54
|
+
>
|
|
55
|
+
<div class="p-5">
|
|
56
|
+
{{ tab.content }}
|
|
57
|
+
</div>
|
|
58
|
+
</Tabs>
|
|
59
|
+
</Variant>
|
|
60
|
+
<Variant title="With Icon">
|
|
61
|
+
<Tabs v-slot="{ tab }" v-model="state.index" :tabs="state.tabs_with_icon">
|
|
62
|
+
<div class="p-5">
|
|
63
|
+
{{ tab.content }}
|
|
64
|
+
</div>
|
|
65
|
+
</Tabs>
|
|
66
|
+
</Variant>
|
|
67
|
+
|
|
68
|
+
<template #controls>
|
|
69
|
+
<HstNumber v-model="state.index" title="Tab Index" />
|
|
70
|
+
</template>
|
|
71
|
+
</Story>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TabGroup
|
|
3
|
+
as="div"
|
|
4
|
+
class="flex flex-1 flex-col"
|
|
5
|
+
:defaultIndex="changedIndex"
|
|
6
|
+
:selectedIndex="changedIndex"
|
|
7
|
+
@change="(idx) => (changedIndex = idx)"
|
|
8
|
+
>
|
|
9
|
+
<TabList class="relative flex items-center gap-6 border-b pl-5">
|
|
10
|
+
<Tab
|
|
11
|
+
ref="tabRef"
|
|
12
|
+
as="template"
|
|
13
|
+
v-for="(tab, i) in tabs"
|
|
14
|
+
:key="i"
|
|
15
|
+
v-slot="{ selected }"
|
|
16
|
+
class="focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-gray-400"
|
|
17
|
+
>
|
|
18
|
+
<slot name="tab" v-bind="{ tab, selected }">
|
|
19
|
+
<button
|
|
20
|
+
class="-mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
|
21
|
+
:class="{ 'text-gray-900': selected }"
|
|
22
|
+
>
|
|
23
|
+
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
|
24
|
+
{{ tab.label }}
|
|
25
|
+
</button>
|
|
26
|
+
</slot>
|
|
27
|
+
</Tab>
|
|
28
|
+
<div
|
|
29
|
+
ref="indicator"
|
|
30
|
+
class="absolute -bottom-px h-px bg-gray-900"
|
|
31
|
+
:style="{ left: `${indicatorLeftValue}px` }"
|
|
32
|
+
/>
|
|
33
|
+
</TabList>
|
|
34
|
+
<TabPanels class="flex flex-1 overflow-hidden">
|
|
35
|
+
<TabPanel
|
|
36
|
+
class="flex flex-1 flex-col overflow-y-auto focus:outline-none"
|
|
37
|
+
v-for="(tab, i) in tabs"
|
|
38
|
+
:key="i"
|
|
39
|
+
>
|
|
40
|
+
<slot v-bind="{ tab }" />
|
|
41
|
+
</TabPanel>
|
|
42
|
+
</TabPanels>
|
|
43
|
+
</TabGroup>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup>
|
|
47
|
+
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
|
48
|
+
import { TransitionPresets, useTransition } from '@vueuse/core'
|
|
49
|
+
import { ref, watch, computed, onMounted, nextTick } from 'vue'
|
|
50
|
+
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
tabs: {
|
|
53
|
+
type: Array,
|
|
54
|
+
required: true,
|
|
55
|
+
},
|
|
56
|
+
modelValue: {
|
|
57
|
+
type: Number,
|
|
58
|
+
default: 0,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits(['update:modelValue'])
|
|
63
|
+
|
|
64
|
+
const changedIndex = computed({
|
|
65
|
+
get: () => props.modelValue,
|
|
66
|
+
set: (index) => emit('update:modelValue', index),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const tabRef = ref([])
|
|
70
|
+
const indicator = ref(null)
|
|
71
|
+
const tabsLength = ref(props.tabs?.length)
|
|
72
|
+
|
|
73
|
+
let indicatorLeft = ref(0)
|
|
74
|
+
|
|
75
|
+
const indicatorLeftValue = useTransition(indicatorLeft, {
|
|
76
|
+
duration: 250,
|
|
77
|
+
ease: TransitionPresets.easeOutCubic,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
function moveIndicator(index) {
|
|
81
|
+
if (index >= tabsLength.value) {
|
|
82
|
+
index = tabsLength.value - 1
|
|
83
|
+
}
|
|
84
|
+
const selectedTab = tabRef.value[index].el
|
|
85
|
+
indicator.value.style.width = `${selectedTab.offsetWidth}px`
|
|
86
|
+
indicatorLeft.value = selectedTab.offsetLeft
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
watch(changedIndex, (index) => {
|
|
90
|
+
if (index >= tabsLength.value) {
|
|
91
|
+
changedIndex.value = tabsLength.value - 1
|
|
92
|
+
}
|
|
93
|
+
nextTick(() => moveIndicator(index))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
onMounted(() => moveIndicator(changedIndex.value))
|
|
97
|
+
</script>
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Alert } from './components/Alert.vue'
|
|
|
3
3
|
export { default as Autocomplete } from './components/Autocomplete.vue'
|
|
4
4
|
export { default as Avatar } from './components/Avatar.vue'
|
|
5
5
|
export { default as Badge } from './components/Badge.vue'
|
|
6
|
+
export { default as Breadcrumbs } from './components/Breadcrumbs.vue'
|
|
6
7
|
export { default as Button } from './components/Button.vue'
|
|
7
8
|
export { default as Card } from './components/Card.vue'
|
|
8
9
|
export { default as Checkbox } from './components/Checkbox.vue'
|
|
@@ -27,6 +28,7 @@ export { default as Select } from './components/Select.vue'
|
|
|
27
28
|
export { default as Spinner } from './components/Spinner.vue'
|
|
28
29
|
export { default as Switch } from './components/Switch.vue'
|
|
29
30
|
export { default as TabButtons } from './components/TabButtons.vue'
|
|
31
|
+
export { default as Tabs } from './components/Tabs.vue'
|
|
30
32
|
export { default as TextInput } from './components/TextInput.vue'
|
|
31
33
|
export { default as Textarea } from './components/Textarea.vue'
|
|
32
34
|
export {
|
|
@@ -36,9 +38,13 @@ export {
|
|
|
36
38
|
TextEditorFloatingMenu,
|
|
37
39
|
TextEditorContent,
|
|
38
40
|
} from './components/TextEditor'
|
|
39
|
-
export { default as
|
|
41
|
+
export { default as ListView } from './components/ListView/ListView.vue'
|
|
42
|
+
export { default as ListHeader } from './components/ListView/ListHeader.vue'
|
|
43
|
+
export { default as ListHeaderItem } from './components/ListView/ListHeaderItem.vue'
|
|
44
|
+
export { default as ListRows } from './components/ListView/ListRows.vue'
|
|
40
45
|
export { default as ListRow } from './components/ListView/ListRow.vue'
|
|
41
46
|
export { default as ListRowItem } from './components/ListView/ListRowItem.vue'
|
|
47
|
+
export { default as ListSelectBanner } from './components/ListView/ListSelectBanner.vue'
|
|
42
48
|
export { default as Toast } from './components/Toast.vue'
|
|
43
49
|
export { toast, Toasts } from './components/toast.js'
|
|
44
50
|
export { default as Tooltip } from './components/Tooltip.vue'
|