@363045841yyt/klinechart-core 0.7.6 → 0.7.7
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/dist/config/chartSettings.d.ts +27 -2
- package/dist/config/chartSettings.d.ts.map +1 -1
- package/dist/config/chartSettings.js +6 -0
- package/dist/config/chartSettings.js.map +1 -1
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +4 -0
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/draw/pixelAlign.d.ts.map +1 -1
- package/dist/engine/draw/pixelAlign.js.map +1 -1
- package/dist/engine/drawing/plugin.js +1 -1
- package/dist/engine/drawing/plugin.js.map +1 -1
- package/dist/engine/renderers/Indicator/atr.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/atr.js +7 -4
- package/dist/engine/renderers/Indicator/atr.js.map +1 -1
- package/dist/engine/renderers/Indicator/boll.js +12 -12
- package/dist/engine/renderers/Indicator/boll.js.map +1 -1
- package/dist/engine/renderers/Indicator/cci.d.ts +1 -2
- package/dist/engine/renderers/Indicator/cci.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/cci.js +9 -9
- package/dist/engine/renderers/Indicator/cci.js.map +1 -1
- package/dist/engine/renderers/Indicator/ene.js +12 -12
- package/dist/engine/renderers/Indicator/ene.js.map +1 -1
- package/dist/engine/renderers/Indicator/expma.js +6 -6
- package/dist/engine/renderers/Indicator/expma.js.map +1 -1
- package/dist/engine/renderers/Indicator/fastk.d.ts +1 -2
- package/dist/engine/renderers/Indicator/fastk.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/fastk.js +7 -7
- package/dist/engine/renderers/Indicator/fastk.js.map +1 -1
- package/dist/engine/renderers/Indicator/kst.d.ts +1 -2
- package/dist/engine/renderers/Indicator/kst.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/kst.js +10 -10
- package/dist/engine/renderers/Indicator/kst.js.map +1 -1
- package/dist/engine/renderers/Indicator/ma.js +5 -5
- package/dist/engine/renderers/Indicator/ma.js.map +1 -1
- package/dist/engine/renderers/Indicator/macd.d.ts +1 -2
- package/dist/engine/renderers/Indicator/macd.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/macd.js +24 -24
- package/dist/engine/renderers/Indicator/macd.js.map +1 -1
- package/dist/engine/renderers/Indicator/macdLegend.js +6 -6
- package/dist/engine/renderers/Indicator/macdLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +16 -16
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/mom.d.ts +1 -2
- package/dist/engine/renderers/Indicator/mom.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mom.js +8 -8
- package/dist/engine/renderers/Indicator/mom.js.map +1 -1
- package/dist/engine/renderers/Indicator/rsi.d.ts +2 -3
- package/dist/engine/renderers/Indicator/rsi.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/rsi.js +15 -15
- package/dist/engine/renderers/Indicator/rsi.js.map +1 -1
- package/dist/engine/renderers/Indicator/scale/indicator_scale.d.ts +1 -2
- package/dist/engine/renderers/Indicator/scale/indicator_scale.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/scale/indicator_scale.js +5 -5
- package/dist/engine/renderers/Indicator/scale/indicator_scale.js.map +1 -1
- package/dist/engine/renderers/Indicator/stoch.d.ts +1 -2
- package/dist/engine/renderers/Indicator/stoch.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/stoch.js +10 -10
- package/dist/engine/renderers/Indicator/stoch.js.map +1 -1
- package/dist/engine/renderers/Indicator/structure.js +5 -5
- package/dist/engine/renderers/Indicator/structure.js.map +1 -1
- package/dist/engine/renderers/Indicator/wmsr.d.ts +1 -2
- package/dist/engine/renderers/Indicator/wmsr.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/wmsr.js +10 -10
- package/dist/engine/renderers/Indicator/wmsr.js.map +1 -1
- package/dist/engine/renderers/Indicator/zones.js +6 -6
- package/dist/engine/renderers/Indicator/zones.js.map +1 -1
- package/dist/engine/renderers/candle.d.ts +1 -1
- package/dist/engine/renderers/candle.d.ts.map +1 -1
- package/dist/engine/renderers/candle.js +21 -21
- package/dist/engine/renderers/candle.js.map +1 -1
- package/dist/engine/renderers/crosshair.js +3 -3
- package/dist/engine/renderers/crosshair.js.map +1 -1
- package/dist/engine/renderers/extremaMarkers.d.ts.map +1 -1
- package/dist/engine/renderers/extremaMarkers.js +12 -12
- package/dist/engine/renderers/extremaMarkers.js.map +1 -1
- package/dist/engine/renderers/gridLines.js +3 -3
- package/dist/engine/renderers/gridLines.js.map +1 -1
- package/dist/engine/renderers/lastPrice.js +7 -7
- package/dist/engine/renderers/lastPrice.js.map +1 -1
- package/dist/engine/renderers/paneTitle.js +6 -6
- package/dist/engine/renderers/paneTitle.js.map +1 -1
- package/dist/engine/renderers/subVolume.d.ts.map +1 -1
- package/dist/engine/renderers/subVolume.js +23 -20
- package/dist/engine/renderers/subVolume.js.map +1 -1
- package/dist/engine/renderers/timeAxis.js +9 -9
- package/dist/engine/renderers/timeAxis.js.map +1 -1
- package/dist/engine/renderers/webgl/candleSurface.d.ts.map +1 -1
- package/dist/engine/renderers/webgl/candleSurface.js +39 -7
- package/dist/engine/renderers/webgl/candleSurface.js.map +1 -1
- package/dist/engine/renderers/yAxis.d.ts.map +1 -1
- package/dist/engine/renderers/yAxis.js +5 -5
- package/dist/engine/renderers/yAxis.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/types.d.ts +5 -1
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/tokens/colorPresetSettings.d.ts +15 -0
- package/dist/tokens/colorPresetSettings.d.ts.map +1 -0
- package/dist/tokens/colorPresetSettings.js +65 -0
- package/dist/tokens/colorPresetSettings.js.map +1 -0
- package/dist/tokens/index.d.ts +17 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +16 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/mergeTheme.d.ts +17 -0
- package/dist/tokens/mergeTheme.d.ts.map +1 -0
- package/dist/tokens/mergeTheme.js +43 -0
- package/dist/tokens/mergeTheme.js.map +1 -0
- package/dist/tokens/theme-china.d.ts +45 -0
- package/dist/tokens/theme-china.d.ts.map +1 -0
- package/dist/tokens/theme-china.js +116 -0
- package/dist/tokens/theme-china.js.map +1 -0
- package/dist/tokens/theme-dark.d.ts +21 -0
- package/dist/tokens/theme-dark.d.ts.map +1 -0
- package/dist/tokens/theme-dark.js +228 -0
- package/dist/tokens/theme-dark.js.map +1 -0
- package/dist/tokens/theme-light.d.ts +23 -0
- package/dist/tokens/theme-light.d.ts.map +1 -0
- package/dist/tokens/theme-light.js +234 -0
- package/dist/tokens/theme-light.js.map +1 -0
- package/dist/tokens/themeToCssVars.d.ts +74 -0
- package/dist/tokens/themeToCssVars.d.ts.map +1 -0
- package/dist/tokens/themeToCssVars.js +108 -0
- package/dist/tokens/themeToCssVars.js.map +1 -0
- package/dist/tokens/types.d.ts +335 -0
- package/dist/tokens/types.d.ts.map +1 -0
- package/dist/tokens/types.js +20 -0
- package/dist/tokens/types.js.map +1 -0
- package/dist/utils/kLineDraw/axis.d.ts +8 -7
- package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/utils/kLineDraw/axis.js +24 -24
- package/dist/utils/kLineDraw/axis.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/config/chartSettings.ts +11 -2
- package/src/engine/chart.ts +4 -0
- package/src/engine/draw/pixelAlign.ts +0 -2
- package/src/engine/drawing/plugin.ts +1 -1
- package/src/engine/renderers/Indicator/atr.ts +7 -3
- package/src/engine/renderers/Indicator/boll.ts +12 -12
- package/src/engine/renderers/Indicator/cci.ts +11 -10
- package/src/engine/renderers/Indicator/ene.ts +12 -12
- package/src/engine/renderers/Indicator/expma.ts +6 -6
- package/src/engine/renderers/Indicator/fastk.ts +9 -8
- package/src/engine/renderers/Indicator/kst.ts +12 -11
- package/src/engine/renderers/Indicator/ma.ts +5 -5
- package/src/engine/renderers/Indicator/macd.ts +27 -25
- package/src/engine/renderers/Indicator/macdLegend.ts +6 -6
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +16 -16
- package/src/engine/renderers/Indicator/mom.ts +11 -10
- package/src/engine/renderers/Indicator/rsi.ts +18 -15
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +6 -6
- package/src/engine/renderers/Indicator/stoch.ts +12 -11
- package/src/engine/renderers/Indicator/structure.ts +5 -5
- package/src/engine/renderers/Indicator/wmsr.ts +13 -12
- package/src/engine/renderers/Indicator/zones.ts +7 -7
- package/src/engine/renderers/candle.ts +21 -21
- package/src/engine/renderers/crosshair.ts +3 -3
- package/src/engine/renderers/extremaMarkers.ts +13 -12
- package/src/engine/renderers/gridLines.ts +3 -3
- package/src/engine/renderers/lastPrice.ts +7 -7
- package/src/engine/renderers/paneTitle.ts +6 -6
- package/src/engine/renderers/subVolume.ts +23 -20
- package/src/engine/renderers/timeAxis.ts +9 -9
- package/src/engine/renderers/webgl/candleSurface.ts +43 -7
- package/src/engine/renderers/yAxis.ts +6 -5
- package/src/index.ts +1 -0
- package/src/plugin/types.ts +5 -1
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +393 -0
- package/src/tokens/__tests__/baseline.test.ts +183 -0
- package/src/tokens/__tests__/themeToCssVars.test.ts +175 -0
- package/src/tokens/__tests__/tokens.test.ts +215 -0
- package/src/tokens/colorPresetSettings.ts +128 -0
- package/src/tokens/index.ts +65 -0
- package/src/tokens/mergeTheme.ts +48 -0
- package/src/tokens/theme-china.ts +132 -0
- package/src/tokens/theme-dark.ts +244 -0
- package/src/tokens/theme-light.ts +250 -0
- package/src/tokens/themeToCssVars.ts +138 -0
- package/src/tokens/types.ts +394 -0
- package/src/utils/kLineDraw/axis.ts +31 -30
- package/src/version.ts +1 -1
- package/dist/engine/theme/colors.d.ts +0 -223
- package/dist/engine/theme/colors.d.ts.map +0 -1
- package/dist/engine/theme/colors.js +0 -375
- package/dist/engine/theme/colors.js.map +0 -1
- package/src/engine/theme/colors.ts +0 -642
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `themeToCssVars` / `toCssDeclarationBlock`.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria:
|
|
5
|
+
* 1. Every theme key (colors × 32 + palette × 10 + spacing × 9
|
|
6
|
+
* + typography × 9 + motion × 5 = 65) maps to a CSS variable.
|
|
7
|
+
* 2. Variable names follow `--klc-{family}-{kebab-key}` exactly.
|
|
8
|
+
* 3. Camel→kebab transform is correct on edge cases (single-letter,
|
|
9
|
+
* already-kebab, trailing digits).
|
|
10
|
+
* 4. Custom prefix is honoured.
|
|
11
|
+
* 5. Prefix without `--` throws (misuse caught early).
|
|
12
|
+
* 6. mergeTheme overrides surface in the emit output.
|
|
13
|
+
* 7. toCssDeclarationBlock produces parseable CSS.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
lightTheme,
|
|
20
|
+
darkTheme,
|
|
21
|
+
mergeTheme,
|
|
22
|
+
} from '..'
|
|
23
|
+
import { camelToKebab, themeToCssVars, toCssDeclarationBlock } from '../themeToCssVars'
|
|
24
|
+
|
|
25
|
+
describe('camelToKebab', () => {
|
|
26
|
+
it.each([
|
|
27
|
+
['candleUpBody', 'candle-up-body'],
|
|
28
|
+
['fontWeightRegular', 'font-weight-regular'],
|
|
29
|
+
['i1', 'i1'],
|
|
30
|
+
['i10', 'i10'],
|
|
31
|
+
['background', 'background'],
|
|
32
|
+
['durationFast', 'duration-fast'],
|
|
33
|
+
['fontFamilyMono', 'font-family-mono'],
|
|
34
|
+
])('%s → %s', (input, expected) => {
|
|
35
|
+
expect(camelToKebab(input)).toBe(expected)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
function countColorLeaves(c: Record<string, unknown>): number {
|
|
40
|
+
let count = 0
|
|
41
|
+
for (const v of Object.values(c)) {
|
|
42
|
+
if (v !== null && typeof v === 'object') {
|
|
43
|
+
count += countColorLeaves(v as Record<string, unknown>)
|
|
44
|
+
} else {
|
|
45
|
+
count++
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return count
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('themeToCssVars — coverage', () => {
|
|
52
|
+
it('emits the expected number of variables for lightTheme', () => {
|
|
53
|
+
const vars = themeToCssVars(lightTheme)
|
|
54
|
+
const expectedCount =
|
|
55
|
+
countColorLeaves(lightTheme.colors as unknown as Record<string, unknown>) +
|
|
56
|
+
Object.keys(lightTheme.spacing).length +
|
|
57
|
+
Object.keys(lightTheme.typography).length +
|
|
58
|
+
Object.keys(lightTheme.motion).length
|
|
59
|
+
expect(Object.keys(vars)).toHaveLength(expectedCount)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('every var name starts with --klc- by default', () => {
|
|
63
|
+
const vars = themeToCssVars(lightTheme)
|
|
64
|
+
for (const k of Object.keys(vars)) {
|
|
65
|
+
expect(k.startsWith('--klc-')).toBe(true)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('every var value is a non-empty string', () => {
|
|
70
|
+
const vars = themeToCssVars(lightTheme)
|
|
71
|
+
for (const v of Object.values(vars)) {
|
|
72
|
+
expect(typeof v).toBe('string')
|
|
73
|
+
expect(v.length).toBeGreaterThan(0)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('themeToCssVars — naming', () => {
|
|
79
|
+
it('colors.background → --klc-color-background', () => {
|
|
80
|
+
const vars = themeToCssVars(lightTheme)
|
|
81
|
+
expect(vars['--klc-color-background']).toBe(lightTheme.colors.background)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('colors.candleUpBody → --klc-color-candle-up-body', () => {
|
|
85
|
+
const vars = themeToCssVars(lightTheme)
|
|
86
|
+
expect(vars['--klc-color-candle-up-body']).toBe(lightTheme.colors.candleUpBody)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('colors.palette.i1 → --klc-color-palette-i1', () => {
|
|
90
|
+
const vars = themeToCssVars(lightTheme)
|
|
91
|
+
expect(vars['--klc-color-palette-i1']).toBe(lightTheme.colors.palette.i1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('spacing.md → --klc-spacing-md', () => {
|
|
95
|
+
const vars = themeToCssVars(lightTheme)
|
|
96
|
+
expect(vars['--klc-spacing-md']).toBe(lightTheme.spacing.md)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('typography.fontWeightRegular → string-cast number', () => {
|
|
100
|
+
const vars = themeToCssVars(lightTheme)
|
|
101
|
+
expect(vars['--klc-typography-font-weight-regular']).toBe(
|
|
102
|
+
String(lightTheme.typography.fontWeightRegular),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('motion.durationFast → --klc-motion-duration-fast', () => {
|
|
107
|
+
const vars = themeToCssVars(lightTheme)
|
|
108
|
+
expect(vars['--klc-motion-duration-fast']).toBe(lightTheme.motion.durationFast)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('themeToCssVars — prefix', () => {
|
|
113
|
+
it('honours a custom prefix', () => {
|
|
114
|
+
const vars = themeToCssVars(lightTheme, { prefix: '--chart-' })
|
|
115
|
+
expect(vars['--chart-color-background']).toBe(lightTheme.colors.background)
|
|
116
|
+
expect(vars['--klc-color-background']).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('throws when the prefix does not start with --', () => {
|
|
120
|
+
expect(() => themeToCssVars(lightTheme, { prefix: 'klc-' })).toThrow(
|
|
121
|
+
/must start with '--'/,
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('themeToCssVars — light vs dark parity', () => {
|
|
127
|
+
it('both themes produce maps with the exact same key set', () => {
|
|
128
|
+
const l = Object.keys(themeToCssVars(lightTheme)).sort()
|
|
129
|
+
const d = Object.keys(themeToCssVars(darkTheme)).sort()
|
|
130
|
+
expect(l).toEqual(d)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('themeToCssVars — mergeTheme propagation', () => {
|
|
135
|
+
it('an override surfaces in the emit output', () => {
|
|
136
|
+
const custom = mergeTheme(lightTheme, {
|
|
137
|
+
colors: { candleUpBody: '#112233' },
|
|
138
|
+
})
|
|
139
|
+
expect(themeToCssVars(custom)['--klc-color-candle-up-body']).toBe('#112233')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('non-overridden keys keep the base value', () => {
|
|
143
|
+
const custom = mergeTheme(lightTheme, {
|
|
144
|
+
colors: { candleUpBody: '#112233' },
|
|
145
|
+
})
|
|
146
|
+
expect(themeToCssVars(custom)['--klc-color-background']).toBe(
|
|
147
|
+
lightTheme.colors.background,
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('toCssDeclarationBlock', () => {
|
|
153
|
+
it('produces a parseable :root block by default', () => {
|
|
154
|
+
const css = toCssDeclarationBlock(themeToCssVars(lightTheme))
|
|
155
|
+
expect(css.startsWith(':root {\n')).toBe(true)
|
|
156
|
+
expect(css.endsWith('\n}')).toBe(true)
|
|
157
|
+
expect(css).toContain('--klc-color-background: ' + lightTheme.colors.background + ';')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('honours a custom selector', () => {
|
|
161
|
+
const css = toCssDeclarationBlock(
|
|
162
|
+
themeToCssVars(darkTheme),
|
|
163
|
+
'[data-theme="dark"]',
|
|
164
|
+
)
|
|
165
|
+
expect(css.startsWith('[data-theme="dark"] {\n')).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('emits one declaration per line', () => {
|
|
169
|
+
const vars = themeToCssVars(lightTheme)
|
|
170
|
+
const css = toCssDeclarationBlock(vars)
|
|
171
|
+
// count of `;` lines should equal the number of vars
|
|
172
|
+
const semicolons = (css.match(/;/g) ?? []).length
|
|
173
|
+
expect(semicolons).toBe(Object.keys(vars).length)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design token contract tests.
|
|
3
|
+
*
|
|
4
|
+
* What this enforces:
|
|
5
|
+
* 1. **Theme parity** — `lightTheme` and `darkTheme` have the exact
|
|
6
|
+
* same key set in every token family. Adding a key to one without
|
|
7
|
+
* the other is the most common source of "looks fine in dev,
|
|
8
|
+
* breaks in production" theme bugs.
|
|
9
|
+
* 2. **Color validity** — every color token parses as a CSS color
|
|
10
|
+
* we can hand the renderer. We accept 6/8-digit hex, plus the
|
|
11
|
+
* single named `transparent` keyword.
|
|
12
|
+
* 3. **WCAG contrast** — bull/bear vs background must reach the AA
|
|
13
|
+
* threshold (≥ 3:1 for non-text graphics). This is the floor;
|
|
14
|
+
* themes are encouraged to exceed it.
|
|
15
|
+
* 4. **Merge semantics** — `mergeTheme(base, override)` is shallow
|
|
16
|
+
* per family, deep on `colors.palette`, and never mutates inputs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
lightTheme,
|
|
23
|
+
darkTheme,
|
|
24
|
+
mergeTheme,
|
|
25
|
+
type Theme,
|
|
26
|
+
type ColorTokens,
|
|
27
|
+
} from '..'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Color parsing & contrast helpers (kept inline so tests don't reach into
|
|
31
|
+
// renderer internals — those will arrive in a later tick).
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const HEX = /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
|
35
|
+
const RGBA = /^rgba?\(/
|
|
36
|
+
const HSLA = /^hsla?\(/
|
|
37
|
+
|
|
38
|
+
function parseHex(color: string): { r: number; g: number; b: number } | null {
|
|
39
|
+
if (!HEX.test(color)) return null
|
|
40
|
+
const r = parseInt(color.slice(1, 3), 16)
|
|
41
|
+
const g = parseInt(color.slice(3, 5), 16)
|
|
42
|
+
const b = parseInt(color.slice(5, 7), 16)
|
|
43
|
+
return { r, g, b }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function relativeLuminance(c: string): number {
|
|
47
|
+
const p = parseHex(c)
|
|
48
|
+
if (p === null) throw new Error(`relativeLuminance: ${c} not parseable`)
|
|
49
|
+
const linear = (v: number): number => {
|
|
50
|
+
const s = v / 255
|
|
51
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
|
|
52
|
+
}
|
|
53
|
+
return 0.2126 * linear(p.r) + 0.7152 * linear(p.g) + 0.0722 * linear(p.b)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function contrast(a: string, b: string): number {
|
|
57
|
+
const la = relativeLuminance(a)
|
|
58
|
+
const lb = relativeLuminance(b)
|
|
59
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
60
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Parity
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function keysSorted(o: object): string[] {
|
|
68
|
+
return Object.keys(o).sort()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('theme parity', () => {
|
|
72
|
+
it('lightTheme and darkTheme expose the same top-level families', () => {
|
|
73
|
+
expect(keysSorted(lightTheme)).toEqual(keysSorted(darkTheme))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it.each([
|
|
77
|
+
['colors', (t: Theme) => t.colors],
|
|
78
|
+
['spacing', (t: Theme) => t.spacing],
|
|
79
|
+
['typography', (t: Theme) => t.typography],
|
|
80
|
+
['motion', (t: Theme) => t.motion],
|
|
81
|
+
] as const)('family %s has the same key set in both themes', (_name, pick) => {
|
|
82
|
+
expect(keysSorted(pick(lightTheme))).toEqual(keysSorted(pick(darkTheme)))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('palette has the same i1..i10 keys in both themes', () => {
|
|
86
|
+
expect(keysSorted(lightTheme.colors.palette)).toEqual(
|
|
87
|
+
keysSorted(darkTheme.colors.palette),
|
|
88
|
+
)
|
|
89
|
+
expect(keysSorted(lightTheme.colors.palette)).toEqual([
|
|
90
|
+
'i1',
|
|
91
|
+
'i10',
|
|
92
|
+
'i2',
|
|
93
|
+
'i3',
|
|
94
|
+
'i4',
|
|
95
|
+
'i5',
|
|
96
|
+
'i6',
|
|
97
|
+
'i7',
|
|
98
|
+
'i8',
|
|
99
|
+
'i9',
|
|
100
|
+
])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Color value validity
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function colorEntries(c: ColorTokens, prefix = ''): Array<[string, string]> {
|
|
109
|
+
const out: Array<[string, string]> = []
|
|
110
|
+
for (const [k, v] of Object.entries(c)) {
|
|
111
|
+
const key = prefix ? `${prefix}.${k}` : k
|
|
112
|
+
if (typeof v === 'object' && v !== null) {
|
|
113
|
+
out.push(...colorEntries(v as unknown as ColorTokens, key))
|
|
114
|
+
} else {
|
|
115
|
+
out.push([key, v as string])
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('color value validity', () => {
|
|
122
|
+
for (const theme of [lightTheme, darkTheme]) {
|
|
123
|
+
for (const [key, value] of colorEntries(theme.colors)) {
|
|
124
|
+
it(`${theme.name}: ${key} is a valid CSS color string`, () => {
|
|
125
|
+
// Top-level tokens are hex; legacy compatibility groups
|
|
126
|
+
// may use rgba/hsl/transparent.
|
|
127
|
+
const valid =
|
|
128
|
+
HEX.test(value) ||
|
|
129
|
+
RGBA.test(value) ||
|
|
130
|
+
HSLA.test(value) ||
|
|
131
|
+
value === 'transparent'
|
|
132
|
+
expect(valid).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// WCAG AA non-text contrast (≥ 3:1) for the bull / bear roles
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
function hexBase(c: string): string {
|
|
143
|
+
// Drop alpha if present so contrast is measured against the unmuted hue.
|
|
144
|
+
return c.length === 9 ? c.slice(0, 7) : c
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe('WCAG AA contrast — candle bull/bear vs background (≥ 3:1)', () => {
|
|
148
|
+
for (const theme of [lightTheme, darkTheme]) {
|
|
149
|
+
for (const role of ['candleUpBody', 'candleDownBody'] as const) {
|
|
150
|
+
it(`${theme.name}: ${role}`, () => {
|
|
151
|
+
const ratio = contrast(
|
|
152
|
+
hexBase(theme.colors[role]),
|
|
153
|
+
hexBase(theme.colors.background),
|
|
154
|
+
)
|
|
155
|
+
expect(ratio).toBeGreaterThanOrEqual(3)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('WCAG AA contrast — foreground text vs background (≥ 4.5:1)', () => {
|
|
162
|
+
for (const theme of [lightTheme, darkTheme]) {
|
|
163
|
+
it(`${theme.name}`, () => {
|
|
164
|
+
const ratio = contrast(
|
|
165
|
+
hexBase(theme.colors.foreground),
|
|
166
|
+
hexBase(theme.colors.background),
|
|
167
|
+
)
|
|
168
|
+
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// mergeTheme
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
describe('mergeTheme', () => {
|
|
178
|
+
it('returns a new theme; never mutates the base', () => {
|
|
179
|
+
const before = JSON.stringify(lightTheme)
|
|
180
|
+
const merged = mergeTheme(lightTheme, {
|
|
181
|
+
name: 'custom',
|
|
182
|
+
colors: { candleUpBody: '#111111' },
|
|
183
|
+
})
|
|
184
|
+
expect(JSON.stringify(lightTheme)).toBe(before)
|
|
185
|
+
expect(merged).not.toBe(lightTheme)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('override wins for each specified key; base wins elsewhere', () => {
|
|
189
|
+
const merged = mergeTheme(lightTheme, {
|
|
190
|
+
colors: { candleUpBody: '#111111' },
|
|
191
|
+
})
|
|
192
|
+
expect(merged.colors.candleUpBody).toBe('#111111')
|
|
193
|
+
expect(merged.colors.candleDownBody).toBe(lightTheme.colors.candleDownBody)
|
|
194
|
+
expect(merged.colors.background).toBe(lightTheme.colors.background)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('palette merges per-key, not whole-replace', () => {
|
|
198
|
+
const merged = mergeTheme(lightTheme, {
|
|
199
|
+
colors: { palette: { i1: '#000000' } },
|
|
200
|
+
})
|
|
201
|
+
expect(merged.colors.palette.i1).toBe('#000000')
|
|
202
|
+
expect(merged.colors.palette.i2).toBe(lightTheme.colors.palette.i2)
|
|
203
|
+
expect(merged.colors.palette.i10).toBe(lightTheme.colors.palette.i10)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('name override propagates', () => {
|
|
207
|
+
const merged = mergeTheme(lightTheme, { name: 'corporate' })
|
|
208
|
+
expect(merged.name).toBe('corporate')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('no override returns a structurally-equal theme', () => {
|
|
212
|
+
const merged = mergeTheme(lightTheme, {})
|
|
213
|
+
expect(merged).toEqual(lightTheme)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ColorTokens, ColorValue } from './types'
|
|
2
|
+
|
|
3
|
+
export type ColorPresetThemeName = 'light' | 'dark'
|
|
4
|
+
|
|
5
|
+
export type ColorPresetKey = keyof Pick<
|
|
6
|
+
ColorTokens,
|
|
7
|
+
| 'background'
|
|
8
|
+
| 'foreground'
|
|
9
|
+
| 'chartBackground'
|
|
10
|
+
| 'candleUpBody'
|
|
11
|
+
| 'candleUpBorder'
|
|
12
|
+
| 'candleUpWick'
|
|
13
|
+
| 'candleDownBody'
|
|
14
|
+
| 'candleDownBorder'
|
|
15
|
+
| 'candleDownWick'
|
|
16
|
+
| 'volumeUp'
|
|
17
|
+
| 'volumeDown'
|
|
18
|
+
| 'axisText'
|
|
19
|
+
| 'axisLine'
|
|
20
|
+
| 'axisTick'
|
|
21
|
+
| 'gridMajor'
|
|
22
|
+
| 'gridMinor'
|
|
23
|
+
| 'crosshairLine'
|
|
24
|
+
| 'crosshairLabelBg'
|
|
25
|
+
| 'crosshairLabelText'
|
|
26
|
+
| 'selectionFill'
|
|
27
|
+
| 'selectionStroke'
|
|
28
|
+
| 'tooltipBg'
|
|
29
|
+
| 'tooltipText'
|
|
30
|
+
| 'tooltipBorder'
|
|
31
|
+
| 'volumeProfilePoc'
|
|
32
|
+
| 'footprintAsk'
|
|
33
|
+
| 'footprintBid'
|
|
34
|
+
| 'footprintImbalance'
|
|
35
|
+
| 'alertActive'
|
|
36
|
+
| 'alertTriggered'
|
|
37
|
+
| 'alertMuted'
|
|
38
|
+
| 'avwapLine'
|
|
39
|
+
| 'avwapBand'
|
|
40
|
+
| 'mtfOverlay'
|
|
41
|
+
>
|
|
42
|
+
|
|
43
|
+
export interface ColorPresetItem {
|
|
44
|
+
readonly key: ColorPresetKey
|
|
45
|
+
readonly label: string
|
|
46
|
+
readonly group: 'canvas' | 'candle' | 'axis' | 'interaction'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ColorPresetOverrides = Partial<Record<ColorPresetKey, ColorValue>>
|
|
50
|
+
|
|
51
|
+
export type ColorPresetSettings = Partial<Record<ColorPresetThemeName, ColorPresetOverrides>>
|
|
52
|
+
|
|
53
|
+
export const COLOR_PRESET_STORAGE_KEY = 'kline-chart-color-presets'
|
|
54
|
+
|
|
55
|
+
export const COLOR_PRESET_ITEMS: readonly ColorPresetItem[] = [
|
|
56
|
+
{ key: 'background', label: '背景', group: 'canvas' },
|
|
57
|
+
{ key: 'chartBackground', label: '图表背景', group: 'canvas' },
|
|
58
|
+
{ key: 'foreground', label: '前景', group: 'canvas' },
|
|
59
|
+
{ key: 'gridMajor', label: '主网格线', group: 'canvas' },
|
|
60
|
+
{ key: 'gridMinor', label: '次网格线', group: 'canvas' },
|
|
61
|
+
|
|
62
|
+
{ key: 'candleUpBody', label: '上涨实体', group: 'candle' },
|
|
63
|
+
{ key: 'candleUpBorder', label: '上涨边框', group: 'candle' },
|
|
64
|
+
{ key: 'candleUpWick', label: '上涨影线', group: 'candle' },
|
|
65
|
+
{ key: 'candleDownBody', label: '下跌实体', group: 'candle' },
|
|
66
|
+
{ key: 'candleDownBorder', label: '下跌边框', group: 'candle' },
|
|
67
|
+
{ key: 'candleDownWick', label: '下跌影线', group: 'candle' },
|
|
68
|
+
{ key: 'volumeUp', label: '上涨成交量', group: 'candle' },
|
|
69
|
+
{ key: 'volumeDown', label: '下跌成交量', group: 'candle' },
|
|
70
|
+
|
|
71
|
+
{ key: 'axisText', label: '坐标文字', group: 'axis' },
|
|
72
|
+
{ key: 'axisLine', label: '坐标轴线', group: 'axis' },
|
|
73
|
+
{ key: 'axisTick', label: '坐标刻度', group: 'axis' },
|
|
74
|
+
|
|
75
|
+
{ key: 'crosshairLine', label: '十字光标', group: 'interaction' },
|
|
76
|
+
{ key: 'crosshairLabelBg', label: '光标标签背景', group: 'interaction' },
|
|
77
|
+
{ key: 'crosshairLabelText', label: '光标标签文字', group: 'interaction' },
|
|
78
|
+
{ key: 'selectionFill', label: '选区填充', group: 'interaction' },
|
|
79
|
+
{ key: 'selectionStroke', label: '选区边框', group: 'interaction' },
|
|
80
|
+
{ key: 'tooltipBg', label: '提示背景', group: 'interaction' },
|
|
81
|
+
{ key: 'tooltipText', label: '提示文字', group: 'interaction' },
|
|
82
|
+
{ key: 'tooltipBorder', label: '提示边框', group: 'interaction' },
|
|
83
|
+
|
|
84
|
+
{ key: 'volumeProfilePoc', label: '成交量 POC', group: 'interaction' },
|
|
85
|
+
{ key: 'footprintAsk', label: '主动买盘', group: 'interaction' },
|
|
86
|
+
{ key: 'footprintBid', label: '主动卖盘', group: 'interaction' },
|
|
87
|
+
{ key: 'footprintImbalance', label: '订单失衡', group: 'interaction' },
|
|
88
|
+
{ key: 'alertActive', label: '活动警报', group: 'interaction' },
|
|
89
|
+
{ key: 'alertTriggered', label: '触发警报', group: 'interaction' },
|
|
90
|
+
{ key: 'alertMuted', label: '静音警报', group: 'interaction' },
|
|
91
|
+
{ key: 'avwapLine', label: 'AVWAP 线', group: 'interaction' },
|
|
92
|
+
{ key: 'avwapBand', label: 'AVWAP 区域', group: 'interaction' },
|
|
93
|
+
{ key: 'mtfOverlay', label: '多周期叠加', group: 'interaction' },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const COLOR_PRESET_KEYS = new Set<ColorPresetKey>(COLOR_PRESET_ITEMS.map((item) => item.key))
|
|
97
|
+
|
|
98
|
+
export function normalizeColorPresetSettings(value: unknown): ColorPresetSettings {
|
|
99
|
+
if (!value || typeof value !== 'object') return {}
|
|
100
|
+
|
|
101
|
+
const source = value as Record<string, unknown>
|
|
102
|
+
const result: ColorPresetSettings = {}
|
|
103
|
+
|
|
104
|
+
for (const themeName of ['light', 'dark'] as const) {
|
|
105
|
+
const themeOverrides = source[themeName]
|
|
106
|
+
if (!themeOverrides || typeof themeOverrides !== 'object') continue
|
|
107
|
+
|
|
108
|
+
const clean: ColorPresetOverrides = {}
|
|
109
|
+
for (const [key, color] of Object.entries(themeOverrides as Record<string, unknown>)) {
|
|
110
|
+
if (COLOR_PRESET_KEYS.has(key as ColorPresetKey) && typeof color === 'string' && color.trim()) {
|
|
111
|
+
clean[key as ColorPresetKey] = color
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(clean).length > 0) result[themeName] = clean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function applyColorPresetOverrides(
|
|
121
|
+
colors: ColorTokens,
|
|
122
|
+
themeName: ColorPresetThemeName,
|
|
123
|
+
settings?: ColorPresetSettings,
|
|
124
|
+
): ColorTokens {
|
|
125
|
+
const overrides = settings?.[themeName]
|
|
126
|
+
if (!overrides || Object.keys(overrides).length === 0) return colors
|
|
127
|
+
return { ...colors, ...overrides }
|
|
128
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @klinechart-quant/core/tokens — semantic design tokens + presets.
|
|
3
|
+
*
|
|
4
|
+
* See `./types.ts` for the contract; `./theme-light.ts` and
|
|
5
|
+
* `./theme-dark.ts` for shipping presets; `./mergeTheme.ts` for the
|
|
6
|
+
* override helper.
|
|
7
|
+
*
|
|
8
|
+
* Public surface from the root `@klinechart-quant/core` barrel.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
Theme,
|
|
13
|
+
ThemeOverride,
|
|
14
|
+
ColorTokens,
|
|
15
|
+
SpacingTokens,
|
|
16
|
+
TypographyTokens,
|
|
17
|
+
MotionTokens,
|
|
18
|
+
IndicatorPalette,
|
|
19
|
+
ColorValue,
|
|
20
|
+
CssLength,
|
|
21
|
+
CssDuration,
|
|
22
|
+
CssEasing,
|
|
23
|
+
TextColors,
|
|
24
|
+
PriceColors,
|
|
25
|
+
TagBgColors,
|
|
26
|
+
BorderColors,
|
|
27
|
+
MAColors,
|
|
28
|
+
BOLLColors,
|
|
29
|
+
MACDColors,
|
|
30
|
+
RSIColors,
|
|
31
|
+
CCIColors,
|
|
32
|
+
KDJColors,
|
|
33
|
+
MOMColors,
|
|
34
|
+
WMSRColors,
|
|
35
|
+
KSTColors,
|
|
36
|
+
EXPMAColors,
|
|
37
|
+
ENEColors,
|
|
38
|
+
LabelColors,
|
|
39
|
+
LastPriceLabelColors,
|
|
40
|
+
VolumePriceColors,
|
|
41
|
+
StructureColors,
|
|
42
|
+
ZonesColors,
|
|
43
|
+
} from './types'
|
|
44
|
+
|
|
45
|
+
export { lightTheme } from './theme-light'
|
|
46
|
+
export { darkTheme } from './theme-dark'
|
|
47
|
+
export { withAsiaMarketColors, resolveThemeColors } from './theme-china'
|
|
48
|
+
export { mergeTheme } from './mergeTheme'
|
|
49
|
+
export {
|
|
50
|
+
COLOR_PRESET_ITEMS,
|
|
51
|
+
COLOR_PRESET_STORAGE_KEY,
|
|
52
|
+
applyColorPresetOverrides,
|
|
53
|
+
normalizeColorPresetSettings,
|
|
54
|
+
type ColorPresetItem,
|
|
55
|
+
type ColorPresetKey,
|
|
56
|
+
type ColorPresetOverrides,
|
|
57
|
+
type ColorPresetSettings,
|
|
58
|
+
type ColorPresetThemeName,
|
|
59
|
+
} from './colorPresetSettings'
|
|
60
|
+
export {
|
|
61
|
+
themeToCssVars,
|
|
62
|
+
toCssDeclarationBlock,
|
|
63
|
+
camelToKebab,
|
|
64
|
+
type ThemeToCssVarsOptions,
|
|
65
|
+
} from './themeToCssVars'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme override / merge helper.
|
|
3
|
+
*
|
|
4
|
+
* const myTheme = mergeTheme(lightTheme, {
|
|
5
|
+
* name: 'my-light',
|
|
6
|
+
* colors: { candleUpBody: '#00C896' },
|
|
7
|
+
* })
|
|
8
|
+
*
|
|
9
|
+
* Shallow-merges each top-level token family. Within a family, the
|
|
10
|
+
* override wins for any key it specifies; missing keys fall back to the
|
|
11
|
+
* base. The override's `name` (if provided) wins.
|
|
12
|
+
*
|
|
13
|
+
* Strictly immutable: returns a new theme; the inputs are untouched.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Theme, ThemeOverride } from './types'
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
function isRecord(v: unknown): v is Record<string, any> {
|
|
20
|
+
return v !== null && typeof v === 'object'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function deepMergeColors(
|
|
24
|
+
base: Theme['colors'],
|
|
25
|
+
override: Partial<Theme['colors']> | undefined,
|
|
26
|
+
): Theme['colors'] {
|
|
27
|
+
const merged = { ...base } as unknown as Record<string, unknown>
|
|
28
|
+
if (!override) return merged as unknown as Theme['colors']
|
|
29
|
+
for (const [k, v] of Object.entries(override)) {
|
|
30
|
+
const baseVal = (base as unknown as Record<string, unknown>)[k]
|
|
31
|
+
if (isRecord(v) && isRecord(baseVal)) {
|
|
32
|
+
merged[k] = { ...baseVal, ...v }
|
|
33
|
+
} else if (v !== undefined) {
|
|
34
|
+
merged[k] = v
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return merged as unknown as Theme['colors']
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mergeTheme(base: Theme, override: ThemeOverride): Theme {
|
|
41
|
+
return {
|
|
42
|
+
name: override.name ?? base.name,
|
|
43
|
+
colors: deepMergeColors(base.colors, override.colors),
|
|
44
|
+
spacing: { ...base.spacing, ...(override.spacing ?? {}) },
|
|
45
|
+
typography: { ...base.typography, ...(override.typography ?? {}) },
|
|
46
|
+
motion: { ...base.motion, ...(override.motion ?? {}) },
|
|
47
|
+
}
|
|
48
|
+
}
|