@geode/opengeodeweb-front 10.14.1 → 10.14.2-rc.1

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/.oxlintrc.json CHANGED
@@ -92,6 +92,12 @@
92
92
  "ignore": [-1, 0, 1, 2, 3, 4],
93
93
  "ignoreArrayIndexes": true
94
94
  }
95
+ ],
96
+ "eslint/no-underscore-dangle": [
97
+ "error",
98
+ {
99
+ "allow": ["_data", "__dirname", "__VEASE_UTILS__", "__VEASE_STORES__", "__VEASE_SCHEMAS__"]
100
+ }
95
101
  ]
96
102
  },
97
103
  "overrides": [
@@ -117,6 +123,7 @@
117
123
  "rules": {
118
124
  "vitest/require-hook": "off",
119
125
  "vitest/no-hooks": "off",
126
+ "jest/no-hooks": "off",
120
127
  "vitest/no-importing-vitest-globals": "off",
121
128
  "max-lines-per-function": "off",
122
129
  "max-statements": "off",
@@ -124,8 +131,11 @@
124
131
  "vitest/prefer-to-be-falsy": "off",
125
132
  "vitest/require-test-timeout": "warn",
126
133
  "vitest/prefer-importing-vitest-globals": "off",
127
- "jest/consistent-test-it": ["error", { "fn": "test", "withinDescribe": "test" }],
128
- "vitest/prefer-spy-on": "off"
134
+ "jest/consistent-test-it": "off",
135
+ "vitest/consistent-test-it": ["error", { "fn": "test", "withinDescribe": "test" }],
136
+ "vitest/prefer-spy-on": "off",
137
+ "vitest/prefer-expect-assertions": "off",
138
+ "jest/prefer-expect-assertions": "off"
129
139
  }
130
140
  },
131
141
  {
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  const { modelValue, label } = defineProps({
3
3
  modelValue: { type: String, default: "" },
4
- label: { type: String, default: "Search..." },
4
+ label: { type: String, default: "" },
5
5
  });
6
6
 
7
7
  const emit = defineEmits(["update:modelValue"]);
@@ -4,7 +4,6 @@ import { useMenuStore } from "@ogw_front/stores/menu";
4
4
 
5
5
  const RADIUS = 80;
6
6
  const MARGIN_OFFSET = 40;
7
- const Z_INDEX_MENU = 1000;
8
7
  const Z_INDEX_ACTIVE_ITEM = 10;
9
8
  const Z_INDEX_BASE_ITEM = 1;
10
9
  const FULL_ANGLE = 360;
@@ -34,7 +33,7 @@ const menuX = ref(x);
34
33
  const menuY = ref(y);
35
34
 
36
35
  watch(
37
- () => [x, y],
36
+ () => [x, y, containerWidth, containerHeight],
38
37
  ([newX, newY]) => {
39
38
  const { x: clampedX, y: clampedY } = clampPosition(newX, newY);
40
39
  menuX.value = clampedX;
@@ -110,7 +109,6 @@ function getMenuStyle() {
110
109
  position: "fixed",
111
110
  left: `${menuStore.containerLeft + menuX.value - RADIUS}px`,
112
111
  top: `${menuStore.containerTop + menuY.value - RADIUS}px`,
113
- zIndex: Z_INDEX_MENU,
114
112
  };
115
113
  }
116
114
 
@@ -29,7 +29,7 @@ const optionsStyle = computed(() => {
29
29
  if (!is_active.value || !optionsHeight.value) {
30
30
  return {};
31
31
  }
32
- const angle = (itemProps.index / itemProps.totalItems) * 2 * Math.PI;
32
+ const angle = (index / itemProps.totalItems) * 2 * Math.PI;
33
33
  const radius = RADIUS;
34
34
  const absoluteButtonY = menuStore.menuY + Math.sin(angle) * radius;
35
35
  const height = optionsHeight.value;
@@ -89,7 +89,6 @@ function toggleOptions() {
89
89
  >
90
90
  <GlassCard
91
91
  @click.stop
92
- :title="tooltip"
93
92
  width="320"
94
93
  :max-height="maxCardHeight"
95
94
  :ripple="false"
@@ -98,6 +97,7 @@ function toggleOptions() {
98
97
  class="elevation-24"
99
98
  style="overflow: hidden; display: flex; flex-direction: column"
100
99
  >
100
+ <v-card-title>{{ tooltip }}</v-card-title>
101
101
  <v-card-text class="pa-5" style="overflow-y: auto; flex: 1; min-height: 0">
102
102
  <slot name="options" />
103
103
  </v-card-text>
@@ -0,0 +1,189 @@
1
+ <script setup>
2
+ import StickyHeader from "@ogw_front/components/Viewer/ObjectTree/Base/StickyHeader.vue";
3
+ import TreeRow from "@ogw_front/components/Viewer/ObjectTree/Base/TreeRow.vue";
4
+ import { useTreeKeyboardNav } from "@ogw_front/composables/tree_keyboard_nav";
5
+ import { useTreeScroll } from "@ogw_front/composables/tree_scroll";
6
+ import { useVirtualTree } from "@ogw_front/composables/virtual_tree";
7
+
8
+ const { items, opened, selected, scrollTop, options } = defineProps({
9
+ items: { type: Array, required: true },
10
+ opened: { type: Array, required: false, default: () => [] },
11
+ selected: { type: Array, required: false, default: () => [] },
12
+ scrollTop: { type: Number, required: false, default: 0 },
13
+ options: { type: Object, required: false, default: () => ({}) },
14
+ });
15
+
16
+ const treeWrapper = ref(undefined);
17
+
18
+ const emit = defineEmits([
19
+ "update:opened",
20
+ "update:selected",
21
+ "click:item",
22
+ "update:scrollTop",
23
+ "hover:enter",
24
+ "hover:leave",
25
+ ]);
26
+
27
+ const {
28
+ actualItemProps,
29
+ actualSelection,
30
+ displayItems,
31
+ toggleOpen,
32
+ toggleSelect,
33
+ isSelected,
34
+ getIndeterminate,
35
+ } = useVirtualTree(
36
+ computed(() => ({
37
+ items,
38
+ opened,
39
+ selected,
40
+ ...options,
41
+ })),
42
+ emit,
43
+ );
44
+
45
+ const { virtualScrollRef, stickyHeader, handleScroll, scrollToIndex } = useTreeScroll(
46
+ computed(() => ({ scrollTop })),
47
+ emit,
48
+ displayItems,
49
+ actualItemProps,
50
+ );
51
+
52
+ function handleItemClick(item, index) {
53
+ if (index !== undefined) {
54
+ focusedIndex.value = index;
55
+ }
56
+ if (item.isLeaf) {
57
+ toggleSelect(item.raw);
58
+ emit("click:item", item.raw);
59
+ } else {
60
+ toggleOpen(item.raw);
61
+ }
62
+ }
63
+
64
+ const { focusedIndex, handleKeyDown } = useTreeKeyboardNav(
65
+ displayItems,
66
+ emit,
67
+ scrollToIndex,
68
+ toggleOpen,
69
+ handleItemClick,
70
+ );
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ ref="treeWrapper"
76
+ class="common-tree-view-wrapper"
77
+ tabindex="0"
78
+ @keydown="handleKeyDown"
79
+ @mousedown="treeWrapper.focus()"
80
+ >
81
+ <StickyHeader
82
+ v-if="stickyHeader"
83
+ :item="stickyHeader"
84
+ :item-props="actualItemProps"
85
+ :selection="actualSelection"
86
+ :is-selected="isSelected"
87
+ :get-indeterminate="getIndeterminate"
88
+ @toggle-open="toggleOpen"
89
+ @toggle-select="toggleSelect"
90
+ >
91
+ <template #title="slotProps">
92
+ <slot name="title" v-bind="slotProps" />
93
+ </template>
94
+ </StickyHeader>
95
+
96
+ <v-virtual-scroll
97
+ ref="virtualScrollRef"
98
+ :items="displayItems"
99
+ :item-height="actualItemProps.height"
100
+ class="common-tree-view"
101
+ @scroll="handleScroll"
102
+ >
103
+ <template #default="{ item, index }">
104
+ <v-list-item
105
+ :class="[
106
+ 'tree-row-wrapper',
107
+ { 'leaf-row': item.isLeaf, 'is-focused': focusedIndex === index },
108
+ ]"
109
+ class="pa-0"
110
+ tabindex="-1"
111
+ @mousedown.prevent
112
+ @click="
113
+ handleItemClick(item, index);
114
+ treeWrapper.focus();
115
+ "
116
+ @mouseenter="emit('hover:enter', { item })"
117
+ @mouseleave="emit('hover:leave', { item })"
118
+ >
119
+ <TreeRow
120
+ :item="item"
121
+ :item-props="actualItemProps"
122
+ :selection="actualSelection"
123
+ :is-selected="isSelected"
124
+ :get-indeterminate="getIndeterminate"
125
+ @toggle-open="toggleOpen"
126
+ @toggle-select="toggleSelect"
127
+ >
128
+ <template #title="slotProps">
129
+ <slot name="title" v-bind="slotProps" />
130
+ </template>
131
+ <template #append="slotProps">
132
+ <slot name="append" v-bind="slotProps" />
133
+ </template>
134
+ </TreeRow>
135
+ </v-list-item>
136
+ </template>
137
+ </v-virtual-scroll>
138
+ </div>
139
+ </template>
140
+
141
+ <style scoped>
142
+ .common-tree-view-wrapper {
143
+ height: 100%;
144
+ position: relative;
145
+ display: flex;
146
+ flex-direction: column;
147
+ min-height: 0;
148
+ }
149
+
150
+ .common-tree-view-wrapper:focus {
151
+ outline: none;
152
+ }
153
+
154
+ .common-tree-view {
155
+ flex-grow: 1;
156
+ min-height: 0;
157
+ overflow-y: auto !important;
158
+ }
159
+
160
+ .v-list-item {
161
+ background-color: transparent !important;
162
+ transition: none !important;
163
+ }
164
+
165
+ .tree-row-wrapper {
166
+ min-height: 44px !important;
167
+ cursor: pointer;
168
+ border-radius: 8px;
169
+ margin: 1px 4px;
170
+ }
171
+
172
+ .tree-row-wrapper.is-focused {
173
+ background-color: rgba(0, 0, 0, 0.08) !important;
174
+ box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.15);
175
+ }
176
+
177
+ .tree-row-wrapper:hover:not(.is-focused) {
178
+ background-color: rgba(0, 0, 0, 0.04) !important;
179
+ }
180
+
181
+ :deep(.v-list-item__content) {
182
+ padding: 0 !important;
183
+ display: block !important;
184
+ }
185
+
186
+ :deep(.v-list-item__overlay) {
187
+ display: none !important;
188
+ }
189
+ </style>
@@ -9,50 +9,136 @@ const { search, sortType, filterOptions, availableFilterOptions } = defineProps(
9
9
  availableFilterOptions: { type: Array, required: true },
10
10
  });
11
11
 
12
- const emit = defineEmits(["update:search", "toggle-sort"]);
12
+ const emit = defineEmits(["update:search", "toggle-sort", "collapse-all"]);
13
+
14
+ const showSearch = ref(false);
15
+
16
+ watch(
17
+ () => showSearch.value,
18
+ (val) => {
19
+ if (!val) {
20
+ emit("update:search", "");
21
+ }
22
+ },
23
+ );
13
24
  </script>
14
25
 
15
26
  <template>
16
- <v-row dense align="center" class="pa-2">
17
- <v-col>
18
- <SearchBar
19
- :model-value="search"
20
- label="Search"
21
- color="black"
22
- base-color="black"
23
- @update:model-value="emit('update:search', $event)"
24
- />
25
- </v-col>
26
- <v-col cols="auto" class="d-flex align-center">
27
- <ActionButton
28
- :tooltip="'Sort by ' + (sortType === 'name' ? 'ID' : 'Name')"
29
- :icon="
30
- sortType === 'name' ? 'mdi-sort-alphabetical-ascending' : 'mdi-sort-numeric-ascending'
31
- "
32
- tooltipLocation="bottom"
33
- @click="emit('toggle-sort')"
34
- />
35
- <v-menu :close-on-content-click="false">
36
- <template #activator="{ props: menuProps }">
27
+ <v-row dense align="center" class="pa-2 py-1">
28
+ <v-col cols="12">
29
+ <div
30
+ class="controls-capsule d-flex align-center rounded-pill px-1 overflow-hidden"
31
+ :class="{ 'is-expanded': showSearch }"
32
+ >
33
+ <ActionButton
34
+ :tooltip="showSearch ? 'Hide search' : 'Show search'"
35
+ icon="mdi-magnify"
36
+ tooltipLocation="bottom"
37
+ variant="text"
38
+ color="black"
39
+ @click="showSearch = !showSearch"
40
+ />
41
+
42
+ <v-expand-x-transition>
43
+ <div v-if="showSearch" class="flex-grow-1 ms-1 text-no-wrap overflow-hidden">
44
+ <SearchBar
45
+ :model-value="search"
46
+ placeholder="Search objects..."
47
+ color="black"
48
+ base-color="black"
49
+ variant="plain"
50
+ density="compact"
51
+ prepend-inner-icon=""
52
+ autofocus
53
+ @update:model-value="emit('update:search', $event)"
54
+ />
55
+ </div>
56
+ </v-expand-x-transition>
57
+
58
+ <div class="d-flex align-center">
59
+ <v-fade-transition>
60
+ <v-divider
61
+ v-if="!showSearch"
62
+ vertical
63
+ inset
64
+ class="mx-1 align-self-center"
65
+ style="height: 20px"
66
+ />
67
+ </v-fade-transition>
37
68
  <ActionButton
38
- tooltip="Filter options"
39
- icon="mdi-filter-variant"
69
+ :tooltip="'Sort by ' + (sortType === 'name' ? 'ID' : 'Name')"
70
+ :icon="
71
+ sortType === 'name' ? 'mdi-sort-alphabetical-ascending' : 'mdi-sort-numeric-ascending'
72
+ "
73
+ variant="text"
74
+ color="black"
40
75
  tooltipLocation="bottom"
41
- class="ml-1"
42
- v-bind="menuProps"
76
+ @click="emit('toggle-sort')"
43
77
  />
44
- </template>
45
- <v-list class="mt-1">
46
- <v-list-item v-for="category_id in availableFilterOptions" :key="category_id">
47
- <v-checkbox
48
- v-model="filterOptions[category_id]"
49
- :label="category_id"
50
- hide-details
51
- density="compact"
52
- />
53
- </v-list-item>
54
- </v-list>
55
- </v-menu>
78
+ <v-menu :close-on-content-click="false">
79
+ <template #activator="{ props: menuProps }">
80
+ <ActionButton
81
+ tooltip="Filter options"
82
+ icon="mdi-filter-variant"
83
+ variant="text"
84
+ color="black"
85
+ tooltipLocation="bottom"
86
+ v-bind="menuProps"
87
+ />
88
+ </template>
89
+ <v-list class="mt-1">
90
+ <v-list-item v-for="category_id in availableFilterOptions" :key="category_id">
91
+ <v-checkbox
92
+ v-model="filterOptions[category_id]"
93
+ :label="category_id"
94
+ hide-details
95
+ density="compact"
96
+ />
97
+ </v-list-item>
98
+ </v-list>
99
+ </v-menu>
100
+ <ActionButton
101
+ tooltip="Collapse All"
102
+ icon="mdi-collapse-all-outline"
103
+ variant="text"
104
+ color="black"
105
+ tooltipLocation="bottom"
106
+ @click="emit('collapse-all')"
107
+ />
108
+ </div>
109
+ </div>
56
110
  </v-col>
57
111
  </v-row>
58
112
  </template>
113
+
114
+ <style scoped>
115
+ .controls-capsule {
116
+ height: 40px;
117
+ border: 1px solid transparent;
118
+ transition: all 0.3s ease;
119
+ width: fit-content;
120
+ }
121
+
122
+ .controls-capsule.is-expanded {
123
+ width: 100%;
124
+ background-color: rgba(0, 0, 0, 0.05);
125
+ border: 1px solid rgba(0, 0, 0, 0.08);
126
+ }
127
+
128
+ .controls-capsule:hover:not(.is-expanded) {
129
+ background-color: rgba(0, 0, 0, 0.05);
130
+ border: 1px solid rgba(0, 0, 0, 0.08);
131
+ }
132
+
133
+ :deep(.v-field__input) {
134
+ padding-top: 0 !important;
135
+ padding-bottom: 0 !important;
136
+ min-height: 40px !important;
137
+ display: flex;
138
+ align-items: center;
139
+ }
140
+
141
+ :deep(.v-field__field) {
142
+ height: 40px !important;
143
+ }
144
+ </style>
@@ -1,37 +1,62 @@
1
1
  <script setup>
2
- const { item, showTooltip } = defineProps({
2
+ const { item, isLeaf } = defineProps({
3
3
  item: { type: Object, required: true },
4
- showTooltip: { type: Boolean, default: false },
4
+ isLeaf: { type: Boolean, required: false, default: undefined },
5
5
  });
6
6
 
7
- const emit = defineEmits(["contextmenu"]);
7
+ const emit = defineEmits(["contextmenu", "mouseenter", "mouseleave"]);
8
8
 
9
9
  const actualItem = computed(() => item.raw || item);
10
+
11
+ const tooltipDisabled = computed(() => {
12
+ if (isLeaf !== undefined) {
13
+ return !isLeaf;
14
+ }
15
+ return actualItem.value.children && actualItem.value.children.length > 0;
16
+ });
10
17
  </script>
11
18
 
12
19
  <template>
13
- <span
14
- class="tree-item-label"
15
- :class="{ 'inactive-item': actualItem.is_active === false }"
16
- @contextmenu.prevent.stop="emit('contextmenu', $event)"
17
- >
18
- {{ actualItem.title }}
19
- <v-tooltip v-if="showTooltip && actualItem.category" activator="parent" location="right">
20
- <div class="d-flex flex-column pa-1">
21
- <span class="text-caption"><strong>ID:</strong> {{ actualItem.id }}</span>
22
- <span v-if="actualItem.title" class="text-caption"
23
- ><strong>Name:</strong> {{ actualItem.title }}</span
20
+ <div class="tree-item-label-container w-100">
21
+ <v-tooltip :disabled="tooltipDisabled" location="right" open-delay="400">
22
+ <template #activator="{ props: tooltipProps }">
23
+ <span
24
+ v-bind="tooltipProps"
25
+ class="tree-item-label"
26
+ :class="{ 'inactive-item': actualItem.is_active === false }"
27
+ @contextmenu.prevent.stop="emit('contextmenu', $event)"
28
+ @mouseenter="emit('mouseenter')"
29
+ @mouseleave="emit('mouseleave')"
24
30
  >
25
- <span class="text-caption font-italic border-t-sm d-flex align-center">
26
- <strong class="mr-1">Status:</strong>
27
- {{ actualItem.is_active ? "Active" : "Inactive" }}
31
+ {{ actualItem.title }}
32
+ </span>
33
+ </template>
34
+
35
+ <div class="d-flex flex-column ga-1">
36
+ <span class="text-caption">
37
+ <strong class="text-white">ID:</strong> {{ actualItem.id }}
38
+ </span>
39
+ <span v-if="actualItem.title" class="text-caption">
40
+ <strong class="text-white">Name:</strong> {{ actualItem.title }}
41
+ </span>
42
+ <span class="text-caption">
43
+ <strong class="text-white">Status:</strong>
44
+ <i class="ml-1">{{ actualItem.is_active ? "Active" : "Inactive" }}</i>
28
45
  </span>
29
46
  </div>
30
47
  </v-tooltip>
31
- </span>
48
+ </div>
32
49
  </template>
33
50
 
34
51
  <style scoped>
52
+ .tree-item-label-container {
53
+ display: flex;
54
+ align-items: center;
55
+ min-width: 0;
56
+ height: 100%;
57
+ width: 100%;
58
+ }
59
+
35
60
  .tree-item-label {
36
61
  white-space: nowrap;
37
62
  overflow: hidden;
@@ -0,0 +1,46 @@
1
+ <script setup>
2
+ import TreeRow from "@ogw_front/components/Viewer/ObjectTree/Base/TreeRow.vue";
3
+
4
+ const { item, itemProps, selection, isSelected, getIndeterminate } = defineProps({
5
+ item: { type: Object, required: true },
6
+ itemProps: { type: Object, required: true },
7
+ selection: { type: Object, required: true },
8
+ isSelected: { type: Function, required: true },
9
+ getIndeterminate: { type: Function, required: true },
10
+ });
11
+
12
+ const emit = defineEmits(["toggle-open", "toggle-select"]);
13
+ </script>
14
+
15
+ <template>
16
+ <div class="sticky-tree-header tree-row" @click="$emit('toggle-open', item.raw)">
17
+ <TreeRow
18
+ :item="item"
19
+ :item-props="itemProps"
20
+ :selection="selection"
21
+ :is-selected="isSelected"
22
+ :get-indeterminate="getIndeterminate"
23
+ @toggle-open="$emit('toggle-open', $event)"
24
+ @toggle-select="$emit('toggle-select', $event)"
25
+ >
26
+ <template #title="slotProps">
27
+ <slot name="title" v-bind="slotProps" />
28
+ </template>
29
+ </TreeRow>
30
+ </div>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .sticky-tree-header {
35
+ flex-shrink: 0;
36
+ background-color: transparent;
37
+ border-bottom: 2px solid rgba(0, 0, 0, 0.05);
38
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
39
+ cursor: pointer;
40
+ z-index: 20;
41
+ }
42
+
43
+ .sticky-tree-header:hover {
44
+ background-color: rgba(0, 0, 0, 0.04);
45
+ }
46
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup>
2
+ const { item, itemProps, selection, isSelected, getIndeterminate } = defineProps({
3
+ item: { type: Object, required: true },
4
+ itemProps: { type: Object, required: true },
5
+ selection: { type: Object, required: true },
6
+ isSelected: { type: Function, required: true },
7
+ getIndeterminate: { type: Function, required: true },
8
+ });
9
+
10
+ defineEmits(["toggle-open", "toggle-select"]);
11
+
12
+ const INDENT_STEP = 16;
13
+ </script>
14
+
15
+ <template>
16
+ <div class="tree-row-content d-flex align-center px-4 ps-3 w-100">
17
+ <div
18
+ v-if="item.depth > 0"
19
+ class="flex-shrink-0"
20
+ :style="{ width: `${item.depth * INDENT_STEP}px` }"
21
+ />
22
+
23
+ <div class="d-flex align-center flex-shrink-0">
24
+ <v-icon
25
+ v-if="!item.isLeaf"
26
+ :icon="item.isOpen ? 'mdi-menu-down' : 'mdi-menu-right'"
27
+ class="me-1"
28
+ color="black"
29
+ @click.stop="$emit('toggle-open', item.raw)"
30
+ />
31
+ <div v-else class="icon-placeholder" />
32
+
33
+ <v-btn
34
+ v-if="selection.selectable"
35
+ :icon="
36
+ getIndeterminate(item.raw)
37
+ ? 'mdi-eye-minus'
38
+ : isSelected(item.raw)
39
+ ? 'mdi-eye'
40
+ : 'mdi-eye-off'
41
+ "
42
+ variant="text"
43
+ density="compact"
44
+ color="black"
45
+ class="flex-shrink-0"
46
+ @click.stop="$emit('toggle-select', item.raw)"
47
+ @mousedown.stop
48
+ />
49
+ </div>
50
+
51
+ <div class="tree-title flex-grow-1 overflow-hidden d-flex align-center ms-1 pt-1">
52
+ <slot name="title" :item="item.raw" :is-leaf="item.isLeaf">
53
+ <v-list-item-title :class="{ 'font-weight-bold': !item.isLeaf }" class="text-black">
54
+ {{ item.raw[itemProps.title] || item.id }}
55
+ </v-list-item-title>
56
+ </slot>
57
+ </div>
58
+
59
+ <div class="ms-auto d-flex align-center">
60
+ <slot name="append" :item="item.raw" />
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <style scoped>
66
+ .tree-row-content {
67
+ min-height: 44px;
68
+ }
69
+
70
+ .icon-placeholder {
71
+ width: 24px;
72
+ }
73
+
74
+ .tree-title {
75
+ min-height: 24px;
76
+ }
77
+ </style>