frappe-ui 0.1.164 → 0.1.166

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.
@@ -61,8 +61,7 @@ createResource({
61
61
  trialEndDays.value = calculateTrialEndDays(data.trial_end_date)
62
62
  baseEndpoint.value = data.base_url
63
63
  siteName.value = data.site_name
64
- showBanner.value =
65
- data.setup_complete && data.plan.is_trial_plan && trialEndDays.value > 0
64
+ showBanner.value = data.plan.is_trial_plan && trialEndDays.value > 0
66
65
  },
67
66
  })
68
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.164",
3
+ "version": "0.1.166",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -57,6 +57,7 @@
57
57
  "@tiptap/vue-3": "^2.0.3",
58
58
  "@vueuse/core": "^10.4.1",
59
59
  "dayjs": "^1.11.13",
60
+ "dompurify": "^3.2.6",
60
61
  "echarts": "^5.6.0",
61
62
  "feather-icons": "^4.28.0",
62
63
  "highlight.js": "^11.11.1",
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { reactive } from 'vue';
3
+ import Sidebar from './Sidebar.vue';
4
+
5
+ import Notifications from '~icons/lucide/bell';
6
+ import Deals from '~icons/lucide/briefcase';
7
+ import Organizations from '~icons/lucide/building';
8
+ import Tasks from '~icons/lucide/check-square';
9
+ import Notes from '~icons/lucide/clipboard';
10
+ import Link from '~icons/lucide/link';
11
+ import EmailTemplates from '~icons/lucide/mail';
12
+ import Moon from '~icons/lucide/moon';
13
+ import CallLogs from '~icons/lucide/phone';
14
+ import Settings from '~icons/lucide/settings';
15
+ import User from '~icons/lucide/user';
16
+ import Contacts from '~icons/lucide/user-check';
17
+ import Leads from '~icons/lucide/users';
18
+
19
+ const crmSidebar = reactive({
20
+ header: {
21
+ title: 'Frappe CRM',
22
+ subtitle: 'Jane Doe',
23
+ menuItems: [
24
+ { label: 'Toggle Theme', icon: Moon, onClick: toggleTheme },
25
+ { label: 'Help', to: '/help', icon: Settings, onClick: () => alert('Help clicked!') },
26
+ { label: 'Logout', to: '/logout', icon: User, onClick: () => alert('Logging out...') },
27
+ ]
28
+ },
29
+ sections: [
30
+ {
31
+ label: '',
32
+ items: [
33
+ { label: 'Notifications', icon: Notifications, to: '' },
34
+ ],
35
+ },
36
+ {
37
+ label: '',
38
+ items: [
39
+ { label: 'Leads', icon: Leads, to: '/leads' },
40
+ { label: 'Deals', icon: Deals, to: '/deals' },
41
+ { label: 'Contacts', icon: Contacts, to: '/contacts' },
42
+ { label: 'Organizations', icon: Organizations, to: '/organizations' },
43
+ { label: 'Notes', icon: Notes, to: '/notes' },
44
+ { label: 'Tasks', icon: Tasks, to: '/tasks' },
45
+ { label: 'Call Logs', icon: CallLogs, to: '/call-logs' },
46
+ { label: 'Email Templates', icon: EmailTemplates, to: '/email-templates' },
47
+ ]
48
+ },
49
+ {
50
+ label: 'Views',
51
+ collapsible: true,
52
+ items: [
53
+ { label: 'My Open Deals', icon: Link, to: '/my-open-deals' },
54
+ { label: 'Partnership Deals', icon: Link, to: '/partnership-deals' },
55
+ { label: 'Unassigned Deals', icon: Link, to: '/unassigned-deals' },
56
+ { label: 'Enterprise Pipeline', icon: Link, to: '/enterprise-pipeline' },
57
+ ]
58
+ }
59
+ ]
60
+ })
61
+ function toggleTheme() {
62
+ const currentTheme = document.documentElement.getAttribute('data-theme');
63
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
64
+ document.documentElement.setAttribute('data-theme', newTheme);
65
+ }
66
+ </script>
67
+
68
+ <template>
69
+ <Story>
70
+ <Variant title="Sidebar">
71
+ <div class="flex h-screen w-full flex-col bg-surface-white shadow">
72
+ <Sidebar
73
+ :header="crmSidebar.header"
74
+ :sections="crmSidebar.sections"
75
+ />
76
+ </div>
77
+ </Variant>
78
+ </Story>
79
+ </template>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div
3
+ class="flex h-full flex-col flex-shrink-0 overflow-y-auto border-r border-outline-gray-1 bg-surface-menu-bar transition-all duration-300 ease-in-out p-2"
4
+ :class="isCollapsed ? 'w-12' : 'w-60'"
5
+ >
6
+ <SidebarHeader
7
+ v-if="props.header"
8
+ :isCollapsed="isCollapsed"
9
+ :title="props.header.title"
10
+ :subtitle="props.header.subtitle"
11
+ :menu-items="props.header.menuItems"
12
+ />
13
+
14
+ <SidebarSection
15
+ v-for="section in props.sections"
16
+ :key="section.label"
17
+ :label="section.label"
18
+ :items="section.items"
19
+ :collapsible="section.collapsible"
20
+ />
21
+
22
+ <div class="mt-auto flex flex-col gap-2">
23
+ <slot name="footer-items" />
24
+ <SidebarItem
25
+ :label="isCollapsed ? 'Expand' : 'Collapse'"
26
+ :isCollapsed="isCollapsed"
27
+ @click="isCollapsed = !isCollapsed"
28
+ >
29
+ <template #icon>
30
+ <LucidePanelRightOpen
31
+ class="size-4 text-ink-gray-6 duration-300 ease-in-out"
32
+ :class="{ 'rotate-180': isCollapsed }"
33
+ />
34
+ </template>
35
+ </SidebarItem>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
42
+ import { provide, ref, watchEffect } from 'vue'
43
+ import SidebarHeader from './SidebarHeader.vue'
44
+ import SidebarItem from './SidebarItem.vue'
45
+ import { SidebarProps } from './types'
46
+
47
+ import LucidePanelRightOpen from '~icons/lucide/panel-right-open'
48
+ import SidebarSection from './SidebarSection.vue'
49
+
50
+ const props = defineProps<SidebarProps>()
51
+
52
+ const isCollapsed = ref(false)
53
+ provide('isSidebarCollapsed', isCollapsed)
54
+
55
+ const breakpoints = useBreakpoints(breakpointsTailwind)
56
+ const isMobile = breakpoints.smaller('sm')
57
+ watchEffect(() => {
58
+ isCollapsed.value = isMobile.value
59
+ })
60
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <Dropdown :options="props.menuItems">
3
+ <template v-slot="{ open }">
4
+ <button
5
+ class="flex h-12 items-center rounded-md py-2 duration-300 ease-in-out w-[14rem]"
6
+ :class="
7
+ isCollapsed
8
+ ? 'w-auto px-0'
9
+ : open
10
+ ? 'bg-surface-white px-2 shadow-sm'
11
+ : 'px-2 hover:bg-surface-gray-3'
12
+ "
13
+ >
14
+ <slot name="logo">
15
+ <div class="w-8 h-8 rounded bg-surface-gray-4 flex items-center justify-center text-ink-gray-7">
16
+ {{ props.title.charAt(0).toUpperCase() }}
17
+ </div>
18
+ </slot>
19
+ <div
20
+ class="flex flex-1 flex-col text-left duration-300 ease-in-out truncate"
21
+ :class="
22
+ isCollapsed
23
+ ? 'ml-0 w-0 overflow-hidden opacity-0'
24
+ : 'ml-2 w-auto opacity-100'
25
+ "
26
+ >
27
+ <div class="text-base font-medium text-ink-gray-8 leading-none">{{ props.title }}</div>
28
+ <div class="mt-1 text-sm text-ink-gray-6 leading-none">
29
+ {{ props.subtitle }}
30
+ </div>
31
+ </div>
32
+ <div
33
+ class="duration-300 ease-in-out"
34
+ :class="
35
+ isCollapsed
36
+ ? 'ml-0 w-0 overflow-hidden opacity-0'
37
+ : 'ml-2 w-auto opacity-100'
38
+ "
39
+ >
40
+ <LucideChevronDown class="h-4 w-4 text-ink-gray-7"/>
41
+ </div>
42
+ </button>
43
+ </template>
44
+ </Dropdown>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import { inject } from 'vue';
49
+ import LucideChevronDown from '~icons/lucide/chevron-down';
50
+ import { SidebarHeaderProps } from './types';
51
+ import Dropdown from '../Dropdown/Dropdown.vue';
52
+
53
+ const props = defineProps<SidebarHeaderProps>();
54
+ const isCollapsed = inject('isSidebarCollapsed', false);
55
+ </script>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <Button
3
+ :label="props.label"
4
+ @click="handleClick"
5
+ class="!w-full"
6
+ :class="
7
+ props.isActive
8
+ ? '!bg-surface-selected shadow-sm'
9
+ : 'hover:bg-surface-gray-2'
10
+ "
11
+ variant="ghost"
12
+ >
13
+ <template #icon>
14
+ <div
15
+ class="flex w-full items-center justify-between transition-all ease-in-out px-2 py-1"
16
+ >
17
+ <div class="flex items-center truncate">
18
+ <Tooltip
19
+ :text="props.label"
20
+ placement="right"
21
+ :disabled="!isCollapsed"
22
+ >
23
+ <span class="grid flex-shrink-0 place-items-center">
24
+ <slot name="icon">
25
+ <span
26
+ v-if="props.icon && typeof props.icon === 'string'"
27
+ class="size-4 text-ink-gray-6"
28
+ >
29
+ {{ props.icon }}
30
+ </span>
31
+ <component
32
+ v-else
33
+ :is="props.icon"
34
+ class="size-4 text-ink-gray-6"
35
+ />
36
+ </slot>
37
+ </span>
38
+ </Tooltip>
39
+ <Tooltip
40
+ :text="props.label"
41
+ placement="right"
42
+ :disabled="isCollapsed"
43
+ :hoverDelay="1.5"
44
+ >
45
+ <span
46
+ class="flex-1 flex-shrink-0 truncate text-sm transition-all ease-in-out"
47
+ :class="
48
+ isCollapsed
49
+ ? 'ml-0 w-0 overflow-hidden opacity-0'
50
+ : 'ml-2 w-auto opacity-100'
51
+ "
52
+ >
53
+ {{ props.label }}
54
+ </span>
55
+ </Tooltip>
56
+ </div>
57
+ <div
58
+ class="transition-all ease-in-out"
59
+ :class="
60
+ isCollapsed
61
+ ? 'ml-0 w-0 overflow-hidden opacity-0'
62
+ : 'ml-auto w-auto opacity-100'
63
+ "
64
+ >
65
+ <slot name="suffix">
66
+ <span v-if="props.suffix" class="text-sm text-ink-gray-4">
67
+ {{ props.suffix }}
68
+ </span>
69
+ </slot>
70
+ </div>
71
+ </div>
72
+ </template>
73
+ </Button>
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ import { inject } from 'vue'
78
+ import { useRouter } from 'vue-router'
79
+ import Button from '../Button/Button.vue'
80
+ import Tooltip from '../Tooltip/Tooltip.vue'
81
+ import { SidebarItemProps } from './types'
82
+
83
+ const props = defineProps<SidebarItemProps>()
84
+ const isCollapsed = inject('isSidebarCollapsed', false)
85
+
86
+ const router = useRouter()
87
+ function handleClick() {
88
+ if (props.onClick) {
89
+ props.onClick()
90
+ } else if (props.to) {
91
+ router.replace(props.to)
92
+ }
93
+ }
94
+ </script>
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="flex flex-col mt-2">
3
+ <div
4
+ v-if="props.label"
5
+ class="relative flex items-center gap-1 px-2 py-1.5"
6
+ :class="props.collapsible ? 'cursor-pointer' : ''"
7
+ @click="isCollapsed = !isCollapsed"
8
+ >
9
+ <h3
10
+ class="h-4 text-sm text-ink-gray-5 transition-all duration-300 ease-in-out"
11
+ :class="
12
+ isSidebarCollapsed
13
+ ? 'w-0 overflow-hidden opacity-0'
14
+ : 'w-auto opacity-100'
15
+ "
16
+ >
17
+ {{ props.label }}
18
+ </h3>
19
+ <div v-if="props.collapsible">
20
+ <LucideChevronRight
21
+ v-if="!isSidebarCollapsed"
22
+ class="w-4 h-4 text-ink-gray-5 transition-all duration-300 ease-in-out"
23
+ :class="{ 'rotate-90': !isCollapsed }"
24
+ />
25
+ </div>
26
+ <div
27
+ v-if="isSidebarCollapsed"
28
+ class="absolute top-0 left-0 flex h-full w-full items-center justify-center transition-all duration-300 ease-in-out"
29
+ :class="isSidebarCollapsed ? 'opacity-100' : 'opacity-0'"
30
+ >
31
+ <hr class="w-full border-t border-ink-gray-3" />
32
+ </div>
33
+ </div>
34
+ <transition
35
+ enter-active-class="duration-300 ease-in"
36
+ leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
37
+ enter-to-class="max-h-[200px] overflow-hidden"
38
+ leave-from-class="max-h-[200px] overflow-hidden"
39
+ enter-from-class="max-h-0 overflow-hidden"
40
+ leave-to-class="max-h-0 overflow-hidden"
41
+ >
42
+ <nav v-if="!isCollapsed" class="space-y-0.5">
43
+ <SidebarItem
44
+ v-for="item in props.items"
45
+ :key="item.label"
46
+ :label="item.label"
47
+ :icon="item.icon"
48
+ :suffix="item.suffix"
49
+ :to="item.to"
50
+ :isActive="item.isActive"
51
+ :isCollapsed="isSidebarCollapsed"
52
+ :onClick="item.onClick"
53
+ >
54
+ </SidebarItem>
55
+ </nav>
56
+ </transition>
57
+ </div>
58
+ </template>
59
+
60
+ <script setup lang="ts">
61
+ import { inject, ref } from 'vue';
62
+ import SidebarItem from './SidebarItem.vue';
63
+ import { SidebarSectionProps } from './types';
64
+
65
+ const props = defineProps<SidebarSectionProps>()
66
+
67
+ const isSidebarCollapsed = inject('isSidebarCollapsed', false)
68
+ const isCollapsed = ref(false)
69
+ </script>
@@ -0,0 +1,2 @@
1
+ export { default as Sidebar } from './Sidebar.vue'
2
+ export type { SidebarProps } from './types'
@@ -0,0 +1,31 @@
1
+ import { RouteLocationRaw } from 'vue-router'
2
+
3
+ export type SidebarHeaderProps = {
4
+ title: string
5
+ subtitle?: string
6
+ menuItems?: {
7
+ label: string
8
+ icon: any // Icon component
9
+ onClick?: () => void
10
+ }[]
11
+ }
12
+
13
+ export type SidebarItemProps = {
14
+ label: string
15
+ icon?: any // Icon component
16
+ suffix?: string
17
+ to?: RouteLocationRaw
18
+ isActive?: boolean
19
+ onClick?: () => void
20
+ }
21
+
22
+ export type SidebarSectionProps = {
23
+ label?: string
24
+ items: SidebarItemProps[]
25
+ collapsible?: boolean
26
+ }
27
+
28
+ export type SidebarProps = {
29
+ header?: SidebarHeaderProps
30
+ sections?: SidebarSectionProps[]
31
+ }
@@ -111,9 +111,6 @@ export default Node.create<VideoOptions>({
111
111
  (editor.isEditable ? ' cursor-pointer' : '')
112
112
 
113
113
  const video = document.createElement('video')
114
- if (editor.isEditable) {
115
- video.className = 'pointer-events-none'
116
- }
117
114
  video.src = node.attrs.src
118
115
  video.setAttribute('controls', '')
119
116
 
@@ -27,9 +27,8 @@
27
27
  <ToastDescription
28
28
  v-if="message"
29
29
  class="text-p-sm break-words text-ink-white"
30
- >
31
- {{ message }}
32
- </ToastDescription>
30
+ v-html="message"
31
+ />
33
32
  </div>
34
33
  </div>
35
34
  <div class="flex items-center gap-2 h-7">
@@ -1,7 +1,8 @@
1
- import { ref, defineComponent, h, Ref } from 'vue'
1
+ import DOMPurify from 'dompurify'
2
+ import { defineComponent, h, ref, Ref } from 'vue'
3
+ import LoadingIndicator from '../LoadingIndicator.vue'
2
4
  import ToastComponent from './Toast.vue'
3
5
  import type { ToastProps } from './types'
4
- import LoadingIndicator from '../LoadingIndicator.vue'
5
6
 
6
7
  interface ToastOptions
7
8
  extends Omit<Partial<ToastProps>, 'open' | 'message' | 'title'> {
@@ -56,10 +57,14 @@ export const toast = {
56
57
  const durationInMs =
57
58
  options.duration != null ? options.duration * 1000 : 5000
58
59
 
60
+ const sanitizedMessage = DOMPurify.sanitize(options.message, {
61
+ ALLOWED_TAGS: ['a', 'em', 'strong', 'i', 'b', 'u'],
62
+ })
63
+
59
64
  const toastItem: ToastItem = {
60
65
  id: options.id || id,
61
66
  open: true,
62
- message: options.message,
67
+ message: sanitizedMessage,
63
68
  type: options.type || 'info',
64
69
  duration: durationInMs,
65
70
  action: options.action,
package/src/index.ts CHANGED
@@ -70,6 +70,7 @@ export { default as NestedPopover } from './components/ListFilter/NestedPopover.
70
70
  export * from './components/CircularProgressBar'
71
71
  export * from './components/Tree'
72
72
  export { default as FrappeUIProvider } from './components/Provider/FrappeUIProvider.vue'
73
+ export { default as Sidebar } from './components/Sidebar/Sidebar.vue'
73
74
 
74
75
  // chart components
75
76
  export { default as AxisChart } from './components/Charts/AxisChart.vue'