quasar-ui-danx 0.4.1 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. package/dist/danx.es.js +7234 -6741
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +11 -5
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -1
  7. package/src/components/ActionTable/ActionTable.vue +31 -43
  8. package/src/components/ActionTable/Columns/ActionTableColumn.vue +19 -18
  9. package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +15 -14
  10. package/src/components/ActionTable/Filters/{FilterFieldList.vue → FilterList.vue} +26 -26
  11. package/src/components/ActionTable/Filters/FilterableField.vue +28 -31
  12. package/src/components/ActionTable/Filters/index.ts +2 -2
  13. package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +71 -0
  14. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +8 -13
  15. package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +34 -33
  16. package/src/components/ActionTable/Form/Fields/MultiFileField.vue +48 -44
  17. package/src/components/ActionTable/Form/Fields/NumberField.vue +60 -59
  18. package/src/components/ActionTable/Form/Fields/SelectField.vue +124 -138
  19. package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +28 -33
  20. package/src/components/ActionTable/Form/Fields/SingleFileField.vue +15 -15
  21. package/src/components/ActionTable/Form/Fields/SliderNumberField.vue +45 -0
  22. package/src/components/ActionTable/Form/Fields/TextField.vue +47 -66
  23. package/src/components/ActionTable/Form/Fields/index.ts +2 -0
  24. package/src/components/ActionTable/Form/RenderedForm.vue +50 -13
  25. package/src/components/ActionTable/Form/Utilities/MaxLengthCounter.vue +17 -0
  26. package/src/components/ActionTable/Form/Utilities/index.ts +1 -0
  27. package/src/components/ActionTable/Form/index.ts +1 -0
  28. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +22 -16
  29. package/src/components/ActionTable/Toolbars/ActionToolbar.vue +11 -11
  30. package/src/components/ActionTable/listControls.ts +104 -166
  31. package/src/components/ActionTable/listHelpers.ts +2 -3
  32. package/src/components/ActionTable/tableColumns.ts +53 -77
  33. package/src/components/AuditHistory/AuditHistoryItemValue.vue +26 -26
  34. package/src/components/PanelsDrawer/PanelsDrawer.vue +17 -4
  35. package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +6 -11
  36. package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +20 -20
  37. package/src/components/Utility/Dialogs/ConfirmActionDialog.vue +39 -0
  38. package/src/components/Utility/Dialogs/ConfirmDialog.vue +57 -117
  39. package/src/components/Utility/Dialogs/DialogLayout.vue +77 -0
  40. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +42 -36
  41. package/src/components/Utility/Dialogs/InfoDialog.vue +40 -80
  42. package/src/components/Utility/Dialogs/index.ts +1 -0
  43. package/src/components/Utility/Files/FilePreview.vue +76 -73
  44. package/src/components/Utility/Layouts/ContentDrawer.vue +24 -31
  45. package/src/components/Utility/Tools/ActionVnode.vue +3 -3
  46. package/src/components/Utility/Tools/RenderVnode.vue +20 -11
  47. package/src/components/Utility/Transitions/MaxHeightTransition.vue +26 -0
  48. package/src/components/Utility/Transitions/index.ts +1 -0
  49. package/src/config/index.ts +36 -31
  50. package/src/helpers/FileUpload.ts +295 -297
  51. package/src/helpers/FlashMessages.ts +80 -71
  52. package/src/helpers/actions.ts +102 -82
  53. package/src/helpers/download.ts +189 -189
  54. package/src/helpers/downloadPdf.ts +55 -52
  55. package/src/helpers/formats.ts +151 -109
  56. package/src/helpers/index.ts +2 -0
  57. package/src/helpers/multiFileUpload.ts +72 -58
  58. package/src/helpers/objectStore.ts +52 -0
  59. package/src/helpers/request.ts +70 -51
  60. package/src/helpers/routes.ts +29 -0
  61. package/src/helpers/storage.ts +7 -3
  62. package/src/helpers/utils.ts +47 -29
  63. package/src/styles/quasar-reset.scss +94 -68
  64. package/src/styles/themes/danx/dialogs.scss +47 -0
  65. package/src/styles/themes/danx/forms.scss +18 -0
  66. package/src/styles/themes/danx/index.scss +4 -0
  67. package/src/types/actions.d.ts +43 -0
  68. package/src/types/config.d.ts +15 -0
  69. package/src/types/controls.d.ts +99 -0
  70. package/src/types/dialogs.d.ts +32 -0
  71. package/src/types/fields.d.ts +20 -0
  72. package/src/types/files.d.ts +54 -0
  73. package/src/types/formats.d.ts +4 -0
  74. package/src/{components/ActionTable/Form/form.d.ts → types/forms.d.ts} +6 -0
  75. package/src/types/index.d.ts +12 -0
  76. package/src/types/requests.d.ts +13 -0
  77. package/src/types/shared.d.ts +15 -0
  78. package/src/types/tables.d.ts +27 -0
  79. package/types/index.d.ts +1 -1
  80. /package/src/components/ActionTable/Filters/{FilterFieldItem.vue → FilterItem.vue} +0 -0
@@ -6,6 +6,7 @@
6
6
  content-class="h-full"
7
7
  class="dx-panels-drawer"
8
8
  title=""
9
+ no-route-dismiss
9
10
  @update:show="$emit('close')"
10
11
  >
11
12
  <div class="flex flex-col flex-nowrap h-full">
@@ -33,15 +34,17 @@
33
34
  <div class="dx-panels-drawer-body flex-grow overflow-hidden h-full">
34
35
  <div class="flex items-stretch flex-nowrap h-full">
35
36
  <PanelsDrawerTabs
37
+ :key="'pd-tabs:' + activeItem.id"
36
38
  v-model="activePanel"
37
39
  :class="tabsClass"
38
40
  :panels="panels"
39
41
  @update:model-value="$emit('update:model-value', $event)"
40
42
  />
41
43
  <PanelsDrawerPanels
44
+ :key="'pd-panels:' + activeItem.id"
42
45
  :panels="panels"
43
46
  :active-panel="activePanel"
44
- :class="panelsClass"
47
+ :class="activePanelOptions?.class || panelsClass"
45
48
  />
46
49
  <div
47
50
  v-if="$slots['right-sidebar']"
@@ -55,16 +58,17 @@
55
58
  </ContentDrawer>
56
59
  </template>
57
60
  <script setup lang="ts">
58
- import { ref, watch } from "vue";
61
+ import { computed, onMounted, ref, watch } from "vue";
59
62
  import { XIcon as CloseIcon } from "../../svg";
60
- import { ActionPanel } from "../ActionTable";
63
+ import { ActionPanel, ActionTargetItem } from "../../types";
61
64
  import { ContentDrawer } from "../Utility";
62
65
  import PanelsDrawerPanels from "./PanelsDrawerPanels";
63
66
  import PanelsDrawerTabs from "./PanelsDrawerTabs";
64
67
 
65
68
  export interface Props {
66
69
  title?: string,
67
- modelValue?: string,
70
+ modelValue?: string | number,
71
+ activeItem?: ActionTargetItem;
68
72
  tabsClass?: string | object,
69
73
  panelsClass?: string | object,
70
74
  panels: ActionPanel[]
@@ -74,10 +78,19 @@ defineEmits(["update:model-value", "close"]);
74
78
  const props = withDefaults(defineProps<Props>(), {
75
79
  title: "",
76
80
  modelValue: null,
81
+ activeItem: null,
77
82
  tabsClass: "w-[13.5rem]",
78
83
  panelsClass: "w-[35.5rem]"
79
84
  });
80
85
 
81
86
  const activePanel = ref(props.modelValue);
87
+ const activePanelOptions = computed(() => props.panels.find((panel) => panel.name === activePanel.value));
82
88
  watch(() => props.modelValue, (value) => activePanel.value = value);
89
+
90
+ onMounted(() => {
91
+ // Resolve the default panel if a panel has not been selected
92
+ if (!activePanel.value && props.panels.length) {
93
+ activePanel.value = props.panels[0].name;
94
+ }
95
+ });
83
96
  </script>
@@ -16,17 +16,12 @@
16
16
  </QTabPanels>
17
17
  </template>
18
18
 
19
- <script setup>
19
+ <script setup lang="ts">
20
+ import { ActionPanel } from "../../types";
20
21
  import { RenderVnode } from "../Utility";
21
22
 
22
- defineProps({
23
- activePanel: {
24
- type: String,
25
- default: null
26
- },
27
- panels: {
28
- type: Array,
29
- required: true
30
- }
31
- });
23
+ defineProps<{
24
+ activePanel?: string | number,
25
+ panels: ActionPanel[]
26
+ }>();
32
27
  </script>
@@ -28,37 +28,37 @@
28
28
  </template>
29
29
  </QTabs>
30
30
  </template>
31
- <script setup>
31
+ <script setup lang="ts">
32
32
  import { QTab } from "quasar";
33
+ import { ActionPanel } from "../../types";
33
34
  import { RenderVnode } from "../Utility";
34
35
 
35
36
  defineEmits(["update:model-value"]);
36
- defineProps({
37
- modelValue: {
38
- type: String,
39
- default: "general"
40
- },
41
- panels: {
42
- type: Array,
43
- required: true
44
- }
37
+
38
+ interface Props {
39
+ modelValue?: string | number;
40
+ panels: ActionPanel[];
41
+ }
42
+
43
+ withDefaults(defineProps<Props>(), {
44
+ modelValue: "general"
45
45
  });
46
46
  </script>
47
47
 
48
48
  <style lang="scss" module="cls">
49
49
  .panel-tabs {
50
- @apply p-4 h-auto;
50
+ @apply p-4 h-auto;
51
51
 
52
- :global(.q-tab) {
53
- justify-content: start !important;
52
+ :global(.q-tab) {
53
+ justify-content: start !important;
54
54
 
55
- :global(.q-focus-helper), :global(.q-tab__indicator) {
56
- display: none;
57
- }
55
+ :global(.q-focus-helper), :global(.q-tab__indicator) {
56
+ display: none;
57
+ }
58
58
 
59
- :global(.q-tab__content) {
60
- @apply p-0;
61
- }
62
- }
59
+ :global(.q-tab__content) {
60
+ @apply p-0;
61
+ }
62
+ }
63
63
  }
64
64
  </style>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <ConfirmDialog
3
+ class="dx-confirm-action-dialog"
4
+ v-bind="props"
5
+ :confirm-text="confirmText || computedConfirmText"
6
+ :title="title || computedTitle"
7
+ :content="content || computedContentText"
8
+ @confirm="$emit('confirm')"
9
+ @close="$emit('close')"
10
+ >
11
+ <template
12
+ v-for="slotName in childSlots"
13
+ #[slotName]
14
+ >
15
+ <slot :name="slotName" />
16
+ </template>
17
+ <slot />
18
+ </ConfirmDialog>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed } from "vue";
23
+ import { fNameOrCount } from "../../../helpers";
24
+ import { ConfirmActionDialogProps } from "../../../types";
25
+ import { default as ConfirmDialog } from "./ConfirmDialog";
26
+
27
+ defineEmits(["confirm", "close"]);
28
+
29
+ const props = withDefaults(defineProps<ConfirmActionDialogProps>(), {
30
+ message: "Are you sure you want to"
31
+ });
32
+
33
+ const nameLabel = computed(() => fNameOrCount(props.target, props.label || props.action));
34
+ const computedTitle = computed(() => `Confirm ${props.action}`);
35
+ const computedConfirmText = computed(() => `${props.action}`);
36
+ const computedContentText = computed(() => `${props.message} ${props.action.toLowerCase()}${nameLabel.value ? " " + nameLabel.value : ""}?`);
37
+
38
+ const childSlots = computed(() => ["title", "subtitle", "default", "toolbar", "actions"]);
39
+ </script>
@@ -1,135 +1,75 @@
1
1
  <template>
2
- <QDialog
3
- :full-height="fullHeight"
4
- :full-width="fullWidth"
5
- :model-value="!!modelValue"
6
- :no-backdrop-dismiss="!backdropDismiss"
7
- :maximized="maximized"
8
- @update:model-value="onClose"
2
+ <DialogLayout
3
+ class="dx-confirm-dialog"
4
+ v-bind="layoutProps"
5
+ @close="onClose"
9
6
  >
10
- <QCard class="flex flex-col flex-nowrap">
11
- <QCardSection
12
- v-if="title || $slots.title"
13
- class="pl-6 pr-10 border-b border-gray-300"
14
- >
15
- <h3
16
- class="font-normal flex items-center"
17
- :class="titleClass"
18
- >
19
- <slot name="title">
20
- {{ title }}
21
- </slot>
22
- </h3>
23
- <div
24
- v-if="subtitle"
25
- class="mt-1 text-sm"
7
+ <template
8
+ v-for="slotName in childSlots"
9
+ #[slotName]
10
+ >
11
+ <slot :name="slotName" />
12
+ </template>
13
+ <slot />
14
+
15
+ <template #actions>
16
+ <div class="dx-dialog-button-cancel">
17
+ <QBtn
18
+ :label="cancelText"
19
+ class="dx-dialog-button"
20
+ @click="onClose"
26
21
  >
27
- {{ subtitle }}
28
- </div>
29
- </QCardSection>
30
- <QCardSection v-if="$slots.toolbar">
31
- <slot name="toolbar" />
32
- </QCardSection>
33
- <QCardSection
34
- v-if="content || $slots.default"
35
- class="px-6 bg-gray-100 flex-grow max-h-full overflow-y-auto"
36
- :class="contentClass"
37
- >
38
- <slot>{{ content }}</slot>
39
- </QCardSection>
40
- <div class="flex px-6 py-4 border-t border-gray-300">
41
- <div class="flex-grow">
42
- <QBtn
43
- :label="cancelText"
44
- class="action-btn btn-white-gray"
45
- @click="onClose"
46
- >
47
- <slot name="cancel-text" />
48
- </QBtn>
49
- </div>
50
- <slot name="actions" />
51
- <div v-if="!hideConfirm">
52
- <QBtn
53
- :label="$slots['confirm-text'] ? '' : confirmText"
54
- class="action-btn ml-4"
55
- :class="confirmClass"
56
- :loading="isSaving"
57
- :disable="disabled"
58
- data-testid="confirm-button"
59
- @click="onConfirm"
60
- >
61
- <slot name="confirm-text" />
62
- </QBtn>
63
- </div>
22
+ <slot name="cancel-text" />
23
+ </QBtn>
64
24
  </div>
65
- <a
66
- class="absolute top-0 right-0 p-4 text-black"
67
- @click="onClose"
25
+ <slot name="actions" />
26
+ <div
27
+ v-if="!hideConfirm"
28
+ class="dx-dialog-button-confirm"
68
29
  >
69
- <CloseIcon class="w-5" />
70
- </a>
71
- </QCard>
72
- </QDialog>
30
+ <QBtn
31
+ :label="$slots['confirm-text'] ? '' : confirmText"
32
+ class="dx-dialog-button"
33
+ :class="confirmClass"
34
+ :loading="isSaving"
35
+ :disable="disabled"
36
+ data-testid="confirm-button"
37
+ @click="onConfirm"
38
+ >
39
+ <slot name="confirm-text" />
40
+ </QBtn>
41
+ </div>
42
+ </template>
43
+ </DialogLayout>
73
44
  </template>
74
45
 
75
- <script setup>
76
- import { XIcon as CloseIcon } from "@heroicons/vue/outline";
46
+ <script setup lang="ts">
47
+ import { computed } from "vue";
48
+ import { ConfirmDialogProps } from "../../../types";
49
+ import DialogLayout from "./DialogLayout";
77
50
 
78
51
  const emit = defineEmits(["update:model-value", "confirm", "close"]);
79
- const props = defineProps({
80
- modelValue: { type: [String, Boolean, Object], default: true },
81
- title: {
82
- type: String,
83
- default: ""
84
- },
85
- titleClass: {
86
- type: String,
87
- default: ""
88
- },
89
- subtitle: {
90
- type: String,
91
- default: ""
92
- },
93
- content: {
94
- type: String,
95
- default: ""
96
- },
97
- backdropDismiss: Boolean,
98
- maximized: Boolean,
99
- fullWidth: Boolean,
100
- fullHeight: Boolean,
101
- disabled: Boolean,
102
- isSaving: Boolean,
103
- closeOnConfirm: Boolean,
104
- hideConfirm: Boolean,
105
- confirmText: {
106
- type: String,
107
- default: "Confirm"
108
- },
109
- cancelText: {
110
- type: String,
111
- default: "Cancel"
112
- },
113
- confirmClass: {
114
- type: String,
115
- default: "bg-blue-600 text-white"
116
- },
117
- contentClass: {
118
- type: String,
119
- default: ""
120
- }
52
+
53
+ const props = withDefaults(defineProps<ConfirmDialogProps>(), {
54
+ confirmText: "Confirm",
55
+ cancelText: "Cancel",
56
+ confirmClass: "",
57
+ contentClass: ""
121
58
  });
122
59
 
60
+ const layoutProps = computed(() => ({ ...props, disabled: undefined }));
61
+ const childSlots = computed(() => ["title", "subtitle", "toolbar"]);
62
+
123
63
  function onConfirm() {
124
- emit("confirm");
64
+ emit("confirm");
125
65
 
126
- if (props.closeOnConfirm) {
127
- emit("close");
128
- }
66
+ if (props.closeOnConfirm) {
67
+ emit("close");
68
+ }
129
69
  }
130
70
 
131
71
  function onClose() {
132
- emit("update:model-value", false);
133
- emit("close");
72
+ emit("update:model-value", false);
73
+ emit("close");
134
74
  }
135
75
  </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <QDialog
3
+ class="dx-dialog"
4
+ :full-height="fullHeight"
5
+ :full-width="fullWidth"
6
+ :model-value="true"
7
+ :no-backdrop-dismiss="!backdropDismiss"
8
+ :maximized="maximized"
9
+ @update:model-value="onClose"
10
+ >
11
+ <QCard class="dx-dialog-card flex flex-col flex-nowrap">
12
+ <QCardSection
13
+ class="dx-dialog-header flex items-center"
14
+ >
15
+ <div class="flex-grow">
16
+ <h3
17
+ v-if="title || $slots.title"
18
+ class="dx-dialog-title flex items-center"
19
+ :class="titleClass"
20
+ >
21
+ <slot name="title">
22
+ {{ title }}
23
+ </slot>
24
+ </h3>
25
+ <div
26
+ v-if="subtitle || $slots.subtitle"
27
+ class="dx-dialog-subtitle"
28
+ >
29
+ <slot name="subtitle">
30
+ {{ subtitle }}
31
+ </slot>
32
+ </div>
33
+ </div>
34
+ <div>
35
+ <div
36
+ class="dx-close-button cursor-pointer"
37
+ @click="onClose"
38
+ >
39
+ <CloseIcon class="w-5" />
40
+ </div>
41
+ </div>
42
+ </QCardSection>
43
+ <QCardSection v-if="$slots.toolbar">
44
+ <slot name="toolbar" />
45
+ </QCardSection>
46
+ <QCardSection
47
+ v-if="content || $slots.default"
48
+ class="dx-dialog-content flex-grow max-h-full overflow-y-auto"
49
+ :class="contentClass"
50
+ >
51
+ <slot>{{ content }}</slot>
52
+ </QCardSection>
53
+ <div class="flex dx-dialog-actions">
54
+ <slot name="actions" />
55
+ </div>
56
+ </QCard>
57
+ </QDialog>
58
+ </template>
59
+
60
+ <script setup lang="ts">
61
+ import { XIcon as CloseIcon } from "@heroicons/vue/outline";
62
+ import { DialogLayoutProps } from "../../../types";
63
+
64
+ const emit = defineEmits(["close"]);
65
+
66
+ withDefaults(defineProps<DialogLayoutProps>(), {
67
+ title: "",
68
+ titleClass: "",
69
+ subtitle: "",
70
+ content: "",
71
+ contentClass: ""
72
+ });
73
+
74
+ function onClose() {
75
+ emit("close");
76
+ }
77
+ </script>
@@ -22,6 +22,7 @@
22
22
  :key="'file-' + file.id"
23
23
  :name="file.id"
24
24
  :img-src="getThumbUrl(file)"
25
+ class="bg-black"
25
26
  >
26
27
  <div :class="cls['slide-image']">
27
28
  <template v-if="isVideo(file)">
@@ -30,7 +31,7 @@
30
31
  controls
31
32
  >
32
33
  <source
33
- :src="file.url + '#t=0.1'"
34
+ :src="getPreviewUrl(file) + '#t=0.1'"
34
35
  :type="file.mime"
35
36
  >
36
37
  </video>
@@ -38,7 +39,7 @@
38
39
  <img
39
40
  v-else
40
41
  :alt="file.filename"
41
- :src="file.url"
42
+ :src="getPreviewUrl(file)"
42
43
  >
43
44
  </div>
44
45
  </QCarouselSlide>
@@ -56,54 +57,59 @@ import { XIcon as CloseIcon } from "../../../svg";
56
57
 
57
58
  defineEmits(["close"]);
58
59
  const props = defineProps({
59
- files: {
60
- type: Array,
61
- default: () => []
62
- },
63
- defaultSlide: {
64
- type: String,
65
- default: ""
66
- }
60
+ files: {
61
+ type: Array,
62
+ default: () => []
63
+ },
64
+ defaultSlide: {
65
+ type: String,
66
+ default: ""
67
+ }
67
68
  });
68
69
 
69
70
  const carousel = ref(null);
70
71
  const currentSlide = ref(props.defaultSlide);
71
72
  function isVideo(file) {
72
- return file.mime?.startsWith("video");
73
+ return file.mime?.startsWith("video");
73
74
  }
75
+
76
+ function getPreviewUrl(file) {
77
+ return file.transcodes?.compress?.url || file.blobUrl || file.url;
78
+ }
79
+
74
80
  function getThumbUrl(file) {
75
- if (file.thumb) {
76
- return file.thumb.url;
77
- } else if (isVideo(file)) {
78
- // Base64 encode a PlayIcon for the placeholder image
79
- return `data:image/svg+xml;base64,${btoa(
80
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>`
81
- )}`;
82
- } else {
83
- return file.url;
84
- }
81
+ if (file.thumb) {
82
+ return file.thumb.url;
83
+ } else if (isVideo(file)) {
84
+ // Base64 encode a PlayIcon for the placeholder image
85
+ return `data:image/svg+xml;base64,${btoa(
86
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>`
87
+ )}`;
88
+ } else {
89
+ return getPreviewUrl(file);
90
+ }
85
91
  }
86
92
  </script>
87
93
  <style module="cls" lang="scss">
88
94
  .slide-image {
89
- width: 100%;
90
- height: 100%;
91
- background: black;
92
- display: flex;
93
- justify-content: center;
94
- align-items: center;
95
+ width: 100%;
96
+ height: 100%;
97
+ background: black;
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
95
101
 
96
- img {
97
- max-height: 100%;
98
- max-width: 100%;
99
- object-fit: contain;
100
- }
102
+ img {
103
+ max-height: 100%;
104
+ max-width: 100%;
105
+ object-fit: contain;
106
+ }
101
107
  }
102
108
 
103
109
  .carousel {
104
- :deep(.q-carousel__navigation--bottom) {
105
- position: relative;
106
- bottom: 8em;
107
- }
110
+ :deep(.q-carousel__navigation--bottom) {
111
+ position: relative;
112
+ bottom: 8em;
113
+ }
108
114
  }
109
115
  </style>