frappe-ui 0.1.5 → 0.1.6
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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,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 {
|