@363045841yyt/klinechart-core 0.7.3 → 0.7.5
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 +201 -201
- package/README.zh-CN.md +201 -201
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +1 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/engine/chart.d.ts +11 -19
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +92 -109
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
- package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
- package/dist/engine/renderers/webgl/candleSurface.js +47 -47
- package/dist/engine/subPaneManager.d.ts +4 -0
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +13 -0
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +129 -122
- package/src/__tests__/signal.test.ts +124 -124
- package/src/config/chartSettings.ts +66 -66
- package/src/controllers/__tests__/drawing.test.ts +214 -214
- package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
- package/src/controllers/__tests__/toolbar.test.ts +225 -225
- package/src/controllers/createChartController.ts +665 -665
- package/src/controllers/createDrawingController.ts +96 -96
- package/src/controllers/createIndicatorSelectorController.ts +307 -307
- package/src/controllers/createToolbarController.ts +146 -146
- package/src/controllers/index.ts +20 -19
- package/src/controllers/types.ts +284 -284
- package/src/engine/__tests__/chart.dpr.test.ts +401 -401
- package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
- package/src/engine/chart-store.ts +121 -121
- package/src/engine/chart.d.ts +617 -617
- package/src/engine/chart.ts +2803 -2815
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
- package/src/engine/controller/interaction.ts +722 -722
- package/src/engine/controller/markerInteraction.ts +130 -130
- package/src/engine/controller/pinchTracker.ts +82 -82
- package/src/engine/controller/tooltipPosition.ts +48 -48
- package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
- package/src/engine/draw/pixelAlign.ts +259 -259
- package/src/engine/drawing/index.ts +655 -655
- package/src/engine/drawing/interaction.ts +842 -842
- package/src/engine/drawing/plugin.ts +343 -343
- package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
- package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
- package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
- package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
- package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
- package/src/engine/indicators/__tests__/atr.test.ts +153 -153
- package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
- package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
- package/src/engine/indicators/__tests__/dema.test.ts +73 -73
- package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
- package/src/engine/indicators/__tests__/hma.test.ts +73 -73
- package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
- package/src/engine/indicators/__tests__/kama.test.ts +80 -80
- package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
- package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
- package/src/engine/indicators/__tests__/roc.test.ts +68 -68
- package/src/engine/indicators/__tests__/sar.test.ts +86 -86
- package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
- package/src/engine/indicators/__tests__/soa.test.ts +533 -533
- package/src/engine/indicators/__tests__/structure.test.ts +110 -110
- package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
- package/src/engine/indicators/__tests__/tema.test.ts +68 -68
- package/src/engine/indicators/__tests__/trix.test.ts +70 -70
- package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
- package/src/engine/indicators/__tests__/volume.test.ts +115 -115
- package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
- package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
- package/src/engine/indicators/__tests__/wma.test.ts +112 -112
- package/src/engine/indicators/__tests__/zones.test.ts +95 -95
- package/src/engine/indicators/atrState.ts +27 -27
- package/src/engine/indicators/bollState.ts +51 -51
- package/src/engine/indicators/calculators.ts +2593 -2593
- package/src/engine/indicators/cciState.ts +25 -25
- package/src/engine/indicators/chaikinVolState.ts +32 -32
- package/src/engine/indicators/cmfState.ts +27 -27
- package/src/engine/indicators/demaState.ts +27 -27
- package/src/engine/indicators/donchianState.ts +43 -43
- package/src/engine/indicators/eneState.ts +43 -43
- package/src/engine/indicators/expmaState.ts +43 -43
- package/src/engine/indicators/fastkState.ts +25 -25
- package/src/engine/indicators/fibState.ts +41 -41
- package/src/engine/indicators/hmaState.ts +27 -27
- package/src/engine/indicators/hvState.ts +28 -28
- package/src/engine/indicators/ichimokuState.ts +70 -70
- package/src/engine/indicators/indicator.worker.ts +169 -169
- package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
- package/src/engine/indicators/indicatorMetadata.ts +110 -110
- package/src/engine/indicators/indicatorRegistry.ts +106 -106
- package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
- package/src/engine/indicators/kamaState.ts +34 -34
- package/src/engine/indicators/keltnerState.ts +49 -49
- package/src/engine/indicators/kstState.ts +42 -42
- package/src/engine/indicators/maState.ts +36 -36
- package/src/engine/indicators/macdState.ts +76 -76
- package/src/engine/indicators/mfiState.ts +27 -27
- package/src/engine/indicators/momState.ts +25 -25
- package/src/engine/indicators/obvState.ts +25 -25
- package/src/engine/indicators/parkinsonState.ts +28 -28
- package/src/engine/indicators/pivotState.ts +51 -51
- package/src/engine/indicators/pvtState.ts +25 -25
- package/src/engine/indicators/rocState.ts +27 -27
- package/src/engine/indicators/rsiState.ts +65 -65
- package/src/engine/indicators/sarState.ts +41 -41
- package/src/engine/indicators/scheduler.ts +1205 -1205
- package/src/engine/indicators/soa.ts +352 -352
- package/src/engine/indicators/stateComposer.ts +1262 -1262
- package/src/engine/indicators/stochState.ts +26 -26
- package/src/engine/indicators/structureState.ts +69 -69
- package/src/engine/indicators/supertrendState.ts +37 -37
- package/src/engine/indicators/temaState.ts +27 -27
- package/src/engine/indicators/trixState.ts +35 -35
- package/src/engine/indicators/vmaState.ts +27 -27
- package/src/engine/indicators/volumeProfileState.ts +63 -63
- package/src/engine/indicators/vwapState.ts +29 -29
- package/src/engine/indicators/wmaState.ts +27 -27
- package/src/engine/indicators/wmsrState.ts +25 -25
- package/src/engine/indicators/workerProtocol.ts +613 -613
- package/src/engine/indicators/zonesState.ts +47 -47
- package/src/engine/layout/pane.ts +161 -161
- package/src/engine/marker/registry.ts +265 -265
- package/src/engine/paneRenderer.ts +169 -169
- package/src/engine/renderers/Indicator/atr.ts +237 -237
- package/src/engine/renderers/Indicator/boll.ts +317 -317
- package/src/engine/renderers/Indicator/cci.ts +275 -275
- package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
- package/src/engine/renderers/Indicator/cmf.ts +137 -137
- package/src/engine/renderers/Indicator/dema.ts +136 -136
- package/src/engine/renderers/Indicator/donchian.ts +137 -137
- package/src/engine/renderers/Indicator/ene.ts +271 -271
- package/src/engine/renderers/Indicator/expma.ts +197 -197
- package/src/engine/renderers/Indicator/fastk.ts +316 -316
- package/src/engine/renderers/Indicator/fib.ts +141 -141
- package/src/engine/renderers/Indicator/hma.ts +136 -136
- package/src/engine/renderers/Indicator/hv.ts +124 -124
- package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
- package/src/engine/renderers/Indicator/index.ts +241 -241
- package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
- package/src/engine/renderers/Indicator/kama.ts +136 -136
- package/src/engine/renderers/Indicator/keltner.ts +137 -137
- package/src/engine/renderers/Indicator/kst.ts +302 -302
- package/src/engine/renderers/Indicator/ma.ts +200 -200
- package/src/engine/renderers/Indicator/macd.ts +477 -477
- package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
- package/src/engine/renderers/Indicator/mfi.ts +142 -142
- package/src/engine/renderers/Indicator/mom.ts +311 -311
- package/src/engine/renderers/Indicator/obv.ts +123 -123
- package/src/engine/renderers/Indicator/parkinson.ts +124 -124
- package/src/engine/renderers/Indicator/pivot.ts +131 -131
- package/src/engine/renderers/Indicator/pvt.ts +123 -123
- package/src/engine/renderers/Indicator/roc.ts +143 -143
- package/src/engine/renderers/Indicator/rsi.ts +390 -390
- package/src/engine/renderers/Indicator/sar.ts +113 -113
- package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
- package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
- package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
- package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/stoch.ts +359 -359
- package/src/engine/renderers/Indicator/structure.ts +126 -126
- package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
- package/src/engine/renderers/Indicator/supertrend.ts +115 -115
- package/src/engine/renderers/Indicator/tema.ts +136 -136
- package/src/engine/renderers/Indicator/trix.ts +158 -158
- package/src/engine/renderers/Indicator/vma.ts +124 -124
- package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
- package/src/engine/renderers/Indicator/vwap.ts +123 -123
- package/src/engine/renderers/Indicator/wma.ts +136 -136
- package/src/engine/renderers/Indicator/wmsr.ts +328 -328
- package/src/engine/renderers/Indicator/zones.ts +104 -104
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
- package/src/engine/renderers/candle.ts +459 -459
- package/src/engine/renderers/crosshair.ts +69 -69
- package/src/engine/renderers/customMarkers.ts +162 -162
- package/src/engine/renderers/extremaMarkers.ts +246 -246
- package/src/engine/renderers/gridLines.ts +90 -90
- package/src/engine/renderers/lastPrice.ts +97 -97
- package/src/engine/renderers/paneTitle.ts +136 -136
- package/src/engine/renderers/subVolume.ts +236 -236
- package/src/engine/renderers/timeAxis.ts +121 -121
- package/src/engine/renderers/webgl/candleSurface.ts +955 -955
- package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
- package/src/engine/renderers/yAxis.ts +105 -105
- package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
- package/src/engine/scale/logFormula.ts +130 -130
- package/src/engine/scale/price.ts +39 -39
- package/src/engine/scale/priceScale.ts +264 -264
- package/src/engine/subPaneManager.ts +442 -427
- package/src/engine/theme/colors.ts +642 -642
- package/src/engine/theme/fonts.ts +20 -20
- package/src/engine/utils/klineConfig.ts +49 -49
- package/src/engine/utils/tickCount.ts +11 -11
- package/src/engine/utils/tickPosition.ts +214 -214
- package/src/engine/utils/zoom.ts +83 -83
- package/src/engine/viewport/viewport.ts +67 -67
- package/src/index.ts +3 -3
- package/src/plugin/ConfigManager.ts +93 -93
- package/src/plugin/EventBus.ts +77 -77
- package/src/plugin/HookSystem.ts +106 -106
- package/src/plugin/PluginHost.ts +243 -243
- package/src/plugin/PluginRegistry.ts +92 -92
- package/src/plugin/StateStore.ts +73 -73
- package/src/plugin/index.ts +19 -19
- package/src/plugin/rendererPluginManager.ts +368 -368
- package/src/plugin/stateKeys.ts +8 -8
- package/src/plugin/types.ts +526 -526
- package/src/reactivity/index.ts +2 -2
- package/src/reactivity/signal.ts +119 -119
- package/src/semantic/controller.ts +251 -251
- package/src/semantic/drawShape.ts +260 -260
- package/src/semantic/index.ts +28 -28
- package/src/semantic/schema.json +256 -256
- package/src/semantic/types.ts +251 -251
- package/src/semantic/validator.ts +349 -349
- package/src/types/kLine.ts +13 -13
- package/src/types/price.ts +56 -56
- package/src/types/volumePrice.ts +33 -33
- package/src/utils/dateFormat.ts +208 -208
- package/src/utils/kLineDraw/axis.ts +562 -562
- package/src/utils/priceToY.ts +34 -34
- package/src/utils/volumePrice.ts +202 -202
- package/src/version.ts +1 -1
|
@@ -1,307 +1,307 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IndicatorSelectorController — framework-agnostic implementation.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from src/components/IndicatorSelector.vue. This module owns the
|
|
5
|
-
* state machine (catalog/active/menu/search), the derived views
|
|
6
|
-
* (filteredMain/filteredSub), and the mutations (add/remove/updateParams/
|
|
7
|
-
* reorder). It exposes everything as signals so React/Vue/Angular adapters
|
|
8
|
-
* can bridge to their own reactivity without coupling to a framework.
|
|
9
|
-
*
|
|
10
|
-
* Rendering (template, CSS, drag-drop DOM events, Teleport, modal overlay)
|
|
11
|
-
* stays in the Vue adapter — the controller only deals with pure data.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createSignal, computed, type Signal } from '../reactivity'
|
|
15
|
-
import type {
|
|
16
|
-
ActiveIndicator,
|
|
17
|
-
IndicatorDefinition,
|
|
18
|
-
IndicatorSelectorController,
|
|
19
|
-
} from './types'
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Helpers
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
type ParamValue = number | string | boolean
|
|
26
|
-
type ParamRecord = Readonly<Record<string, ParamValue>>
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Build the default param map for a definition. Falls back to `min` then `0`
|
|
30
|
-
* when a param has no `default` declared.
|
|
31
|
-
*/
|
|
32
|
-
function buildDefaultParams(definition: IndicatorDefinition): ParamRecord {
|
|
33
|
-
const out: Record<string, ParamValue> = {}
|
|
34
|
-
for (const p of definition.params) {
|
|
35
|
-
if (p.default !== undefined) {
|
|
36
|
-
out[p.key] = p.default
|
|
37
|
-
} else if (typeof p.min === 'number') {
|
|
38
|
-
out[p.key] = p.min
|
|
39
|
-
} else {
|
|
40
|
-
out[p.key] = 0
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return out
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Generate a unique instance id. The id is opaque to consumers — they should
|
|
48
|
-
* not parse it. Uses crypto.randomUUID when available, falls back to a counter.
|
|
49
|
-
*/
|
|
50
|
-
let instanceCounter = 0
|
|
51
|
-
function nextInstanceId(definitionId: string): string {
|
|
52
|
-
instanceCounter += 1
|
|
53
|
-
// Avoid any dependency on the runtime crypto API — a monotonically
|
|
54
|
-
// increasing counter scoped to the module is sufficient for uniqueness
|
|
55
|
-
// within a single process and keeps test output deterministic.
|
|
56
|
-
return `${definitionId}#${instanceCounter}`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Case-insensitive partial match on either label or name.
|
|
61
|
-
*/
|
|
62
|
-
function matchesQuery(def: IndicatorDefinition, q: string): boolean {
|
|
63
|
-
if (q.length === 0) return true
|
|
64
|
-
const needle = q.toLowerCase()
|
|
65
|
-
return (
|
|
66
|
-
def.label.toLowerCase().includes(needle) ||
|
|
67
|
-
def.name?.toLowerCase().includes(needle) === true
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Factory
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
export interface IndicatorSelectorInit {
|
|
76
|
-
catalog?: ReadonlyArray<IndicatorDefinition>
|
|
77
|
-
active?: ReadonlyArray<ActiveIndicator>
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function createIndicatorSelectorController(
|
|
81
|
-
initial?: IndicatorSelectorInit,
|
|
82
|
-
): IndicatorSelectorController {
|
|
83
|
-
// -------------------------------------------------------------------
|
|
84
|
-
// Signals (state)
|
|
85
|
-
// -------------------------------------------------------------------
|
|
86
|
-
const catalog: Signal<ReadonlyArray<IndicatorDefinition>> = createSignal<
|
|
87
|
-
ReadonlyArray<IndicatorDefinition>
|
|
88
|
-
>(initial?.catalog ?? [])
|
|
89
|
-
const active: Signal<ReadonlyArray<ActiveIndicator>> = createSignal<
|
|
90
|
-
ReadonlyArray<ActiveIndicator>
|
|
91
|
-
>(initial?.active ?? [])
|
|
92
|
-
const menuOpen = createSignal(false)
|
|
93
|
-
const searchQuery = createSignal('')
|
|
94
|
-
|
|
95
|
-
// -------------------------------------------------------------------
|
|
96
|
-
// Computed (derived views)
|
|
97
|
-
//
|
|
98
|
-
// The interface types these as Signal<T>, but they are derived state. We
|
|
99
|
-
// expose them through a Signal-shaped facade where `set` is a no-op so
|
|
100
|
-
// consumers can't accidentally write to a derived view (it would be
|
|
101
|
-
// overwritten on the next dependency change anyway). The read/peek/
|
|
102
|
-
// subscribe semantics match a real Signal exactly.
|
|
103
|
-
// -------------------------------------------------------------------
|
|
104
|
-
function toReadonlySignal<T>(
|
|
105
|
-
c: ReturnType<typeof computed<T>>,
|
|
106
|
-
): Signal<T> {
|
|
107
|
-
const read = (): T => c()
|
|
108
|
-
return Object.assign(read, {
|
|
109
|
-
peek: c.peek,
|
|
110
|
-
subscribe: c.subscribe,
|
|
111
|
-
set: (_: T): void => {
|
|
112
|
-
// derived signal — writes are intentionally a no-op
|
|
113
|
-
},
|
|
114
|
-
}) as Signal<T>
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const filteredMain: Signal<ReadonlyArray<IndicatorDefinition>> =
|
|
118
|
-
toReadonlySignal(
|
|
119
|
-
computed<ReadonlyArray<IndicatorDefinition>>(() => {
|
|
120
|
-
const q = searchQuery()
|
|
121
|
-
return catalog().filter(
|
|
122
|
-
(d) => d.role === 'main' && matchesQuery(d, q),
|
|
123
|
-
)
|
|
124
|
-
}),
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
const filteredSub: Signal<ReadonlyArray<IndicatorDefinition>> =
|
|
128
|
-
toReadonlySignal(
|
|
129
|
-
computed<ReadonlyArray<IndicatorDefinition>>(() => {
|
|
130
|
-
const q = searchQuery()
|
|
131
|
-
return catalog().filter(
|
|
132
|
-
(d) => d.role === 'sub' && matchesQuery(d, q),
|
|
133
|
-
)
|
|
134
|
-
}),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
// -------------------------------------------------------------------
|
|
138
|
-
// Lookup helpers (use peek — we don't want mutations to track)
|
|
139
|
-
// -------------------------------------------------------------------
|
|
140
|
-
function findDefinition(definitionId: string): IndicatorDefinition | null {
|
|
141
|
-
const list = catalog.peek()
|
|
142
|
-
for (const d of list) {
|
|
143
|
-
if (d.id === definitionId) return d
|
|
144
|
-
}
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function isActiveByDefinitionId(definitionId: string): boolean {
|
|
149
|
-
const list = active.peek()
|
|
150
|
-
for (const a of list) {
|
|
151
|
-
if (a.definitionId === definitionId) return true
|
|
152
|
-
}
|
|
153
|
-
return false
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// -------------------------------------------------------------------
|
|
157
|
-
// Mutations
|
|
158
|
-
// -------------------------------------------------------------------
|
|
159
|
-
function add(definitionId: string): string | null {
|
|
160
|
-
if (isActiveByDefinitionId(definitionId)) return null
|
|
161
|
-
const def = findDefinition(definitionId)
|
|
162
|
-
if (def === null) return null
|
|
163
|
-
|
|
164
|
-
const instanceId = nextInstanceId(definitionId)
|
|
165
|
-
const newInstance: ActiveIndicator = {
|
|
166
|
-
id: instanceId,
|
|
167
|
-
definitionId: def.id,
|
|
168
|
-
label: def.label,
|
|
169
|
-
name: def.name ?? def.label,
|
|
170
|
-
role: def.role,
|
|
171
|
-
params: buildDefaultParams(def),
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Main indicators are mutually exclusive — replace any existing main.
|
|
175
|
-
// Sub indicators append to the end (display ordering: mains first,
|
|
176
|
-
// then subs in insertion order).
|
|
177
|
-
const current = active.peek()
|
|
178
|
-
if (def.role === 'main') {
|
|
179
|
-
const withoutMains = current.filter((a) => a.role !== 'main')
|
|
180
|
-
// mains come first
|
|
181
|
-
active.set([newInstance, ...withoutMains])
|
|
182
|
-
} else {
|
|
183
|
-
active.set([...current, newInstance])
|
|
184
|
-
}
|
|
185
|
-
return instanceId
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function remove(instanceId: string): boolean {
|
|
189
|
-
const current = active.peek()
|
|
190
|
-
const next = current.filter((a) => a.id !== instanceId)
|
|
191
|
-
if (next.length === current.length) return false
|
|
192
|
-
active.set(next)
|
|
193
|
-
return true
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function updateParams(
|
|
197
|
-
instanceId: string,
|
|
198
|
-
params: Record<string, ParamValue>,
|
|
199
|
-
): boolean {
|
|
200
|
-
const current = active.peek()
|
|
201
|
-
let found = false
|
|
202
|
-
const next = current.map((a) => {
|
|
203
|
-
if (a.id !== instanceId) return a
|
|
204
|
-
found = true
|
|
205
|
-
return {
|
|
206
|
-
...a,
|
|
207
|
-
params: { ...a.params, ...params },
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
if (!found) return false
|
|
211
|
-
active.set(next)
|
|
212
|
-
return true
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function reorder(fromInstanceId: string, toInstanceId: string): boolean {
|
|
216
|
-
if (fromInstanceId === toInstanceId) return false
|
|
217
|
-
const current = active.peek()
|
|
218
|
-
const fromIdx = current.findIndex((a) => a.id === fromInstanceId)
|
|
219
|
-
const toIdx = current.findIndex((a) => a.id === toInstanceId)
|
|
220
|
-
if (fromIdx < 0 || toIdx < 0) return false
|
|
221
|
-
|
|
222
|
-
const fromItem = current[fromIdx]
|
|
223
|
-
const toItem = current[toIdx]
|
|
224
|
-
if (fromItem === undefined || toItem === undefined) return false
|
|
225
|
-
|
|
226
|
-
// Reordering is only allowed within the sub-pane indicators.
|
|
227
|
-
// Main indicators are pinned to the front and cannot be reordered.
|
|
228
|
-
if (fromItem.role !== 'sub' || toItem.role !== 'sub') return false
|
|
229
|
-
|
|
230
|
-
const next = current.slice()
|
|
231
|
-
next.splice(fromIdx, 1)
|
|
232
|
-
next.splice(toIdx, 0, fromItem)
|
|
233
|
-
active.set(next)
|
|
234
|
-
return true
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// -------------------------------------------------------------------
|
|
238
|
-
// Menu / search
|
|
239
|
-
// -------------------------------------------------------------------
|
|
240
|
-
function openMenu(): void {
|
|
241
|
-
menuOpen.set(true)
|
|
242
|
-
}
|
|
243
|
-
function closeMenu(): void {
|
|
244
|
-
menuOpen.set(false)
|
|
245
|
-
}
|
|
246
|
-
function toggleMenu(): void {
|
|
247
|
-
menuOpen.set(!menuOpen.peek())
|
|
248
|
-
}
|
|
249
|
-
function setSearchQuery(q: string): void {
|
|
250
|
-
searchQuery.set(q)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function isActive(definitionId: string): boolean {
|
|
254
|
-
return isActiveByDefinitionId(definitionId)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// -------------------------------------------------------------------
|
|
258
|
-
// Disposal
|
|
259
|
-
// -------------------------------------------------------------------
|
|
260
|
-
// After dispose the controller becomes inert: all signals are replaced
|
|
261
|
-
// with fresh empty signals so previously-attached listeners can no
|
|
262
|
-
// longer receive notifications. We intentionally swap the underlying
|
|
263
|
-
// writers so future calls to add/remove/etc become silent no-ops from
|
|
264
|
-
// the consumer's perspective.
|
|
265
|
-
let disposed = false
|
|
266
|
-
|
|
267
|
-
// Snapshot the signals we expose via the interface so we can rebind them
|
|
268
|
-
// through a stable indirection object. Adapter code holding a reference
|
|
269
|
-
// to `controller.active` still sees a Signal — its subscribers just stop
|
|
270
|
-
// firing because no further writes occur on the original signal.
|
|
271
|
-
function dispose(): void {
|
|
272
|
-
if (disposed) return
|
|
273
|
-
disposed = true
|
|
274
|
-
// We simply guard all mutators so they become no-ops. Existing
|
|
275
|
-
// subscribers receive no further notifications because we don't
|
|
276
|
-
// call .set on the originals after this point.
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Wrap mutators so calls after dispose() are silent no-ops. This is what
|
|
280
|
-
// delivers the "subsequent mutations do not emit" guarantee from the
|
|
281
|
-
// test plan.
|
|
282
|
-
function guard<T extends (...args: never[]) => unknown>(fn: T): T {
|
|
283
|
-
return ((...args: Parameters<T>): ReturnType<T> => {
|
|
284
|
-
if (disposed) return undefined as ReturnType<T>
|
|
285
|
-
return fn(...args) as ReturnType<T>
|
|
286
|
-
}) as T
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
catalog,
|
|
291
|
-
active,
|
|
292
|
-
menuOpen,
|
|
293
|
-
searchQuery,
|
|
294
|
-
filteredMain,
|
|
295
|
-
filteredSub,
|
|
296
|
-
add: guard(add),
|
|
297
|
-
remove: guard(remove),
|
|
298
|
-
updateParams: guard(updateParams),
|
|
299
|
-
reorder: guard(reorder),
|
|
300
|
-
openMenu: guard(openMenu),
|
|
301
|
-
closeMenu: guard(closeMenu),
|
|
302
|
-
toggleMenu: guard(toggleMenu),
|
|
303
|
-
setSearchQuery: guard(setSearchQuery),
|
|
304
|
-
isActive,
|
|
305
|
-
dispose,
|
|
306
|
-
}
|
|
307
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* IndicatorSelectorController — framework-agnostic implementation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from src/components/IndicatorSelector.vue. This module owns the
|
|
5
|
+
* state machine (catalog/active/menu/search), the derived views
|
|
6
|
+
* (filteredMain/filteredSub), and the mutations (add/remove/updateParams/
|
|
7
|
+
* reorder). It exposes everything as signals so React/Vue/Angular adapters
|
|
8
|
+
* can bridge to their own reactivity without coupling to a framework.
|
|
9
|
+
*
|
|
10
|
+
* Rendering (template, CSS, drag-drop DOM events, Teleport, modal overlay)
|
|
11
|
+
* stays in the Vue adapter — the controller only deals with pure data.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createSignal, computed, type Signal } from '../reactivity'
|
|
15
|
+
import type {
|
|
16
|
+
ActiveIndicator,
|
|
17
|
+
IndicatorDefinition,
|
|
18
|
+
IndicatorSelectorController,
|
|
19
|
+
} from './types'
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
type ParamValue = number | string | boolean
|
|
26
|
+
type ParamRecord = Readonly<Record<string, ParamValue>>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the default param map for a definition. Falls back to `min` then `0`
|
|
30
|
+
* when a param has no `default` declared.
|
|
31
|
+
*/
|
|
32
|
+
function buildDefaultParams(definition: IndicatorDefinition): ParamRecord {
|
|
33
|
+
const out: Record<string, ParamValue> = {}
|
|
34
|
+
for (const p of definition.params) {
|
|
35
|
+
if (p.default !== undefined) {
|
|
36
|
+
out[p.key] = p.default
|
|
37
|
+
} else if (typeof p.min === 'number') {
|
|
38
|
+
out[p.key] = p.min
|
|
39
|
+
} else {
|
|
40
|
+
out[p.key] = 0
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a unique instance id. The id is opaque to consumers — they should
|
|
48
|
+
* not parse it. Uses crypto.randomUUID when available, falls back to a counter.
|
|
49
|
+
*/
|
|
50
|
+
let instanceCounter = 0
|
|
51
|
+
function nextInstanceId(definitionId: string): string {
|
|
52
|
+
instanceCounter += 1
|
|
53
|
+
// Avoid any dependency on the runtime crypto API — a monotonically
|
|
54
|
+
// increasing counter scoped to the module is sufficient for uniqueness
|
|
55
|
+
// within a single process and keeps test output deterministic.
|
|
56
|
+
return `${definitionId}#${instanceCounter}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Case-insensitive partial match on either label or name.
|
|
61
|
+
*/
|
|
62
|
+
function matchesQuery(def: IndicatorDefinition, q: string): boolean {
|
|
63
|
+
if (q.length === 0) return true
|
|
64
|
+
const needle = q.toLowerCase()
|
|
65
|
+
return (
|
|
66
|
+
def.label.toLowerCase().includes(needle) ||
|
|
67
|
+
def.name?.toLowerCase().includes(needle) === true
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Factory
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export interface IndicatorSelectorInit {
|
|
76
|
+
catalog?: ReadonlyArray<IndicatorDefinition>
|
|
77
|
+
active?: ReadonlyArray<ActiveIndicator>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createIndicatorSelectorController(
|
|
81
|
+
initial?: IndicatorSelectorInit,
|
|
82
|
+
): IndicatorSelectorController {
|
|
83
|
+
// -------------------------------------------------------------------
|
|
84
|
+
// Signals (state)
|
|
85
|
+
// -------------------------------------------------------------------
|
|
86
|
+
const catalog: Signal<ReadonlyArray<IndicatorDefinition>> = createSignal<
|
|
87
|
+
ReadonlyArray<IndicatorDefinition>
|
|
88
|
+
>(initial?.catalog ?? [])
|
|
89
|
+
const active: Signal<ReadonlyArray<ActiveIndicator>> = createSignal<
|
|
90
|
+
ReadonlyArray<ActiveIndicator>
|
|
91
|
+
>(initial?.active ?? [])
|
|
92
|
+
const menuOpen = createSignal(false)
|
|
93
|
+
const searchQuery = createSignal('')
|
|
94
|
+
|
|
95
|
+
// -------------------------------------------------------------------
|
|
96
|
+
// Computed (derived views)
|
|
97
|
+
//
|
|
98
|
+
// The interface types these as Signal<T>, but they are derived state. We
|
|
99
|
+
// expose them through a Signal-shaped facade where `set` is a no-op so
|
|
100
|
+
// consumers can't accidentally write to a derived view (it would be
|
|
101
|
+
// overwritten on the next dependency change anyway). The read/peek/
|
|
102
|
+
// subscribe semantics match a real Signal exactly.
|
|
103
|
+
// -------------------------------------------------------------------
|
|
104
|
+
function toReadonlySignal<T>(
|
|
105
|
+
c: ReturnType<typeof computed<T>>,
|
|
106
|
+
): Signal<T> {
|
|
107
|
+
const read = (): T => c()
|
|
108
|
+
return Object.assign(read, {
|
|
109
|
+
peek: c.peek,
|
|
110
|
+
subscribe: c.subscribe,
|
|
111
|
+
set: (_: T): void => {
|
|
112
|
+
// derived signal — writes are intentionally a no-op
|
|
113
|
+
},
|
|
114
|
+
}) as Signal<T>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filteredMain: Signal<ReadonlyArray<IndicatorDefinition>> =
|
|
118
|
+
toReadonlySignal(
|
|
119
|
+
computed<ReadonlyArray<IndicatorDefinition>>(() => {
|
|
120
|
+
const q = searchQuery()
|
|
121
|
+
return catalog().filter(
|
|
122
|
+
(d) => d.role === 'main' && matchesQuery(d, q),
|
|
123
|
+
)
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const filteredSub: Signal<ReadonlyArray<IndicatorDefinition>> =
|
|
128
|
+
toReadonlySignal(
|
|
129
|
+
computed<ReadonlyArray<IndicatorDefinition>>(() => {
|
|
130
|
+
const q = searchQuery()
|
|
131
|
+
return catalog().filter(
|
|
132
|
+
(d) => d.role === 'sub' && matchesQuery(d, q),
|
|
133
|
+
)
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// -------------------------------------------------------------------
|
|
138
|
+
// Lookup helpers (use peek — we don't want mutations to track)
|
|
139
|
+
// -------------------------------------------------------------------
|
|
140
|
+
function findDefinition(definitionId: string): IndicatorDefinition | null {
|
|
141
|
+
const list = catalog.peek()
|
|
142
|
+
for (const d of list) {
|
|
143
|
+
if (d.id === definitionId) return d
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isActiveByDefinitionId(definitionId: string): boolean {
|
|
149
|
+
const list = active.peek()
|
|
150
|
+
for (const a of list) {
|
|
151
|
+
if (a.definitionId === definitionId) return true
|
|
152
|
+
}
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -------------------------------------------------------------------
|
|
157
|
+
// Mutations
|
|
158
|
+
// -------------------------------------------------------------------
|
|
159
|
+
function add(definitionId: string): string | null {
|
|
160
|
+
if (isActiveByDefinitionId(definitionId)) return null
|
|
161
|
+
const def = findDefinition(definitionId)
|
|
162
|
+
if (def === null) return null
|
|
163
|
+
|
|
164
|
+
const instanceId = nextInstanceId(definitionId)
|
|
165
|
+
const newInstance: ActiveIndicator = {
|
|
166
|
+
id: instanceId,
|
|
167
|
+
definitionId: def.id,
|
|
168
|
+
label: def.label,
|
|
169
|
+
name: def.name ?? def.label,
|
|
170
|
+
role: def.role,
|
|
171
|
+
params: buildDefaultParams(def),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Main indicators are mutually exclusive — replace any existing main.
|
|
175
|
+
// Sub indicators append to the end (display ordering: mains first,
|
|
176
|
+
// then subs in insertion order).
|
|
177
|
+
const current = active.peek()
|
|
178
|
+
if (def.role === 'main') {
|
|
179
|
+
const withoutMains = current.filter((a) => a.role !== 'main')
|
|
180
|
+
// mains come first
|
|
181
|
+
active.set([newInstance, ...withoutMains])
|
|
182
|
+
} else {
|
|
183
|
+
active.set([...current, newInstance])
|
|
184
|
+
}
|
|
185
|
+
return instanceId
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function remove(instanceId: string): boolean {
|
|
189
|
+
const current = active.peek()
|
|
190
|
+
const next = current.filter((a) => a.id !== instanceId)
|
|
191
|
+
if (next.length === current.length) return false
|
|
192
|
+
active.set(next)
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateParams(
|
|
197
|
+
instanceId: string,
|
|
198
|
+
params: Record<string, ParamValue>,
|
|
199
|
+
): boolean {
|
|
200
|
+
const current = active.peek()
|
|
201
|
+
let found = false
|
|
202
|
+
const next = current.map((a) => {
|
|
203
|
+
if (a.id !== instanceId) return a
|
|
204
|
+
found = true
|
|
205
|
+
return {
|
|
206
|
+
...a,
|
|
207
|
+
params: { ...a.params, ...params },
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
if (!found) return false
|
|
211
|
+
active.set(next)
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function reorder(fromInstanceId: string, toInstanceId: string): boolean {
|
|
216
|
+
if (fromInstanceId === toInstanceId) return false
|
|
217
|
+
const current = active.peek()
|
|
218
|
+
const fromIdx = current.findIndex((a) => a.id === fromInstanceId)
|
|
219
|
+
const toIdx = current.findIndex((a) => a.id === toInstanceId)
|
|
220
|
+
if (fromIdx < 0 || toIdx < 0) return false
|
|
221
|
+
|
|
222
|
+
const fromItem = current[fromIdx]
|
|
223
|
+
const toItem = current[toIdx]
|
|
224
|
+
if (fromItem === undefined || toItem === undefined) return false
|
|
225
|
+
|
|
226
|
+
// Reordering is only allowed within the sub-pane indicators.
|
|
227
|
+
// Main indicators are pinned to the front and cannot be reordered.
|
|
228
|
+
if (fromItem.role !== 'sub' || toItem.role !== 'sub') return false
|
|
229
|
+
|
|
230
|
+
const next = current.slice()
|
|
231
|
+
next.splice(fromIdx, 1)
|
|
232
|
+
next.splice(toIdx, 0, fromItem)
|
|
233
|
+
active.set(next)
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -------------------------------------------------------------------
|
|
238
|
+
// Menu / search
|
|
239
|
+
// -------------------------------------------------------------------
|
|
240
|
+
function openMenu(): void {
|
|
241
|
+
menuOpen.set(true)
|
|
242
|
+
}
|
|
243
|
+
function closeMenu(): void {
|
|
244
|
+
menuOpen.set(false)
|
|
245
|
+
}
|
|
246
|
+
function toggleMenu(): void {
|
|
247
|
+
menuOpen.set(!menuOpen.peek())
|
|
248
|
+
}
|
|
249
|
+
function setSearchQuery(q: string): void {
|
|
250
|
+
searchQuery.set(q)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isActive(definitionId: string): boolean {
|
|
254
|
+
return isActiveByDefinitionId(definitionId)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -------------------------------------------------------------------
|
|
258
|
+
// Disposal
|
|
259
|
+
// -------------------------------------------------------------------
|
|
260
|
+
// After dispose the controller becomes inert: all signals are replaced
|
|
261
|
+
// with fresh empty signals so previously-attached listeners can no
|
|
262
|
+
// longer receive notifications. We intentionally swap the underlying
|
|
263
|
+
// writers so future calls to add/remove/etc become silent no-ops from
|
|
264
|
+
// the consumer's perspective.
|
|
265
|
+
let disposed = false
|
|
266
|
+
|
|
267
|
+
// Snapshot the signals we expose via the interface so we can rebind them
|
|
268
|
+
// through a stable indirection object. Adapter code holding a reference
|
|
269
|
+
// to `controller.active` still sees a Signal — its subscribers just stop
|
|
270
|
+
// firing because no further writes occur on the original signal.
|
|
271
|
+
function dispose(): void {
|
|
272
|
+
if (disposed) return
|
|
273
|
+
disposed = true
|
|
274
|
+
// We simply guard all mutators so they become no-ops. Existing
|
|
275
|
+
// subscribers receive no further notifications because we don't
|
|
276
|
+
// call .set on the originals after this point.
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Wrap mutators so calls after dispose() are silent no-ops. This is what
|
|
280
|
+
// delivers the "subsequent mutations do not emit" guarantee from the
|
|
281
|
+
// test plan.
|
|
282
|
+
function guard<T extends (...args: never[]) => unknown>(fn: T): T {
|
|
283
|
+
return ((...args: Parameters<T>): ReturnType<T> => {
|
|
284
|
+
if (disposed) return undefined as ReturnType<T>
|
|
285
|
+
return fn(...args) as ReturnType<T>
|
|
286
|
+
}) as T
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
catalog,
|
|
291
|
+
active,
|
|
292
|
+
menuOpen,
|
|
293
|
+
searchQuery,
|
|
294
|
+
filteredMain,
|
|
295
|
+
filteredSub,
|
|
296
|
+
add: guard(add),
|
|
297
|
+
remove: guard(remove),
|
|
298
|
+
updateParams: guard(updateParams),
|
|
299
|
+
reorder: guard(reorder),
|
|
300
|
+
openMenu: guard(openMenu),
|
|
301
|
+
closeMenu: guard(closeMenu),
|
|
302
|
+
toggleMenu: guard(toggleMenu),
|
|
303
|
+
setSearchQuery: guard(setSearchQuery),
|
|
304
|
+
isActive,
|
|
305
|
+
dispose,
|
|
306
|
+
}
|
|
307
|
+
}
|