@363045841yyt/klinechart 0.8.4 → 0.8.6
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/README.md +6 -1
- package/dist/components/BaseModal.vue.d.ts +54 -0
- package/dist/components/BaseModal.vue.d.ts.map +1 -0
- package/dist/components/BatchStockDialog.vue.d.ts +13 -0
- package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
- package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -1
- package/dist/components/ColorPresetPanel.vue.d.ts +4 -1
- package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -1
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ExportProgressDialog.vue.d.ts +15 -0
- package/dist/components/ExportProgressDialog.vue.d.ts.map +1 -0
- package/dist/components/IndicatorParams.vue.d.ts.map +1 -1
- package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
- package/dist/components/KLineChart.vue.d.ts +5 -9
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
- package/dist/components/RangeSelectionExport.vue.d.ts +23 -0
- package/dist/components/RangeSelectionExport.vue.d.ts.map +1 -0
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/components/common/CanvasToolbar.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbar.vue.d.ts.map +1 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts.map +1 -0
- package/dist/composables/chart/useChartTheme.d.ts +329 -0
- package/dist/composables/chart/useChartTheme.d.ts.map +1 -0
- package/dist/composables/chart/useDrawingManager.d.ts +86 -0
- package/dist/composables/chart/useDrawingManager.d.ts.map +1 -0
- package/dist/composables/chart/useIndicatorManager.d.ts +38 -0
- package/dist/composables/chart/useIndicatorManager.d.ts.map +1 -0
- package/dist/composables/chart/useRangeSelection.d.ts +66 -0
- package/dist/composables/chart/useRangeSelection.d.ts.map +1 -0
- package/dist/composables/useTeleportedPopup.d.ts +8 -0
- package/dist/composables/useTeleportedPopup.d.ts.map +1 -0
- package/dist/index.cjs +9 -2
- package/dist/index.css +1 -1
- package/dist/index.js +2149 -1409
- package/dist/tools/calcRangeOverlayPixel.d.ts +15 -0
- package/dist/tools/calcRangeOverlayPixel.d.ts.map +1 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts +4 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts.map +1 -0
- package/dist/web-component.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BaseModal.vue +292 -0
- package/src/components/BatchStockDialog.vue +128 -0
- package/src/components/ChartSettingsDialog.vue +248 -405
- package/src/components/ColorPresetPanel.vue +58 -106
- package/src/components/CompareSymbolSelector.vue +37 -10
- package/src/components/DrawingStyleToolbar.vue +33 -72
- package/src/components/Dropdown.vue +42 -19
- package/src/components/ExportProgressDialog.vue +118 -0
- package/src/components/IndicatorParams.vue +194 -321
- package/src/components/IndicatorSelector.vue +188 -405
- package/src/components/KLineChart.vue +228 -403
- package/src/components/LeftToolbar.vue +3 -2
- package/src/components/RangeSelectionExport.vue +117 -0
- package/src/components/SymbolSelector.vue +37 -10
- package/src/components/TopToolbar.vue +55 -2
- package/src/components/common/CanvasToolbar.vue +70 -0
- package/src/components/common/CanvasToolbarStack.vue +32 -0
- package/src/composables/chart/useChartTheme.ts +86 -0
- package/src/composables/chart/useDrawingManager.ts +67 -0
- package/src/composables/chart/useIndicatorManager.ts +307 -0
- package/src/composables/chart/useRangeSelection.ts +424 -0
- package/src/composables/useTeleportedPopup.ts +46 -0
- package/src/tools/calcRangeOverlayPixel.ts +28 -0
- package/src/tools/getKLineIndexByTimestamp.ts +40 -0
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div class="
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{{ option.label }}
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
<button type="button" class="color-reset-btn" @click="resetCurrentThemeColors">
|
|
17
|
-
重置颜色
|
|
2
|
+
<div class="color-preset-container">
|
|
3
|
+
<div class="theme-tabs" role="tablist" aria-label="颜色主题">
|
|
4
|
+
<button
|
|
5
|
+
v-for="option in themeOptions"
|
|
6
|
+
:key="option.value"
|
|
7
|
+
type="button"
|
|
8
|
+
class="theme-tab"
|
|
9
|
+
:class="{ active: editingTheme === option.value }"
|
|
10
|
+
@click="editingTheme = option.value"
|
|
11
|
+
>
|
|
12
|
+
{{ option.label }}
|
|
18
13
|
</button>
|
|
19
14
|
</div>
|
|
15
|
+
|
|
16
|
+
<!-- 颜色分组列表 -->
|
|
20
17
|
<template v-for="group in colorPresetGroups" :key="group.group">
|
|
21
18
|
<div class="color-group-label">{{ group.label }}</div>
|
|
22
19
|
<div class="color-grid">
|
|
23
20
|
<label v-for="item in group.items" :key="item.key" class="color-item">
|
|
24
|
-
<span>{{ item.label }}</span>
|
|
21
|
+
<span class="color-item-text">{{ item.label }}</span>
|
|
25
22
|
<input
|
|
26
23
|
type="color"
|
|
27
24
|
class="color-input"
|
|
@@ -105,30 +102,27 @@ function resetCurrentThemeColors(): void {
|
|
|
105
102
|
delete nextColorSettings[editingTheme.value]
|
|
106
103
|
emit('update:colorPresetSettings', nextColorSettings)
|
|
107
104
|
}
|
|
105
|
+
|
|
106
|
+
defineExpose({ resetCurrentThemeColors })
|
|
108
107
|
</script>
|
|
109
108
|
|
|
110
109
|
<style scoped>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
display: grid;
|
|
114
|
-
grid-template-columns: 1fr auto;
|
|
115
|
-
align-items: center;
|
|
116
|
-
gap: 8px;
|
|
117
|
-
margin-bottom: 4px;
|
|
110
|
+
.color-preset-container {
|
|
111
|
+
padding: 4px 0;
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
/* ── 主题切换 ── */
|
|
121
114
|
.theme-tabs {
|
|
122
|
-
display:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: 4px;
|
|
117
|
+
padding: 4px;
|
|
118
|
+
margin-bottom: 12px;
|
|
126
119
|
border: 1px solid var(--klc-color-border-button);
|
|
127
120
|
border-radius: 8px;
|
|
128
121
|
background: var(--klc-color-grid-minor);
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
.theme-tab {
|
|
125
|
+
flex: 1;
|
|
132
126
|
height: 28px;
|
|
133
127
|
border: none;
|
|
134
128
|
border-radius: 6px;
|
|
@@ -137,149 +131,107 @@ function resetCurrentThemeColors(): void {
|
|
|
137
131
|
font-size: 12px;
|
|
138
132
|
font-weight: 500;
|
|
139
133
|
cursor: pointer;
|
|
140
|
-
transition:
|
|
141
|
-
|
|
142
|
-
color 0.18s ease,
|
|
143
|
-
box-shadow 0.18s ease;
|
|
134
|
+
transition: all 0.18s ease;
|
|
135
|
+
white-space: nowrap;
|
|
144
136
|
}
|
|
145
137
|
|
|
146
138
|
.theme-tab:not(.active):hover {
|
|
147
139
|
color: var(--klc-color-foreground);
|
|
148
|
-
background: color-mix(in srgb, var(--klc-color-
|
|
140
|
+
background: color-mix(in srgb, var(--klc-color-background) 60%, transparent);
|
|
149
141
|
}
|
|
150
142
|
|
|
151
143
|
.theme-tab.active {
|
|
152
|
-
background: var(--klc-color-tag-bg-white);
|
|
153
144
|
color: var(--klc-color-foreground);
|
|
154
145
|
font-weight: 600;
|
|
155
|
-
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
|
156
146
|
}
|
|
157
147
|
|
|
158
|
-
/* ──
|
|
159
|
-
.color-
|
|
160
|
-
|
|
161
|
-
padding: 0 14px;
|
|
162
|
-
border: 1px solid var(--klc-color-axis-line);
|
|
163
|
-
border-radius: 8px;
|
|
164
|
-
background: var(--klc-color-tag-bg-white);
|
|
148
|
+
/* ── 分组标签 ── */
|
|
149
|
+
.color-group-label {
|
|
150
|
+
margin: 18px 0 6px;
|
|
165
151
|
color: var(--klc-color-axis-text);
|
|
166
152
|
font-size: 12px;
|
|
167
153
|
font-weight: 500;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
transition:
|
|
171
|
-
background 0.18s ease,
|
|
172
|
-
border-color 0.18s ease,
|
|
173
|
-
color 0.18s ease,
|
|
174
|
-
box-shadow 0.18s ease;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.color-reset-btn:hover {
|
|
178
|
-
border-color: var(--klc-color-axis-text);
|
|
179
|
-
background: var(--klc-color-background);
|
|
180
|
-
color: var(--klc-color-foreground);
|
|
181
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.color-reset-btn:active {
|
|
185
|
-
background: var(--klc-color-tag-bg-hover);
|
|
186
|
-
box-shadow: none;
|
|
154
|
+
letter-spacing: 0.3px;
|
|
155
|
+
line-height: 1;
|
|
187
156
|
}
|
|
188
157
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
margin: 6px 0 6px;
|
|
192
|
-
color: var(--klc-color-axis-text);
|
|
193
|
-
font-size: 12px;
|
|
194
|
-
font-weight: 600;
|
|
195
|
-
line-height: 1.3;
|
|
158
|
+
.color-group-label:first-of-type {
|
|
159
|
+
margin-top: 0;
|
|
196
160
|
}
|
|
197
161
|
|
|
198
162
|
/* ── 颜色网格 ── */
|
|
199
163
|
.color-grid {
|
|
200
164
|
display: grid;
|
|
201
165
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
202
|
-
gap:
|
|
166
|
+
gap: 4px;
|
|
203
167
|
}
|
|
204
168
|
|
|
205
|
-
/* ── 颜色条目 ── */
|
|
169
|
+
/* ── 颜色条目 (扁平化样式) ── */
|
|
206
170
|
.color-item {
|
|
207
171
|
display: flex;
|
|
208
172
|
align-items: center;
|
|
209
173
|
justify-content: space-between;
|
|
210
174
|
gap: 8px;
|
|
211
|
-
min-height:
|
|
212
|
-
padding:
|
|
213
|
-
border:
|
|
214
|
-
|
|
215
|
-
background: var(--klc-color-background);
|
|
175
|
+
min-height: 40px;
|
|
176
|
+
padding: 8px 12px;
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
background: transparent;
|
|
216
179
|
color: var(--klc-color-foreground);
|
|
217
|
-
font-size:
|
|
218
|
-
line-height: 1.3;
|
|
180
|
+
font-size: 13px;
|
|
219
181
|
cursor: pointer;
|
|
220
|
-
transition:
|
|
221
|
-
border-color 0.18s ease,
|
|
222
|
-
background 0.18s ease,
|
|
223
|
-
box-shadow 0.18s ease;
|
|
182
|
+
transition: background 0.15s ease;
|
|
224
183
|
}
|
|
225
184
|
|
|
226
185
|
.color-item:hover {
|
|
227
|
-
|
|
228
|
-
background: var(--klc-color-tag-bg-hover);
|
|
229
|
-
box-shadow: 0 1px 4px color-mix(in srgb, var(--klc-color-foreground) 6%, transparent);
|
|
186
|
+
background: var(--klc-color-grid-minor);
|
|
230
187
|
}
|
|
231
188
|
|
|
232
|
-
.color-item
|
|
189
|
+
.color-item-text {
|
|
233
190
|
min-width: 0;
|
|
234
191
|
overflow: hidden;
|
|
235
192
|
text-overflow: ellipsis;
|
|
236
193
|
white-space: nowrap;
|
|
237
194
|
user-select: none;
|
|
195
|
+
line-height: 1.4;
|
|
238
196
|
}
|
|
239
197
|
|
|
240
|
-
/* ── 颜色输入 ── */
|
|
198
|
+
/* ── 颜色输入 (无边框圆角矩形) ── */
|
|
241
199
|
.color-input {
|
|
242
200
|
flex: 0 0 auto;
|
|
243
201
|
width: 26px;
|
|
244
202
|
height: 26px;
|
|
245
203
|
padding: 0;
|
|
246
|
-
border: 1px solid var(--klc-color-
|
|
204
|
+
border: 1px solid var(--klc-color-border-button);
|
|
247
205
|
border-radius: 6px;
|
|
248
206
|
background: transparent;
|
|
249
207
|
cursor: pointer;
|
|
250
|
-
transition:
|
|
251
|
-
|
|
252
|
-
box-shadow 0.18s ease;
|
|
208
|
+
transition: transform 0.15s ease;
|
|
209
|
+
overflow: hidden;
|
|
253
210
|
}
|
|
254
211
|
|
|
255
212
|
.color-input:hover {
|
|
256
|
-
|
|
257
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-foreground) 6%, transparent);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.color-input:focus-visible {
|
|
261
|
-
outline: none;
|
|
262
|
-
border-color: var(--klc-color-axis-text);
|
|
263
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-foreground) 10%, transparent);
|
|
213
|
+
transform: scale(1.1);
|
|
264
214
|
}
|
|
265
215
|
|
|
266
216
|
.color-input::-webkit-color-swatch-wrapper {
|
|
267
|
-
padding:
|
|
217
|
+
padding: 0;
|
|
268
218
|
}
|
|
269
219
|
|
|
270
220
|
.color-input::-webkit-color-swatch {
|
|
271
221
|
border: none;
|
|
272
|
-
border-radius:
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.color-input::-moz-color-swatch {
|
|
226
|
+
border: none;
|
|
227
|
+
border-radius: 6px;
|
|
273
228
|
}
|
|
274
229
|
|
|
275
230
|
/* ── 响应式 ── */
|
|
276
231
|
@media (max-width: 480px) {
|
|
277
232
|
.color-preset-tools {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.color-reset-btn {
|
|
282
|
-
width: 100%;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
align-items: stretch;
|
|
283
235
|
}
|
|
284
236
|
|
|
285
237
|
.color-grid {
|
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
<span v-if="comparisonLoading" class="compare-chip__spinner" />
|
|
15
15
|
<span v-if="selected.length > 0" class="compare-chip__badge">{{ selected.length }}</span>
|
|
16
16
|
</button>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
17
|
+
<Teleport :to="teleportTarget">
|
|
18
|
+
<Transition name="symbol-popover">
|
|
19
|
+
<div
|
|
20
|
+
v-if="showPopup"
|
|
21
|
+
ref="popupRef"
|
|
22
|
+
class="compare-popover"
|
|
23
|
+
:style="popupStyle"
|
|
24
|
+
role="dialog"
|
|
25
|
+
aria-label="比较商品"
|
|
26
|
+
>
|
|
19
27
|
<div class="compare-search">
|
|
20
28
|
<span class="compare-search__icon" aria-hidden="true">
|
|
21
29
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
@@ -134,13 +142,16 @@
|
|
|
134
142
|
</button>
|
|
135
143
|
</div>
|
|
136
144
|
</div>
|
|
137
|
-
|
|
145
|
+
</Transition>
|
|
146
|
+
</Teleport>
|
|
138
147
|
</div>
|
|
139
148
|
</template>
|
|
140
149
|
|
|
141
150
|
<script setup lang="ts">
|
|
142
|
-
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
151
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
143
152
|
import type { SymbolItem } from './SymbolSelector.vue'
|
|
153
|
+
import { useTeleportedPopup } from '../composables/useTeleportedPopup'
|
|
154
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
144
155
|
|
|
145
156
|
const props = withDefaults(defineProps<{
|
|
146
157
|
symbols: SymbolItem[]
|
|
@@ -160,6 +171,15 @@ const showPopup = ref(false)
|
|
|
160
171
|
const searchQuery = ref('')
|
|
161
172
|
const searchInputRef = ref<HTMLInputElement | null>(null)
|
|
162
173
|
const rootRef = ref<HTMLElement | null>(null)
|
|
174
|
+
const popupRef = ref<HTMLElement | null>(null)
|
|
175
|
+
|
|
176
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
177
|
+
|
|
178
|
+
const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
|
|
179
|
+
rootRef,
|
|
180
|
+
popupRef,
|
|
181
|
+
8,
|
|
182
|
+
)
|
|
163
183
|
|
|
164
184
|
const selectedSet = computed(() => new Set(props.selected ?? []))
|
|
165
185
|
|
|
@@ -202,13 +222,23 @@ function togglePopup() {
|
|
|
202
222
|
}
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
watch(showPopup, (val) => {
|
|
226
|
+
if (val) {
|
|
227
|
+
startPositionSync()
|
|
228
|
+
} else {
|
|
229
|
+
stopPositionSync()
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
205
233
|
function clearSearch() {
|
|
206
234
|
searchQuery.value = ''
|
|
207
235
|
searchInputRef.value?.focus()
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
function onDocumentClick(e: MouseEvent) {
|
|
211
|
-
|
|
239
|
+
const root = rootRef.value
|
|
240
|
+
const popup = popupRef.value
|
|
241
|
+
if (root && !root.contains(e.target as Node) && !popup?.contains(e.target as Node)) {
|
|
212
242
|
showPopup.value = false
|
|
213
243
|
searchQuery.value = ''
|
|
214
244
|
}
|
|
@@ -302,15 +332,12 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentClick)
|
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
.compare-popover {
|
|
305
|
-
|
|
306
|
-
top: calc(100% + 8px);
|
|
307
|
-
left: 0;
|
|
308
|
-
z-index: 20;
|
|
335
|
+
z-index: 110;
|
|
309
336
|
width: min(360px, calc(100vw - 24px));
|
|
310
337
|
padding: 14px;
|
|
311
338
|
border: 1px solid var(--klc-color-border-button);
|
|
312
339
|
border-radius: 3px;
|
|
313
|
-
background: var(--klc-color-
|
|
340
|
+
background: var(--klc-color-background);
|
|
314
341
|
color: var(--klc-color-foreground);
|
|
315
342
|
|
|
316
343
|
box-sizing: border-box;
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="
|
|
4
|
-
@pointerdown.stop
|
|
5
|
-
@pointermove.stop
|
|
6
|
-
@pointerup.stop
|
|
7
|
-
>
|
|
8
|
-
<div class="toolbar-item color-item" title="颜色">
|
|
2
|
+
<CanvasToolbar>
|
|
3
|
+
<div class="color-item" title="颜色">
|
|
9
4
|
<span class="color-swatch" :style="{ background: drawing.style.stroke ?? '#2962ff' }"></span>
|
|
10
5
|
<input
|
|
11
6
|
type="color"
|
|
@@ -31,20 +26,35 @@
|
|
|
31
26
|
@update:model-value="onLineStyleChange($event as 'solid' | 'dashed' | 'dotted')"
|
|
32
27
|
/>
|
|
33
28
|
|
|
34
|
-
<button
|
|
35
|
-
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
class="toolbar-btn toolbar-btn--delete"
|
|
32
|
+
title="删除"
|
|
33
|
+
@click="$emit('delete')"
|
|
34
|
+
>
|
|
35
|
+
<svg
|
|
36
|
+
class="delete-icon"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
stroke-width="2"
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
stroke-linejoin="round"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
>
|
|
36
45
|
<path d="M3 6h18" />
|
|
37
46
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
38
47
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
39
48
|
</svg>
|
|
40
49
|
</button>
|
|
41
|
-
</
|
|
50
|
+
</CanvasToolbar>
|
|
42
51
|
</template>
|
|
43
52
|
|
|
44
53
|
<script setup lang="ts">
|
|
45
54
|
import { onMounted, onUnmounted } from 'vue'
|
|
46
55
|
import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
|
|
47
56
|
import Dropdown from './Dropdown.vue'
|
|
57
|
+
import CanvasToolbar from './common/CanvasToolbar.vue'
|
|
48
58
|
|
|
49
59
|
const widthOptions = [
|
|
50
60
|
{ label: '1px', value: '1' },
|
|
@@ -92,46 +102,29 @@ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
|
|
|
92
102
|
</script>
|
|
93
103
|
|
|
94
104
|
<style scoped>
|
|
95
|
-
.
|
|
96
|
-
position:
|
|
97
|
-
left: 50%;
|
|
98
|
-
top: 8px;
|
|
99
|
-
transform: translateX(-50%);
|
|
100
|
-
display: flex;
|
|
101
|
-
align-items: center;
|
|
102
|
-
gap: 6px;
|
|
103
|
-
padding: 4px 8px;
|
|
104
|
-
height: 32px;
|
|
105
|
-
background: color-mix(in srgb, var(--klc-color-tag-bg-white) 88%, transparent);
|
|
106
|
-
backdrop-filter: blur(8px);
|
|
107
|
-
-webkit-backdrop-filter: blur(8px);
|
|
108
|
-
border: 1px solid var(--klc-color-border-button);
|
|
109
|
-
border-radius: 6px;
|
|
110
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
111
|
-
z-index: 100;
|
|
112
|
-
user-select: none;
|
|
113
|
-
pointer-events: auto;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.toolbar-item {
|
|
105
|
+
.color-item {
|
|
106
|
+
position: relative;
|
|
117
107
|
display: inline-flex;
|
|
118
108
|
align-items: center;
|
|
119
109
|
justify-content: center;
|
|
110
|
+
width: 26px;
|
|
111
|
+
height: 26px;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
transition: background 0.15s ease;
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
.color-item {
|
|
123
|
-
|
|
124
|
-
width: 24px;
|
|
125
|
-
height: 24px;
|
|
117
|
+
.color-item:hover {
|
|
118
|
+
background: var(--klc-color-grid-minor);
|
|
126
119
|
}
|
|
127
120
|
|
|
128
121
|
.color-swatch {
|
|
129
122
|
display: block;
|
|
130
|
-
width:
|
|
131
|
-
height:
|
|
132
|
-
border: 1px solid
|
|
123
|
+
width: 16px;
|
|
124
|
+
height: 16px;
|
|
125
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
133
126
|
border-radius: 4px;
|
|
134
|
-
|
|
127
|
+
pointer-events: none;
|
|
135
128
|
}
|
|
136
129
|
|
|
137
130
|
.color-input {
|
|
@@ -142,36 +135,4 @@ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
|
|
|
142
135
|
width: 100%;
|
|
143
136
|
height: 100%;
|
|
144
137
|
}
|
|
145
|
-
|
|
146
|
-
.toolbar-btn {
|
|
147
|
-
display: inline-flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
150
|
-
width: 24px;
|
|
151
|
-
height: 24px;
|
|
152
|
-
padding: 0;
|
|
153
|
-
border: 1px solid transparent;
|
|
154
|
-
border-radius: 4px;
|
|
155
|
-
background: transparent;
|
|
156
|
-
color: var(--klc-color-axis-text);
|
|
157
|
-
cursor: pointer;
|
|
158
|
-
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.toolbar-btn:hover {
|
|
162
|
-
border-color: var(--klc-color-axis-line);
|
|
163
|
-
background: var(--klc-color-grid-minor);
|
|
164
|
-
color: var(--klc-color-foreground);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.delete-btn:hover {
|
|
168
|
-
color: #dc2626;
|
|
169
|
-
border-color: #fca5a5;
|
|
170
|
-
background: #fef2f2;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.delete-icon {
|
|
174
|
-
width: 14px;
|
|
175
|
-
height: 14px;
|
|
176
|
-
}
|
|
177
138
|
</style>
|
|
@@ -19,20 +19,29 @@
|
|
|
19
19
|
<span class="dropdown__chevron" aria-hidden="true"></span>
|
|
20
20
|
</button>
|
|
21
21
|
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
v-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:aria-selected="option.value === selectedValue"
|
|
31
|
-
@click="selectOption(option.value)"
|
|
22
|
+
<Teleport :to="teleportTarget">
|
|
23
|
+
<div
|
|
24
|
+
v-if="isOpen"
|
|
25
|
+
ref="menuRef"
|
|
26
|
+
class="dropdown__menu"
|
|
27
|
+
:style="menuStyle"
|
|
28
|
+
role="listbox"
|
|
29
|
+
tabindex="-1"
|
|
32
30
|
>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
<button
|
|
32
|
+
v-for="option in options"
|
|
33
|
+
:key="option.value"
|
|
34
|
+
type="button"
|
|
35
|
+
class="dropdown__option"
|
|
36
|
+
:class="{ 'is-selected': option.value === selectedValue }"
|
|
37
|
+
role="option"
|
|
38
|
+
:aria-selected="option.value === selectedValue"
|
|
39
|
+
@click="selectOption(option.value)"
|
|
40
|
+
>
|
|
41
|
+
{{ option.label }}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</Teleport>
|
|
36
45
|
</div>
|
|
37
46
|
</template>
|
|
38
47
|
|
|
@@ -44,6 +53,8 @@ let dropdownIdSeed = 0
|
|
|
44
53
|
|
|
45
54
|
<script setup lang="ts">
|
|
46
55
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
56
|
+
import { useTeleportedPopup } from '../composables/useTeleportedPopup'
|
|
57
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
47
58
|
|
|
48
59
|
export interface DropdownOption<T extends string = string> {
|
|
49
60
|
label: string
|
|
@@ -71,10 +82,19 @@ const emit = defineEmits<{
|
|
|
71
82
|
|
|
72
83
|
const rootRef = ref<HTMLElement | null>(null)
|
|
73
84
|
const triggerRef = ref<HTMLElement | null>(null)
|
|
85
|
+
const menuRef = ref<HTMLElement | null>(null)
|
|
74
86
|
const isOpen = ref(false)
|
|
75
87
|
const menuWidth = ref(0)
|
|
76
88
|
const dropdownId = ++dropdownIdSeed
|
|
77
89
|
|
|
90
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
91
|
+
|
|
92
|
+
const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
|
|
93
|
+
triggerRef,
|
|
94
|
+
menuRef,
|
|
95
|
+
4,
|
|
96
|
+
)
|
|
97
|
+
|
|
78
98
|
const triggerStyle = computed(() => {
|
|
79
99
|
if (props.minWidth) return { minWidth: props.minWidth }
|
|
80
100
|
return {}
|
|
@@ -83,7 +103,11 @@ const triggerStyle = computed(() => {
|
|
|
83
103
|
const menuStyle = computed(() => {
|
|
84
104
|
if (!isOpen.value) return undefined
|
|
85
105
|
const w = menuWidth.value || (props.minWidth ? parseInt(props.minWidth) : 0)
|
|
86
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
width: w ? `${w}px` : undefined,
|
|
108
|
+
zIndex: 1010,
|
|
109
|
+
...popupStyle.value,
|
|
110
|
+
}
|
|
87
111
|
})
|
|
88
112
|
|
|
89
113
|
const selectedValue = computed(() => {
|
|
@@ -107,6 +131,7 @@ function open() {
|
|
|
107
131
|
activeDropdownClose = close
|
|
108
132
|
menuWidth.value = triggerRef.value?.offsetWidth ?? 0
|
|
109
133
|
isOpen.value = true
|
|
134
|
+
startPositionSync()
|
|
110
135
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
|
111
136
|
}
|
|
112
137
|
|
|
@@ -117,6 +142,7 @@ function close() {
|
|
|
117
142
|
activeDropdownId = 0
|
|
118
143
|
activeDropdownClose = null
|
|
119
144
|
}
|
|
145
|
+
stopPositionSync()
|
|
120
146
|
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
|
121
147
|
}
|
|
122
148
|
|
|
@@ -135,7 +161,8 @@ function selectOption(value: string) {
|
|
|
135
161
|
|
|
136
162
|
function handleDocumentPointerDown(event: PointerEvent) {
|
|
137
163
|
const root = rootRef.value
|
|
138
|
-
|
|
164
|
+
const menu = menuRef.value
|
|
165
|
+
if (root && !root.contains(event.target as Node | null) && !menu?.contains(event.target as Node | null)) {
|
|
139
166
|
close()
|
|
140
167
|
}
|
|
141
168
|
}
|
|
@@ -219,10 +246,6 @@ onBeforeUnmount(close)
|
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
.dropdown__menu {
|
|
222
|
-
position: absolute;
|
|
223
|
-
z-index: 30;
|
|
224
|
-
top: calc(100% + 4px);
|
|
225
|
-
left: 0;
|
|
226
249
|
padding: 4px;
|
|
227
250
|
border: 1px solid var(--klc-color-border-button);
|
|
228
251
|
border-radius: 4px;
|