@yangyongtao/gaea 1.1.17 → 1.1.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.19] - 2026-07-13
4
+
5
+ ### Added
6
+ - `GaeaApp.setCategoryVisible(key, visible)` / `getCategoryVisible(key)` — 按分类控制屏幕图标显隐
7
+ - `LayerTree` 每种分类左侧新增复选框,点击可切换该类图标的显示/隐藏
8
+ - `LayerTree` 所有核心样式通过 `--lt-*` CSS 变量暴露,消费者可在外部覆盖
9
+
10
+ ## [1.1.18] - 2026-07-13
11
+
12
+ ### Fixed
13
+ - 修复引擎初始化时 `querySelector('#')` 报错:`_startEngine` 显式传 `canvas` 元素,避免引擎全局取第一个 canvas(可能命中其他无 id 的 canvas 导致 `.id` 为空)
14
+
3
15
  ## [1.1.17] - 2026-07-10
4
16
 
5
17
  ### Removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangyongtao/gaea",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Gaea 3D visualization component library - Vue 3 components based on Godot WASM engine. Includes WMTS layer loading, camera view control, Vite plugin auto-injection, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -275,6 +275,7 @@ export class GaeaApp {
275
275
  this._secIsClickArr = [];
276
276
  this._secScreenIconKeyValue = {};
277
277
  this._groundPolygonElements = [];
278
+ this._categoryIcons = {};
278
279
  this.tweenGroup = new TWEEN.Group();
279
280
  this._tourConfig = {
280
281
  '沿浦水闸': { direction: [-0.5, 0.3, 0.3], positionList: [
@@ -417,8 +418,10 @@ export class GaeaApp {
417
418
  }
418
419
 
419
420
  const cfg = this.config;
421
+ const canvasEl = document.getElementById('canvas');
420
422
  const engine = new window.Engine({
421
423
  args: [],
424
+ canvas: canvasEl,
422
425
  canvasResizePolicy: cfg.canvasResizePolicy,
423
426
  executable: cfg.executable,
424
427
  experimentalVK: cfg.experimentalVK,
@@ -803,13 +806,18 @@ export class GaeaApp {
803
806
  };
804
807
  console.log('[GaeaApp] 开始创建屏幕图标... categories:', cats.length);
805
808
  let count = 0;
809
+ this._categoryIcons = {};
806
810
  for (const cat of cats) {
807
811
  if (!cat.children?.length) continue;
812
+ this._categoryIcons[cat.key] = [];
808
813
  const cfg = catConfig[cat.key] || { img: '', method: 'textImage', visibleRange: [10, 50000], offset: [-40, -35], drawLine: false };
809
814
  for (const child of cat.children) {
810
815
  if (!child.position) continue;
811
816
  count++;
812
817
  try {
818
+ const safeId = child.text.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
819
+ const divId = 'gaea-icon-' + safeId;
820
+ this._categoryIcons[cat.key].push(divId);
813
821
  this.addScreenIcon({
814
822
  name: child.text,
815
823
  text: child.text,
@@ -832,6 +840,34 @@ export class GaeaApp {
832
840
  console.log('[GaeaApp] 屏幕图标创建完成, 共:', count);
833
841
  }
834
842
 
843
+ // ---- 图层分类显隐控制 ----
844
+
845
+ /**
846
+ * 设置某个分类图标的可见性
847
+ * @param {string} categoryKey 分类 key(如 'seawall', 'hydrometricStations' 等)
848
+ * @param {boolean} visible 是否可见
849
+ */
850
+ setCategoryVisible(categoryKey, visible) {
851
+ const divIds = this._categoryIcons?.[categoryKey];
852
+ if (!divIds?.length) return;
853
+ divIds.forEach(divId => {
854
+ const el = document.getElementById(divId);
855
+ if (el) el.style.display = visible ? '' : 'none';
856
+ });
857
+ }
858
+
859
+ /**
860
+ * 获取某个分类的当前可见性(根据第一个图标的 display 状态判断)
861
+ * @param {string} categoryKey
862
+ * @returns {boolean}
863
+ */
864
+ getCategoryVisible(categoryKey) {
865
+ const divIds = this._categoryIcons?.[categoryKey];
866
+ if (!divIds?.length) return true;
867
+ const el = document.getElementById(divIds[0]);
868
+ return el ? el.style.display !== 'none' : true;
869
+ }
870
+
835
871
  // ---- 公开方法 ----
836
872
 
837
873
  /** 初始化默认图层和相机视角(手动调用,可传参覆盖配置) */
@@ -1,264 +1,359 @@
1
- <template>
2
- <div class="layer-tree-dialog" v-drag>
3
- <div class="title">
4
- <div class="title_text">图层控制</div>
5
- <div class="closeIcon" @click="$emit('close')"></div>
6
- </div>
7
-
8
- <div class="search-container">
9
- <input type="text" class="search-input" v-model="searchKey" placeholder="搜索图层..." />
10
- </div>
11
-
12
- <div class="layer-tree-container">
13
- <div class="layer-tree-content">
14
- <div v-for="rootNode in filteredData" :key="rootNode.key" class="layer-tree-node">
15
- <div class="node-header" @click.stop="rootNode.isExpanded = !rootNode.isExpanded">
16
- <span class="expand-icon">
17
- <span v-if="!rootNode.isExpanded">▸</span>
18
- <span v-else>▾</span>
19
- </span>
20
- <span class="node-text" :class="{ 'expanded-yellow': rootNode.isExpanded }">{{ rootNode.text }}</span>
21
- </div>
22
-
23
- <div class="node-children" v-if="rootNode.isExpanded">
24
- <div v-for="childNode in rootNode.filteredChildren" :key="childNode.text + (childNode.stcd || '')" class="child-node">
25
- <div class="node-header" @click.stop="$emit('node-click', { category: rootNode, node: childNode })">
26
- <span class="empty-icon"></span>
27
- <span class="node-text">{{ childNode.text }}</span>
28
- </div>
29
- </div>
30
- </div>
31
- </div>
32
- </div>
33
- </div>
34
- </div>
35
- </template>
36
-
37
- <script setup>
38
- import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
39
- import vDrag from '../directives/vDrag.js';
40
-
41
- const props = defineProps({
42
- appInstance: { type: Object, required: true },
43
- });
44
-
45
- const emit = defineEmits(['close', 'node-click']);
46
-
47
- const categories = ref([]);
48
- const searchKey = ref('');
49
-
50
- function handleHideLayerTree() {
51
- emit('close');
52
- }
53
-
54
- onMounted(() => {
55
- const raw = props.appInstance?.layerCategories || [];
56
- categories.value = raw
57
- .filter(c => c.visible !== false)
58
- .map(c => ({ ...c, isExpanded: false }));
59
- window.addEventListener('gaea-hide-layertree', handleHideLayerTree);
60
- });
61
-
62
- onBeforeUnmount(() => {
63
- window.removeEventListener('gaea-hide-layertree', handleHideLayerTree);
64
- });
65
-
66
- const filteredData = computed(() => {
67
- const kw = searchKey.value.trim().toLowerCase();
68
- return categories.value.map(rootNode => {
69
- if (!kw) {
70
- rootNode.filteredChildren = rootNode.children || [];
71
- return rootNode;
72
- }
73
- const matchRoot = rootNode.text.toLowerCase().includes(kw);
74
- const matchedChildren = (rootNode.children || []).filter(c => c.text.toLowerCase().includes(kw));
75
- rootNode.filteredChildren = matchedChildren;
76
- if (matchRoot || matchedChildren.length) return rootNode;
77
- return null;
78
- }).filter(Boolean);
79
- });
80
-
81
- </script>
82
-
83
- <style scoped>
84
- .layer-tree-dialog {
85
- position: fixed;
86
- top: 10px;
87
- left: 10px;
88
- width: 300px;
89
- background: rgba(1, 79, 152, 0.85);
90
- border-radius: 8px;
91
- overflow: hidden;
92
- z-index: 10000;
93
- }
94
-
95
- .title {
96
- height: 42px;
97
- line-height: 42px;
98
- padding: 0 14px;
99
- color: #fff;
100
- font-size: 15px;
101
- font-weight: 500;
102
- display: flex;
103
- justify-content: space-between;
104
- align-items: center;
105
- cursor: move;
106
- user-select: none;
107
- background-size: 100% 100%;
108
- }
109
-
110
- .title .title_text {
111
- font-size: 20px;
112
- width: 100px;
113
- text-align: left;
114
- }
115
-
116
- .closeIcon {
117
- width: 22px;
118
- height: 22px;
119
- position: relative;
120
- cursor: pointer;
121
- transition: all 0.2s ease;
122
- }
123
-
124
- .closeIcon::before,
125
- .closeIcon::after {
126
- content: '';
127
- position: absolute;
128
- top: 25%;
129
- left: 50%;
130
- width: 14px;
131
- height: 2px;
132
- background-color: #fff;
133
- border-radius: 1px;
134
- transition: background-color 0.2s ease;
135
- }
136
-
137
- .closeIcon::before {
138
- transform: translate(-50%, -50%) rotate(45deg);
139
- }
140
-
141
- .closeIcon::after {
142
- transform: translate(-50%, -50%) rotate(-45deg);
143
- }
144
-
145
- .closeIcon:hover {
146
- transform: scale(1.05);
147
- }
148
-
149
- .closeIcon:hover::before,
150
- .closeIcon:hover::after {
151
- background-color: #f0f9ff;
152
- }
153
-
154
- .search-container {
155
- padding: 10px 14px 0 14px;
156
- }
157
-
158
- .search-input {
159
- width: 100%;
160
- padding: 7px 12px;
161
- border: none;
162
- border-radius: 6px;
163
- font-size: 14px;
164
- outline: none;
165
- box-sizing: border-box;
166
- transition: all 0.2s ease;
167
- background: rgba(0,0,0,0.25);
168
- color: #fff;
169
- }
170
-
171
- .search-input::placeholder {
172
- color: #adb5bd;
173
- font-size: 13px;
174
- }
175
-
176
- .layer-tree-container {
177
- padding: 6px 0;
178
- max-height: 390px;
179
- overflow-y: auto;
180
- }
181
-
182
- .layer-tree-container::-webkit-scrollbar {
183
- width: 5px;
184
- height: 5px;
185
- }
186
-
187
- .layer-tree-container::-webkit-scrollbar-track {
188
- background: #f8f9fa;
189
- border-radius: 3px;
190
- }
191
-
192
- .layer-tree-container::-webkit-scrollbar-thumb {
193
- background: #ced4da;
194
- border-radius: 3px;
195
- transition: background 0.2s ease;
196
- }
197
-
198
- .layer-tree-container::-webkit-scrollbar-thumb:hover {
199
- background: #adb5bd;
200
- }
201
-
202
- .layer-tree-node {
203
- margin-left: 6px;
204
- line-height: 1.7;
205
- color: #fff;
206
- }
207
-
208
- .node-header {
209
- display: flex;
210
- align-items: center;
211
- gap: 5px;
212
- padding: 3px 8px;
213
- border-radius: 4px;
214
- cursor: pointer;
215
- transition: background-color 0.15s ease;
216
- }
217
-
218
- .node-header:hover {
219
- background-color: #007ffd;
220
- }
221
-
222
- .expand-icon {
223
- font-size: 11px;
224
- color: #6c757d;
225
- width: 12px;
226
- height: 12px;
227
- text-align: center;
228
- user-select: none;
229
- cursor: pointer;
230
- }
231
-
232
- .empty-icon {
233
- width: 12px;
234
- height: 12px;
235
- display: inline-block;
236
- }
237
-
238
- .node-icon {
239
- font-size: 13px;
240
- width: 14px;
241
- text-align: center;
242
- }
243
-
244
- .node-text {
245
- flex: 1;
246
- font-size: 13.5px;
247
- text-align: left;
248
- transition: color 0.15s ease;
249
- }
250
-
251
- .expanded-yellow {
252
- color: #FDB144 !important;
253
- }
254
-
255
- .node-children {
256
- margin-left: 14px;
257
- padding-left: 4px;
258
- border-left: 1px dashed #dee2e6;
259
- }
260
-
261
- .child-node {
262
- margin-left: 2px;
263
- }
264
- </style>
1
+ <template>
2
+ <div class="layer-tree-dialog" v-drag>
3
+ <div class="title">
4
+ <div class="title_text">图层控制</div>
5
+ <div class="closeIcon" @click="$emit('close')"></div>
6
+ </div>
7
+
8
+ <div class="search-container">
9
+ <input type="text" class="search-input" v-model="searchKey" placeholder="搜索图层..." />
10
+ </div>
11
+
12
+ <div class="layer-tree-container">
13
+ <div class="layer-tree-content">
14
+ <div v-for="rootNode in filteredData" :key="rootNode.key" class="layer-tree-node">
15
+ <div class="node-header" @click.stop="rootNode.isExpanded = !rootNode.isExpanded">
16
+ <span class="expand-icon">
17
+ <span v-if="!rootNode.isExpanded">▸</span>
18
+ <span v-else>▾</span>
19
+ </span>
20
+ <!-- 分类复选框 -->
21
+ <label class="category-checkbox" @click.stop>
22
+ <input
23
+ type="checkbox"
24
+ :checked="categoryVisibility[rootNode.key] !== false"
25
+ @change="onCategoryToggle(rootNode.key, $event.target.checked)"
26
+ />
27
+ <span class="checkmark"></span>
28
+ </label>
29
+ <span class="node-text" :class="{ 'expanded-yellow': rootNode.isExpanded }">{{ rootNode.text }}</span>
30
+ </div>
31
+
32
+ <div class="node-children" v-if="rootNode.isExpanded">
33
+ <div v-for="childNode in rootNode.filteredChildren" :key="childNode.text + (childNode.stcd || '')" class="child-node">
34
+ <div class="node-header" @click.stop="$emit('node-click', { category: rootNode, node: childNode })">
35
+ <span class="empty-icon"></span>
36
+ <span class="node-text">{{ childNode.text }}</span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup>
47
+ import { ref, onMounted, onBeforeUnmount, computed, reactive } from 'vue';
48
+ import vDrag from '../directives/vDrag.js';
49
+
50
+ const props = defineProps({
51
+ appInstance: { type: Object, required: true },
52
+ });
53
+
54
+ const emit = defineEmits(['close', 'node-click']);
55
+
56
+ const categories = ref([]);
57
+ const searchKey = ref('');
58
+ const categoryVisibility = reactive({});
59
+
60
+ function handleHideLayerTree() {
61
+ emit('close');
62
+ }
63
+
64
+ onMounted(() => {
65
+ const raw = props.appInstance?.layerCategories || [];
66
+ categories.value = raw
67
+ .filter(c => c.visible !== false)
68
+ .map(c => ({ ...c, isExpanded: false }));
69
+ // 初始化每个分类的可见性状态
70
+ for (const cat of categories.value) {
71
+ categoryVisibility[cat.key] = props.appInstance?.getCategoryVisible?.(cat.key) ?? true;
72
+ }
73
+ window.addEventListener('gaea-hide-layertree', handleHideLayerTree);
74
+ });
75
+
76
+ onBeforeUnmount(() => {
77
+ window.removeEventListener('gaea-hide-layertree', handleHideLayerTree);
78
+ });
79
+
80
+ function onCategoryToggle(categoryKey, checked) {
81
+ categoryVisibility[categoryKey] = checked;
82
+ props.appInstance?.setCategoryVisible?.(categoryKey, checked);
83
+ }
84
+
85
+ const filteredData = computed(() => {
86
+ const kw = searchKey.value.trim().toLowerCase();
87
+ return categories.value.map(rootNode => {
88
+ if (!kw) {
89
+ rootNode.filteredChildren = rootNode.children || [];
90
+ return rootNode;
91
+ }
92
+ const matchRoot = rootNode.text.toLowerCase().includes(kw);
93
+ const matchedChildren = (rootNode.children || []).filter(c => c.text.toLowerCase().includes(kw));
94
+ rootNode.filteredChildren = matchedChildren;
95
+ if (matchRoot || matchedChildren.length) return rootNode;
96
+ return null;
97
+ }).filter(Boolean);
98
+ });
99
+
100
+ </script>
101
+
102
+ <style>
103
+ /* ===== CSS 变量暴露(消费者可覆盖) ===== */
104
+ :root {
105
+ --lt-bg: rgba(1, 79, 152, 0.85);
106
+ --lt-border-radius: 8px;
107
+ --lt-title-height: 42px;
108
+ --lt-title-color: #fff;
109
+ --lt-title-font-size: 20px;
110
+ --lt-close-color: #fff;
111
+ --lt-close-hover-color: #f0f9ff;
112
+ --lt-search-bg: rgba(0, 0, 0, 0.25);
113
+ --lt-search-color: #fff;
114
+ --lt-search-placeholder: #adb5bd;
115
+ --lt-node-color: #fff;
116
+ --lt-node-hover-bg: #007ffd;
117
+ --lt-node-expanded-color: #FDB144;
118
+ --lt-scrollbar-track: #f8f9fa;
119
+ --lt-scrollbar-thumb: #ced4da;
120
+ --lt-scrollbar-thumb-hover: #adb5bd;
121
+ --lt-child-border-color: #dee2e6;
122
+ --lt-checkbox-size: 16px;
123
+ --lt-checkbox-bg: rgba(255, 255, 255, 0.2);
124
+ --lt-checkbox-checked-bg: #3b82f6;
125
+ --lt-checkbox-border: rgba(255, 255, 255, 0.5);
126
+ }
127
+ </style>
128
+
129
+ <style scoped>
130
+ .layer-tree-dialog {
131
+ position: fixed;
132
+ top: 10px;
133
+ left: 10px;
134
+ width: 300px;
135
+ background: var(--lt-bg);
136
+ border-radius: var(--lt-border-radius);
137
+ overflow: hidden;
138
+ z-index: 10000;
139
+ }
140
+
141
+ .title {
142
+ height: var(--lt-title-height);
143
+ line-height: var(--lt-title-height);
144
+ padding: 0 14px;
145
+ color: var(--lt-title-color);
146
+ font-size: 15px;
147
+ font-weight: 500;
148
+ display: flex;
149
+ justify-content: space-between;
150
+ align-items: center;
151
+ cursor: move;
152
+ user-select: none;
153
+ background-size: 100% 100%;
154
+ }
155
+
156
+ .title .title_text {
157
+ font-size: var(--lt-title-font-size);
158
+ width: 100px;
159
+ text-align: left;
160
+ }
161
+
162
+ .closeIcon {
163
+ width: 22px;
164
+ height: 22px;
165
+ position: relative;
166
+ cursor: pointer;
167
+ transition: all 0.2s ease;
168
+ }
169
+
170
+ .closeIcon::before,
171
+ .closeIcon::after {
172
+ content: '';
173
+ position: absolute;
174
+ top: 25%;
175
+ left: 50%;
176
+ width: 14px;
177
+ height: 2px;
178
+ background-color: var(--lt-close-color);
179
+ border-radius: 1px;
180
+ transition: background-color 0.2s ease;
181
+ }
182
+
183
+ .closeIcon::before {
184
+ transform: translate(-50%, -50%) rotate(45deg);
185
+ }
186
+
187
+ .closeIcon::after {
188
+ transform: translate(-50%, -50%) rotate(-45deg);
189
+ }
190
+
191
+ .closeIcon:hover {
192
+ transform: scale(1.05);
193
+ }
194
+
195
+ .closeIcon:hover::before,
196
+ .closeIcon:hover::after {
197
+ background-color: var(--lt-close-hover-color);
198
+ }
199
+
200
+ .search-container {
201
+ padding: 10px 14px 0 14px;
202
+ }
203
+
204
+ .search-input {
205
+ width: 100%;
206
+ padding: 7px 12px;
207
+ border: none;
208
+ border-radius: 6px;
209
+ font-size: 14px;
210
+ outline: none;
211
+ box-sizing: border-box;
212
+ transition: all 0.2s ease;
213
+ background: var(--lt-search-bg);
214
+ color: var(--lt-search-color);
215
+ }
216
+
217
+ .search-input::placeholder {
218
+ color: var(--lt-search-placeholder);
219
+ font-size: 13px;
220
+ }
221
+
222
+ .layer-tree-container {
223
+ padding: 6px 0;
224
+ max-height: 390px;
225
+ overflow-y: auto;
226
+ }
227
+
228
+ .layer-tree-container::-webkit-scrollbar {
229
+ width: 5px;
230
+ height: 5px;
231
+ }
232
+
233
+ .layer-tree-container::-webkit-scrollbar-track {
234
+ background: var(--lt-scrollbar-track);
235
+ border-radius: 3px;
236
+ }
237
+
238
+ .layer-tree-container::-webkit-scrollbar-thumb {
239
+ background: var(--lt-scrollbar-thumb);
240
+ border-radius: 3px;
241
+ transition: background 0.2s ease;
242
+ }
243
+
244
+ .layer-tree-container::-webkit-scrollbar-thumb:hover {
245
+ background: var(--lt-scrollbar-thumb-hover);
246
+ }
247
+
248
+ .layer-tree-node {
249
+ margin-left: 6px;
250
+ line-height: 1.7;
251
+ color: var(--lt-node-color);
252
+ }
253
+
254
+ .node-header {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 5px;
258
+ padding: 3px 8px;
259
+ border-radius: 4px;
260
+ cursor: pointer;
261
+ transition: background-color 0.15s ease;
262
+ }
263
+
264
+ .node-header:hover {
265
+ background-color: var(--lt-node-hover-bg);
266
+ }
267
+
268
+ .expand-icon {
269
+ font-size: 11px;
270
+ color: #6c757d;
271
+ width: 12px;
272
+ height: 12px;
273
+ text-align: center;
274
+ user-select: none;
275
+ cursor: pointer;
276
+ flex-shrink: 0;
277
+ }
278
+
279
+ .empty-icon {
280
+ width: 12px;
281
+ height: 12px;
282
+ display: inline-block;
283
+ flex-shrink: 0;
284
+ }
285
+
286
+ .node-icon {
287
+ font-size: 13px;
288
+ width: 14px;
289
+ text-align: center;
290
+ }
291
+
292
+ .node-text {
293
+ flex: 1;
294
+ font-size: 13.5px;
295
+ text-align: left;
296
+ transition: color 0.15s ease;
297
+ }
298
+
299
+ .expanded-yellow {
300
+ color: var(--lt-node-expanded-color) !important;
301
+ }
302
+
303
+ .node-children {
304
+ margin-left: 14px;
305
+ padding-left: 4px;
306
+ border-left: 1px dashed var(--lt-child-border-color);
307
+ }
308
+
309
+ .child-node {
310
+ margin-left: 2px;
311
+ }
312
+
313
+ /* ===== 分类复选框 ===== */
314
+ .category-checkbox {
315
+ position: relative;
316
+ display: inline-flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ width: var(--lt-checkbox-size);
320
+ height: var(--lt-checkbox-size);
321
+ flex-shrink: 0;
322
+ cursor: pointer;
323
+ user-select: none;
324
+ }
325
+
326
+ .category-checkbox input[type="checkbox"] {
327
+ position: absolute;
328
+ opacity: 0;
329
+ width: 0;
330
+ height: 0;
331
+ }
332
+
333
+ .checkmark {
334
+ width: 100%;
335
+ height: 100%;
336
+ border: 1.5px solid var(--lt-checkbox-border);
337
+ border-radius: 3px;
338
+ background: var(--lt-checkbox-bg);
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ transition: all 0.15s ease;
343
+ }
344
+
345
+ .category-checkbox input:checked + .checkmark {
346
+ background: var(--lt-checkbox-checked-bg);
347
+ border-color: var(--lt-checkbox-checked-bg);
348
+ }
349
+
350
+ .category-checkbox input:checked + .checkmark::after {
351
+ content: '';
352
+ width: 4px;
353
+ height: 8px;
354
+ border: solid #fff;
355
+ border-width: 0 2px 2px 0;
356
+ transform: rotate(45deg);
357
+ margin-top: -1px;
358
+ }
359
+ </style>