@363045841yyt/klinechart 0.7.4 → 0.7.5-alpha.2

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.
@@ -1,132 +1,132 @@
1
- /**
2
- * Contract test for @363045841yyt/klinechart.
3
- *
4
- * Phase 1D agent's brief: make these pass without weakening assertions,
5
- * preserving the legacy KMapPlugin.install signature.
6
- */
7
-
8
- import { describe, it, expect, vi, afterEach } from 'vitest'
9
- import { defineComponent, h, nextTick, ref, shallowRef } from 'vue'
10
- import { mount } from '@vue/test-utils'
11
- import * as VueAdapter from '../index'
12
- import { coreSignalToVueRef } from '../index'
13
- import { createMockChartController, createTestSignal } from './_mockController'
14
-
15
- describe('@363045841yyt/klinechart �?public API surface', () => {
16
- it('exports createChart, useChart, useIndicatorSelector, KLineChart, KMapPlugin', () => {
17
- expect(typeof VueAdapter.createChart).toBe('function')
18
- expect(typeof VueAdapter.useChart).toBe('function')
19
- expect(typeof VueAdapter.useIndicatorSelector).toBe('function')
20
- expect(VueAdapter.KLineChart).toBeDefined()
21
- expect(typeof VueAdapter.KMapPlugin.install).toBe('function')
22
- })
23
-
24
- it('KMapPlugin.install is callable with a mock app and registers KLineChart', () => {
25
- const registered: Record<string, unknown> = {}
26
- const mockApp = {
27
- component(name: string, comp: unknown) {
28
- registered[name] = comp
29
- },
30
- } as unknown as Parameters<typeof VueAdapter.KMapPlugin.install>[0]
31
- VueAdapter.KMapPlugin.install(mockApp)
32
- expect(registered.KLineChart).toBe(VueAdapter.KLineChart)
33
- })
34
- })
35
-
36
- describe('@363045841yyt/klinechart �?SSR safety', () => {
37
- it('module import does not touch window or document', () => {
38
- // Import above ran in node env without jsdom. If it touched window, this
39
- // file would not have loaded. Test documents the contract.
40
- expect(true).toBe(true)
41
- })
42
- })
43
-
44
- describe('@363045841yyt/klinechart �?useChart lifecycle', () => {
45
- afterEach(() => {
46
- // Reset the injected factory so other tests start clean.
47
- VueAdapter.__setControllerFactory(null)
48
- })
49
-
50
- it('mounts on first render via template ref', async () => {
51
- const mockController = createMockChartController({ data: [] })
52
- const factorySpy = vi.fn(() => mockController)
53
- VueAdapter.__setControllerFactory(factorySpy)
54
-
55
- const HostComponent = defineComponent({
56
- name: 'Host',
57
- setup() {
58
- const containerRef = ref<HTMLElement | null>(null)
59
- const { chart } = VueAdapter.useChart(containerRef, { data: [] })
60
- return { containerRef, chart }
61
- },
62
- render() {
63
- return h('div', { ref: 'containerRef' })
64
- },
65
- })
66
-
67
- const wrapper = mount(HostComponent, { attachTo: document.body })
68
- await nextTick()
69
-
70
- expect(factorySpy).toHaveBeenCalledTimes(1)
71
- const factoryArg = factorySpy.mock.calls[0]?.[0]
72
- expect(factoryArg?.container).toBeInstanceOf(HTMLElement)
73
- expect(wrapper.vm.chart).toBe(mockController)
74
-
75
- wrapper.unmount()
76
- })
77
-
78
- it('disposes on unmount', async () => {
79
- const mockController = createMockChartController({ data: [] })
80
- VueAdapter.__setControllerFactory(() => mockController)
81
-
82
- const HostComponent = defineComponent({
83
- name: 'Host',
84
- setup() {
85
- const containerRef = ref<HTMLElement | null>(null)
86
- const { chart } = VueAdapter.useChart(containerRef, { data: [] })
87
- return { containerRef, chart }
88
- },
89
- render() {
90
- return h('div', { ref: 'containerRef' })
91
- },
92
- })
93
-
94
- const wrapper = mount(HostComponent, { attachTo: document.body })
95
- await nextTick()
96
-
97
- expect(mockController.disposeCalls()).toBe(0)
98
- wrapper.unmount()
99
- // Allow lifecycle hooks to settle.
100
- await nextTick()
101
- expect(mockController.disposeCalls()).toBe(1)
102
- })
103
-
104
- it('reactivity bridge: signal change updates returned ref', async () => {
105
- // Mount a tiny scoped component so coreSignalToVueRef can register
106
- // its onScopeDispose cleanup. Without a setup scope the ref is still
107
- // wired up correctly, but cleanup would not be automatic.
108
- const signal = createTestSignal<number>(1)
109
- const bridgedRef = shallowRef<{ value: number } | null>(null)
110
-
111
- const HostComponent = defineComponent({
112
- name: 'BridgeHost',
113
- setup() {
114
- const r = coreSignalToVueRef(signal)
115
- bridgedRef.value = r as unknown as { value: number }
116
- return () => h('div', String(r.value))
117
- },
118
- })
119
-
120
- const wrapper = mount(HostComponent, { attachTo: document.body })
121
- expect(bridgedRef.value?.value).toBe(1)
122
- expect(wrapper.text()).toBe('1')
123
-
124
- signal.set(42)
125
- await nextTick()
126
-
127
- expect(bridgedRef.value?.value).toBe(42)
128
- expect(wrapper.text()).toBe('42')
129
-
130
- wrapper.unmount()
131
- })
132
- })
1
+ /**
2
+ * Contract test for @363045841yyt/klinechart.
3
+ *
4
+ * Phase 1D agent's brief: make these pass without weakening assertions,
5
+ * preserving the legacy KMapPlugin.install signature.
6
+ */
7
+
8
+ import { describe, it, expect, vi, afterEach } from 'vitest'
9
+ import { defineComponent, h, nextTick, ref, shallowRef } from 'vue'
10
+ import { mount } from '@vue/test-utils'
11
+ import * as VueAdapter from '../index'
12
+ import { coreSignalToVueRef } from '../index'
13
+ import { createMockChartController, createTestSignal } from './_mockController'
14
+
15
+ describe('@363045841yyt/klinechart �?public API surface', () => {
16
+ it('exports createChart, useChart, useIndicatorSelector, KLineChart, KMapPlugin', () => {
17
+ expect(typeof VueAdapter.createChart).toBe('function')
18
+ expect(typeof VueAdapter.useChart).toBe('function')
19
+ expect(typeof VueAdapter.useIndicatorSelector).toBe('function')
20
+ expect(VueAdapter.KLineChart).toBeDefined()
21
+ expect(typeof VueAdapter.KMapPlugin.install).toBe('function')
22
+ })
23
+
24
+ it('KMapPlugin.install is callable with a mock app and registers KLineChart', () => {
25
+ const registered: Record<string, unknown> = {}
26
+ const mockApp = {
27
+ component(name: string, comp: unknown) {
28
+ registered[name] = comp
29
+ },
30
+ } as unknown as Parameters<typeof VueAdapter.KMapPlugin.install>[0]
31
+ VueAdapter.KMapPlugin.install(mockApp)
32
+ expect(registered.KLineChart).toBe(VueAdapter.KLineChart)
33
+ })
34
+ })
35
+
36
+ describe('@363045841yyt/klinechart �?SSR safety', () => {
37
+ it('module import does not touch window or document', () => {
38
+ // Import above ran in node env without jsdom. If it touched window, this
39
+ // file would not have loaded. Test documents the contract.
40
+ expect(true).toBe(true)
41
+ })
42
+ })
43
+
44
+ describe('@363045841yyt/klinechart �?useChart lifecycle', () => {
45
+ afterEach(() => {
46
+ // Reset the injected factory so other tests start clean.
47
+ VueAdapter.__setControllerFactory(null)
48
+ })
49
+
50
+ it('mounts on first render via template ref', async () => {
51
+ const mockController = createMockChartController({ data: [] })
52
+ const factorySpy = vi.fn(() => mockController)
53
+ VueAdapter.__setControllerFactory(factorySpy)
54
+
55
+ const HostComponent = defineComponent({
56
+ name: 'Host',
57
+ setup() {
58
+ const containerRef = ref<HTMLElement | null>(null)
59
+ const { chart } = VueAdapter.useChart(containerRef, { data: [] })
60
+ return { containerRef, chart }
61
+ },
62
+ render() {
63
+ return h('div', { ref: 'containerRef' })
64
+ },
65
+ })
66
+
67
+ const wrapper = mount(HostComponent, { attachTo: document.body })
68
+ await nextTick()
69
+
70
+ expect(factorySpy).toHaveBeenCalledTimes(1)
71
+ const factoryArg = factorySpy.mock.calls[0]?.[0]
72
+ expect(factoryArg?.container).toBeInstanceOf(HTMLElement)
73
+ expect(wrapper.vm.chart).toBe(mockController)
74
+
75
+ wrapper.unmount()
76
+ })
77
+
78
+ it('disposes on unmount', async () => {
79
+ const mockController = createMockChartController({ data: [] })
80
+ VueAdapter.__setControllerFactory(() => mockController)
81
+
82
+ const HostComponent = defineComponent({
83
+ name: 'Host',
84
+ setup() {
85
+ const containerRef = ref<HTMLElement | null>(null)
86
+ const { chart } = VueAdapter.useChart(containerRef, { data: [] })
87
+ return { containerRef, chart }
88
+ },
89
+ render() {
90
+ return h('div', { ref: 'containerRef' })
91
+ },
92
+ })
93
+
94
+ const wrapper = mount(HostComponent, { attachTo: document.body })
95
+ await nextTick()
96
+
97
+ expect(mockController.disposeCalls()).toBe(0)
98
+ wrapper.unmount()
99
+ // Allow lifecycle hooks to settle.
100
+ await nextTick()
101
+ expect(mockController.disposeCalls()).toBe(1)
102
+ })
103
+
104
+ it('reactivity bridge: signal change updates returned ref', async () => {
105
+ // Mount a tiny scoped component so coreSignalToVueRef can register
106
+ // its onScopeDispose cleanup. Without a setup scope the ref is still
107
+ // wired up correctly, but cleanup would not be automatic.
108
+ const signal = createTestSignal<number>(1)
109
+ const bridgedRef = shallowRef<{ value: number } | null>(null)
110
+
111
+ const HostComponent = defineComponent({
112
+ name: 'BridgeHost',
113
+ setup() {
114
+ const r = coreSignalToVueRef(signal)
115
+ bridgedRef.value = r as unknown as { value: number }
116
+ return () => h('div', String(r.value))
117
+ },
118
+ })
119
+
120
+ const wrapper = mount(HostComponent, { attachTo: document.body })
121
+ expect(bridgedRef.value?.value).toBe(1)
122
+ expect(wrapper.text()).toBe('1')
123
+
124
+ signal.set(42)
125
+ await nextTick()
126
+
127
+ expect(bridgedRef.value?.value).toBe(42)
128
+ expect(wrapper.text()).toBe('42')
129
+
130
+ wrapper.unmount()
131
+ })
132
+ })
@@ -1,199 +1,199 @@
1
- <template>
2
- <div
3
- class="drawing-style-toolbar"
4
- @pointerdown.stop
5
- @pointermove.stop
6
- @pointerup.stop
7
- >
8
- <div class="toolbar-item color-item" title="颜色">
9
- <span class="color-swatch" :style="{ background: drawing.style.stroke ?? '#2962ff' }"></span>
10
- <input
11
- type="color"
12
- class="color-input"
13
- :value="drawing.style.stroke ?? '#2962ff'"
14
- @input="onColorChange(($event.target as HTMLInputElement).value)"
15
- />
16
- </div>
17
-
18
- <select
19
- class="toolbar-select"
20
- :value="drawing.style.strokeWidth ?? 1"
21
- @change="onWidthChange(Number(($event.target as HTMLSelectElement).value))"
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')"
34
- title="线型"
35
- >
36
- <option value="solid">实线</option>
37
- <option value="dashed">虚线</option>
38
- <option value="dotted">点线</option>
39
- </select>
40
-
41
- <button type="button" class="toolbar-btn delete-btn" title="删除" @click="$emit('delete')">
42
- <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">
43
- <path d="M3 6h18" />
44
- <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
45
- <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
46
- </svg>
47
- </button>
48
- </div>
49
- </template>
50
-
51
- <script setup lang="ts">
52
- import { onMounted, onUnmounted } from 'vue'
53
-
54
- export interface DrawingStyle {
55
- stroke?: string
56
- strokeWidth?: number
57
- strokeStyle?: 'solid' | 'dashed' | 'dotted'
58
- fill?: string
59
- }
60
-
61
- export interface DrawingObject {
62
- id: string
63
- type: string
64
- points: { x: number; y: number }[]
65
- style: DrawingStyle
66
- }
67
-
68
- const props = defineProps<{
69
- drawing: DrawingObject
70
- }>()
71
-
72
- const emit = defineEmits<{
73
- (e: 'updateStyle', style: Partial<DrawingStyle>): void
74
- (e: 'delete'): void
75
- }>()
76
-
77
- function onKeyDown(e: KeyboardEvent) {
78
- if (e.key === 'Delete') {
79
- e.preventDefault()
80
- emit('delete')
81
- }
82
- }
83
-
84
- onMounted(() => document.addEventListener('keydown', onKeyDown))
85
- onUnmounted(() => document.removeEventListener('keydown', onKeyDown))
86
-
87
- function onColorChange(color: string) {
88
- emit('updateStyle', { stroke: color })
89
- }
90
-
91
- function onWidthChange(width: number) {
92
- emit('updateStyle', { strokeWidth: width })
93
- }
94
-
95
- function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
96
- emit('updateStyle', { strokeStyle: style })
97
- }
98
- </script>
99
-
100
- <style scoped>
101
- .drawing-style-toolbar {
102
- position: absolute;
103
- left: 50%;
104
- top: 8px;
105
- transform: translateX(-50%);
106
- display: flex;
107
- align-items: center;
108
- gap: 6px;
109
- padding: 4px 8px;
110
- height: 32px;
111
- background: rgba(250, 251, 252, 0.88);
112
- backdrop-filter: blur(8px);
113
- -webkit-backdrop-filter: blur(8px);
114
- border: 1px solid #e5e7eb;
115
- border-radius: 6px;
116
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
117
- z-index: 100;
118
- user-select: none;
119
- pointer-events: auto;
120
- }
121
-
122
- .toolbar-item {
123
- display: inline-flex;
124
- align-items: center;
125
- justify-content: center;
126
- }
127
-
128
- .color-item {
129
- position: relative;
130
- width: 24px;
131
- height: 24px;
132
- }
133
-
134
- .color-swatch {
135
- display: block;
136
- width: 100%;
137
- height: 100%;
138
- border: 1px solid #d1d5db;
139
- border-radius: 4px;
140
- cursor: pointer;
141
- }
142
-
143
- .color-input {
144
- position: absolute;
145
- inset: 0;
146
- opacity: 0;
147
- cursor: pointer;
148
- width: 100%;
149
- height: 100%;
150
- }
151
-
152
- .toolbar-select {
153
- height: 24px;
154
- padding: 0 4px;
155
- border: 1px solid #d1d5db;
156
- border-radius: 4px;
157
- background: #fff;
158
- color: #374151;
159
- font-size: 12px;
160
- cursor: pointer;
161
- outline: none;
162
- }
163
-
164
- .toolbar-select:hover {
165
- border-color: #9ca3af;
166
- }
167
-
168
- .toolbar-btn {
169
- display: inline-flex;
170
- align-items: center;
171
- justify-content: center;
172
- width: 24px;
173
- height: 24px;
174
- padding: 0;
175
- border: 1px solid transparent;
176
- border-radius: 4px;
177
- background: transparent;
178
- color: #6b7280;
179
- cursor: pointer;
180
- transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
181
- }
182
-
183
- .toolbar-btn:hover {
184
- border-color: #d1d5db;
185
- background: #f3f4f6;
186
- color: #374151;
187
- }
188
-
189
- .delete-btn:hover {
190
- color: #dc2626;
191
- border-color: #fca5a5;
192
- background: #fef2f2;
193
- }
194
-
195
- .delete-icon {
196
- width: 14px;
197
- height: 14px;
198
- }
199
- </style>
1
+ <template>
2
+ <div
3
+ class="drawing-style-toolbar"
4
+ @pointerdown.stop
5
+ @pointermove.stop
6
+ @pointerup.stop
7
+ >
8
+ <div class="toolbar-item color-item" title="颜色">
9
+ <span class="color-swatch" :style="{ background: drawing.style.stroke ?? '#2962ff' }"></span>
10
+ <input
11
+ type="color"
12
+ class="color-input"
13
+ :value="drawing.style.stroke ?? '#2962ff'"
14
+ @input="onColorChange(($event.target as HTMLInputElement).value)"
15
+ />
16
+ </div>
17
+
18
+ <select
19
+ class="toolbar-select"
20
+ :value="drawing.style.strokeWidth ?? 1"
21
+ @change="onWidthChange(Number(($event.target as HTMLSelectElement).value))"
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')"
34
+ title="线型"
35
+ >
36
+ <option value="solid">实线</option>
37
+ <option value="dashed">虚线</option>
38
+ <option value="dotted">点线</option>
39
+ </select>
40
+
41
+ <button type="button" class="toolbar-btn delete-btn" title="删除" @click="$emit('delete')">
42
+ <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">
43
+ <path d="M3 6h18" />
44
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
45
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
46
+ </svg>
47
+ </button>
48
+ </div>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ import { onMounted, onUnmounted } from 'vue'
53
+
54
+ export interface DrawingStyle {
55
+ stroke?: string
56
+ strokeWidth?: number
57
+ strokeStyle?: 'solid' | 'dashed' | 'dotted'
58
+ fill?: string
59
+ }
60
+
61
+ export interface DrawingObject {
62
+ id: string
63
+ type: string
64
+ points: { x: number; y: number }[]
65
+ style: DrawingStyle
66
+ }
67
+
68
+ const props = defineProps<{
69
+ drawing: DrawingObject
70
+ }>()
71
+
72
+ const emit = defineEmits<{
73
+ (e: 'updateStyle', style: Partial<DrawingStyle>): void
74
+ (e: 'delete'): void
75
+ }>()
76
+
77
+ function onKeyDown(e: KeyboardEvent) {
78
+ if (e.key === 'Delete') {
79
+ e.preventDefault()
80
+ emit('delete')
81
+ }
82
+ }
83
+
84
+ onMounted(() => document.addEventListener('keydown', onKeyDown))
85
+ onUnmounted(() => document.removeEventListener('keydown', onKeyDown))
86
+
87
+ function onColorChange(color: string) {
88
+ emit('updateStyle', { stroke: color })
89
+ }
90
+
91
+ function onWidthChange(width: number) {
92
+ emit('updateStyle', { strokeWidth: width })
93
+ }
94
+
95
+ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
96
+ emit('updateStyle', { strokeStyle: style })
97
+ }
98
+ </script>
99
+
100
+ <style scoped>
101
+ .drawing-style-toolbar {
102
+ position: absolute;
103
+ left: 50%;
104
+ top: 8px;
105
+ transform: translateX(-50%);
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 6px;
109
+ padding: 4px 8px;
110
+ height: 32px;
111
+ background: rgba(250, 251, 252, 0.88);
112
+ backdrop-filter: blur(8px);
113
+ -webkit-backdrop-filter: blur(8px);
114
+ border: 1px solid #e5e7eb;
115
+ border-radius: 6px;
116
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
117
+ z-index: 100;
118
+ user-select: none;
119
+ pointer-events: auto;
120
+ }
121
+
122
+ .toolbar-item {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ }
127
+
128
+ .color-item {
129
+ position: relative;
130
+ width: 24px;
131
+ height: 24px;
132
+ }
133
+
134
+ .color-swatch {
135
+ display: block;
136
+ width: 100%;
137
+ height: 100%;
138
+ border: 1px solid #d1d5db;
139
+ border-radius: 4px;
140
+ cursor: pointer;
141
+ }
142
+
143
+ .color-input {
144
+ position: absolute;
145
+ inset: 0;
146
+ opacity: 0;
147
+ cursor: pointer;
148
+ width: 100%;
149
+ height: 100%;
150
+ }
151
+
152
+ .toolbar-select {
153
+ height: 24px;
154
+ padding: 0 4px;
155
+ border: 1px solid #d1d5db;
156
+ border-radius: 4px;
157
+ background: #fff;
158
+ color: #374151;
159
+ font-size: 12px;
160
+ cursor: pointer;
161
+ outline: none;
162
+ }
163
+
164
+ .toolbar-select:hover {
165
+ border-color: #9ca3af;
166
+ }
167
+
168
+ .toolbar-btn {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ width: 24px;
173
+ height: 24px;
174
+ padding: 0;
175
+ border: 1px solid transparent;
176
+ border-radius: 4px;
177
+ background: transparent;
178
+ color: #6b7280;
179
+ cursor: pointer;
180
+ transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
181
+ }
182
+
183
+ .toolbar-btn:hover {
184
+ border-color: #d1d5db;
185
+ background: #f3f4f6;
186
+ color: #374151;
187
+ }
188
+
189
+ .delete-btn:hover {
190
+ color: #dc2626;
191
+ border-color: #fca5a5;
192
+ background: #fef2f2;
193
+ }
194
+
195
+ .delete-icon {
196
+ width: 14px;
197
+ height: 14px;
198
+ }
199
+ </style>