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.5",
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",
@@ -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 {