@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.124",
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.59",
51
- "@startsimpli/i18n": "^0.1.3",
52
- "@startsimpli/auth": "^0.4.35"
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/realtime": "0.0.2",
127
- "@startsimpli/i18n": "0.1.3"
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
- value: number
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({ trend }: { trend: MetricCardTrend }) {
116
+ function TrendIndicator({ value, isPositive }: { value: number; isPositive?: boolean }) {
112
117
  const colorClass =
113
- trend.isPositive === true
118
+ isPositive === true
114
119
  ? 'text-green-600'
115
- : trend.isPositive === false
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
- {trend.isPositive === true && (
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
- {trend.isPositive === false && (
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
- {trend.isPositive === undefined && (
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
- {trend.value > 0 ? '+' : ''}{trend.value}%
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')