@startsimpli/ui 0.4.139 → 0.4.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.139",
3
+ "version": "0.4.140",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -126,8 +126,8 @@
126
126
  "tailwindcss": "^3.4.19",
127
127
  "tsup": "^8.5.1",
128
128
  "typescript": "^6.0.3",
129
- "@startsimpli/realtime": "0.0.2",
130
- "@startsimpli/i18n": "0.1.3"
129
+ "@startsimpli/i18n": "0.1.3",
130
+ "@startsimpli/realtime": "0.0.2"
131
131
  },
132
132
  "keywords": [
133
133
  "ui",
@@ -53,25 +53,32 @@ export interface TimeSeriesChartProps {
53
53
  /**
54
54
  * Series colours, in order, from the theme contract (theme/contract.css) — so
55
55
  * they follow the app's brand and flip with `.dark` like every other surface.
56
- * `--success` carries the same fallback channels as the Tailwind preset, for
57
- * apps that predate the token.
56
+ *
57
+ * Categorical only: a series' colour says which series it is, nothing more.
58
+ * `--success` and `--destructive` are deliberately absent — they mean good/bad
59
+ * (gain/loss), and a series that happened to land on one would assert that.
60
+ * `--secondary` and `--accent` are surface tones, too faint to read as a
61
+ * stroke. That leaves three tokens, so distinguishability comes mostly from
62
+ * the dash cycle below.
58
63
  */
59
64
  export const SERIES_COLORS = [
60
65
  'hsl(var(--primary))',
61
66
  'hsl(var(--muted-foreground))',
62
- 'hsl(var(--success, 142.1 76.2% 36.3%))',
63
- 'hsl(var(--destructive))',
64
67
  'hsl(var(--foreground))',
65
68
  ] as const
66
69
 
67
- /** After the palette runs out, series repeat its colours with a dash. */
68
- const DASHES = [undefined, '6 4', '2 3'] as const
70
+ /**
71
+ * Each pass through the palette gets the next dash: solid, dashed, dotted,
72
+ * dash-dot. 3 colours x 4 dashes = 12 series before a style repeats. The
73
+ * patterns fit the 16px legend swatch so each one shows its full rhythm.
74
+ */
75
+ export const SERIES_DASHES = [undefined, '6 4', '2 3', '8 3 2 3'] as const
69
76
 
70
77
  export function seriesStroke(index: number): { color: string; dash: string | undefined } {
71
78
  const n = SERIES_COLORS.length
72
79
  return {
73
80
  color: SERIES_COLORS[index % n],
74
- dash: DASHES[Math.floor(index / n) % DASHES.length],
81
+ dash: SERIES_DASHES[Math.floor(index / n) % SERIES_DASHES.length],
75
82
  }
76
83
  }
77
84
 
@@ -1,5 +1,5 @@
1
1
  import { render, screen } from '@testing-library/react'
2
- import { TimeSeriesChart, SERIES_COLORS, seriesStroke } from '../TimeSeriesChart'
2
+ import { TimeSeriesChart, SERIES_COLORS, SERIES_DASHES, seriesStroke } from '../TimeSeriesChart'
3
3
  import type { TimeSeries } from '../lib/series'
4
4
 
5
5
  // jsdom lays nothing out: recharts' ResponsiveContainer renders no chart at a
@@ -74,6 +74,19 @@ describe('TimeSeriesChart', () => {
74
74
  expect(new Set(strokes)).toEqual(new Set([SERIES_COLORS[0], SERIES_COLORS[1]]))
75
75
  })
76
76
 
77
+ it('dashes the series that repeat a colour, on the line and in the legend', () => {
78
+ const four = [0, 1, 2, 3].map((k) => ({ ...holding, name: `S${k}` }))
79
+ const { container } = render(<TimeSeriesChart series={four} unit="$" />)
80
+ const lines = linePaths(container)
81
+ expect(lines).toHaveLength(4)
82
+ expect(lines[3].getAttribute('stroke')).toBe(lines[0].getAttribute('stroke'))
83
+ expect(lines[3].getAttribute('stroke-dasharray')).toBe('6 4')
84
+ expect(lines[0].getAttribute('stroke-dasharray')).toBeNull()
85
+
86
+ const swatches = Array.from(container.querySelectorAll('ul svg line'))
87
+ expect(swatches.map((l) => l.getAttribute('stroke-dasharray'))).toEqual([null, null, null, '6 4'])
88
+ })
89
+
77
90
  it('shows a legend for two or more series and none for one', () => {
78
91
  const { rerender } = render(<TimeSeriesChart series={[strategy, holding]} unit="$" />)
79
92
  expect(screen.getByText('Strategy')).toBeInTheDocument()
@@ -132,10 +145,42 @@ describe('TimeSeriesChart', () => {
132
145
  })
133
146
 
134
147
  describe('seriesStroke', () => {
135
- it('cycles the token palette and dashes the repeats', () => {
148
+ it('uses only non-semantic tokens, in order', () => {
149
+ expect([...SERIES_COLORS]).toEqual([
150
+ 'hsl(var(--primary))',
151
+ 'hsl(var(--muted-foreground))',
152
+ 'hsl(var(--foreground))',
153
+ ])
154
+ })
155
+
156
+ it('never colours a series as gain or loss', () => {
157
+ // --success / --destructive mean good / bad; a categorical series must not
158
+ // inherit that meaning by landing on one.
159
+ const styles = Array.from({ length: 40 }, (_, i) => seriesStroke(i).color)
160
+ for (const color of styles) {
161
+ expect(color).not.toMatch(/--(success|destructive)\b/)
162
+ }
163
+ })
164
+
165
+ it('cycles the token palette and steps the dash on each pass', () => {
136
166
  const n = SERIES_COLORS.length
137
167
  expect(seriesStroke(0)).toEqual({ color: SERIES_COLORS[0], dash: undefined })
168
+ expect(seriesStroke(n - 1)).toEqual({ color: SERIES_COLORS[n - 1], dash: undefined })
138
169
  expect(seriesStroke(n)).toEqual({ color: SERIES_COLORS[0], dash: '6 4' })
139
170
  expect(seriesStroke(n + 1).color).toBe(SERIES_COLORS[1])
171
+ expect(seriesStroke(2 * n).dash).toBe('2 3')
172
+ expect(seriesStroke(3 * n).dash).toBe('8 3 2 3')
173
+ })
174
+
175
+ it('keeps every series distinct until colours x dashes run out', () => {
176
+ const distinct = SERIES_COLORS.length * SERIES_DASHES.length
177
+ expect(distinct).toBe(12)
178
+ const keys = Array.from({ length: distinct }, (_, i) => {
179
+ const { color, dash } = seriesStroke(i)
180
+ return `${color}|${dash ?? 'solid'}`
181
+ })
182
+ expect(new Set(keys).size).toBe(distinct)
183
+ // Then the cycle starts over.
184
+ expect(seriesStroke(distinct)).toEqual(seriesStroke(0))
140
185
  })
141
186
  })
@@ -1,6 +1,6 @@
1
1
  // Charts live behind their own subpath (`@startsimpli/ui/charts`) so recharts
2
2
  // is only bundled by apps that draw one.
3
- export { TimeSeriesChart, SERIES_COLORS, seriesStroke } from './TimeSeriesChart'
3
+ export { TimeSeriesChart, SERIES_COLORS, SERIES_DASHES, seriesStroke } from './TimeSeriesChart'
4
4
  export type { TimeSeriesChartProps, TimeSeriesReferenceLine } from './TimeSeriesChart'
5
5
  export type {
6
6
  TimeSeries,