@startsimpli/ui 0.4.124 → 0.4.125
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.
|
|
3
|
+
"version": "0.4.125",
|
|
4
4
|
"description": "Shared UI components package for StartSimpli applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
48
48
|
"react": "^18.0.0 || ^19.0.0",
|
|
49
49
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
50
|
-
"@startsimpli/api": "^0.5.
|
|
51
|
-
"@startsimpli/
|
|
52
|
-
"@startsimpli/
|
|
50
|
+
"@startsimpli/api": "^0.5.60",
|
|
51
|
+
"@startsimpli/auth": "^0.4.35",
|
|
52
|
+
"@startsimpli/i18n": "^0.1.3"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@startsimpli/api": {
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
"tailwindcss": "^3.4.19",
|
|
124
124
|
"tsup": "^8.5.1",
|
|
125
125
|
"typescript": "^6.0.3",
|
|
126
|
-
"@startsimpli/
|
|
127
|
-
"@startsimpli/
|
|
126
|
+
"@startsimpli/i18n": "0.1.3",
|
|
127
|
+
"@startsimpli/realtime": "0.0.2"
|
|
128
128
|
},
|
|
129
129
|
"keywords": [
|
|
130
130
|
"ui",
|
|
@@ -3,7 +3,7 @@ import * as React from 'react'
|
|
|
3
3
|
export interface DashboardGridProps {
|
|
4
4
|
children: React.ReactNode
|
|
5
5
|
/** Number of columns at lg breakpoint */
|
|
6
|
-
columns?: 1 | 2 | 3 | 4
|
|
6
|
+
columns?: 1 | 2 | 3 | 4 | 5
|
|
7
7
|
/** Gap size between items */
|
|
8
8
|
gap?: 'sm' | 'md' | 'lg'
|
|
9
9
|
className?: string
|
|
@@ -16,6 +16,8 @@ const colMap = {
|
|
|
16
16
|
2: 'lg:grid-cols-2',
|
|
17
17
|
3: 'lg:grid-cols-3',
|
|
18
18
|
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
|
19
|
+
// Five cards are too narrow at lg once a sidebar takes its share.
|
|
20
|
+
5: 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
|
|
19
21
|
} as const
|
|
20
22
|
|
|
21
23
|
export function DashboardGrid({
|
|
@@ -4,7 +4,12 @@ import { Numeral } from './Numeral'
|
|
|
4
4
|
import { SparklineTrend } from './SparklineTrend'
|
|
5
5
|
|
|
6
6
|
export interface MetricCardTrend {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Percent change, already in percent units (12 renders '+12%'). Omit it to
|
|
9
|
+
* show only `label`, for a caption that is not a percent change (a ratio,
|
|
10
|
+
* a rank) and must not be rendered as one.
|
|
11
|
+
*/
|
|
12
|
+
value?: number
|
|
8
13
|
isPositive?: boolean
|
|
9
14
|
label?: string
|
|
10
15
|
}
|
|
@@ -70,8 +75,8 @@ export function MetricCard({
|
|
|
70
75
|
</p>
|
|
71
76
|
|
|
72
77
|
{/* Trend Indicator */}
|
|
73
|
-
{trend && !isLoading && !error && (
|
|
74
|
-
<TrendIndicator trend={trend} />
|
|
78
|
+
{trend?.value != null && !isLoading && !error && (
|
|
79
|
+
<TrendIndicator value={trend.value} isPositive={trend.isPositive} />
|
|
75
80
|
)}
|
|
76
81
|
</div>
|
|
77
82
|
|
|
@@ -108,34 +113,34 @@ export function MetricCard({
|
|
|
108
113
|
return content
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
function TrendIndicator({
|
|
116
|
+
function TrendIndicator({ value, isPositive }: { value: number; isPositive?: boolean }) {
|
|
112
117
|
const colorClass =
|
|
113
|
-
|
|
118
|
+
isPositive === true
|
|
114
119
|
? 'text-green-600'
|
|
115
|
-
:
|
|
120
|
+
: isPositive === false
|
|
116
121
|
? 'text-red-600'
|
|
117
122
|
: 'text-gray-500'
|
|
118
123
|
|
|
119
124
|
return (
|
|
120
125
|
<div className={`flex items-center gap-1 text-xs font-medium ${colorClass}`}>
|
|
121
|
-
{
|
|
126
|
+
{isPositive === true && (
|
|
122
127
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
|
123
128
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2 17L12 7l10 10" />
|
|
124
129
|
</svg>
|
|
125
130
|
)}
|
|
126
|
-
{
|
|
131
|
+
{isPositive === false && (
|
|
127
132
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
|
128
133
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2 7l10 10L22 7" />
|
|
129
134
|
</svg>
|
|
130
135
|
)}
|
|
131
|
-
{
|
|
136
|
+
{isPositive === undefined && (
|
|
132
137
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
|
133
138
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" />
|
|
134
139
|
</svg>
|
|
135
140
|
)}
|
|
136
141
|
{/* Sign and '%' are both bidi-neutral — unpinned, '+12%' reorders. */}
|
|
137
142
|
<Numeral>
|
|
138
|
-
{
|
|
143
|
+
{value > 0 ? '+' : ''}{value}%
|
|
139
144
|
</Numeral>
|
|
140
145
|
</div>
|
|
141
146
|
)
|
|
@@ -122,6 +122,17 @@ describe('MetricCard', () => {
|
|
|
122
122
|
)
|
|
123
123
|
expect(screen.queryByText('+5%')).not.toBeInTheDocument()
|
|
124
124
|
})
|
|
125
|
+
|
|
126
|
+
// A caption that is not a percent change (a ratio, a rank) must not be
|
|
127
|
+
// dressed up as one: label only, no '%' chip.
|
|
128
|
+
it('renders a label-only trend without a percent chip', () => {
|
|
129
|
+
const { container } = render(
|
|
130
|
+
<MetricCard {...baseProps} trend={{ label: 'OI 1.41' }} />
|
|
131
|
+
)
|
|
132
|
+
expect(screen.getByText('OI 1.41')).toBeInTheDocument()
|
|
133
|
+
expect(container.textContent).not.toContain('%')
|
|
134
|
+
expect(container.querySelector('.text-gray-500 svg, .text-green-600 svg, .text-red-600 svg')).toBeNull()
|
|
135
|
+
})
|
|
125
136
|
})
|
|
126
137
|
|
|
127
138
|
describe('sparkline', () => {
|
|
@@ -674,6 +685,14 @@ describe('DashboardGrid', () => {
|
|
|
674
685
|
expect(grid).toHaveClass('lg:grid-cols-4')
|
|
675
686
|
})
|
|
676
687
|
|
|
688
|
+
it('steps 5 columns down to 3 at lg so cards keep their width', () => {
|
|
689
|
+
const { container } = render(<DashboardGrid columns={5}><div /></DashboardGrid>)
|
|
690
|
+
const grid = container.firstChild as HTMLElement
|
|
691
|
+
expect(grid).toHaveClass('sm:grid-cols-2')
|
|
692
|
+
expect(grid).toHaveClass('lg:grid-cols-3')
|
|
693
|
+
expect(grid).toHaveClass('xl:grid-cols-5')
|
|
694
|
+
})
|
|
695
|
+
|
|
677
696
|
it('applies gap-4 for gap="sm"', () => {
|
|
678
697
|
const { container } = render(<DashboardGrid gap="sm"><div /></DashboardGrid>)
|
|
679
698
|
expect(container.firstChild).toHaveClass('gap-4')
|