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.
Files changed (153) hide show
  1. package/package.json +2 -1
  2. package/src/components/Avatar/Avatar.api.md +7 -1
  3. package/src/components/Avatar/Avatar.cy.ts +21 -0
  4. package/src/components/Avatar/Avatar.md +5 -2
  5. package/src/components/Avatar/Avatar.vue +39 -6
  6. package/src/components/Avatar/index.ts +1 -1
  7. package/src/components/Avatar/stories/Themes.vue +23 -0
  8. package/src/components/Avatar/types.ts +11 -0
  9. package/src/components/BottomSheet/BottomSheet.api.md +57 -0
  10. package/src/components/BottomSheet/BottomSheet.vue +232 -0
  11. package/src/components/BottomSheet/index.ts +6 -0
  12. package/src/components/BottomSheet/types.ts +23 -0
  13. package/src/components/Calendar/Calendar.api.md +16 -16
  14. package/src/components/Calendar/Calendar.vue +99 -72
  15. package/src/components/Calendar/CalendarDaily.vue +20 -19
  16. package/src/components/Calendar/CalendarMonthEvent.vue +30 -9
  17. package/src/components/Calendar/CalendarMonthly.vue +65 -43
  18. package/src/components/Calendar/CalendarTimeMarker.vue +9 -8
  19. package/src/components/Calendar/CalendarWeekDayEvent.vue +96 -40
  20. package/src/components/Calendar/CalendarWeekly.vue +76 -40
  21. package/src/components/Calendar/EventModalContent.vue +26 -10
  22. package/src/components/Calendar/NewEventModal.vue +19 -16
  23. package/src/components/Calendar/ShowMoreCalendarEvent.vue +10 -16
  24. package/src/components/Calendar/{calendarUtils.js → calendarUtils.ts} +146 -200
  25. package/src/components/Calendar/composables/{useCalendarData.js → useCalendarData.ts} +23 -11
  26. package/src/components/Calendar/composables/{useEventModal.js → useEventModal.ts} +16 -12
  27. package/src/components/Calendar/index.ts +12 -2
  28. package/src/components/Calendar/stories/CustomHeader.vue +8 -6
  29. package/src/components/Calendar/stories/Examples.vue +7 -5
  30. package/src/components/Calendar/style.css +1 -1
  31. package/src/components/Calendar/types.ts +106 -0
  32. package/src/components/Calendar/{useEventBase.js → useEventBase.ts} +51 -25
  33. package/src/components/Charts/eChartOptions.ts +22 -7
  34. package/src/components/Checkbox/Checkbox.api.md +6 -0
  35. package/src/components/Checkbox/Checkbox.vue +1 -0
  36. package/src/components/Checkbox/types.ts +7 -0
  37. package/src/components/CodeEditor/CodeEditor.api.md +8 -9
  38. package/src/components/Combobox/Combobox.api.md +16 -17
  39. package/src/components/DatePicker/DateRangePicker.vue +3 -1
  40. package/src/components/DesktopShell/DesktopShell.api.md +9 -1
  41. package/src/components/DesktopShell/DesktopShell.vue +24 -1
  42. package/src/components/Dialog/Dialog.api.md +7 -8
  43. package/src/components/Dialogs.vue +11 -9
  44. package/src/components/Dropdown/Dropdown.api.md +9 -4
  45. package/src/components/FileUploader/FileUploader.api.md +36 -11
  46. package/src/components/FileUploader/FileUploader.cy.ts +66 -0
  47. package/src/components/FileUploader/FileUploader.md +4 -1
  48. package/src/components/FileUploader/FileUploader.vue +147 -106
  49. package/src/components/FileUploader/index.ts +6 -0
  50. package/src/components/FileUploader/types.ts +43 -0
  51. package/src/components/FormControl/FormControl.api.md +3 -4
  52. package/src/components/HoverCard/HoverCard.api.md +5 -6
  53. package/src/components/KeyboardShortcutsModal/KeyboardShortcutsModal.api.md +22 -14
  54. package/src/components/ListFilter/ListFilter.api.md +5 -6
  55. package/src/components/ListFilter/ListFilter.vue +159 -55
  56. package/src/components/ListFilter/NestedPopover.vue +17 -10
  57. package/src/components/ListFilter/SearchComplete.vue +31 -38
  58. package/src/components/ListView/ListView.api.md +322 -6
  59. package/src/components/ListView/index.ts +25 -0
  60. package/src/components/MobileNav/MobileNav.api.md +6 -7
  61. package/src/components/MonthPicker/MonthPicker.cy.ts +16 -0
  62. package/src/components/MonthPicker/MonthPicker.vue +18 -7
  63. package/src/components/MultiSelect/MultiSelect.api.md +5 -6
  64. package/src/components/PageHeader/PageHeader.api.md +0 -1
  65. package/src/components/PageHeader/PageHeaderTitle.vue +12 -0
  66. package/src/components/PageHeader/index.ts +1 -0
  67. package/src/components/Popover/Popover.api.md +8 -9
  68. package/src/components/Rail/Rail.api.md +7 -8
  69. package/src/components/ScrollArea/ScrollArea.api.md +6 -6
  70. package/src/components/ScrollArea/ScrollArea.vue +4 -1
  71. package/src/components/ScrollArea/ScrollBar.vue +11 -5
  72. package/src/components/SettingsDialog/SettingsContent.vue +2 -0
  73. package/src/components/SettingsDialog/SettingsDialog.vue +11 -7
  74. package/src/components/Sidebar/Sidebar.api.md +28 -12
  75. package/src/components/Sidebar/SidebarHeader.vue +76 -52
  76. package/src/components/Sidebar/SidebarItem.vue +42 -11
  77. package/src/components/Sidebar/types.ts +8 -0
  78. package/src/components/{Pill → TabButtons}/Pill.vue +1 -1
  79. package/src/components/TabButtons/TabButtons.vue +2 -1
  80. package/src/components/TabButtons/types.ts +1 -1
  81. package/src/components/TextEditor/TextEditor.md +3 -0
  82. package/src/components/TextEditor/TextEditor.vue +2 -2
  83. package/src/components/TextEditor/index.ts +13 -0
  84. package/src/components/ThemeSwitcher/ThemeSwitcher.cy.ts +13 -0
  85. package/src/components/ThemeSwitcher/ThemeSwitcher.vue +3 -0
  86. package/src/composables/useAutofocusOnOpen.ts +2 -0
  87. package/src/composables/useShortcut.ts +3 -0
  88. package/src/data-fetching/docStore.test.ts +2 -1
  89. package/src/data-fetching/docStore.ts +11 -13
  90. package/src/data-fetching/useCall/types.ts +1 -0
  91. package/src/data-fetching/useCall/useCall.test.ts +43 -0
  92. package/src/data-fetching/useCall/useCall.ts +17 -4
  93. package/src/data-fetching/useDoc/useDoc.test.ts +2 -2
  94. package/src/data-fetching/useDoc/useDoc.ts +10 -6
  95. package/src/data-fetching/useDoctype/useDoctype.ts +6 -5
  96. package/src/data-fetching/useFrappeFetch.ts +17 -4
  97. package/src/data-fetching/useList/types.ts +1 -0
  98. package/src/data-fetching/useList/useList.test.ts +40 -0
  99. package/src/data-fetching/useList/useList.ts +20 -13
  100. package/src/data-fetching/useNewDoc/useNewDoc.ts +9 -5
  101. package/src/directives/focus.ts +3 -3
  102. package/src/index.ts +101 -88
  103. package/src/mocks/handlers.ts +8 -0
  104. package/src/molecules/editor/Editor.vue +6 -2
  105. package/src/molecules/editor/EditorContent.vue +2 -2
  106. package/src/molecules/editor/extensions/code-block/CodeBlockComponent.css +10 -0
  107. package/src/molecules/editor/extensions/toc-node/toc-render.ts +7 -3
  108. package/src/molecules/editor/extensions.ts +5 -0
  109. package/src/molecules/editor/index.ts +2 -4
  110. package/src/molecules/editor/useEditor.test.ts +72 -0
  111. package/src/molecules/editor/useEditor.ts +31 -4
  112. package/src/molecules/list/List.cy.ts +400 -79
  113. package/src/molecules/list/List.vue +52 -1
  114. package/src/molecules/list/ListGroup.vue +40 -0
  115. package/src/molecules/list/ListHeader.vue +37 -1
  116. package/src/molecules/list/ListHeaderCellSort.vue +44 -10
  117. package/src/molecules/list/ListRow.vue +10 -3
  118. package/src/molecules/list/ListRowBase.vue +30 -6
  119. package/src/molecules/list/ListRows.vue +44 -6
  120. package/src/molecules/list/index.ts +1 -0
  121. package/src/molecules/list/list-context.ts +16 -0
  122. package/src/molecules/list/list.api.md +81 -28
  123. package/src/molecules/list/list.md +70 -42
  124. package/src/molecules/list/style.css +39 -6
  125. package/src/molecules/list/types.ts +17 -1
  126. package/src/resources/documentResource.d.ts +28 -0
  127. package/src/resources/listResource.d.ts +33 -0
  128. package/src/resources/plugin.d.ts +7 -0
  129. package/src/resources/resources.d.ts +35 -0
  130. package/src/utils/{call.js → call.ts} +50 -9
  131. package/src/utils/config.ts +1 -0
  132. package/src/utils/confirmDialog.d.ts +17 -0
  133. package/src/utils/debounce.ts +6 -7
  134. package/src/utils/fileUploadHandler.ts +3 -7
  135. package/src/utils/frappeRequest.test.ts +24 -9
  136. package/src/utils/{frappeRequest.js → frappeRequest.ts} +55 -13
  137. package/src/utils/pageMeta.ts +1 -1
  138. package/src/utils/{plugin.js → plugin.ts} +21 -6
  139. package/src/utils/{request.js → request.ts} +27 -3
  140. package/src/utils/{socketio.js → socketio.ts} +11 -1
  141. package/src/utils/useFileUpload.ts +17 -6
  142. package/tailwind/preset.js +15 -0
  143. package/tsconfig.base.json +2 -2
  144. package/vitepress/components/Docs/Sidebar.vue +3 -3
  145. package/vitepress/components/Navbar.vue +5 -2
  146. package/src/components/Pill/Pill.api.md +0 -99
  147. package/src/components/Pill/Pill.md +0 -20
  148. package/src/components/Pill/index.ts +0 -10
  149. package/src/components/Pill/stories/BrowserEdges.vue +0 -42
  150. package/src/components/Pill/stories/Sizes.vue +0 -49
  151. package/src/components/Pill/stories/Variants.vue +0 -53
  152. package/src/components/Resource.vue +0 -21
  153. /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.20",
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
- ## Themes
9
+ ## Shapes
10
10
  <ComponentPreview name="Avatar-Shapes" />
11
11
 
12
- ## Controlled state
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 bg-surface-gray-2 uppercase text-ink-gray-5 select-none"
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,6 @@
1
+ export { default as BottomSheet } from './BottomSheet.vue'
2
+ export type {
3
+ BottomSheetProps,
4
+ BottomSheetEmits,
5
+ BottomSheetSlots,
6
+ } from './types'
@@ -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: false,
12
- type: 'Record<string, any>',
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: 'Record<string, any>'
19
+ type: 'Partial<CalendarConfig>',
20
+ default: '{}'
20
21
  },
21
22
  {
22
23
  name: 'onClick',
23
24
  description: '',
24
25
  required: false,
25
- type: 'Function'
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: 'Function'
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: 'Function'
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: any; currentYear: number; currentMonth: number; enabledModes: { label: string; v'
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: 'any'
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: 'any[]'
64
+ type: '[eventID: string | number | undefined]'
64
65
  },
65
66
  {
66
67
  name: 'create',
67
68
  description: '',
68
- type: 'any[]'
69
+ type: '[event: CalendarEvent]'
69
70
  },
70
71
  {
71
72
  name: 'update',
72
73
  description: '',
73
- type: 'any[]'
74
+ type: '[event: CalendarEvent]'
74
75
  },
75
76
  {
76
77
  name: 'rangeChange',
77
78
  description: '',
78
- type: 'any[]'
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
- <EmitsTable :data="emitsData"/>
87
+ <SlotsTable :data="slotsData"/>
89
88
 
89
+ <EmitsTable :data="emitsData"/>