@cloudron/pankow 4.4.0 → 4.4.2

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 (37) hide show
  1. package/components/BottomBar.vue +1 -1
  2. package/components/BreadCrumb.vue +3 -3
  3. package/components/Button.vue +6 -0
  4. package/components/ClipboardAction.vue +1 -1
  5. package/components/DateTimeInput.vue +2 -1
  6. package/components/Dialog.vue +4 -2
  7. package/components/DirectoryView.vue +1 -0
  8. package/components/DirectoryViewGridItem.vue +7 -3
  9. package/components/DirectoryViewListItem.vue +5 -3
  10. package/components/EmailInput.vue +2 -1
  11. package/components/FileUploader.vue +2 -2
  12. package/components/MaskedInput.vue +2 -1
  13. package/components/Menu.vue +1 -1
  14. package/components/MenuItem.vue +1 -1
  15. package/components/MenuItemLink.vue +1 -1
  16. package/components/MultiSelect.vue +1 -1
  17. package/components/Notification.vue +4 -2
  18. package/components/NumberInput.vue +2 -1
  19. package/components/OfflineBanner.vue +1 -1
  20. package/components/PasswordInput.vue +1 -1
  21. package/components/Popover.vue +1 -1
  22. package/components/ProgressBar.vue +1 -1
  23. package/components/SaveIndicator.vue +1 -1
  24. package/components/SideBar.vue +2 -2
  25. package/components/SingleSelect.vue +1 -1
  26. package/components/Spinner.vue +1 -1
  27. package/components/SplitLayout.vue +146 -34
  28. package/components/Switch.vue +1 -1
  29. package/components/TabView.vue +42 -6
  30. package/components/TableView.vue +6 -0
  31. package/components/TagInput.vue +1 -1
  32. package/components/TextInput.vue +2 -1
  33. package/components/TextInputRaw.vue +2 -1
  34. package/components/TreeView.vue +1 -1
  35. package/components/TreeViewNode.vue +6 -0
  36. package/package.json +1 -1
  37. package/viewers/GenericViewer.vue +1 -1
@@ -4,7 +4,7 @@
4
4
  <template>
5
5
  <div class="pankow-bottom-bar">
6
6
  <slot>
7
- © 2024 &nbsp; <a href="https://cloudron.io" rel="noopener noreferrer" target="_blank">Cloudron</a> &nbsp; &nbsp; &nbsp; <a href="https://forum.cloudron.io" rel="noopener noreferrer" target="_blank">Forum <i class="fa fa-comments"></i></a>
7
+ © 2024 &nbsp; <a href="https://cloudron.io" rel="noopener noreferrer" target="_blank">Cloudron</a> &nbsp; &nbsp; &nbsp; <a href="https://forum.cloudron.io" rel="noopener noreferrer" target="_blank" aria-label="Forum">Forum <i class="fa fa-comments" aria-hidden="true"></i></a>
8
8
  </slot>
9
9
  </div>
10
10
  </template>
@@ -25,13 +25,13 @@ function onClick() {
25
25
  </script>
26
26
 
27
27
  <template>
28
- <div class="pankow-breadcrumb">
28
+ <div class="pankow-breadcrumb" aria-label="Breadcrumb">
29
29
  <a class="pankow-breadcrumb-item" :href="home.route" @click="home.action ? home.action(home, $event) : null" @keydown.enter="home.action ? home.action(home, $event) : null" tabindex="0">
30
30
  <Icon :class="home.icon"/> {{ home.label }}
31
31
  </a>
32
- <span v-for="item in items">
32
+ <span v-for="(item, index) in items">
33
33
  <Icon icon="fa-solid fa-chevron-right" class="pankow-breadcrumb-separator"/>
34
- <a class="pankow-breadcrumb-item" :href="item.route" @click="item.action ? item.action(home, $event) : null" @keydown.enter="item.action ? item.action(home, $event) : null" tabindex="0">
34
+ <a class="pankow-breadcrumb-item" :href="item.route" @click="item.action ? item.action(home, $event) : null" @keydown.enter="item.action ? item.action(home, $event) : null" tabindex="0" :aria-current="index === items.length - 1 ? 'page' : null">
35
35
  <Icon v-if="item.icon" :class="item.icon"/> {{ item.label }}
36
36
  </a>
37
37
  </span>
@@ -53,6 +53,7 @@ function onClick(event) {
53
53
  <template>
54
54
  <component ref="elem" :is="href ? 'a' : 'div'" class="pankow-button" role="button" type="button" :class="{ 'pankow-button-disabled': disabled }"
55
55
  @click="onClick" @keydown.enter="onClick" :href="disabled ? null : href" :target="target" tabindex="0"
56
+ :aria-label="!$slots['default'] && icon ? icon : undefined"
56
57
  :success="success || undefined" :danger="danger || undefined">
57
58
  <Menu ref="menuItem" :model="menu" v-if="menu"></Menu>
58
59
  <Spinner v-show="loading" style="stroke: white;" :class="{ 'pankow-button-icon-with-text': $slots['default'] }"/>
@@ -108,6 +109,11 @@ a.pankow-button:visited {
108
109
  line-height: inherit;
109
110
  }
110
111
 
112
+ .pankow-button .pankow-spinner {
113
+ width: 1.2em;
114
+ height: 1.2em;
115
+ }
116
+
111
117
  .pankow-button-icon-right-with-text {
112
118
  padding-left: 7px;
113
119
  }
@@ -19,7 +19,7 @@ function onClipboard() {
19
19
  </script>
20
20
 
21
21
  <template>
22
- <i class="pankow-clipboard-action fa-solid fa-copy" @click="onClipboard()"></i>
22
+ <i class="pankow-clipboard-action fa-solid fa-copy" tabindex="0" role="button" aria-label="Copy to clipboard" @click="onClipboard()" @keydown.enter="onClipboard()" @keydown.space.prevent="onClipboard()"></i>
23
23
  </template>
24
24
 
25
25
  <style>
@@ -2,6 +2,7 @@
2
2
 
3
3
  const model = defineModel();
4
4
  const props = defineProps({
5
+ id: String,
5
6
  readonly: {
6
7
  type: Boolean,
7
8
  default: false
@@ -15,7 +16,7 @@ const props = defineProps({
15
16
  </script>
16
17
 
17
18
  <template>
18
- <input class="pankow-text-input" :type="dateOnly ? 'date' : 'datetime-local'" :readonly="readonly" v-model="model"/>
19
+ <input :id="id" class="pankow-text-input" :type="dateOnly ? 'date' : 'datetime-local'" :readonly="readonly" v-model="model"/>
19
20
  </template>
20
21
 
21
22
  <style>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
 
3
3
  import { ref, useTemplateRef, nextTick, computed } from 'vue';
4
+ import { uuidv4 } from '../utils.js';
4
5
  import Button from './Button.vue';
5
6
  import Icon from './Icon.vue';
6
7
 
@@ -60,6 +61,7 @@ const mergedStyle = computed(() => {
60
61
  })
61
62
 
62
63
  const visible = ref(false);
64
+ const dialogTitleId = ref(uuidv4());
63
65
  const dialog = useTemplateRef('dialog');
64
66
  const dialogBody = useTemplateRef('dialogBody');
65
67
  const confirmButton = useTemplateRef('confirmButton');
@@ -117,8 +119,8 @@ defineExpose({ open, close });
117
119
  <div class="pankow-dialog-backdrop" @click="onDismiss" v-show="visible" :style="{ 'z-index': zIndex }"></div>
118
120
  </Transition>
119
121
  <Transition :name="center ? 'pankow-bounce-center' : 'pankow-bounce-center-top'">
120
- <div ref="dialog" class="pankow-dialog" v-bind="$attrs" :class="{ 'pankow-dialog-center': center }" v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="mergedStyle">
121
- <div class="pankow-dialog-header" v-show="title">
122
+ <div ref="dialog" class="pankow-dialog" role="dialog" aria-modal="true" :aria-labelledby="title ? dialogTitleId : null" v-bind="$attrs" :class="{ 'pankow-dialog-center': center }" v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="mergedStyle">
123
+ <div :id="dialogTitleId" class="pankow-dialog-header" v-show="title">
122
124
  {{ title }}
123
125
  </div>
124
126
  <div class="pankow-dialog-body" ref="dialogBody" :class="{ 'pankow-dialog-body-no-header': !title, 'pankow-dialog-body-no-buttons': !(rejectLabel || confirmLabel) }">
@@ -3,6 +3,7 @@
3
3
  class="directory-view"
4
4
  :class="{ 'drop-target-active': dropTargetActive }"
5
5
  tabindex="0"
6
+ role="grid"
6
7
  ref="main"
7
8
  @keydown.up.prevent="onKeyUp($event)"
8
9
  @keydown.down.prevent="onKeyDown($event)"
@@ -8,15 +8,19 @@
8
8
 
9
9
  <div class="grid-item" :class="{ 'focused': item.focused, 'selected': item.selected, 'drop-target-active': dropTargetActive }">
10
10
  <div v-if="visible && showStar" class="grid-item-star" @dblclick.stop.prevent @click.stop.prevent="onToggleStar">
11
- <Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" />
11
+ <span tabindex="0" role="button" :aria-label="item.star ? 'Unstar' : 'Star'" @keydown.enter.prevent="onToggleStar" @keydown.space.prevent="onToggleStar">
12
+ <Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" />
13
+ </span>
12
14
  </div>
13
15
  <div v-if="visible" class="grid-item-actions" @mouseup.stop="onActionMenu($event)">
14
- <i class="fa-solid fa-ellipsis-vertical"/>
16
+ <span tabindex="0" role="button" aria-label="Actions" @click="onActionMenu($event)" @keydown.enter="onActionMenu($event)" @keydown.space.prevent="onActionMenu($event)">
17
+ <i class="fa-solid fa-ellipsis-vertical"/>
18
+ </span>
15
19
  </div>
16
20
 
17
21
  <div class="grid-item-icon">
18
22
  <template v-if="visible">
19
- <img :src="item.previewUrl || item.icon" ref="iconImage" @error="iconError($event)"/>
23
+ <img :src="item.previewUrl || item.icon" ref="iconImage" :alt="item.name" @error="iconError($event)"/>
20
24
  <i v-show="shareIndicatorProperty !== '' && !!item[shareIndicatorProperty]" class="fa-solid fa-share-nodes is-shared"></i>
21
25
  <i v-show="fileDropIndicatorProperty !== '' && !!item[fileDropIndicatorProperty]" class="fa-solid fa-cloud-arrow-up is-filedrop"></i>
22
26
  </template>
@@ -11,7 +11,7 @@
11
11
  <div v-if="!visible" class="col label"></div>
12
12
 
13
13
  <div v-if="visible" class="col icon">
14
- <img :src="item.previewUrl || item.icon" ref="iconImage" @error="iconError($event)"/>
14
+ <img :src="item.previewUrl || item.icon" ref="iconImage" :alt="item.name" @error="iconError($event)"/>
15
15
  <i v-show="shareIndicatorProperty !== '' && !!item[shareIndicatorProperty]" class="fa-solid fa-share-nodes is-shared"></i>
16
16
  <i v-show="fileDropIndicatorProperty !== '' && !!item[fileDropIndicatorProperty]" class="fa-solid fa-cloud-arrow-up is-filedrop"></i>
17
17
  </div>
@@ -26,13 +26,15 @@
26
26
  </div>
27
27
 
28
28
  <div v-if="visible && showStar" class="col star">
29
- <Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" @dblclick.stop.prevent @click.stop.prevent="onToggleStar" />
29
+ <span tabindex="0" role="button" :aria-label="item.star ? 'Unstar' : 'Star'" @dblclick.stop.prevent @click.stop.prevent="onToggleStar" @keydown.enter.prevent="onToggleStar" @keydown.space.prevent="onToggleStar">
30
+ <Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" />
31
+ </span>
30
32
  </div>
31
33
  <div v-if="visible && showOwner" class="col owner">{{ item.owner }}</div>
32
34
  <div v-if="visible && showSize" class="col size">{{ prettyFileSize(item.size) }}</div>
33
35
  <div v-if="visible && showModified" class="col modified" :title="item.modified.toLocaleString()">{{ prettyDate(item.modified) }}</div>
34
36
  <div v-if="visible" class="col actions">
35
- <div @mouseup.stop="onActionMenu($event)"><i class="fa-solid fa-ellipsis-vertical"/></div>
37
+ <div @mouseup.stop="onActionMenu($event)"><span tabindex="0" role="button" aria-label="Actions" @click="onActionMenu($event)" @keydown.enter="onActionMenu($event)" @keydown.space.prevent="onActionMenu($event)"><i class="fa-solid fa-ellipsis-vertical"/></span></div>
36
38
  </div>
37
39
  </div>
38
40
  </div>
@@ -4,6 +4,7 @@ import { useTemplateRef } from 'vue';
4
4
 
5
5
  const model = defineModel();
6
6
  const props = defineProps({
7
+ id: String,
7
8
  placeholder: String,
8
9
  readonly: {
9
10
  type: Boolean,
@@ -21,7 +22,7 @@ defineExpose({ focus });
21
22
  </script>
22
23
 
23
24
  <template>
24
- <input ref="inputElement" class="pankow-text-input" type="email" :placeholder="placeholder" :readonly="readonly" :required="$attrs['required']" v-model.trim="model"/>
25
+ <input :id="id" ref="inputElement" class="pankow-text-input" type="email" :placeholder="placeholder" :readonly="readonly" :required="$attrs['required']" v-model.trim="model"/>
25
26
  </template>
26
27
 
27
28
  <style>
@@ -6,8 +6,8 @@
6
6
  <Button danger small tool @click="onCancelUpload" icon="fa-solid fa-xmark"></Button>
7
7
  </div>
8
8
 
9
- <input type="file" ref="uploadFileInput" style="display: none" multiple/>
10
- <input type="file" ref="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
9
+ <input type="file" ref="uploadFileInput" style="display: none" multiple aria-label="Upload files"/>
10
+ <input type="file" ref="uploadFolderInput" style="display: none" multiple webkitdirectory directory aria-label="Upload folder"/>
11
11
 
12
12
  <InputDialog ref="inputDialog" />
13
13
  </div>
@@ -5,6 +5,7 @@ import { computed } from 'vue';
5
5
  const model = defineModel({ type: [String, null], default: null });
6
6
 
7
7
  const props = defineProps({
8
+ id: String,
8
9
  multiline: {
9
10
  type: Boolean,
10
11
  default: false
@@ -27,7 +28,7 @@ const displayValue = computed({
27
28
 
28
29
  <template>
29
30
  <template v-if="!multiline">
30
- <input class="pankow-masked-input" :value="displayValue" @input="displayValue = $event.target.value" @focus="$event.target.select()" />
31
+ <input :id="id" class="pankow-masked-input" :value="displayValue" @input="displayValue = $event.target.value" @focus="$event.target.select()" />
31
32
  </template>
32
33
  <template v-else>
33
34
  <textarea :value="displayValue" @input="displayValue = $event.target.value" @focus="$event.target.select()"></textarea>
@@ -325,7 +325,7 @@ defineExpose({
325
325
  <teleport to="#app">
326
326
  <div class="pankow-menu-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
327
327
  <Transition :name="rollUp ? 'pankow-roll-up' : 'pankow-roll-down'">
328
- <div class="pankow-menu" :class="{ 'pankow-menu-keyboard-nav': keyboardNav }" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown" @mousemove="onMouseMove">
328
+ <div class="pankow-menu" role="menu" :aria-label="searchThreshold < model.length ? 'Filterable menu' : 'Menu'" :class="{ 'pankow-menu-keyboard-nav': keyboardNav }" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown" @mousemove="onMouseMove">
329
329
  <TextInput placeholder="Filter ..." autocomplete="off" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.stop @keydown.esc.stop="close()" @click.stop style="width: 100%; border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
330
330
  <component v-for="item in visibleItems" ref="itemElements" :is="item.type || (item.href ? MenuItemLink : MenuItem)" @activated="onItemActivated(item)" :item="item" :has-icons="hasIcons" />
331
331
  <MenuItem v-if="model.length === 0" :item="emptyItem"/>
@@ -42,7 +42,7 @@ function onActivated() {
42
42
  </script>
43
43
 
44
44
  <template>
45
- <div class="pankow-menu-item" :class="{'pankow-menu-item-disabled': isDisabled }" @click.stop="onActivated" :separator="item.separator" v-if="isVisible" tabindex="0" @keydown.enter.stop="onActivated" @keydown.space.stop="onActivated">
45
+ <div class="pankow-menu-item" role="menuitem" :aria-checked="item.checkbox ? item.checked : undefined" :class="{'pankow-menu-item-disabled': isDisabled }" @click.stop="onActivated" :separator="item.separator" v-if="isVisible" tabindex="0" @keydown.enter.stop="onActivated" @keydown.space.stop="onActivated">
46
46
  <CheckBox v-model="item.checked" v-if="item.checkbox" style="margin-right: 4px; display: inline-flex;"/>
47
47
  <span v-if="!item.separator" :style="{ width: (hasIcons && !item.checkbox) ? '29px' : 0 }"><Icon v-show="hasIcons" :icon="item.icon"/></span>{{ item.label }}
48
48
  <div class="pankow-menu-item-separator-line" v-if="item.separator"></div>
@@ -40,7 +40,7 @@ function onActivated() {
40
40
  </script>
41
41
 
42
42
  <template>
43
- <a class="pankow-menu-item" style="display: block" :href="isDisabled ? null : item.href" :target="item.target || null" @click="onActivated" :class="{ 'pankow-menu-item-disabled': isDisabled }" v-if="isVisible">
43
+ <a class="pankow-menu-item" role="menuitem" style="display: block" :href="isDisabled ? null : item.href" :target="item.target || null" @click="onActivated" :class="{ 'pankow-menu-item-disabled': isDisabled }" v-if="isVisible">
44
44
  <span :style="{ width: hasIcons ? '29px' : 0 }" style="display: inline-block;"><Icon v-show="hasIcons" :icon="item.icon" /></span>{{ item.label }}
45
45
  </a>
46
46
  </template>
@@ -162,7 +162,7 @@ onMounted(() => {
162
162
  </script>
163
163
 
164
164
  <template>
165
- <div class="pankow-multiselect" :class="{ 'pankow-multiselect-disabled': disabled }" ref="elem" tabindex="0" @click="onClick" @keydown.enter="onOpen" @keydown.down.stop="onOpen" @keydown.up.stop="onOpen" @keydown.esc="onEscape">
165
+ <div class="pankow-multiselect" role="combobox" :aria-expanded="menu?.isOpen ?? false" aria-haspopup="listbox" :class="{ 'pankow-multiselect-disabled': disabled }" ref="elem" tabindex="0" @click="onClick" @keydown.enter="onOpen" @keydown.down.stop="onOpen" @keydown.up.stop="onOpen" @keydown.esc="onEscape">
166
166
  <!-- native select for required and accessibility handling -->
167
167
  <select ref="nativeSelect" :required="$attrs['required']" multiple style="display: none">
168
168
  <option value=""></option>
@@ -1,9 +1,11 @@
1
1
  <template>
2
- <TransitionGroup name="pankow-notification-fade" :class="`pankow-notification-container ${position}`" tag="div">
2
+ <TransitionGroup name="pankow-notification-fade" :class="`pankow-notification-container ${position}`" tag="div" role="alert" aria-live="polite">
3
3
  <div v-for="message in messages" :key="message.id">
4
4
  <div class="pankow-notification" :[message.type]="''">
5
5
  {{ message.text }}
6
- <Icon @click="removeNotify(message.id)" icon="fa-solid fa-xmark" class="pankow-notification-close"/>
6
+ <span tabindex="0" role="button" aria-label="Close notification" @keydown.enter="removeNotify(message.id)" @keydown.space.prevent="removeNotify(message.id)" @click="removeNotify(message.id)" class="pankow-notification-close">
7
+ <Icon icon="fa-solid fa-xmark"/>
8
+ </span>
7
9
  </div>
8
10
  </div>
9
11
  </TransitionGroup>
@@ -2,6 +2,7 @@
2
2
 
3
3
  const model = defineModel();
4
4
  const props = defineProps({
5
+ id: String,
5
6
  placeholder: String,
6
7
  readonly: {
7
8
  type: Boolean,
@@ -21,7 +22,7 @@ const props = defineProps({
21
22
  </script>
22
23
 
23
24
  <template>
24
- <input class="pankow-text-input" type="number" :step="step" :min="min" :max="max" :placeholder="placeholder" :readonly="readonly" :value="model" @input="$emit('update:modelValue', parseInt($event.target.value))"/>
25
+ <input :id="id" class="pankow-text-input" type="number" :step="step" :min="min" :max="max" :placeholder="placeholder" :readonly="readonly" :value="model" @input="$emit('update:modelValue', parseInt($event.target.value))"/>
25
26
  </template>
26
27
 
27
28
  <style>
@@ -19,7 +19,7 @@ const props = defineProps({
19
19
 
20
20
  <template>
21
21
  <Transition name="pankow-roll-down">
22
- <a class="pankow-offline-banner" v-show="active" :href="href" target="_blank">
22
+ <a class="pankow-offline-banner" role="alert" v-show="active" :href="href" target="_blank">
23
23
  <i class="fa fa-solid fa-spin fa-circle-notch"/> {{ label }}
24
24
  </a>
25
25
  </Transition>
@@ -30,7 +30,7 @@ function toggleReveal() {
30
30
  <template>
31
31
  <div class="pankow-password">
32
32
  <input class="pankow-password-input" :id="id" :type="revealed ? 'text' : 'password'" :disabled="disabled ? true : undefined" :placeholder="placeholder" :autofocus="$attrs['autofocus']" :required="$attrs['required']" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"/>
33
- <i class="pankow-password-reveal" v-html="revealIcon" @click="toggleReveal"></i>
33
+ <span class="pankow-password-reveal" tabindex="0" role="button" :aria-label="revealed ? 'Hide password' : 'Show password'" @keydown.enter="toggleReveal" @keydown.space.prevent="toggleReveal" @click="toggleReveal" v-html="revealIcon"></span>
34
34
  </div>
35
35
  </template>
36
36
 
@@ -144,7 +144,7 @@ defineExpose({
144
144
  <teleport to="#app">
145
145
  <div class="pankow-popover-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
146
146
  <Transition :name="rollUp ? 'pankow-animation-pop-up' : 'pankow-animation-pop-down'">
147
- <div v-show="isOpen" class="pankow-popover" ref="container" :style="{ 'max-width': width || null, 'max-height': height || null }">
147
+ <div v-show="isOpen" class="pankow-popover" ref="container" role="dialog" aria-label="Popover" :style="{ 'max-width': width || null, 'max-height': height || null }">
148
148
  <slot></slot>
149
149
  </div>
150
150
  </Transition>
@@ -35,7 +35,7 @@ const normalizedValue = computed(() => {
35
35
  </script>
36
36
 
37
37
  <template>
38
- <div class="pankow-progress-bar">
38
+ <div class="pankow-progress-bar" role="progressbar" :aria-valuenow="normalizedValue" aria-valuemin="0" aria-valuemax="100" :aria-label="'Progress: ' + normalizedValue + '%'">
39
39
  <div class="pankow-progress-bar-label" v-show="showLabel"><slot>{{ value + ' %' }}</slot></div>
40
40
  <div :class="{ 'pankow-progress-bar-background': showTrack }">
41
41
  <div v-if="mode !== 'indeterminate'" class="pankow-progress-bar-filled" :style="{ width: normalizedValue+'%', height: slim ? '2px' : '6px' }">
@@ -42,7 +42,7 @@ defineExpose({ success: showSuccess, error: showError });
42
42
  <div class="pankow-save-indicator-wrapper">
43
43
  <slot />
44
44
  <Transition name="pankow-save-indicator-bounce">
45
- <div class="pankow-save-indicator" v-if="visible" :class="{ success: success, error: !success }"><Icon :icon="success ? 'fa-solid fa-check' : 'fa-solid fa-xmark'"/></div>
45
+ <div class="pankow-save-indicator" role="status" aria-live="polite" v-if="visible" :class="{ success: success, error: !success }"><Icon :icon="success ? 'fa-solid fa-check' : 'fa-solid fa-xmark'"/></div>
46
46
  </Transition>
47
47
  </div>
48
48
  </template>
@@ -38,8 +38,8 @@ defineExpose({ open, close });
38
38
  </Transition>
39
39
  <div class="pankow-sidebar" ref="sideBar" :class="{ 'pankow-sidebar-closed': !isVisible }">
40
40
  <Transition name="pankow-scale">
41
- <div class="pankow-sidebar-close-action" v-if="isVisible" @click="close()"><i class="fa-solid fa-xmark"></i></div>
42
- <div class="pankow-sidebar-open-action" v-else-if="showOpenAction" @click="open()"><i class="fa-solid fa-bars"></i></div>
41
+ <div class="pankow-sidebar-close-action" v-if="isVisible" tabindex="0" role="button" aria-label="Close sidebar" @click="close()" @keydown.enter="close()" @keydown.space.prevent="close()"><i class="fa-solid fa-xmark"></i></div>
42
+ <div class="pankow-sidebar-open-action" v-else-if="showOpenAction" tabindex="0" role="button" aria-label="Open sidebar" @click="open()" @keydown.enter="open()" @keydown.space.prevent="open()"><i class="fa-solid fa-bars"></i></div>
43
43
  </Transition>
44
44
  <div class="pankow-sidebar-inner">
45
45
  <slot></slot>
@@ -176,7 +176,7 @@ watchEffect(handleDefaultSelect);
176
176
  </script>
177
177
 
178
178
  <template>
179
- <div class="pankow-singleselect" :class="{ 'pankow-singleselect-disabled': disabled }" ref="elem" tabindex="0" @click="onClick" @keydown.enter="onOpen" @keydown.down.stop.prevent="onSelectNext" @keydown.up.stop.prevent="onSelectPrev" @keydown="onKeyDown($event)">
179
+ <div class="pankow-singleselect" role="combobox" :aria-expanded="menu?.isOpen ?? false" aria-haspopup="listbox" :class="{ 'pankow-singleselect-disabled': disabled }" ref="elem" tabindex="0" @click="onClick" @keydown.enter="onOpen" @keydown.down.stop.prevent="onSelectNext" @keydown.up.stop.prevent="onSelectPrev" @keydown="onKeyDown($event)">
180
180
  <!-- native select for required and accessibility handling -->
181
181
  <select v-model="selectedKey" :id="id" ref="nativeSelect" :required="$attrs['required']" style="display: none">
182
182
  <option value=""></option>
@@ -2,7 +2,7 @@
2
2
  </script>
3
3
 
4
4
  <template>
5
- <div class="pankow-spinner"></div>
5
+ <div class="pankow-spinner" role="status" aria-label="Loading"></div>
6
6
  </template>
7
7
 
8
8
  <style>
@@ -2,6 +2,11 @@
2
2
  import { ref, computed, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
3
3
 
4
4
  const props = defineProps({
5
+ orientation: {
6
+ type: String,
7
+ default: 'horizontal'
8
+ },
9
+ // horizontal mode props
5
10
  leftWidth: {
6
11
  type: Number,
7
12
  default: 50
@@ -13,40 +18,88 @@ const props = defineProps({
13
18
  minRightWidth: {
14
19
  type: Number,
15
20
  default: 20
21
+ },
22
+ // vertical mode props
23
+ topHeight: {
24
+ type: Number,
25
+ default: 50
26
+ },
27
+ minTopHeight: {
28
+ type: Number,
29
+ default: 20
30
+ },
31
+ minBottomHeight: {
32
+ type: Number,
33
+ default: 20
34
+ },
35
+ opaque: {
36
+ type: Boolean,
37
+ default: false
16
38
  }
17
39
  });
18
40
 
19
- const emit = defineEmits(['update:leftWidth']);
41
+ const emit = defineEmits(['update:leftWidth', 'update:topHeight']);
42
+
43
+ const isHorizontal = computed(() => props.orientation === 'horizontal');
20
44
 
21
45
  const containerRef = useTemplateRef('container');
22
46
  const isDragging = ref(false);
23
- const dragStartX = ref(0);
24
- const dragStartWidth = ref(0);
25
- const currentLeftWidth = ref(props.leftWidth);
47
+ const dragStartCoord = ref(0);
48
+ const dragStartSize = ref(0);
49
+
50
+ const currentFirstSize = ref(
51
+ isHorizontal.value ? props.leftWidth : props.topHeight
52
+ );
26
53
 
27
54
  watch(() => props.leftWidth, (val) => {
55
+ if (!isDragging.value && isHorizontal.value) {
56
+ currentFirstSize.value = val;
57
+ }
58
+ });
59
+
60
+ watch(() => props.topHeight, (val) => {
61
+ if (!isDragging.value && !isHorizontal.value) {
62
+ currentFirstSize.value = val;
63
+ }
64
+ });
65
+
66
+ watch(() => props.orientation, () => {
28
67
  if (!isDragging.value) {
29
- currentLeftWidth.value = val;
68
+ currentFirstSize.value = isHorizontal.value ? props.leftWidth : props.topHeight;
30
69
  }
31
70
  });
32
71
 
72
+ const minFirst = computed(() =>
73
+ isHorizontal.value ? props.minLeftWidth : props.minTopHeight
74
+ );
75
+
76
+ const minSecond = computed(() =>
77
+ isHorizontal.value ? props.minRightWidth : props.minBottomHeight
78
+ );
79
+
33
80
  function startDrag(e) {
34
81
  isDragging.value = true;
35
- dragStartX.value = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
36
- dragStartWidth.value = currentLeftWidth.value;
37
- document.body.style.cursor = 'col-resize';
82
+ dragStartCoord.value = isHorizontal.value
83
+ ? (e.clientX ?? e.touches?.[0]?.clientX ?? 0)
84
+ : (e.clientY ?? e.touches?.[0]?.clientY ?? 0);
85
+ dragStartSize.value = currentFirstSize.value;
86
+ document.body.style.cursor = isHorizontal.value ? 'col-resize' : 'row-resize';
38
87
  document.body.style.userSelect = 'none';
39
88
  e.preventDefault();
40
89
  }
41
90
 
42
91
  function onDrag(e) {
43
92
  if (!isDragging.value || !containerRef.value) return;
44
- const clientX = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
45
- const containerWidth = containerRef.value.getBoundingClientRect().width;
46
- if (containerWidth === 0) return;
47
- const delta = ((clientX - dragStartX.value) / containerWidth) * 100;
48
- const newWidth = dragStartWidth.value + delta;
49
- currentLeftWidth.value = Math.max(props.minLeftWidth, Math.min(100 - props.minRightWidth, newWidth));
93
+ const coord = isHorizontal.value
94
+ ? (e.clientX ?? e.touches?.[0]?.clientX ?? 0)
95
+ : (e.clientY ?? e.touches?.[0]?.clientY ?? 0);
96
+ const containerSize = isHorizontal.value
97
+ ? containerRef.value.getBoundingClientRect().width
98
+ : containerRef.value.getBoundingClientRect().height;
99
+ if (containerSize === 0) return;
100
+ const delta = ((coord - dragStartCoord.value) / containerSize) * 100;
101
+ const newSize = dragStartSize.value + delta;
102
+ currentFirstSize.value = Math.max(minFirst.value, Math.min(100 - minSecond.value, newSize));
50
103
  }
51
104
 
52
105
  function stopDrag() {
@@ -54,7 +107,11 @@ function stopDrag() {
54
107
  isDragging.value = false;
55
108
  document.body.style.cursor = '';
56
109
  document.body.style.userSelect = '';
57
- emit('update:leftWidth', Math.round(currentLeftWidth.value));
110
+ if (isHorizontal.value) {
111
+ emit('update:leftWidth', Math.round(currentFirstSize.value));
112
+ } else {
113
+ emit('update:topHeight', Math.round(currentFirstSize.value));
114
+ }
58
115
  }
59
116
 
60
117
  onMounted(() => {
@@ -71,21 +128,35 @@ onUnmounted(() => {
71
128
  document.removeEventListener('touchend', stopDrag);
72
129
  });
73
130
 
74
- const leftStyle = computed(() => ({
75
- flex: `0 0 ${currentLeftWidth.value}%`,
76
- maxWidth: `${currentLeftWidth.value}%`
77
- }));
131
+ const firstStyle = computed(() => {
132
+ const size = currentFirstSize.value;
133
+ return isHorizontal.value
134
+ ? { flex: `0 0 ${size}%`, maxWidth: `${size}%` }
135
+ : { flex: `0 0 ${size}%`, maxHeight: `${size}%` };
136
+ });
78
137
  </script>
79
138
 
80
139
  <template>
81
- <div ref="container" class="pankow-split-layout" :class="{ 'pankow-split-layout-dragging': isDragging }">
82
- <div class="pankow-split-layout-left" :style="leftStyle">
140
+ <div
141
+ ref="container"
142
+ class="pankow-split-layout"
143
+ :class="{
144
+ 'pankow-split-layout-dragging': isDragging,
145
+ 'pankow-split-layout-vertical': !isHorizontal,
146
+ 'pankow-split-layout-divider-opaque': opaque
147
+ }"
148
+ >
149
+ <div class="pankow-split-layout-first" :style="firstStyle">
83
150
  <slot name="left" />
84
151
  </div>
85
- <div class="pankow-split-layout-divider" @mousedown="startDrag" @touchstart.prevent="startDrag">
152
+ <div
153
+ class="pankow-split-layout-divider"
154
+ @mousedown="startDrag"
155
+ @touchstart.prevent="startDrag"
156
+ >
86
157
  <div class="pankow-split-layout-handle"><span></span><span></span></div>
87
158
  </div>
88
- <div class="pankow-split-layout-right">
159
+ <div class="pankow-split-layout-second">
89
160
  <slot name="right" />
90
161
  </div>
91
162
  </div>
@@ -100,15 +171,22 @@ const leftStyle = computed(() => ({
100
171
  overflow: hidden;
101
172
  }
102
173
 
103
- .pankow-split-layout-left {
104
- height: 100%;
174
+ .pankow-split-layout-vertical {
175
+ flex-direction: column;
176
+ }
177
+
178
+ .pankow-split-layout-first {
105
179
  min-width: 0;
106
180
  overflow: hidden;
107
181
  }
108
182
 
183
+ .pankow-split-layout-vertical .pankow-split-layout-first {
184
+ height: auto;
185
+ min-height: 0;
186
+ min-width: 0;
187
+ }
188
+
109
189
  .pankow-split-layout-divider {
110
- width: 6px;
111
- cursor: col-resize;
112
190
  background: transparent;
113
191
  position: relative;
114
192
  flex-shrink: 0;
@@ -117,6 +195,20 @@ const leftStyle = computed(() => ({
117
195
  justify-content: center;
118
196
  }
119
197
 
198
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-divider {
199
+ width: 6px;
200
+ cursor: col-resize;
201
+ }
202
+
203
+ .pankow-split-layout-vertical .pankow-split-layout-divider {
204
+ height: 6px;
205
+ cursor: row-resize;
206
+ }
207
+
208
+ .pankow-split-layout-divider-opaque .pankow-split-layout-divider {
209
+ background: var(--pankow-color-background);
210
+ }
211
+
120
212
  .pankow-split-layout-divider:hover,
121
213
  .pankow-split-layout-dragging .pankow-split-layout-divider {
122
214
  background: var(--pankow-input-border-color);
@@ -128,13 +220,20 @@ const leftStyle = computed(() => ({
128
220
  left: 50%;
129
221
  transform: translate(-50%, -50%);
130
222
  display: flex;
131
- flex-direction: column;
132
223
  gap: 2px;
133
224
  opacity: 0;
134
225
  transition: opacity 0.15s;
135
226
  pointer-events: none;
136
227
  }
137
228
 
229
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle {
230
+ flex-direction: column;
231
+ }
232
+
233
+ .pankow-split-layout-vertical .pankow-split-layout-handle {
234
+ flex-direction: row;
235
+ }
236
+
138
237
  .pankow-split-layout-divider:hover .pankow-split-layout-handle,
139
238
  .pankow-split-layout-dragging .pankow-split-layout-handle {
140
239
  opacity: 0.5;
@@ -142,21 +241,34 @@ const leftStyle = computed(() => ({
142
241
 
143
242
  .pankow-split-layout-handle span {
144
243
  display: block;
145
- width: 2px;
146
- height: 16px;
147
244
  border-radius: 1px;
148
245
  background: var(--pankow-color-text);
149
246
  }
150
247
 
151
- .pankow-split-layout-right {
248
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle span {
249
+ width: 2px;
250
+ height: 16px;
251
+ }
252
+
253
+ .pankow-split-layout-vertical .pankow-split-layout-handle span {
254
+ width: 16px;
255
+ height: 2px;
256
+ }
257
+
258
+ .pankow-split-layout-second {
152
259
  flex: 1;
153
- height: 100%;
154
260
  min-width: 0;
155
261
  overflow: hidden;
156
262
  }
157
263
 
158
- .pankow-split-layout-dragging .pankow-split-layout-left,
159
- .pankow-split-layout-dragging .pankow-split-layout-right {
264
+ .pankow-split-layout-vertical .pankow-split-layout-second {
265
+ height: auto;
266
+ min-height: 0;
267
+ min-width: 0;
268
+ }
269
+
270
+ .pankow-split-layout-dragging .pankow-split-layout-first,
271
+ .pankow-split-layout-dragging .pankow-split-layout-second {
160
272
  pointer-events: none;
161
273
  }
162
274
  </style>
@@ -22,7 +22,7 @@ function onChange(event) {
22
22
 
23
23
  <template>
24
24
  <label :for="id || internalId" class="pankow-switch" :class="{ 'pankow-switch-disabled': disabled }">
25
- <input :id="id || internalId" class="pankow-switch-input" type="checkbox" v-model="model" :disabled="disabled" @change="onChange"/>
25
+ <input :id="id || internalId" class="pankow-switch-input" type="checkbox" role="switch" :aria-checked="model" v-model="model" :disabled="disabled" @change="onChange"/>
26
26
  <span></span>
27
27
  <div class="pankow-switch-input-label">
28
28
  <slot>{{ label }}</slot>
@@ -1,15 +1,15 @@
1
1
  <template>
2
2
  <div class="pankow-tabview" :top="tabPosition === 'top' ? true : null" :left="tabPosition === 'left' ? true : null" :right="tabPosition === 'right' ? true : null" :bottom="tabPosition === 'bottom' ? true : null">
3
- <div class="pankow-tabview-tabs" v-if="tabPosition === 'top' || tabPosition === 'left'">
4
- <div class="pankow-tabview-tab" :class="{'active': active === tab }" v-for="tab in Object.keys(tabs)" @click="open(tab)">{{ tabs[tab] }}</div>
3
+ <div class="pankow-tabview-tabs" role="tablist" :aria-label="tabPosition === 'top' || tabPosition === 'bottom' ? 'Tabs' : 'Tabs'" v-if="tabPosition === 'top' || tabPosition === 'left'">
4
+ <div class="pankow-tabview-tab" role="tab" :tabindex="active === tab ? 0 : -1" :aria-selected="active === tab ? 'true' : 'false'" :aria-controls="'tabpanel-'+tab" :class="{'active': active === tab }" v-for="tab in Object.keys(tabs)" @click="open(tab)" @keydown="onTabKeydown($event, tab)">{{ tabs[tab] }}</div>
5
5
  </div>
6
6
  <div class="pankow-tabview-content">
7
- <div v-for="tab in Object.keys(tabs)">
7
+ <div v-for="tab in Object.keys(tabs)" :id="'tabpanel-'+tab" role="tabpanel" :aria-labelledby="tab" tabindex="0">
8
8
  <slot :name="tab" v-if="active === tab"/>
9
9
  </div>
10
10
  </div>
11
- <div class="pankow-tabview-tabs" v-if="tabPosition === 'bottom' || tabPosition === 'right'">
12
- <div class="pankow-tabview-tab" :class="{'active': active === tab }" v-for="tab in Object.keys(tabs)" @click="open(tab)">{{ tabs[tab] }}</div>
11
+ <div class="pankow-tabview-tabs" role="tablist" :aria-label="tabPosition === 'top' || tabPosition === 'bottom' ? 'Tabs' : 'Tabs'" v-if="tabPosition === 'bottom' || tabPosition === 'right'">
12
+ <div class="pankow-tabview-tab" role="tab" :tabindex="active === tab ? 0 : -1" :aria-selected="active === tab ? 'true' : 'false'" :aria-controls="'tabpanel-'+tab" :class="{'active': active === tab }" v-for="tab in Object.keys(tabs)" @click="open(tab)" @keydown="onTabKeydown($event, tab)">{{ tabs[tab] }}</div>
13
13
  </div>
14
14
  </div>
15
15
  </template>
@@ -43,6 +43,42 @@ function open(key) {
43
43
  emit('changed', key);
44
44
  }
45
45
 
46
+ function onTabKeydown(event, currentKey) {
47
+ const keys = Object.keys(props.tabs);
48
+ const index = keys.indexOf(currentKey);
49
+ const isVertical = props.tabPosition === 'left' || props.tabPosition === 'right';
50
+
51
+ if (isVertical) {
52
+ if (event.key === 'ArrowDown') {
53
+ event.preventDefault();
54
+ const next = index + 1 >= keys.length ? 0 : index + 1;
55
+ open(keys[next]);
56
+ } else if (event.key === 'ArrowUp') {
57
+ event.preventDefault();
58
+ const prev = index <= 0 ? keys.length - 1 : index - 1;
59
+ open(keys[prev]);
60
+ }
61
+ } else {
62
+ if (event.key === 'ArrowRight') {
63
+ event.preventDefault();
64
+ const next = index + 1 >= keys.length ? 0 : index + 1;
65
+ open(keys[next]);
66
+ } else if (event.key === 'ArrowLeft') {
67
+ event.preventDefault();
68
+ const prev = index <= 0 ? keys.length - 1 : index - 1;
69
+ open(keys[prev]);
70
+ }
71
+ }
72
+
73
+ if (event.key === 'Home') {
74
+ event.preventDefault();
75
+ open(keys[0]);
76
+ } else if (event.key === 'End') {
77
+ event.preventDefault();
78
+ open(keys[keys.length - 1]);
79
+ }
80
+ }
81
+
46
82
  </script>
47
83
 
48
84
  <style>
@@ -153,4 +189,4 @@ function open(key) {
153
189
  margin-right: 10px;
154
190
  }
155
191
 
156
- </style>
192
+ </style>
@@ -103,7 +103,13 @@ onMounted(() => {
103
103
  class="pankow-table-header-column"
104
104
  :class="{ 'pankow-table-header-column-sortable': columns[column].sort, 'pankow-table-cell-hide-mobile': columns[column].hideMobile }"
105
105
  :style="{ 'text-align': columns[column].align || null, width: typeof columns[column].width === 'string' ? columns[column].width : 'auto' }"
106
+ :aria-sort="sortBy === column ? (sortOrder === SORT_ORDER.ASC ? 'ascending' : 'descending') : 'none'"
107
+ scope="col"
108
+ :tabindex="columns[column].sort ? 0 : null"
109
+ :aria-label="columns[column].sort ? 'Sort by ' + columns[column].label : columns[column].label"
106
110
  @click="toggleSort(column)"
111
+ @keydown.enter="toggleSort(column)"
112
+ @keydown.space.prevent="toggleSort(column)"
107
113
  >
108
114
  <Icon v-if="columns[column].icon" :icon="columns[column].icon" />
109
115
  {{ columns[column].label }}
@@ -41,7 +41,7 @@ function onRemoveIndex(index) {
41
41
 
42
42
  <template>
43
43
  <div class="pankow-tag-input">
44
- <div class="pankow-tag-input-tag" v-for="(tag, index) in model" :key="index">{{ tag }} <i class="fa-solid fa-xmark pankow-tag-input-tag-remove" @click="onRemoveIndex(index)"></i></div>
44
+ <div class="pankow-tag-input-tag" v-for="(tag, index) in model" :key="index">{{ tag }} <span tabindex="0" role="button" :aria-label="'Remove ' + tag" class="pankow-tag-input-tag-remove" @click="onRemoveIndex(index)" @keydown.enter="onRemoveIndex(index)" @keydown.space.prevent="onRemoveIndex(index)"><i class="fa-solid fa-xmark"></i></span></div>
45
45
  <input class="pankow-tag-input-input" :id="id" :placeholder="placeholder" :autofocus="$attrs['autofocus']" @keydown="onKey($event)" @blur="onBlur"/>
46
46
  </div>
47
47
  </template>
@@ -6,6 +6,7 @@ import { useTemplateRef } from 'vue';
6
6
 
7
7
  const model = defineModel();
8
8
  const props = defineProps({
9
+ id: String,
9
10
  placeholder: String,
10
11
  readonly: {
11
12
  type: Boolean,
@@ -23,7 +24,7 @@ defineExpose({ focus });
23
24
  </script>
24
25
 
25
26
  <template>
26
- <input ref="inputElement" class="pankow-text-input" type="text" :placeholder="placeholder" :readonly="readonly" v-model.trim="model"/>
27
+ <input :id="id" ref="inputElement" class="pankow-text-input" type="text" :placeholder="placeholder" :readonly="readonly" v-model.trim="model"/>
27
28
  </template>
28
29
 
29
30
  <style>
@@ -4,6 +4,7 @@
4
4
 
5
5
  const model = defineModel();
6
6
  const props = defineProps({
7
+ id: String,
7
8
  placeholder: String,
8
9
  readonly: {
9
10
  type: Boolean,
@@ -14,7 +15,7 @@ const props = defineProps({
14
15
  </script>
15
16
 
16
17
  <template>
17
- <input class="pankow-text-input" type="text" :placeholder="placeholder" :readonly="readonly" v-model="model"/>
18
+ <input :id="id" class="pankow-text-input" type="text" :placeholder="placeholder" :readonly="readonly" v-model="model"/>
18
19
  </template>
19
20
 
20
21
  <style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="tree-view">
2
+ <div class="tree-view" role="tree">
3
3
  <TreeViewNode
4
4
  :entry="rootEntry"
5
5
  :list-files="listFiles"
@@ -4,7 +4,13 @@
4
4
  class="tree-view-node-row"
5
5
  :class="{ 'active': isActive, 'drop-target': dropTargetActive }"
6
6
  :style="{ paddingLeft: (props.depth * 16 + 4) + 'px' }"
7
+ tabindex="0"
8
+ role="treeitem"
9
+ :aria-expanded="expanded"
10
+ :aria-selected="isActive"
7
11
  @click="onRowClick"
12
+ @keydown.enter="onRowClick"
13
+ @keydown.space.prevent="onRowClick"
8
14
  @dragover.stop.prevent="onDragOver"
9
15
  @dragenter.stop.prevent="onDragEnter"
10
16
  @dragleave="onDragLeave"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "4.4.0",
4
+ "version": "4.4.2",
5
5
  "description": "Vue 3 UI component library for Cloudron apps",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  <template #body>
14
14
  <div class="content">
15
15
  <div>
16
- <img class="preview-icon" :src="entry.previewUrl" />
16
+ <img class="preview-icon" :src="entry.previewUrl" :alt="entry.fileName" />
17
17
  <h1>{{ entry.fileName }}</h1>
18
18
  <Button icon="fa-solid fa-download" :href="entry.downloadFileUrl">Download</Button>
19
19
  </div>