frappe-ui 1.0.0-beta.20 → 1.0.0-beta.24
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/Avatar/Avatar.api.md +7 -1
- package/src/components/Avatar/Avatar.cy.ts +21 -0
- package/src/components/Avatar/Avatar.md +5 -2
- package/src/components/Avatar/Avatar.vue +39 -6
- package/src/components/Avatar/index.ts +1 -1
- package/src/components/Avatar/stories/Themes.vue +23 -0
- package/src/components/Avatar/types.ts +11 -0
- package/src/components/BottomSheet/BottomSheet.api.md +57 -0
- package/src/components/BottomSheet/BottomSheet.vue +232 -0
- package/src/components/BottomSheet/index.ts +6 -0
- package/src/components/BottomSheet/types.ts +23 -0
- package/src/components/Calendar/Calendar.api.md +16 -16
- package/src/components/Calendar/Calendar.vue +99 -72
- package/src/components/Calendar/CalendarDaily.vue +20 -19
- package/src/components/Calendar/CalendarMonthEvent.vue +30 -9
- package/src/components/Calendar/CalendarMonthly.vue +65 -43
- package/src/components/Calendar/CalendarTimeMarker.vue +9 -8
- package/src/components/Calendar/CalendarWeekDayEvent.vue +96 -40
- package/src/components/Calendar/CalendarWeekly.vue +76 -40
- package/src/components/Calendar/EventModalContent.vue +26 -10
- package/src/components/Calendar/NewEventModal.vue +19 -16
- package/src/components/Calendar/ShowMoreCalendarEvent.vue +10 -16
- package/src/components/Calendar/{calendarUtils.js → calendarUtils.ts} +146 -200
- package/src/components/Calendar/composables/{useCalendarData.js → useCalendarData.ts} +23 -11
- package/src/components/Calendar/composables/{useEventModal.js → useEventModal.ts} +16 -12
- package/src/components/Calendar/index.ts +12 -2
- package/src/components/Calendar/stories/CustomHeader.vue +8 -6
- package/src/components/Calendar/stories/Examples.vue +7 -5
- package/src/components/Calendar/style.css +1 -1
- package/src/components/Calendar/types.ts +106 -0
- package/src/components/Calendar/{useEventBase.js → useEventBase.ts} +51 -25
- package/src/components/Charts/eChartOptions.ts +22 -7
- package/src/components/Checkbox/Checkbox.api.md +6 -0
- package/src/components/Checkbox/Checkbox.vue +1 -0
- package/src/components/Checkbox/types.ts +7 -0
- package/src/components/CodeEditor/CodeEditor.api.md +8 -9
- package/src/components/Combobox/Combobox.api.md +16 -17
- package/src/components/DatePicker/DateRangePicker.vue +3 -1
- package/src/components/DesktopShell/DesktopShell.api.md +9 -1
- package/src/components/DesktopShell/DesktopShell.vue +24 -1
- package/src/components/Dialog/Dialog.api.md +7 -8
- package/src/components/Dialogs.vue +11 -9
- package/src/components/Dropdown/Dropdown.api.md +9 -4
- package/src/components/FileUploader/FileUploader.api.md +36 -11
- package/src/components/FileUploader/FileUploader.cy.ts +66 -0
- package/src/components/FileUploader/FileUploader.md +4 -1
- package/src/components/FileUploader/FileUploader.vue +147 -106
- package/src/components/FileUploader/index.ts +6 -0
- package/src/components/FileUploader/types.ts +43 -0
- package/src/components/FormControl/FormControl.api.md +3 -4
- package/src/components/HoverCard/HoverCard.api.md +5 -6
- package/src/components/KeyboardShortcutsModal/KeyboardShortcutsModal.api.md +22 -14
- package/src/components/ListFilter/ListFilter.api.md +5 -6
- package/src/components/ListFilter/ListFilter.vue +159 -55
- package/src/components/ListFilter/NestedPopover.vue +17 -10
- package/src/components/ListFilter/SearchComplete.vue +31 -38
- package/src/components/ListView/ListView.api.md +322 -6
- package/src/components/ListView/index.ts +25 -0
- package/src/components/MobileNav/MobileNav.api.md +6 -7
- package/src/components/MonthPicker/MonthPicker.cy.ts +16 -0
- package/src/components/MonthPicker/MonthPicker.vue +18 -7
- package/src/components/MultiSelect/MultiSelect.api.md +5 -6
- package/src/components/PageHeader/PageHeader.api.md +0 -1
- package/src/components/PageHeader/PageHeaderTitle.vue +12 -0
- package/src/components/PageHeader/index.ts +1 -0
- package/src/components/Popover/Popover.api.md +8 -9
- package/src/components/Rail/Rail.api.md +7 -8
- package/src/components/ScrollArea/ScrollArea.api.md +6 -6
- package/src/components/ScrollArea/ScrollArea.vue +4 -1
- package/src/components/ScrollArea/ScrollBar.vue +11 -5
- package/src/components/SettingsDialog/SettingsContent.vue +2 -0
- package/src/components/SettingsDialog/SettingsDialog.vue +11 -7
- package/src/components/Sidebar/Sidebar.api.md +28 -12
- package/src/components/Sidebar/SidebarHeader.vue +76 -52
- package/src/components/Sidebar/SidebarItem.vue +42 -11
- package/src/components/Sidebar/types.ts +8 -0
- package/src/components/{Pill → TabButtons}/Pill.vue +1 -1
- package/src/components/TabButtons/TabButtons.vue +2 -1
- package/src/components/TabButtons/types.ts +1 -1
- package/src/components/TextEditor/TextEditor.md +3 -0
- package/src/components/TextEditor/TextEditor.vue +2 -2
- package/src/components/TextEditor/index.ts +13 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.cy.ts +13 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.vue +3 -0
- package/src/composables/useAutofocusOnOpen.ts +2 -0
- package/src/composables/useShortcut.ts +3 -0
- package/src/data-fetching/docStore.test.ts +2 -1
- package/src/data-fetching/docStore.ts +11 -13
- package/src/data-fetching/useCall/types.ts +1 -0
- package/src/data-fetching/useCall/useCall.test.ts +43 -0
- package/src/data-fetching/useCall/useCall.ts +17 -4
- package/src/data-fetching/useDoc/useDoc.test.ts +2 -2
- package/src/data-fetching/useDoc/useDoc.ts +10 -6
- package/src/data-fetching/useDoctype/useDoctype.ts +6 -5
- package/src/data-fetching/useFrappeFetch.ts +17 -4
- package/src/data-fetching/useList/types.ts +1 -0
- package/src/data-fetching/useList/useList.test.ts +40 -0
- package/src/data-fetching/useList/useList.ts +20 -13
- package/src/data-fetching/useNewDoc/useNewDoc.ts +9 -5
- package/src/directives/focus.ts +3 -3
- package/src/index.ts +101 -88
- package/src/mocks/handlers.ts +8 -0
- package/src/molecules/editor/Editor.vue +6 -2
- package/src/molecules/editor/EditorContent.vue +2 -2
- package/src/molecules/editor/extensions/code-block/CodeBlockComponent.css +10 -0
- package/src/molecules/editor/extensions/toc-node/toc-render.ts +7 -3
- package/src/molecules/editor/extensions.ts +5 -0
- package/src/molecules/editor/index.ts +2 -4
- package/src/molecules/editor/useEditor.test.ts +72 -0
- package/src/molecules/editor/useEditor.ts +31 -4
- package/src/molecules/list/List.cy.ts +400 -79
- package/src/molecules/list/List.vue +52 -1
- package/src/molecules/list/ListGroup.vue +40 -0
- package/src/molecules/list/ListHeader.vue +37 -1
- package/src/molecules/list/ListHeaderCellSort.vue +44 -10
- package/src/molecules/list/ListRow.vue +10 -3
- package/src/molecules/list/ListRowBase.vue +30 -6
- package/src/molecules/list/ListRows.vue +44 -6
- package/src/molecules/list/index.ts +1 -0
- package/src/molecules/list/list-context.ts +16 -0
- package/src/molecules/list/list.api.md +81 -28
- package/src/molecules/list/list.md +70 -42
- package/src/molecules/list/style.css +39 -6
- package/src/molecules/list/types.ts +17 -1
- package/src/resources/documentResource.d.ts +28 -0
- package/src/resources/listResource.d.ts +33 -0
- package/src/resources/plugin.d.ts +7 -0
- package/src/resources/resources.d.ts +35 -0
- package/src/utils/{call.js → call.ts} +50 -9
- package/src/utils/config.ts +1 -0
- package/src/utils/confirmDialog.d.ts +17 -0
- package/src/utils/debounce.ts +6 -7
- package/src/utils/fileUploadHandler.ts +3 -7
- package/src/utils/frappeRequest.test.ts +24 -9
- package/src/utils/{frappeRequest.js → frappeRequest.ts} +55 -13
- package/src/utils/pageMeta.ts +1 -1
- package/src/utils/{plugin.js → plugin.ts} +21 -6
- package/src/utils/{request.js → request.ts} +27 -3
- package/src/utils/{socketio.js → socketio.ts} +11 -1
- package/src/utils/useFileUpload.ts +17 -6
- package/tailwind/preset.js +15 -0
- package/tsconfig.base.json +2 -2
- package/vitepress/components/Docs/Sidebar.vue +3 -3
- package/vitepress/components/Navbar.vue +5 -2
- package/src/components/Pill/Pill.api.md +0 -99
- package/src/components/Pill/Pill.md +0 -20
- package/src/components/Pill/index.ts +0 -10
- package/src/components/Pill/stories/BrowserEdges.vue +0 -42
- package/src/components/Pill/stories/Sizes.vue +0 -49
- package/src/components/Pill/stories/Variants.vue +0 -53
- package/src/components/Resource.vue +0 -21
- /package/src/components/{Pill/types.ts → TabButtons/pillTypes.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.24",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
"@tiptap/extension-typography": "^3.26.0",
|
|
173
173
|
"@tiptap/extension-underline": "^3.26.0",
|
|
174
174
|
"@tiptap/extensions": "^3.26.0",
|
|
175
|
+
"@tiptap/markdown": "^3.26.0",
|
|
175
176
|
"@tiptap/pm": "^3.26.0",
|
|
176
177
|
"@tiptap/starter-kit": "^3.26.0",
|
|
177
178
|
"@tiptap/suggestion": "^3.26.0",
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
required: false,
|
|
31
31
|
type: '"circle" | "square"',
|
|
32
32
|
default: '"circle"'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'theme',
|
|
36
|
+
description: 'Visual color theme used for the fallback avatar',
|
|
37
|
+
required: false,
|
|
38
|
+
type: '"gray" | "blue" | "green" | "amber" | "red" | "violet"',
|
|
39
|
+
default: '"gray"'
|
|
33
40
|
}
|
|
34
41
|
]
|
|
35
42
|
|
|
@@ -51,4 +58,3 @@
|
|
|
51
58
|
<PropsTable name="Avatar" :data="propsData"/>
|
|
52
59
|
|
|
53
60
|
<SlotsTable :data="slotsData"/>
|
|
54
|
-
|
|
@@ -44,4 +44,25 @@ describe('Avatar', () => {
|
|
|
44
44
|
|
|
45
45
|
cy.get('[data-cy="avatar"]').should('have.text', 'A')
|
|
46
46
|
})
|
|
47
|
+
|
|
48
|
+
it('Uses gray fallback theme by default', () => {
|
|
49
|
+
cy.mount(Avatar, {
|
|
50
|
+
props: { 'data-cy': 'avatar', label: 'Abc' },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
cy.get('[data-cy="avatar"] > div')
|
|
54
|
+
.should('have.class', 'bg-surface-gray-2')
|
|
55
|
+
.and('have.class', 'text-ink-gray-5')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('Supports colorful fallback themes', () => {
|
|
59
|
+
cy.mount(Avatar, {
|
|
60
|
+
props: { 'data-cy': 'avatar', label: 'Abc', theme: 'blue' },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
cy.get('[data-cy="avatar"] > div')
|
|
64
|
+
.should('have.class', 'bg-surface-blue-2')
|
|
65
|
+
.and('have.class', 'text-ink-blue-8')
|
|
66
|
+
})
|
|
67
|
+
|
|
47
68
|
})
|
|
@@ -6,10 +6,13 @@ A visual representation of a user, typically shown as an image, initials, or an
|
|
|
6
6
|
|
|
7
7
|
<ClientOnly><AvatarBuilder /></ClientOnly>
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Shapes
|
|
10
10
|
<ComponentPreview name="Avatar-Shapes" />
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Sizes
|
|
13
13
|
<ComponentPreview name="Avatar-Sizes" />
|
|
14
14
|
|
|
15
|
+
## Themes
|
|
16
|
+
<ComponentPreview name="Avatar-Themes" />
|
|
17
|
+
|
|
15
18
|
<!-- @include: ./Avatar.api.md -->
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
/>
|
|
13
13
|
<div
|
|
14
14
|
v-else
|
|
15
|
-
class="flex h-full w-full items-center justify-center
|
|
16
|
-
:class="[labelClasses, shapeClasses]"
|
|
15
|
+
class="flex h-full w-full items-center justify-center uppercase select-none"
|
|
16
|
+
:class="[labelClasses, fallbackThemeClasses, shapeClasses]"
|
|
17
17
|
>
|
|
18
18
|
<div :class="iconClasses" v-if="$slots.default">
|
|
19
19
|
<slot></slot>
|
|
@@ -37,14 +37,47 @@
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
|
-
import { ref, computed } from 'vue'
|
|
41
|
-
import type { AvatarProps } from './types'
|
|
40
|
+
import { ref, computed, useAttrs } from 'vue'
|
|
41
|
+
import type { AvatarProps, AvatarTheme } from './types'
|
|
42
42
|
|
|
43
43
|
const imgFetchError = ref(false)
|
|
44
44
|
|
|
45
45
|
const props = withDefaults(defineProps<AvatarProps>(), {
|
|
46
46
|
size: 'md',
|
|
47
47
|
shape: 'circle',
|
|
48
|
+
theme: 'gray',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const fallbackThemeClasses = computed(() => {
|
|
52
|
+
const classes: Record<AvatarTheme, string> = {
|
|
53
|
+
gray: 'bg-surface-gray-2 text-ink-gray-5',
|
|
54
|
+
blue: 'bg-surface-blue-2 text-ink-blue-8',
|
|
55
|
+
green: 'bg-surface-green-2 text-ink-green-8',
|
|
56
|
+
amber: 'bg-surface-amber-2 text-ink-amber-8',
|
|
57
|
+
red: 'bg-surface-red-2 text-ink-red-8',
|
|
58
|
+
violet: 'bg-surface-violet-2 text-ink-violet-8',
|
|
59
|
+
}
|
|
60
|
+
return classes[props.theme]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Let a consumer size the avatar with a Tailwind utility (`class="size-16"`)
|
|
64
|
+
// instead of the `size` prop enum. When a sizing utility is present we drop the
|
|
65
|
+
// enum's `w-*/h-*` so the two don't both land on the root and fight; the class
|
|
66
|
+
// (applied via inheritAttrs) then wins on its own. Handles responsive/variant
|
|
67
|
+
// prefixes like `sm:size-16`.
|
|
68
|
+
const attrs = useAttrs()
|
|
69
|
+
const hasSizeOverride = computed(() => {
|
|
70
|
+
const cls = Array.isArray(attrs.class)
|
|
71
|
+
? attrs.class.join(' ')
|
|
72
|
+
: typeof attrs.class === 'string'
|
|
73
|
+
? attrs.class
|
|
74
|
+
: ''
|
|
75
|
+
return cls.split(/\s+/).some((token) => {
|
|
76
|
+
const base = token.includes(':')
|
|
77
|
+
? token.slice(token.lastIndexOf(':') + 1)
|
|
78
|
+
: token
|
|
79
|
+
return /^-?(size|w|h|min-w|max-w|min-h|max-h)-/.test(base)
|
|
80
|
+
})
|
|
48
81
|
})
|
|
49
82
|
|
|
50
83
|
const shapeClasses = computed(() => {
|
|
@@ -63,6 +96,7 @@ const shapeClasses = computed(() => {
|
|
|
63
96
|
})
|
|
64
97
|
|
|
65
98
|
const sizeClasses = computed(() => {
|
|
99
|
+
if (hasSizeOverride.value) return ''
|
|
66
100
|
return {
|
|
67
101
|
xs: 'w-4 h-4',
|
|
68
102
|
sm: 'w-5 h-5',
|
|
@@ -123,7 +157,7 @@ const iconClasses = computed(() => {
|
|
|
123
157
|
}[props.size]
|
|
124
158
|
})
|
|
125
159
|
|
|
126
|
-
function handleImageError(err) {
|
|
160
|
+
function handleImageError(err: Event) {
|
|
127
161
|
if (err.type) {
|
|
128
162
|
imgFetchError.value = true
|
|
129
163
|
}
|
|
@@ -136,5 +170,4 @@ defineSlots<{
|
|
|
136
170
|
/** Small indicator shown at the bottom-right of the avatar */
|
|
137
171
|
indicator?: () => any
|
|
138
172
|
}>()
|
|
139
|
-
|
|
140
173
|
</script>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Avatar } from './Avatar.vue'
|
|
2
|
-
export type { AvatarProps } from './types'
|
|
2
|
+
export type { AvatarProps, AvatarTheme } from './types'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Avatar } from 'frappe-ui'
|
|
3
|
+
import type { AvatarTheme } from 'frappe-ui'
|
|
4
|
+
|
|
5
|
+
const themes: AvatarTheme[] = [
|
|
6
|
+
'gray',
|
|
7
|
+
'blue',
|
|
8
|
+
'green',
|
|
9
|
+
'amber',
|
|
10
|
+
'red',
|
|
11
|
+
'violet',
|
|
12
|
+
]
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<Avatar
|
|
17
|
+
v-for="theme in themes"
|
|
18
|
+
:key="theme"
|
|
19
|
+
:label="theme"
|
|
20
|
+
:theme="theme"
|
|
21
|
+
size="lg"
|
|
22
|
+
/>
|
|
23
|
+
</template>
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export type AvatarTheme =
|
|
2
|
+
| 'gray'
|
|
3
|
+
| 'blue'
|
|
4
|
+
| 'green'
|
|
5
|
+
| 'amber'
|
|
6
|
+
| 'red'
|
|
7
|
+
| 'violet'
|
|
8
|
+
|
|
1
9
|
export interface AvatarProps {
|
|
2
10
|
/** Image URL used for the avatar */
|
|
3
11
|
image?: string
|
|
@@ -10,4 +18,7 @@ export interface AvatarProps {
|
|
|
10
18
|
|
|
11
19
|
/** Defines the avatar shape */
|
|
12
20
|
shape?: 'circle' | 'square'
|
|
21
|
+
|
|
22
|
+
/** Visual color theme used for the fallback avatar */
|
|
23
|
+
theme?: AvatarTheme
|
|
13
24
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- Auto Generated by scripts/propsgen.ts -->
|
|
2
|
+
<script setup>
|
|
3
|
+
import PropsTable from '@/components/Docs/PropsTable.vue'
|
|
4
|
+
import SlotsTable from '@/components/Docs/SlotsTable.vue'
|
|
5
|
+
import EmitsTable from '@/components/Docs/EmitsTable.vue'
|
|
6
|
+
|
|
7
|
+
const propsData = [
|
|
8
|
+
{
|
|
9
|
+
name: 'open',
|
|
10
|
+
description: 'Controls whether the sheet is open (`v-model:open`).',
|
|
11
|
+
required: false,
|
|
12
|
+
type: 'boolean'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'title',
|
|
16
|
+
description: 'Optional centered title rendered in the drag-handle area.',
|
|
17
|
+
required: false,
|
|
18
|
+
type: 'string'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'dismissible',
|
|
22
|
+
description: 'Allow outside-click, Escape, and swipe-down to close. Default `true`.',
|
|
23
|
+
required: false,
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
default: 'true'
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const slotsData = [
|
|
30
|
+
{
|
|
31
|
+
name: 'default',
|
|
32
|
+
description: '',
|
|
33
|
+
type: '{}'
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const emitsData = [
|
|
38
|
+
{
|
|
39
|
+
name: 'update:open',
|
|
40
|
+
description: 'Fired when the open state changes.',
|
|
41
|
+
type: '[value: boolean]'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'after-leave',
|
|
45
|
+
description: '',
|
|
46
|
+
type: '[]'
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
## API Reference
|
|
52
|
+
|
|
53
|
+
<PropsTable name="BottomSheet" :data="propsData"/>
|
|
54
|
+
|
|
55
|
+
<SlotsTable :data="slotsData"/>
|
|
56
|
+
|
|
57
|
+
<EmitsTable :data="emitsData"/>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DialogRoot v-model:open="isOpen">
|
|
3
|
+
<DialogPortal>
|
|
4
|
+
<DialogOverlay
|
|
5
|
+
class="fixed inset-0 z-50 bg-black-overlay-200 dark:bg-black-overlay-700 bottom-sheet-overlay outline-none"
|
|
6
|
+
@after-leave="emit('after-leave')"
|
|
7
|
+
/>
|
|
8
|
+
<DialogContent
|
|
9
|
+
class="fixed inset-x-0 bottom-0 z-50 mx-auto w-full max-w-2xl transform-gpu rounded-t-[36px] bg-surface-base shadow-lg [corner-shape:squircle] bottom-sheet-content focus:outline-none after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-screen after:bg-surface-base"
|
|
10
|
+
:style="dragStyle"
|
|
11
|
+
:aria-label="title || 'Bottom sheet'"
|
|
12
|
+
@escape-key-down="onDismissAttempt"
|
|
13
|
+
@interact-outside="onDismissAttempt"
|
|
14
|
+
@open-auto-focus="(e: Event) => e.preventDefault()"
|
|
15
|
+
>
|
|
16
|
+
<div
|
|
17
|
+
ref="handleRef"
|
|
18
|
+
class="cursor-grab touch-none select-none active:cursor-grabbing"
|
|
19
|
+
>
|
|
20
|
+
<div class="flex justify-center pb-2 pt-3">
|
|
21
|
+
<div class="h-1.5 w-10 rounded-full bg-surface-gray-3" />
|
|
22
|
+
</div>
|
|
23
|
+
<DialogTitle
|
|
24
|
+
v-if="title"
|
|
25
|
+
class="px-2 pb-3 pt-2 text-center text-3xl-semibold text-ink-gray-9"
|
|
26
|
+
>
|
|
27
|
+
{{ title }}
|
|
28
|
+
</DialogTitle>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="h-[70vh] overflow-y-auto">
|
|
31
|
+
<slot />
|
|
32
|
+
</div>
|
|
33
|
+
</DialogContent>
|
|
34
|
+
</DialogPortal>
|
|
35
|
+
</DialogRoot>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
import { computed, ref, watch } from 'vue'
|
|
40
|
+
import {
|
|
41
|
+
DialogRoot,
|
|
42
|
+
DialogPortal,
|
|
43
|
+
DialogOverlay,
|
|
44
|
+
DialogContent,
|
|
45
|
+
DialogTitle,
|
|
46
|
+
} from 'reka-ui'
|
|
47
|
+
import { usePointerSwipe } from '@vueuse/core'
|
|
48
|
+
import type { BottomSheetProps, BottomSheetEmits } from './types'
|
|
49
|
+
|
|
50
|
+
const props = withDefaults(defineProps<BottomSheetProps>(), {
|
|
51
|
+
dismissible: true,
|
|
52
|
+
})
|
|
53
|
+
const emit = defineEmits<BottomSheetEmits>()
|
|
54
|
+
|
|
55
|
+
const isOpen = computed({
|
|
56
|
+
get: () => props.open || false,
|
|
57
|
+
set(value: boolean) {
|
|
58
|
+
emit('update:open', value)
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function onDismissAttempt(event: Event) {
|
|
63
|
+
if (!props.dismissible) event.preventDefault()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Swipe-down-to-dismiss on the handle. While dragging we translate the sheet
|
|
67
|
+
// 1:1 with the pointer (transition off). On release we either fling it shut
|
|
68
|
+
// *from the release point* or spring it back to rest.
|
|
69
|
+
const handleRef = ref<HTMLElement | null>(null)
|
|
70
|
+
const dragY = ref(0)
|
|
71
|
+
const dragging = ref(false)
|
|
72
|
+
const closing = ref(false)
|
|
73
|
+
|
|
74
|
+
// Dismiss when the drag passes this fraction of the sheet's own height...
|
|
75
|
+
const CLOSE_HEIGHT_RATIO = 0.25
|
|
76
|
+
// ...or when released with at least this downward speed (px per ms) — a flick.
|
|
77
|
+
const CLOSE_VELOCITY = 0.5
|
|
78
|
+
const CLOSE_DURATION = 260
|
|
79
|
+
|
|
80
|
+
// Recent (offset, timestamp) samples during a drag, for release-velocity.
|
|
81
|
+
let samples: { y: number; t: number }[] = []
|
|
82
|
+
// Sheet height captured at drag start — drives both the rubber-band and the
|
|
83
|
+
// height-relative close threshold without re-measuring on every move.
|
|
84
|
+
let dragHeight = 0
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* iOS-style resistance for the upward overscroll: the further you pull past the
|
|
88
|
+
* resting position, the less the sheet follows, asymptotically. `overscroll` is
|
|
89
|
+
* a positive distance; returns the (negative) damped offset.
|
|
90
|
+
*/
|
|
91
|
+
function rubberband(overscroll: number, dimension: number, constant = 0.55) {
|
|
92
|
+
const d = dimension || window.innerHeight
|
|
93
|
+
return -((overscroll * d * constant) / (d + constant * overscroll))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Downward speed (px/ms) over the last ~100ms of the drag; positive = down. */
|
|
97
|
+
function flickVelocity() {
|
|
98
|
+
if (samples.length < 2) return 0
|
|
99
|
+
const latest = samples[samples.length - 1]
|
|
100
|
+
const oldest = samples.find((s) => latest.t - s.t <= 100) ?? samples[0]
|
|
101
|
+
const dt = latest.t - oldest.t
|
|
102
|
+
return dt > 0 ? (latest.y - oldest.y) / dt : 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const dragStyle = computed(() => {
|
|
106
|
+
// Fling shut: keep sliding down from the current offset and off-screen.
|
|
107
|
+
// `animation: none` suppresses reka's exit keyframe so it can't restart the
|
|
108
|
+
// slide from translateY(0) — which is what caused the "jump back up" flash.
|
|
109
|
+
if (closing.value) {
|
|
110
|
+
return {
|
|
111
|
+
transform: 'translateY(100%)',
|
|
112
|
+
transition: `transform ${CLOSE_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1)`,
|
|
113
|
+
animation: 'none',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Live drag: track the pointer with no transition.
|
|
117
|
+
if (dragging.value) {
|
|
118
|
+
return { transform: `translateY(${dragY.value}px)`, transition: 'none' }
|
|
119
|
+
}
|
|
120
|
+
// Rest / spring-back.
|
|
121
|
+
return {
|
|
122
|
+
transform: dragY.value ? `translateY(${dragY.value}px)` : undefined,
|
|
123
|
+
transition: 'transform 200ms ease-out',
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const { distanceY, direction } = usePointerSwipe(handleRef, {
|
|
128
|
+
// Accept mouse and pen as well as touch, so the handle is draggable on desktop.
|
|
129
|
+
pointerTypes: ['mouse', 'touch', 'pen'],
|
|
130
|
+
// Track from the first pixel — the default 50px threshold makes the sheet sit
|
|
131
|
+
// still, then jump 50px once crossed, which reads as choppy.
|
|
132
|
+
threshold: 0,
|
|
133
|
+
onSwipeStart() {
|
|
134
|
+
samples = []
|
|
135
|
+
dragHeight =
|
|
136
|
+
handleRef.value?.closest<HTMLElement>('.bottom-sheet-content')
|
|
137
|
+
?.offsetHeight ?? window.innerHeight
|
|
138
|
+
},
|
|
139
|
+
onSwipe() {
|
|
140
|
+
if (!props.dismissible || closing.value) return
|
|
141
|
+
dragging.value = true
|
|
142
|
+
// distanceY is start.y - current.y, so -distanceY is +down / -up.
|
|
143
|
+
// Down tracks 1:1; up gets rubber-band resistance and springs back on release.
|
|
144
|
+
const raw = -distanceY.value
|
|
145
|
+
dragY.value = raw >= 0 ? raw : rubberband(-raw, dragHeight)
|
|
146
|
+
samples.push({ y: dragY.value, t: performance.now() })
|
|
147
|
+
},
|
|
148
|
+
onSwipeEnd() {
|
|
149
|
+
if (!props.dismissible || closing.value) return
|
|
150
|
+
dragging.value = false
|
|
151
|
+
// Close on either a long-enough downward pull (relative to the sheet's
|
|
152
|
+
// height) or a fast downward flick; anything else (incl. an upward pull)
|
|
153
|
+
// springs back to rest.
|
|
154
|
+
const draggedDown = direction.value === 'down' && dragY.value > 0
|
|
155
|
+
const shouldClose =
|
|
156
|
+
draggedDown &&
|
|
157
|
+
(dragY.value > dragHeight * CLOSE_HEIGHT_RATIO ||
|
|
158
|
+
flickVelocity() > CLOSE_VELOCITY)
|
|
159
|
+
if (shouldClose) {
|
|
160
|
+
// Animate the fling, then unmount. `closing` stays true through the
|
|
161
|
+
// unmount so reka drops the node without replaying its own exit.
|
|
162
|
+
closing.value = true
|
|
163
|
+
window.setTimeout(() => (isOpen.value = false), CLOSE_DURATION + 20)
|
|
164
|
+
} else {
|
|
165
|
+
dragY.value = 0
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Fresh drag state each time the sheet opens.
|
|
171
|
+
watch(isOpen, (open) => {
|
|
172
|
+
if (open) {
|
|
173
|
+
closing.value = false
|
|
174
|
+
dragY.value = 0
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<style scoped>
|
|
180
|
+
@keyframes bottom-sheet-overlay-in {
|
|
181
|
+
from {
|
|
182
|
+
opacity: 0;
|
|
183
|
+
}
|
|
184
|
+
to {
|
|
185
|
+
opacity: 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@keyframes bottom-sheet-overlay-out {
|
|
190
|
+
from {
|
|
191
|
+
opacity: 1;
|
|
192
|
+
}
|
|
193
|
+
to {
|
|
194
|
+
opacity: 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes bottom-sheet-content-in {
|
|
199
|
+
from {
|
|
200
|
+
transform: translateY(100%);
|
|
201
|
+
}
|
|
202
|
+
to {
|
|
203
|
+
transform: translateY(0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@keyframes bottom-sheet-content-out {
|
|
208
|
+
from {
|
|
209
|
+
transform: translateY(0);
|
|
210
|
+
}
|
|
211
|
+
to {
|
|
212
|
+
transform: translateY(100%);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
:global(.bottom-sheet-overlay[data-state='open']) {
|
|
217
|
+
animation: bottom-sheet-overlay-in 150ms ease-out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
:global(.bottom-sheet-overlay[data-state='closed']) {
|
|
221
|
+
animation: bottom-sheet-overlay-out 200ms ease-in;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* iOS-style sheet easing (fast-out, settle-in) — a spring feel without a lib. */
|
|
225
|
+
:global(.bottom-sheet-content[data-state='open']) {
|
|
226
|
+
animation: bottom-sheet-content-in 300ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
:global(.bottom-sheet-content[data-state='closed']) {
|
|
230
|
+
animation: bottom-sheet-content-out 200ms ease-in;
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BottomSheetProps {
|
|
2
|
+
/** Controls whether the sheet is open (`v-model:open`). */
|
|
3
|
+
open?: boolean
|
|
4
|
+
|
|
5
|
+
/** Optional centered title rendered in the drag-handle area. */
|
|
6
|
+
title?: string
|
|
7
|
+
|
|
8
|
+
/** Allow outside-click, Escape, and swipe-down to close. Default `true`. */
|
|
9
|
+
dismissible?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BottomSheetEmits {
|
|
13
|
+
/** Fired when the open state changes via `v-model:open`. */
|
|
14
|
+
'update:open': [value: boolean]
|
|
15
|
+
|
|
16
|
+
/** Fired after the close animation finishes. */
|
|
17
|
+
'after-leave': []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BottomSheetSlots {
|
|
21
|
+
/** Sheet body, rendered in a scrollable region below the handle. */
|
|
22
|
+
default?: () => any
|
|
23
|
+
}
|
|
@@ -8,33 +8,34 @@
|
|
|
8
8
|
{
|
|
9
9
|
name: 'events',
|
|
10
10
|
description: '',
|
|
11
|
-
required:
|
|
12
|
-
type: '
|
|
11
|
+
required: true,
|
|
12
|
+
type: 'CalendarEvent[]',
|
|
13
13
|
default: '[]'
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
name: 'config',
|
|
17
17
|
description: '',
|
|
18
18
|
required: false,
|
|
19
|
-
type: '
|
|
19
|
+
type: 'Partial<CalendarConfig>',
|
|
20
|
+
default: '{}'
|
|
20
21
|
},
|
|
21
22
|
{
|
|
22
23
|
name: 'onClick',
|
|
23
24
|
description: '',
|
|
24
25
|
required: false,
|
|
25
|
-
type: '
|
|
26
|
+
type: '((data: { e: MouseEvent; calendarEvent: CalendarEvent; }) => void)'
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
name: 'onDblClick',
|
|
29
30
|
description: '',
|
|
30
31
|
required: false,
|
|
31
|
-
type: '
|
|
32
|
+
type: '((data: { e: MouseEvent | null; calendarEvent: CalendarEvent; }) => void)'
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
name: 'onCellClick',
|
|
35
36
|
description: '',
|
|
36
37
|
required: false,
|
|
37
|
-
type: '
|
|
38
|
+
type: '((data: CalendarCellClickData) => void)'
|
|
38
39
|
}
|
|
39
40
|
]
|
|
40
41
|
|
|
@@ -42,12 +43,12 @@
|
|
|
42
43
|
{
|
|
43
44
|
name: 'header',
|
|
44
45
|
description: '',
|
|
45
|
-
type: '{ currentMonthYear:
|
|
46
|
+
type: '{ currentMonthYear: string; currentYear: number; currentMonth: number; enabledModes: CalendarActionO'
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
49
|
name: 'event-popover-content',
|
|
49
50
|
description: '',
|
|
50
|
-
type: '
|
|
51
|
+
type: '{ calendarEvent: { [x: string]: unknown; id?: string | number | undefined; name?: string | number | '
|
|
51
52
|
},
|
|
52
53
|
{
|
|
53
54
|
name: 'daily-header',
|
|
@@ -60,30 +61,29 @@
|
|
|
60
61
|
{
|
|
61
62
|
name: 'delete',
|
|
62
63
|
description: '',
|
|
63
|
-
type: '
|
|
64
|
+
type: '[eventID: string | number | undefined]'
|
|
64
65
|
},
|
|
65
66
|
{
|
|
66
67
|
name: 'create',
|
|
67
68
|
description: '',
|
|
68
|
-
type: '
|
|
69
|
+
type: '[event: CalendarEvent]'
|
|
69
70
|
},
|
|
70
71
|
{
|
|
71
72
|
name: 'update',
|
|
72
73
|
description: '',
|
|
73
|
-
type: '
|
|
74
|
+
type: '[event: CalendarEvent]'
|
|
74
75
|
},
|
|
75
76
|
{
|
|
76
77
|
name: 'rangeChange',
|
|
77
78
|
description: '',
|
|
78
|
-
type: '
|
|
79
|
+
type: '[payload: { view: CalendarMode; startDate: string; endDate: string; }]'
|
|
79
80
|
}
|
|
80
81
|
]
|
|
81
82
|
</script>
|
|
82
83
|
## API Reference
|
|
83
84
|
|
|
84
|
-
<PropsTable name="Calendar" :data="propsData"/>
|
|
85
|
-
|
|
86
|
-
<SlotsTable :data="slotsData"/>
|
|
85
|
+
<PropsTable name="Calendar" :data="propsData"/>
|
|
87
86
|
|
|
88
|
-
<
|
|
87
|
+
<SlotsTable :data="slotsData"/>
|
|
89
88
|
|
|
89
|
+
<EmitsTable :data="emitsData"/>
|