@363045841yyt/klinechart 0.7.12 → 0.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@363045841yyt/klinechart",
3
- "version": "0.7.12",
3
+ "version": "0.8.0",
4
4
  "description": "Vue 3 bindings for @363045841yyt/klinechart-core. Idiomatic composables, SFC components.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,19 +73,19 @@
73
73
  "devDependencies": {
74
74
  "@arethetypeswrong/cli": "^0.18.3",
75
75
  "@size-limit/preset-small-lib": "^12.1.0",
76
- "@vitejs/plugin-vue": "^6.0.0",
77
- "@vue/test-utils": "^2.4.10",
76
+ "@vitejs/plugin-vue": "^6.0.7",
77
+ "@vue/test-utils": "^2.4.11",
78
78
  "cross-env": "^7.0.3",
79
79
  "jsdom": "^29.1.1",
80
- "publint": "^0.3.0",
80
+ "publint": "^0.3.21",
81
81
  "size-limit": "^12.1.0",
82
82
  "typescript": "~6.0.3",
83
83
  "unplugin-icons": "^23.0.1",
84
- "vite": "^8.0.14",
85
- "vite-plugin-babel": "^1.3.2",
84
+ "vite": "^8.0.16",
85
+ "vite-plugin-babel": "^1.7.3",
86
86
  "vite-plugin-css-injected-by-js": "^5.0.1",
87
- "vite-plugin-dts": "^5.0.1",
88
- "vitest": "^4.1.5",
89
- "vue": "^3.5.0"
87
+ "vite-plugin-dts": "^5.0.2",
88
+ "vitest": "^4.1.8",
89
+ "vue": "^3.5.35"
90
90
  }
91
91
  }
@@ -35,11 +35,13 @@
35
35
  />
36
36
  </template>
37
37
  <template v-else-if="item.type === 'select' && item.options">
38
- <select class="settings-select" v-model="settings[item.key]">
39
- <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
40
- {{ opt.label }}
41
- </option>
42
- </select>
38
+ <Dropdown
39
+ :model-value="String(settings[item.key])"
40
+ :options="item.options"
41
+ size="sm"
42
+ min-width="100px"
43
+ @update:model-value="settings[item.key] = $event"
44
+ />
43
45
  </template>
44
46
  </label>
45
47
  </div>
@@ -82,11 +84,13 @@
82
84
  />
83
85
  </template>
84
86
  <template v-else-if="item.type === 'select' && item.options">
85
- <select class="settings-select" v-model="settings[item.key]">
86
- <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
87
- {{ opt.label }}
88
- </option>
89
- </select>
87
+ <Dropdown
88
+ :model-value="String(settings[item.key])"
89
+ :options="item.options"
90
+ size="sm"
91
+ min-width="100px"
92
+ @update:model-value="settings[item.key] = $event"
93
+ />
90
94
  </template>
91
95
  </label>
92
96
  </div>
@@ -164,6 +168,7 @@ import {
164
168
  import { normalizeColorPresetSettings } from '@363045841yyt/klinechart-core'
165
169
  import ColorPresetPanel from './ColorPresetPanel.vue'
166
170
  import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
171
+ import Dropdown from './Dropdown.vue'
167
172
 
168
173
  const props = defineProps<{
169
174
  show: boolean
@@ -387,30 +392,6 @@ function confirmSettings() {
387
392
  accent-color: var(--klc-color-foreground);
388
393
  }
389
394
 
390
- .settings-select {
391
- flex: 0 0 auto;
392
- height: 30px;
393
- padding: 4px 28px 4px 9px;
394
- border: 1px solid var(--klc-color-axis-line);
395
- border-radius: 6px;
396
- background: var(--klc-color-tag-bg-white);
397
- color: var(--klc-color-foreground);
398
- font-size: 12px;
399
- cursor: pointer;
400
- outline: none;
401
- min-width: 150px;
402
- max-width: 190px;
403
- }
404
-
405
- .settings-select:hover {
406
- border-color: var(--klc-color-axis-text);
407
- }
408
-
409
- .settings-select:focus {
410
- border-color: var(--klc-color-axis-text);
411
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-axis-text) 15%, transparent);
412
- }
413
-
414
395
  .settings-section-divider {
415
396
  display: flex;
416
397
  align-items: center;
@@ -602,11 +583,6 @@ function confirmSettings() {
602
583
  margin-top: -26px;
603
584
  }
604
585
 
605
- .settings-select {
606
- width: 100%;
607
- max-width: none;
608
- }
609
-
610
586
  .settings-footer {
611
587
  align-items: stretch;
612
588
  flex-direction: column-reverse;
@@ -15,28 +15,21 @@
15
15
  />
16
16
  </div>
17
17
 
18
- <select
19
- class="toolbar-select"
20
- :value="drawing.style.strokeWidth ?? 1"
21
- @change="onWidthChange(Number(($event.target as HTMLSelectElement).value))"
18
+ <Dropdown
19
+ :model-value="String(drawing.style.strokeWidth ?? 1)"
20
+ :options="widthOptions"
21
+ size="sm"
22
22
  title="线宽"
23
- >
24
- <option :value="1">1px</option>
25
- <option :value="2">2px</option>
26
- <option :value="3">3px</option>
27
- <option :value="4">4px</option>
28
- </select>
29
-
30
- <select
31
- class="toolbar-select"
32
- :value="drawing.style.strokeStyle ?? 'solid'"
33
- @change="onLineStyleChange(($event.target as HTMLSelectElement).value as 'solid' | 'dashed' | 'dotted')"
23
+ @update:model-value="onWidthChange(Number($event))"
24
+ />
25
+
26
+ <Dropdown
27
+ :model-value="drawing.style.strokeStyle ?? 'solid'"
28
+ :options="styleOptions"
29
+ size="sm"
34
30
  title="线型"
35
- >
36
- <option value="solid">实线</option>
37
- <option value="dashed">虚线</option>
38
- <option value="dotted">点线</option>
39
- </select>
31
+ @update:model-value="onLineStyleChange($event as 'solid' | 'dashed' | 'dotted')"
32
+ />
40
33
 
41
34
  <button type="button" class="toolbar-btn delete-btn" title="删除" @click="$emit('delete')">
42
35
  <svg class="delete-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -51,6 +44,20 @@
51
44
  <script setup lang="ts">
52
45
  import { onMounted, onUnmounted } from 'vue'
53
46
  import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
47
+ import Dropdown from './Dropdown.vue'
48
+
49
+ const widthOptions = [
50
+ { label: '1px', value: '1' },
51
+ { label: '2px', value: '2' },
52
+ { label: '3px', value: '3' },
53
+ { label: '4px', value: '4' },
54
+ ]
55
+
56
+ const styleOptions = [
57
+ { label: '实线', value: 'solid' },
58
+ { label: '虚线', value: 'dashed' },
59
+ { label: '点线', value: 'dotted' },
60
+ ]
54
61
 
55
62
  const props = defineProps<{
56
63
  drawing: DrawingObject
@@ -136,22 +143,6 @@ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
136
143
  height: 100%;
137
144
  }
138
145
 
139
- .toolbar-select {
140
- height: 24px;
141
- padding: 0 4px;
142
- border: 1px solid var(--klc-color-axis-line);
143
- border-radius: 4px;
144
- background: var(--klc-color-tag-bg-white);
145
- color: var(--klc-color-foreground);
146
- font-size: 12px;
147
- cursor: pointer;
148
- outline: none;
149
- }
150
-
151
- .toolbar-select:hover {
152
- border-color: var(--klc-color-axis-text);
153
- }
154
-
155
146
  .toolbar-btn {
156
147
  display: inline-flex;
157
148
  align-items: center;
@@ -0,0 +1,291 @@
1
+ <template>
2
+ <div ref="rootRef" class="dropdown" :class="[`dropdown--${size}`, { 'is-open': isOpen }]">
3
+ <button
4
+ ref="triggerRef"
5
+ type="button"
6
+ class="dropdown__trigger"
7
+ :title="title"
8
+ :style="triggerStyle"
9
+ aria-haspopup="listbox"
10
+ :aria-expanded="isOpen"
11
+ @click="toggleOpen"
12
+ @keydown.escape.stop="close"
13
+ @keydown.down.prevent="open"
14
+ @keydown.enter.prevent="toggleOpen"
15
+ @keydown.space.prevent="toggleOpen"
16
+ >
17
+ <span v-if="label" class="dropdown__label">{{ label }}</span>
18
+ <span class="dropdown__value">{{ selectedOption.label }}</span>
19
+ <span class="dropdown__chevron" aria-hidden="true"></span>
20
+ </button>
21
+
22
+ <div v-if="isOpen" class="dropdown__menu" :style="menuStyle" role="listbox" tabindex="-1">
23
+ <button
24
+ v-for="option in options"
25
+ :key="option.value"
26
+ type="button"
27
+ class="dropdown__option"
28
+ :class="{ 'is-selected': option.value === selectedValue }"
29
+ role="option"
30
+ :aria-selected="option.value === selectedValue"
31
+ @click="selectOption(option.value)"
32
+ >
33
+ {{ option.label }}
34
+ </button>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script lang="ts">
40
+ let activeDropdownId = 0
41
+ let activeDropdownClose: (() => void) | null = null
42
+ let dropdownIdSeed = 0
43
+ </script>
44
+
45
+ <script setup lang="ts">
46
+ import { computed, onBeforeUnmount, ref } from 'vue'
47
+
48
+ export interface DropdownOption<T extends string = string> {
49
+ label: string
50
+ value: T
51
+ }
52
+
53
+ const props = withDefaults(
54
+ defineProps<{
55
+ modelValue?: string
56
+ options: DropdownOption[]
57
+ size?: 'sm' | 'md'
58
+ minWidth?: string
59
+ label?: string
60
+ title?: string
61
+ }>(),
62
+ {
63
+ size: 'md',
64
+ title: '',
65
+ },
66
+ )
67
+
68
+ const emit = defineEmits<{
69
+ (e: 'update:modelValue', level: string): void
70
+ }>()
71
+
72
+ const rootRef = ref<HTMLElement | null>(null)
73
+ const triggerRef = ref<HTMLElement | null>(null)
74
+ const isOpen = ref(false)
75
+ const menuWidth = ref(0)
76
+ const dropdownId = ++dropdownIdSeed
77
+
78
+ const triggerStyle = computed(() => {
79
+ if (props.minWidth) return { minWidth: props.minWidth }
80
+ return {}
81
+ })
82
+
83
+ const menuStyle = computed(() => {
84
+ if (!isOpen.value) return undefined
85
+ const w = menuWidth.value || (props.minWidth ? parseInt(props.minWidth) : 0)
86
+ return { width: w ? `${w}px` : undefined }
87
+ })
88
+
89
+ const selectedValue = computed(() => {
90
+ const val = props.modelValue?.trim()
91
+ const found = val && props.options.some((option) => option.value === val)
92
+ return found ? val : (props.options[0]?.value ?? '')
93
+ })
94
+
95
+ const selectedOption = computed(() => {
96
+ return props.options.find((option) => option.value === selectedValue.value) ?? props.options[0]
97
+ })
98
+
99
+ function open() {
100
+ if (activeDropdownId !== dropdownId && activeDropdownClose) {
101
+ activeDropdownClose()
102
+ }
103
+
104
+ if (isOpen.value) return
105
+
106
+ activeDropdownId = dropdownId
107
+ activeDropdownClose = close
108
+ menuWidth.value = triggerRef.value?.offsetWidth ?? 0
109
+ isOpen.value = true
110
+ document.addEventListener('pointerdown', handleDocumentPointerDown)
111
+ }
112
+
113
+ function close() {
114
+ if (!isOpen.value) return
115
+ isOpen.value = false
116
+ if (activeDropdownId === dropdownId) {
117
+ activeDropdownId = 0
118
+ activeDropdownClose = null
119
+ }
120
+ document.removeEventListener('pointerdown', handleDocumentPointerDown)
121
+ }
122
+
123
+ function toggleOpen() {
124
+ if (isOpen.value) {
125
+ close()
126
+ } else {
127
+ open()
128
+ }
129
+ }
130
+
131
+ function selectOption(value: string) {
132
+ emit('update:modelValue', value)
133
+ close()
134
+ }
135
+
136
+ function handleDocumentPointerDown(event: PointerEvent) {
137
+ const root = rootRef.value
138
+ if (root && !root.contains(event.target as Node | null)) {
139
+ close()
140
+ }
141
+ }
142
+
143
+ onBeforeUnmount(close)
144
+ </script>
145
+
146
+ <style scoped>
147
+ .dropdown {
148
+ position: relative;
149
+ flex: 0 0 auto;
150
+ }
151
+
152
+ .dropdown__trigger {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ padding: 0 8px;
157
+ border: 1px solid var(--klc-color-border-button);
158
+ border-radius: 4px;
159
+ background: var(--klc-color-background);
160
+ color: var(--klc-color-foreground);
161
+ font: inherit;
162
+ cursor: pointer;
163
+ transition:
164
+ background 0.15s ease,
165
+ border-color 0.15s ease;
166
+ }
167
+
168
+ .dropdown--md .dropdown__trigger {
169
+ height: 28px;
170
+ }
171
+
172
+ .dropdown--sm .dropdown__trigger {
173
+ height: 24px;
174
+ padding: 0 6px;
175
+ gap: 4px;
176
+ }
177
+
178
+ .dropdown__trigger:hover,
179
+ .dropdown__trigger:focus-visible,
180
+ .dropdown.is-open .dropdown__trigger {
181
+ border-color: var(--klc-color-axis-text);
182
+ background: var(--klc-color-grid-minor);
183
+ outline: 0;
184
+ }
185
+
186
+ .dropdown__label {
187
+ color: var(--klc-color-axis-text);
188
+ font-size: 12px;
189
+ line-height: 1;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ .dropdown__value {
194
+ flex: 1 1 auto;
195
+ color: var(--klc-color-foreground);
196
+ font-size: 13px;
197
+ font-weight: 600;
198
+ line-height: 1;
199
+ text-align: left;
200
+ white-space: nowrap;
201
+ }
202
+
203
+ .dropdown--sm .dropdown__value {
204
+ font-size: 12px;
205
+ min-width: 24px;
206
+ }
207
+
208
+ .dropdown__chevron {
209
+ width: 0;
210
+ height: 0;
211
+ border-left: 4px solid transparent;
212
+ border-right: 4px solid transparent;
213
+ border-top: 5px solid var(--klc-color-axis-text);
214
+ transition: transform 0.15s ease;
215
+ }
216
+
217
+ .dropdown.is-open .dropdown__chevron {
218
+ transform: rotate(180deg);
219
+ }
220
+
221
+ .dropdown__menu {
222
+ position: absolute;
223
+ z-index: 30;
224
+ top: calc(100% + 4px);
225
+ left: 0;
226
+ padding: 4px;
227
+ border: 1px solid var(--klc-color-border-button);
228
+ border-radius: 4px;
229
+ background: var(--klc-color-background);
230
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16);
231
+ box-sizing: border-box;
232
+ }
233
+
234
+ .dropdown__option {
235
+ width: 100%;
236
+ height: 28px;
237
+ display: flex;
238
+ align-items: center;
239
+ padding: 0 8px;
240
+ border: 0;
241
+ border-radius: 3px;
242
+ background: transparent;
243
+ color: var(--klc-color-foreground);
244
+ font: inherit;
245
+ font-size: 13px;
246
+ font-weight: 500;
247
+ text-align: left;
248
+ white-space: nowrap;
249
+ cursor: pointer;
250
+ }
251
+
252
+ .dropdown--sm .dropdown__option {
253
+ height: 24px;
254
+ padding: 0 6px;
255
+ font-size: 12px;
256
+ white-space: nowrap;
257
+ }
258
+
259
+ .dropdown__option:hover,
260
+ .dropdown__option:focus-visible {
261
+ background: var(--klc-color-grid-minor);
262
+ outline: 0;
263
+ }
264
+
265
+ .dropdown__option.is-selected {
266
+ color: var(--klc-color-up);
267
+ font-weight: 700;
268
+ }
269
+
270
+ @media (max-width: 768px), (max-height: 640px) {
271
+ .dropdown--md .dropdown__trigger {
272
+ height: 26px;
273
+ gap: 4px;
274
+ padding: 0 6px;
275
+ }
276
+
277
+ .dropdown--md .dropdown__label {
278
+ display: none;
279
+ }
280
+
281
+ .dropdown--md .dropdown__value {
282
+ min-width: 42px;
283
+ font-size: 12px;
284
+ }
285
+
286
+ .dropdown--md .dropdown__option {
287
+ height: 26px;
288
+ font-size: 12px;
289
+ }
290
+ }
291
+ </style>