frappe-ui 0.1.127 → 0.1.129

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,8 +1,8 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.127",
3
+ "version": "0.1.129",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
- "main": "./src/index.js",
5
+ "main": "./src/index.ts",
6
6
  "scripts": {
7
7
  "test": "vitest",
8
8
  "prettier": "yarn prettier -w ./src",
@@ -0,0 +1,299 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ type Component,
5
+ ref,
6
+ watch,
7
+ h,
8
+ FunctionalComponent,
9
+ } from 'vue'
10
+ import {
11
+ ComboboxAnchor,
12
+ ComboboxContent,
13
+ ComboboxEmpty,
14
+ ComboboxGroup,
15
+ ComboboxInput,
16
+ ComboboxItem,
17
+ ComboboxItemIndicator,
18
+ ComboboxLabel,
19
+ ComboboxPortal,
20
+ ComboboxRoot,
21
+ ComboboxSeparator,
22
+ ComboboxTrigger,
23
+ ComboboxViewport,
24
+ } from 'reka-ui'
25
+ import LucideCheck from '~icons/lucide/check'
26
+ import LucideChevronDown from '~icons/lucide/chevron-down'
27
+
28
+ type SimpleOption =
29
+ | string
30
+ | {
31
+ label: string
32
+ value: string
33
+ icon?: string | Component
34
+ disabled?: boolean
35
+ }
36
+ type GroupedOption = { group: string; options: SimpleOption[] }
37
+ type ComboboxOption = SimpleOption | GroupedOption
38
+
39
+ interface ComboboxProps {
40
+ options: Array<ComboboxOption>
41
+ modelValue?: string | null
42
+ placeholder?: string
43
+ disabled?: boolean
44
+ }
45
+
46
+ const props = defineProps<ComboboxProps>()
47
+ const emit = defineEmits(['update:modelValue', 'update:selectedOption'])
48
+
49
+ const searchTerm = ref('')
50
+ const internalModelValue = ref(props.modelValue)
51
+ const isOpen = ref(false)
52
+ const userHasTyped = ref(false)
53
+
54
+ watch(
55
+ () => props.modelValue,
56
+ (newValue) => {
57
+ internalModelValue.value = newValue
58
+ searchTerm.value = getDisplayValue(newValue)
59
+ },
60
+ )
61
+
62
+ const onUpdateModelValue = (value: string | null) => {
63
+ internalModelValue.value = value
64
+ emit('update:modelValue', value)
65
+ searchTerm.value = getDisplayValue(value)
66
+ userHasTyped.value = false
67
+
68
+ const selectedOpt = value
69
+ ? allOptionsFlat.value.find((opt) => getValue(opt) === value) || null
70
+ : null
71
+ emit('update:selectedOption', selectedOpt)
72
+ }
73
+
74
+ const isGroup = (option: ComboboxOption): option is GroupedOption => {
75
+ return typeof option === 'object' && 'group' in option
76
+ }
77
+
78
+ const getLabel = (option: SimpleOption): string => {
79
+ return typeof option === 'string' ? option : option.label
80
+ }
81
+
82
+ const getValue = (option: SimpleOption): string => {
83
+ return typeof option === 'string' ? option : option.value
84
+ }
85
+
86
+ const isDisabled = (option: SimpleOption): boolean => {
87
+ return typeof option === 'object' && !!option.disabled
88
+ }
89
+
90
+ const getIcon = (option: SimpleOption): string | Component | undefined => {
91
+ return typeof option === 'object' ? option.icon : undefined
92
+ }
93
+
94
+ const allOptionsFlat = computed(() => {
95
+ const flatOptions: SimpleOption[] = []
96
+ props.options.forEach((optionOrGroup) => {
97
+ if (isGroup(optionOrGroup)) {
98
+ flatOptions.push(...optionOrGroup.options)
99
+ } else {
100
+ flatOptions.push(optionOrGroup)
101
+ }
102
+ })
103
+ return flatOptions
104
+ })
105
+
106
+ function getDisplayValue(selectedValue: string | null | undefined): string {
107
+ if (!selectedValue) return ''
108
+ const selectedOption = allOptionsFlat.value.find(
109
+ (opt) => getValue(opt) === selectedValue,
110
+ )
111
+ return selectedOption ? getLabel(selectedOption) : selectedValue || ''
112
+ }
113
+
114
+ const selectedOption = computed(() => {
115
+ if (!internalModelValue.value) return null
116
+ return allOptionsFlat.value.find(
117
+ (opt) => getValue(opt) === internalModelValue.value,
118
+ )
119
+ })
120
+
121
+ const selectedOptionIcon = computed(() => {
122
+ return selectedOption.value ? getIcon(selectedOption.value) : undefined
123
+ })
124
+
125
+ const RenderIcon: FunctionalComponent<{ icon?: string | Component }> = (
126
+ props,
127
+ ) => {
128
+ if (!props.icon) return null
129
+ const iconContent =
130
+ typeof props.icon === 'string'
131
+ ? h('span', props.icon)
132
+ : h(props.icon, { class: 'w-4 h-4' })
133
+
134
+ return h(
135
+ 'span',
136
+ {
137
+ class: 'flex-shrink-0 w-4 h-4 inline-flex items-center justify-center',
138
+ },
139
+ [iconContent],
140
+ )
141
+ }
142
+
143
+ const filterFunction = (options: ComboboxOption[], search: string) => {
144
+ if (!search) return options
145
+
146
+ const lowerSearch = search.toLowerCase()
147
+ const filtered: ComboboxOption[] = []
148
+
149
+ options.forEach((optionOrGroup) => {
150
+ if (isGroup(optionOrGroup)) {
151
+ const filteredGroupOptions = optionOrGroup.options.filter((opt) => {
152
+ const label = getLabel(opt).toLowerCase()
153
+ const value = getValue(opt).toLowerCase()
154
+
155
+ return label.includes(lowerSearch) || value.includes(lowerSearch)
156
+ })
157
+ if (filteredGroupOptions.length > 0) {
158
+ filtered.push({ ...optionOrGroup, options: filteredGroupOptions })
159
+ }
160
+ } else {
161
+ const label = getLabel(optionOrGroup).toLowerCase()
162
+ const value = getValue(optionOrGroup).toLowerCase()
163
+ if (label.includes(lowerSearch) || value.includes(lowerSearch)) {
164
+ filtered.push(optionOrGroup)
165
+ }
166
+ }
167
+ })
168
+ return filtered
169
+ }
170
+
171
+ const filteredOptions = computed(() => {
172
+ if (isOpen.value && !userHasTyped.value && internalModelValue.value) {
173
+ return props.options
174
+ }
175
+ return filterFunction(props.options, searchTerm.value)
176
+ })
177
+
178
+ const handleInputChange = (event: Event) => {
179
+ const target = event.target as HTMLInputElement
180
+ searchTerm.value = target.value
181
+ userHasTyped.value = true
182
+
183
+ if (searchTerm.value === '') {
184
+ internalModelValue.value = null
185
+ emit('update:modelValue', null)
186
+ }
187
+ }
188
+
189
+ const handleOpenChange = (open: boolean) => {
190
+ isOpen.value = open
191
+ if (!open) {
192
+ searchTerm.value = getDisplayValue(internalModelValue.value)
193
+ userHasTyped.value = false
194
+ } else {
195
+ userHasTyped.value = false
196
+ }
197
+ }
198
+ </script>
199
+
200
+ <template>
201
+ <ComboboxRoot
202
+ :model-value="internalModelValue"
203
+ @update:modelValue="onUpdateModelValue"
204
+ @update:open="handleOpenChange"
205
+ class="relative"
206
+ :ignore-filter="true"
207
+ >
208
+ <ComboboxAnchor
209
+ class="flex h-7 w-full items-center justify-between gap-2 rounded bg-surface-gray-2 px-2 py-1 transition-colors hover:bg-surface-gray-3 border border-transparent focus-within:border-outline-gray-4 focus-within:ring-2 focus-within:ring-outline-gray-3"
210
+ :class="{ 'opacity-50 pointer-events-none': disabled }"
211
+ >
212
+ <div class="flex items-center gap-2 flex-1 overflow-hidden">
213
+ <RenderIcon v-if="selectedOptionIcon" :icon="selectedOptionIcon" />
214
+ <ComboboxInput
215
+ :value="searchTerm"
216
+ @input="handleInputChange"
217
+ class="bg-transparent p-0 focus:outline-0 border-0 focus:border-0 focus:ring-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full"
218
+ :placeholder="placeholder || ''"
219
+ :disabled="disabled"
220
+ autocomplete="off"
221
+ />
222
+ </div>
223
+ <ComboboxTrigger :disabled="disabled">
224
+ <LucideChevronDown class="h-4 w-4 text-ink-gray-5" />
225
+ </ComboboxTrigger>
226
+ </ComboboxAnchor>
227
+ <ComboboxPortal>
228
+ <ComboboxContent
229
+ class="z-10 min-w-[--reka-combobox-trigger-width] mt-1 bg-surface-modal overflow-hidden rounded-lg shadow-2xl"
230
+ position="popper"
231
+ @openAutoFocus.prevent
232
+ @closeAutoFocus.prevent
233
+ :align="'start'"
234
+ >
235
+ <ComboboxViewport
236
+ class="max-h-60 overflow-auto pb-1.5"
237
+ :class="{ 'px-1.5 pt-1.5': !isGroup(filteredOptions[0]) }"
238
+ >
239
+ <ComboboxEmpty
240
+ class="text-ink-gray-5 text-base text-center py-1.5 px-2.5"
241
+ >
242
+ No results found for "{{ searchTerm }}"
243
+ </ComboboxEmpty>
244
+
245
+ <template
246
+ v-for="(optionOrGroup, index) in filteredOptions"
247
+ :key="index"
248
+ >
249
+ <ComboboxGroup class="px-1.5" v-if="isGroup(optionOrGroup)">
250
+ <ComboboxLabel
251
+ class="px-2.5 pt-3 pb-1.5 text-sm font-medium text-ink-gray-5 sticky top-0 bg-surface-modal z-10"
252
+ >
253
+ {{ optionOrGroup.group }}
254
+ </ComboboxLabel>
255
+ <ComboboxItem
256
+ v-for="(option, idx) in optionOrGroup.options"
257
+ :key="`${index}-${idx}`"
258
+ :value="getValue(option)"
259
+ :disabled="isDisabled(option)"
260
+ class="text-base leading-none text-ink-gray-7 rounded flex items-center h-7 px-2.5 py-1.5 relative select-none data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3"
261
+ >
262
+ <span class="flex items-center gap-2 pr-6 flex-1">
263
+ <RenderIcon :icon="getIcon(option)" />
264
+ {{ getLabel(option) }}
265
+ </span>
266
+ <ComboboxItemIndicator
267
+ class="inline-flex ml-2 items-center justify-center"
268
+ >
269
+ <LucideCheck class="size-4" />
270
+ </ComboboxItemIndicator>
271
+ </ComboboxItem>
272
+ </ComboboxGroup>
273
+
274
+ <ComboboxItem
275
+ v-else
276
+ :key="index"
277
+ :value="getValue(optionOrGroup)"
278
+ :disabled="isDisabled(optionOrGroup)"
279
+ class="text-base leading-none text-ink-gray-7 rounded flex items-center h-7 px-2.5 py-1.5 relative select-none data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3"
280
+ >
281
+ <span class="flex items-center gap-2 pr-6 flex-1">
282
+ <RenderIcon
283
+ v-if="getIcon(optionOrGroup)"
284
+ :icon="getIcon(optionOrGroup)"
285
+ />
286
+ {{ getLabel(optionOrGroup) }}
287
+ </span>
288
+ <ComboboxItemIndicator
289
+ class="absolute right-0 w-6 inline-flex items-center justify-center"
290
+ >
291
+ <LucideCheck class="h-4 w-4" />
292
+ </ComboboxItemIndicator>
293
+ </ComboboxItem>
294
+ </template>
295
+ </ComboboxViewport>
296
+ </ComboboxContent>
297
+ </ComboboxPortal>
298
+ </ComboboxRoot>
299
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Combobox } from './Combobox.vue'
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div
3
+ class="p-2 flex min-w-72 items-center gap-2 bg-surface-white shadow-xl rounded-lg"
4
+ >
5
+ <TextInput
6
+ ref="input"
7
+ type="text"
8
+ class="w-full"
9
+ placeholder="https://example.com"
10
+ v-model="_href"
11
+ @keydown.enter="submitLink"
12
+ @keydown.esc="$emit('close')"
13
+ />
14
+ <div class="shrink-0 flex items-center gap-2">
15
+ <Tooltip text="Submit" placement="top">
16
+ <Button label="Submit" @click="submitLink">
17
+ <template #icon><LucideCheck class="size-4" /></template>
18
+ </Button>
19
+ </Tooltip>
20
+ <Tooltip text="Remove link" placement="top">
21
+ <Button label="Remove link" @click="$emit('updateHref', '')">
22
+ <template #icon><LucideX class="size-4" /></template>
23
+ </Button>
24
+ </Tooltip>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { onMounted, ref, useTemplateRef, defineEmits } from 'vue'
31
+ import Button from '../Button/Button.vue'
32
+ import TextInput from '../TextInput.vue'
33
+ import Tooltip from '../Tooltip/Tooltip.vue'
34
+
35
+ const props = defineProps<{
36
+ show: boolean
37
+ href: string
38
+ onClose: () => void
39
+ onUpdateHref: (href: string) => void
40
+ }>()
41
+
42
+ const emit = defineEmits<{
43
+ (e: 'updateHref', href: string): void
44
+ (e: 'close'): void
45
+ }>()
46
+
47
+ const _href = ref(props.href)
48
+ const input = useTemplateRef('input')
49
+
50
+ // Simple URL validation regex
51
+ const isValidUrl = (url: string) => {
52
+ if (!url) return true
53
+ const regex =
54
+ /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i
55
+ return regex.test(url)
56
+ }
57
+
58
+ const submitLink = () => {
59
+ if (isValidUrl(_href.value)) {
60
+ emit('updateHref', _href.value)
61
+ }
62
+ }
63
+
64
+ onMounted(() => {
65
+ if (props.show) {
66
+ setTimeout(() => {
67
+ if (input.value?.el) {
68
+ input.value.el.focus()
69
+ input.value.el.select()
70
+ }
71
+ }, 0)
72
+ }
73
+ })
74
+ </script>
@@ -1,111 +1,50 @@
1
1
  <template>
2
- <div>
3
- <div
4
- v-if="items.length"
5
- class="min-w-40 rounded-lg border bg-surface-white p-1 text-base shadow-lg"
6
- >
7
- <button
8
- v-for="(item, index) in items"
9
- :key="index"
10
- :class="[
11
- index === selectedIndex ? 'bg-surface-gray-2' : '',
12
- 'flex w-full items-center whitespace-nowrap rounded-md px-2 py-2 text-sm text-ink-gray-9',
13
- ]"
14
- @click="selectItem(index)"
15
- @mouseover="selectedIndex = index"
16
- >
17
- <span class="mr-2">{{ item.emoji }}</span>
18
- <span>{{ item.name }}</span>
19
- </button>
20
- </div>
21
- </div>
2
+ <SuggestionList
3
+ ref="suggestionList"
4
+ :items="items"
5
+ :command="selectItem"
6
+ item-class="py-2"
7
+ >
8
+ <template #default="{ item }">
9
+ <span class="mr-2">{{ item.emoji }}</span>
10
+ <span>{{ item.name }}</span>
11
+ </template>
12
+ </SuggestionList>
22
13
  </template>
23
14
 
24
- <script>
25
- export default {
26
- props: {
27
- items: {
28
- type: Array,
29
- required: true,
30
- },
31
- command: {
32
- type: Function,
33
- required: true,
34
- },
35
- },
36
- data() {
37
- return {
38
- selectedIndex: 0,
39
- }
40
- },
41
- watch: {
42
- items() {
43
- this.selectedIndex = 0
44
- },
45
- },
46
- methods: {
47
- onKeyDown({ event }) {
48
- if (event.key === 'ArrowUp') {
49
- this.upHandler()
50
- return true
51
- }
52
- if (event.key === 'ArrowDown') {
53
- this.downHandler()
54
- return true
55
- }
56
- if (event.key === 'Enter') {
57
- this.enterHandler()
58
- return true
59
- }
60
- return false
61
- },
62
- upHandler() {
63
- this.selectedIndex =
64
- (this.selectedIndex + this.items.length - 1) % this.items.length
65
- },
66
- downHandler() {
67
- this.selectedIndex = (this.selectedIndex + 1) % this.items.length
68
- },
69
- enterHandler() {
70
- this.selectItem(this.selectedIndex)
71
- },
72
- selectItem(index) {
73
- const item = this.items[index]
74
- if (item) {
75
- this.command({ emoji: item.emoji })
76
- }
77
- },
78
- },
79
- }
80
- </script>
15
+ <script setup lang="ts">
16
+ import { ref, type PropType } from 'vue'
17
+ import SuggestionList, { type SuggestionItem } from './SuggestionList.vue'
81
18
 
82
- <style>
83
- .emoji-suggestions {
84
- background: #fff;
85
- border-radius: 0.5rem;
86
- box-shadow:
87
- 0 0 0 1px rgba(0, 0, 0, 0.05),
88
- 0px 10px 20px rgba(0, 0, 0, 0.1);
89
- padding: 0.2rem;
19
+ interface EmojiItem extends SuggestionItem {
20
+ name: string
21
+ emoji: string
90
22
  }
91
23
 
92
- .emoji-item {
93
- display: flex;
94
- align-items: center;
95
- padding: 0.5rem;
96
- border-radius: 0.3rem;
97
- cursor: pointer;
98
- }
24
+ const props = defineProps({
25
+ items: {
26
+ type: Array as PropType<EmojiItem[]>,
27
+ required: true,
28
+ },
29
+ command: {
30
+ type: Function as PropType<(params: { emoji: string }) => void>,
31
+ required: true,
32
+ },
33
+ })
99
34
 
100
- .emoji-item.is-selected {
101
- background: #eee;
102
- }
35
+ const suggestionList = ref<InstanceType<typeof SuggestionList> | null>(null)
103
36
 
104
- .emoji {
105
- margin-right: 0.5rem;
37
+ const selectItem = (item: SuggestionItem) => {
38
+ if (item) {
39
+ props.command({ emoji: item.emoji })
40
+ }
106
41
  }
107
42
 
108
- .name {
109
- font-size: 0.9rem;
43
+ const onKeyDown = ({ event }: { event: KeyboardEvent }) => {
44
+ return suggestionList.value?.onKeyDown({ event }) ?? false
110
45
  }
111
- </style>
46
+
47
+ defineExpose({
48
+ onKeyDown,
49
+ })
50
+ </script>
@@ -11,6 +11,9 @@
11
11
  v-if="props.show"
12
12
  class="fixed top-0 left-0 w-full h-full bg-black sm:bg-black/90 z-[50] flex flex-col justify-center items-center overflow-hidden touch-none"
13
13
  ref="imageContainer"
14
+ @mousemove="handleActivity"
15
+ @touchstart="handleActivity"
16
+ @touchmove="handleActivity"
14
17
  >
15
18
  <!-- Dedicated Backdrop -->
16
19
  <div
@@ -46,7 +49,8 @@
46
49
  <!-- Caption -->
47
50
  <div
48
51
  v-if="currentImage.alt"
49
- class="absolute bottom-4 p-2 text-center rounded-sm text-white text-sm bg-black/50 z-10"
52
+ class="absolute bottom-4 p-2 text-center rounded-sm text-white text-sm bg-black/65 z-10 transition-opacity duration-300 ease-in-out"
53
+ :class="{ 'opacity-0 pointer-events-none': !isControlsVisible }"
50
54
  >
51
55
  {{ currentImage.alt }}
52
56
  </div>
@@ -54,7 +58,8 @@
54
58
  <!-- Controls bar -->
55
59
  <div
56
60
  ref="controlsBar"
57
- class="absolute top-4 flex items-center space-x-3 p-2 text-white z-20"
61
+ class="absolute top-4 flex items-center space-x-3 p-2 text-white z-20 transition-opacity duration-300 ease-in-out"
62
+ :class="{ 'opacity-0 pointer-events-none': !isControlsVisible }"
58
63
  @touchstart.stop
59
64
  @touchmove.stop
60
65
  @touchend.stop
@@ -62,7 +67,7 @@
62
67
  @wheel.stop
63
68
  >
64
69
  <!-- Navigation controls -->
65
- <div class="bg-black bg-opacity-50 rounded flex items-center">
70
+ <div class="bg-black/65 rounded flex items-center">
66
71
  <Tooltip text="Previous image">
67
72
  <button
68
73
  class="p-2 hover:bg-gray-900 rounded-l focus:outline-none"
@@ -117,7 +122,7 @@
117
122
  </div>
118
123
 
119
124
  <!-- Action controls -->
120
- <div class="bg-black bg-opacity-50 rounded flex items-center">
125
+ <div class="bg-black/65 rounded flex items-center">
121
126
  <Tooltip text="Download image">
122
127
  <button
123
128
  class="p-2 hover:bg-gray-900 rounded-l focus:outline-none"
@@ -141,7 +146,7 @@
141
146
  </div>
142
147
 
143
148
  <!-- Close button -->
144
- <div class="bg-black bg-opacity-50 rounded flex items-center">
149
+ <div class="bg-black/65 rounded flex items-center">
145
150
  <Tooltip text="Close">
146
151
  <button
147
152
  class="p-2 hover:bg-gray-900 rounded focus:outline-none"
@@ -204,6 +209,10 @@ const controlsBarHeight = ref(0)
204
209
  const isFullscreen = ref(false)
205
210
  const touchStartZoom = ref(100)
206
211
 
212
+ const isControlsVisible = ref(true)
213
+ const inactivityTimer = ref<ReturnType<typeof setTimeout> | null>(null)
214
+ const INACTIVITY_TIMEOUT = 3000 // 3 seconds
215
+
207
216
  const {
208
217
  zoomLevel,
209
218
  panPosition,
@@ -297,9 +306,33 @@ const {
297
306
 
298
307
  const isPanning = computed(() => isMousePanning.value || isTouchPanning.value)
299
308
 
309
+ function showControlsAndResetTimer() {
310
+ isControlsVisible.value = true
311
+ if (inactivityTimer.value) {
312
+ clearTimeout(inactivityTimer.value)
313
+ }
314
+ inactivityTimer.value = setTimeout(() => {
315
+ if (!isPanning.value && !isPinching.value) {
316
+ isControlsVisible.value = false
317
+ } else {
318
+ showControlsAndResetTimer()
319
+ }
320
+ }, INACTIVITY_TIMEOUT)
321
+ }
322
+
323
+ function handleActivity() {
324
+ if (!isPinching.value || !isControlsVisible.value) {
325
+ showControlsAndResetTimer()
326
+ }
327
+ }
328
+
300
329
  function close() {
301
330
  emit('update:show', false)
302
331
  resetZoom()
332
+ if (inactivityTimer.value) {
333
+ clearTimeout(inactivityTimer.value)
334
+ inactivityTimer.value = null
335
+ }
303
336
  }
304
337
 
305
338
  function downloadImage() {
@@ -342,6 +375,8 @@ function handleFullscreenChange() {
342
375
  function handleKeyDown(event: KeyboardEvent) {
343
376
  if (!props.show) return
344
377
 
378
+ handleActivity()
379
+
345
380
  switch (event.key) {
346
381
  case 'ArrowLeft':
347
382
  if (!isPanning.value) previousImage()
@@ -372,6 +407,25 @@ function handleKeyDown(event: KeyboardEvent) {
372
407
  }
373
408
  }
374
409
 
410
+ watch(
411
+ () => props.show,
412
+ (newValue) => {
413
+ if (newValue) {
414
+ isControlsVisible.value = true
415
+ resetZoom()
416
+ showControlsAndResetTimer()
417
+ } else {
418
+ if (inactivityTimer.value) {
419
+ clearTimeout(inactivityTimer.value)
420
+ inactivityTimer.value = null
421
+ }
422
+ if (isFullscreen.value && document.exitFullscreen) {
423
+ document.exitFullscreen()
424
+ }
425
+ }
426
+ },
427
+ )
428
+
375
429
  watch(controlsBar, (newVal) => {
376
430
  if (newVal) {
377
431
  const updateHeight = () => {
@@ -395,6 +449,10 @@ onUnmounted(() => {
395
449
  document.removeEventListener('fullscreenchange', handleFullscreenChange)
396
450
  document.removeEventListener('keydown', handleKeyDown)
397
451
 
452
+ if (inactivityTimer.value) {
453
+ clearTimeout(inactivityTimer.value)
454
+ }
455
+
398
456
  if (isFullscreen.value && document.exitFullscreen) {
399
457
  document.exitFullscreen()
400
458
  }