@startsimpli/ui 0.4.67 → 0.4.68

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.67",
3
+ "version": "0.4.68",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -42,8 +42,8 @@
42
42
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
- "@startsimpli/auth": "0.4.30",
46
- "@startsimpli/api": "0.5.44"
45
+ "@startsimpli/api": "0.5.44",
46
+ "@startsimpli/auth": "0.4.31"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@startsimpli/api": {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Package-wide theme-contract guard (startsim-1f3n.3).
3
+ *
4
+ * A shared component may only use colors that packages/ui/theme/contract.css
5
+ * guarantees — the SEMANTIC tokens (`primary`, `card`, `border`, `ring`, …).
6
+ * The numeric Tailwind ramp (`bg-primary-600`, `text-primary-700`, …) is NOT
7
+ * part of the contract, and Tailwind v4 emits no rule at all for a color it
8
+ * has no `--color-*` entry for. That is how the Foundry control plane ended up
9
+ * with an invisible "Invite member" button and a blank active-nav pill: the app
10
+ * declares `--color-primary` and never declared the ramp.
11
+ *
12
+ * Requiring every app to also declare a ramp is the wrong fix — it makes N apps
13
+ * responsible for a promise this package should not have made. So the ramp is
14
+ * banned here instead, and this test keeps it out.
15
+ *
16
+ * Need a lighter/darker brand shade? Use an opacity modifier on the token:
17
+ * bg-primary-50 -> bg-primary/10 text-primary-700 -> text-primary
18
+ * hover:bg-primary-700 -> hover:bg-primary/90
19
+ */
20
+
21
+ import * as fs from 'fs'
22
+ import * as path from 'path'
23
+
24
+ const SRC = path.resolve(__dirname, '..')
25
+
26
+ /** `bg-primary-600`, `hover:text-primary-700`, `focus:ring-primary-500`, … */
27
+ const PRIMARY_RAMP = /\b[a-z-]*-primary-\d{2,3}\b/
28
+
29
+ function sourceFiles(dir: string, acc: string[] = []): string[] {
30
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
31
+ const full = path.join(dir, entry.name)
32
+ if (entry.isDirectory()) {
33
+ if (entry.name === '__tests__' || entry.name === 'node_modules') continue
34
+ sourceFiles(full, acc)
35
+ } else if (/\.tsx?$/.test(entry.name)) {
36
+ acc.push(full)
37
+ }
38
+ }
39
+ return acc
40
+ }
41
+
42
+ describe('packages/ui only uses colors the theme contract guarantees', () => {
43
+ it('no component styles against the numeric primary ramp', () => {
44
+ const offenders: string[] = []
45
+ for (const file of sourceFiles(SRC)) {
46
+ fs.readFileSync(file, 'utf8')
47
+ .split('\n')
48
+ .forEach((line, i) => {
49
+ // Prose in a doc comment may legitimately name the banned class.
50
+ const code = line.replace(/^\s*(\*|\/\/).*$/, '')
51
+ const hit = code.match(PRIMARY_RAMP)
52
+ if (hit) offenders.push(`${path.relative(SRC, file)}:${i + 1} ${hit[0]}`)
53
+ })
54
+ }
55
+ expect(offenders).toEqual([])
56
+ })
57
+ })
@@ -162,7 +162,7 @@ export function ActivityTimeline({
162
162
  <div className="text-center pt-4">
163
163
  <button
164
164
  onClick={onLoadMore}
165
- className="px-4 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-md transition-colors"
165
+ className="px-4 py-2 text-sm text-primary hover:text-primary/80 hover:bg-primary/10 rounded-md transition-colors"
166
166
  >
167
167
  Load more activities
168
168
  </button>
@@ -148,7 +148,7 @@ export function LogActivityDialog({
148
148
  onClick={() => setActivityType(value)}
149
149
  className={`flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg border-2 transition-colors ${
150
150
  activityType === value
151
- ? 'border-primary-600 bg-primary-50 text-primary-700'
151
+ ? 'border-primary bg-primary/10 text-primary'
152
152
  : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
153
153
  }`}
154
154
  >
@@ -258,13 +258,13 @@ export function LogActivityDialog({
258
258
 
259
259
  {/* Follow-up Task */}
260
260
  {showFollowUp && (
261
- <div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
261
+ <div className="bg-primary/10 border border-primary/30 rounded-lg p-4">
262
262
  <label className="flex items-center gap-2 cursor-pointer">
263
263
  <input
264
264
  type="checkbox"
265
265
  checked={createFollowUp}
266
266
  onChange={(e) => setCreateFollowUp(e.target.checked)}
267
- className="w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
267
+ className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-ring"
268
268
  />
269
269
  <span className="text-sm font-medium text-blue-900">
270
270
  Create follow-up task
@@ -291,7 +291,7 @@ export function LogActivityDialog({
291
291
  <button
292
292
  type="submit"
293
293
  disabled={isSubmitting}
294
- className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
294
+ className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
295
295
  >
296
296
  {isSubmitting ? submittingLabel : submitLabel}
297
297
  </button>
@@ -285,7 +285,7 @@ function ReviewDrawerInner({
285
285
 
286
286
  <div className="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
287
287
  {rank ? (
288
- <span className="rounded-full bg-primary-50 px-2 py-0.5 font-semibold text-primary-700">
288
+ <span className="rounded-full bg-primary/10 px-2 py-0.5 font-semibold text-primary">
289
289
  rank {rank}
290
290
  </span>
291
291
  ) : null}
@@ -327,7 +327,7 @@ function ReviewDrawerInner({
327
327
  onClick={() => setNoteOpen((o) => !o)}
328
328
  className={cn(
329
329
  'inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs hover:bg-muted',
330
- note && 'text-primary-700',
330
+ note && 'text-primary',
331
331
  )}
332
332
  >
333
333
  <MessageSquarePlus className="h-3.5 w-3.5" />
@@ -213,7 +213,7 @@ function TopicRow({
213
213
  <li className={`rounded-lg border border-border bg-card p-4 ${muted ? 'opacity-70' : ''}`}>
214
214
  <div className="flex items-start gap-3">
215
215
  {rank != null && (
216
- <span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary-50 text-xs font-semibold text-primary-700">
216
+ <span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
217
217
  {rank}
218
218
  </span>
219
219
  )}
@@ -85,7 +85,7 @@ export function MetricCard({
85
85
  return (
86
86
  <a
87
87
  href={href}
88
- className="block focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg"
88
+ className="block focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-lg"
89
89
  aria-label={`View ${label} details`}
90
90
  >
91
91
  {content}
@@ -42,7 +42,7 @@ export function PeriodSelector({
42
42
  className={cn(
43
43
  'px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
44
44
  selected === value
45
- ? 'bg-primary-100 text-primary-700'
45
+ ? 'bg-primary/15 text-primary'
46
46
  : 'text-gray-600 hover:bg-gray-100',
47
47
  )}
48
48
  aria-pressed={selected === value}
@@ -44,10 +44,10 @@ export function PipelineFunnel({
44
44
  : 0;
45
45
 
46
46
  const defaultColors = [
47
- 'from-accent-400 to-accent-500',
48
- 'from-primary-400 to-primary-500',
49
- 'from-primary-500 to-primary-600',
50
- 'from-primary-600 to-primary-700',
47
+ 'from-accent/70 to-accent',
48
+ 'from-primary/50 to-primary/70',
49
+ 'from-primary/70 to-primary/90',
50
+ 'from-primary/90 to-primary',
51
51
  ];
52
52
 
53
53
  const gradientColor = stage.color || defaultColors[index % defaultColors.length];
@@ -84,7 +84,7 @@ export function PipelineFunnel({
84
84
  absolute inset-y-0 left-0 bg-gradient-to-r ${gradientColor}
85
85
  transition-all duration-300 group-hover:opacity-90
86
86
  ${onStageClick ? 'cursor-pointer' : 'cursor-default'}
87
- focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
87
+ focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
88
88
  `}
89
89
  style={{ width: `${percentage}%` }}
90
90
  aria-label={`${stage.stage}: ${stage.count} prospects, ${stagePercentage}% of total`}
@@ -43,7 +43,7 @@ export function TopCampaigns({
43
43
  {onViewAll && campaigns.length > maxItems && (
44
44
  <button
45
45
  onClick={onViewAll}
46
- className="text-sm text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded"
46
+ className="text-sm text-primary hover:text-primary/80 font-medium flex items-center gap-1 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
47
47
  >
48
48
  View all
49
49
  <ArrowRight className="w-4 h-4" />
@@ -55,7 +55,7 @@ export function TopCampaigns({
55
55
  {displayCampaigns.map((campaign, index) => {
56
56
  const getProgressColor = (rate: number) => {
57
57
  if (rate >= 75) return 'bg-success-500';
58
- if (rate >= 50) return 'bg-primary-500';
58
+ if (rate >= 50) return 'bg-primary';
59
59
  if (rate >= 25) return 'bg-warning-500';
60
60
  return 'bg-gray-400';
61
61
  };
@@ -72,21 +72,21 @@ export function TopCampaigns({
72
72
  <div className="flex items-start gap-2 min-w-0 flex-1">
73
73
  <span className={`
74
74
  flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full text-xs font-bold
75
- ${index === 0 ? 'bg-accent-100 text-accent-700 border border-accent-200' : 'bg-gray-100 text-gray-600'}
75
+ ${index === 0 ? 'bg-accent/15 text-accent border border-accent/30' : 'bg-gray-100 text-gray-600'}
76
76
  `}>
77
77
  {index + 1}
78
78
  </span>
79
79
 
80
80
  <div className="min-w-0 flex-1">
81
- <h3 className="text-sm font-medium text-gray-900 truncate group-hover:text-primary-600 transition-colors">
81
+ <h3 className="text-sm font-medium text-gray-900 truncate group-hover:text-primary transition-colors">
82
82
  {campaign.name}
83
83
  </h3>
84
84
  {campaign.status && (
85
85
  <span className={`
86
86
  inline-block text-xs px-2 py-0.5 rounded-full mt-1 border
87
- ${campaign.status === 'active' ? 'bg-primary-50 text-primary-700 border-primary-200' : ''}
87
+ ${campaign.status === 'active' ? 'bg-primary/10 text-primary border-primary/30' : ''}
88
88
  ${campaign.status === 'paused' ? 'bg-warning-50 text-warning-700 border-warning-200' : ''}
89
- ${campaign.status === 'completed' ? 'bg-accent-50 text-accent-700 border-accent-200' : ''}
89
+ ${campaign.status === 'completed' ? 'bg-accent/10 text-accent border-accent/30' : ''}
90
90
  `}>
91
91
  {campaign.status}
92
92
  </span>
@@ -320,8 +320,8 @@ describe('PeriodSelector', () => {
320
320
  render(<PeriodSelector selected="week" onChange={onChange} />)
321
321
  const weekBtn = screen.getByText('Week')
322
322
  const monthBtn = screen.getByText('Month')
323
- expect(weekBtn).toHaveClass('bg-primary-100')
324
- expect(monthBtn).not.toHaveClass('bg-primary-100')
323
+ expect(weekBtn).toHaveClass('bg-primary/15')
324
+ expect(monthBtn).not.toHaveClass('bg-primary/15')
325
325
  })
326
326
  })
327
327
 
@@ -613,7 +613,7 @@ describe('TopCampaigns', () => {
613
613
  <TopCampaigns campaigns={[{ id: '1', name: 'Mid', engagementRate: 60 }]} />
614
614
  )
615
615
  const bar = container.querySelector('[role="progressbar"]')
616
- expect(bar).toHaveClass('bg-primary-500')
616
+ expect(bar).toHaveClass('bg-primary')
617
617
  })
618
618
 
619
619
  it('applies warning color for engagement >= 25 and < 50', () => {
@@ -110,7 +110,7 @@ export function ApolloEnrichButton({
110
110
  onClick={handleClick}
111
111
  disabled={buttonDisabled}
112
112
  aria-label={label}
113
- className={`flex items-center gap-1.5 font-medium text-primary-700 bg-primary-50 hover:bg-primary-100 rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${sizeClasses} ${className}`}
113
+ className={`flex items-center gap-1.5 font-medium text-primary bg-primary/10 hover:bg-primary/20 rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${sizeClasses} ${className}`}
114
114
  >
115
115
  <Sparkles className={iconClasses} />
116
116
  {isRunning ? 'Enriching…' : label}
@@ -24,7 +24,7 @@ export function EnrichButton({
24
24
  <button
25
25
  onClick={onClick}
26
26
  disabled={isLoading}
27
- className={`flex items-center gap-1.5 font-medium text-primary-700 bg-primary-50 hover:bg-primary-100 rounded-md disabled:opacity-50 transition-colors ${sizeClasses} ${className}`}
27
+ className={`flex items-center gap-1.5 font-medium text-primary bg-primary/10 hover:bg-primary/20 rounded-md disabled:opacity-50 transition-colors ${sizeClasses} ${className}`}
28
28
  >
29
29
  <Zap className={size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} />
30
30
  {isLoading ? 'Enriching...' : label}
@@ -78,7 +78,7 @@ export function AttentionFeed({
78
78
  <a
79
79
  href={item.href}
80
80
  className={cn(
81
- 'block rounded px-1 -mx-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500',
81
+ 'block rounded px-1 -mx-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring',
82
82
  )}
83
83
  >
84
84
  {body}
@@ -77,7 +77,7 @@ export function PipelineHealth({
77
77
  onClick={() => onStageClick?.(stage.label)}
78
78
  className={cn(
79
79
  'w-full text-left group focus:outline-none',
80
- clickable && 'cursor-pointer focus:ring-2 focus:ring-primary-500 rounded',
80
+ clickable && 'cursor-pointer focus:ring-2 focus:ring-ring rounded',
81
81
  )}
82
82
  >
83
83
  <div className="flex items-center justify-between mb-1">
@@ -141,13 +141,13 @@ export function DeckBuilderWorkspace({ backHref, builder }: DeckBuilderWorkspace
141
141
  title={styleTitle || 'Deck style'}
142
142
  >
143
143
  {deckStyle.source === 'reference' ? (
144
- <Sparkles className="h-3.5 w-3.5 shrink-0 text-primary-600" aria-hidden="true" />
144
+ <Sparkles className="h-3.5 w-3.5 shrink-0 text-primary" aria-hidden="true" />
145
145
  ) : (
146
146
  <Palette className="h-3.5 w-3.5 shrink-0 text-gray-400" aria-hidden="true" />
147
147
  )}
148
148
  <span className="truncate font-medium capitalize">{styleLabel || 'Custom style'}</span>
149
149
  {deckStyle.source === 'reference' && (
150
- <span className="shrink-0 text-primary-600">· cloned</span>
150
+ <span className="shrink-0 text-primary">· cloned</span>
151
151
  )}
152
152
  </div>
153
153
  )
@@ -164,7 +164,7 @@ export function DeckBuilderWorkspace({ backHref, builder }: DeckBuilderWorkspace
164
164
  <ArrowLeft className="h-4 w-4" />
165
165
  </Link>
166
166
  )}
167
- <Presentation className="h-5 w-5 text-primary-600" aria-hidden="true" />
167
+ <Presentation className="h-5 w-5 text-primary" aria-hidden="true" />
168
168
  <div className="leading-tight">
169
169
  <div className="text-sm font-semibold text-gray-900">{deckTitle || 'New presentation'}</div>
170
170
  <div className="text-xs text-gray-400">
@@ -37,6 +37,12 @@ export interface MembersTableProps {
37
37
  classNames?: MembersTableClassNames
38
38
  /** Restrict role-selector options (e.g. hide 'owner' when caller isn't owner). */
39
39
  availableRoles?: TeamRole[]
40
+ /**
41
+ * The signed-in user's id. When it matches a row, that row is badged "You" —
42
+ * otherwise the owner is an anonymous line sitting next to a live Remove
43
+ * button with nothing marking it as themselves.
44
+ */
45
+ currentUserId?: string
40
46
  }
41
47
 
42
48
  const ROLE_LABEL: Record<TeamRole, string> = {
@@ -54,8 +60,13 @@ export function MembersTable({
54
60
  emptyState,
55
61
  classNames,
56
62
  availableRoles,
63
+ currentUserId,
57
64
  }: MembersTableProps) {
58
65
  const cls = { ...MEMBERS_TABLE_DEFAULTS, ...(classNames ?? {}) }
66
+ // Role + actions hug their content so the Member column takes the slack —
67
+ // one shared cell class used to split the table down the middle and leave
68
+ // the controls floating in a gap.
69
+ const tight = (base: string, extra: string) => `${base} ${extra}`.trim()
59
70
 
60
71
  return (
61
72
  <div className={cls.root}>
@@ -63,9 +74,9 @@ export function MembersTable({
63
74
  <thead className={cls.thead}>
64
75
  <tr className={cls.headerRow}>
65
76
  <th className={cls.th} scope="col">Member</th>
66
- <th className={cls.th} scope="col">Role</th>
77
+ <th className={tight(cls.th, cls.tightColumn)} scope="col">Role</th>
67
78
  {canManage && (
68
- <th className={cls.th} scope="col" aria-label="Actions" />
79
+ <th className={tight(cls.th, cls.tightColumn)} scope="col" aria-label="Actions" />
69
80
  )}
70
81
  </tr>
71
82
  </thead>
@@ -82,6 +93,7 @@ export function MembersTable({
82
93
  // Members with no name fall back to their email, so printing the
83
94
  // email underneath stacked the same string on itself.
84
95
  const secondary = m.user?.email && m.user.email !== displayName ? m.user.email : null
96
+ const isSelf = !!currentUserId && m.userId === currentUserId
85
97
  return (
86
98
  <tr key={m.id} className={cls.row}>
87
99
  <td className={cls.cell}>
@@ -99,12 +111,15 @@ export function MembersTable({
99
111
  </span>
100
112
  )}
101
113
  <div className="flex min-w-0 flex-col">
102
- <span className={cls.name}>{displayName}</span>
114
+ <span className="flex items-center gap-2">
115
+ <span className={cls.name}>{displayName}</span>
116
+ {isSelf && <span className={cls.selfBadge}>You</span>}
117
+ </span>
103
118
  {secondary && <span className={cls.email}>{secondary}</span>}
104
119
  </div>
105
120
  </div>
106
121
  </td>
107
- <td className={cls.cell}>
122
+ <td className={tight(cls.cell, cls.tightColumn)}>
108
123
  {canManage && onChangeRole ? (
109
124
  <RoleSelector
110
125
  value={m.role}
@@ -116,7 +131,7 @@ export function MembersTable({
116
131
  )}
117
132
  </td>
118
133
  {canManage && (
119
- <td className={cls.cell}>
134
+ <td className={tight(cls.cell, cls.tightColumn)}>
120
135
  <div className={cls.actions}>
121
136
  {onRemove && (
122
137
  <button
@@ -94,6 +94,29 @@ describe('MembersTable', () => {
94
94
  expect(screen.getAllByText('qosha@debugg.ai')).toHaveLength(1)
95
95
  })
96
96
 
97
+ it('marks the signed-in user so they can tell which row is them', () => {
98
+ // startsim-1f3n.7 — the owner appeared as an anonymous row next to a live
99
+ // Remove button with nothing saying "this one is you".
100
+ render(<MembersTable members={baseMembers} currentUserId="u2" />)
101
+ expect(screen.getByText('You')).toBeInTheDocument()
102
+ const selfRow = screen.getByText('Mike Member').closest('tr')!
103
+ expect(selfRow).toContainElement(screen.getByText('You'))
104
+ })
105
+
106
+ it('renders no You badge when currentUserId matches nobody', () => {
107
+ render(<MembersTable members={baseMembers} currentUserId="nobody" />)
108
+ expect(screen.queryByText('You')).not.toBeInTheDocument()
109
+ })
110
+
111
+ it('keeps the Role and actions columns to their content width', () => {
112
+ // startsim-1f3n.6 — one shared cell class meant Member ate ~55% of the
113
+ // table and Role/Remove floated in a gap.
114
+ render(<MembersTable members={baseMembers} canManage onChangeRole={jest.fn()} />)
115
+ const roleHeader = screen.getByRole('columnheader', { name: 'Role' })
116
+ expect(roleHeader.className).toContain('w-px')
117
+ expect(roleHeader.className).toContain('whitespace-nowrap')
118
+ })
119
+
97
120
  it('shows the empty state when no members', () => {
98
121
  render(<MembersTable members={[]} />)
99
122
  expect(screen.getByText('No members yet.')).toBeInTheDocument()
@@ -16,6 +16,10 @@ export interface MembersTableClassNames {
16
16
  actions?: string
17
17
  removeButton?: string
18
18
  emptyRow?: string
19
+ /** Applied to the Role + actions header/body cells so they hug their content. */
20
+ tightColumn?: string
21
+ /** "You" pill next to the signed-in user's own row. */
22
+ selfBadge?: string
19
23
  }
20
24
 
21
25
  export const MEMBERS_TABLE_DEFAULTS: Required<MembersTableClassNames> = {
@@ -37,4 +41,7 @@ export const MEMBERS_TABLE_DEFAULTS: Required<MembersTableClassNames> = {
37
41
  removeButton:
38
42
  'rounded-md border border-border px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-destructive/40 hover:bg-destructive/10 hover:text-destructive disabled:opacity-50',
39
43
  emptyRow: 'px-4 py-6 text-center text-sm text-muted-foreground',
44
+ tightColumn: 'w-px whitespace-nowrap',
45
+ selfBadge:
46
+ 'rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground',
40
47
  }
@@ -27,9 +27,9 @@ export function StepIndicator({ steps, currentIndex, className = '' }: StepIndic
27
27
  className={[
28
28
  'flex h-8 w-8 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors',
29
29
  isCompleted
30
- ? 'border-primary-600 bg-primary-600 text-white'
30
+ ? 'border-primary bg-primary text-primary-foreground'
31
31
  : isActive
32
- ? 'border-primary-600 bg-white text-primary-600'
32
+ ? 'border-primary bg-white text-primary'
33
33
  : 'border-gray-300 bg-white text-gray-400',
34
34
  ].join(' ')}
35
35
  >
@@ -38,7 +38,7 @@ export function StepIndicator({ steps, currentIndex, className = '' }: StepIndic
38
38
  <span
39
39
  className={[
40
40
  'mt-1 text-xs font-medium',
41
- isActive ? 'text-primary-600' : isCompleted ? 'text-gray-600' : 'text-gray-400',
41
+ isActive ? 'text-primary' : isCompleted ? 'text-gray-600' : 'text-gray-400',
42
42
  ].join(' ')}
43
43
  >
44
44
  {step.label}
@@ -48,7 +48,7 @@ export function StepIndicator({ steps, currentIndex, className = '' }: StepIndic
48
48
  <div
49
49
  className={[
50
50
  'h-0.5 flex-1 mx-2 mb-4 transition-colors',
51
- isCompleted ? 'bg-primary-600' : 'bg-gray-200',
51
+ isCompleted ? 'bg-primary' : 'bg-gray-200',
52
52
  ].join(' ')}
53
53
  />
54
54
  )}
@@ -31,7 +31,7 @@ describe('StepIndicator', () => {
31
31
  it('applies active styles to the current step label', () => {
32
32
  render(<StepIndicator steps={steps} currentIndex={1} />)
33
33
  const profileLabel = screen.getByText('Profile')
34
- expect(profileLabel).toHaveClass('text-primary-600')
34
+ expect(profileLabel).toHaveClass('text-primary')
35
35
  })
36
36
 
37
37
  it('applies completed styles to past step labels', () => {
@@ -60,7 +60,7 @@ describe('StepIndicator', () => {
60
60
  const connectors = container.querySelectorAll('.h-0\\.5')
61
61
  // Both connectors (between steps 1-2 and 2-3) should be completed
62
62
  connectors.forEach((connector) => {
63
- expect(connector).toHaveClass('bg-primary-600')
63
+ expect(connector).toHaveClass('bg-primary')
64
64
  })
65
65
  })
66
66
 
@@ -92,6 +92,6 @@ describe('StepIndicator', () => {
92
92
  it('renders with currentIndex at last step', () => {
93
93
  render(<StepIndicator steps={steps} currentIndex={2} />)
94
94
  // "Review" should be active
95
- expect(screen.getByText('Review')).toHaveClass('text-primary-600')
95
+ expect(screen.getByText('Review')).toHaveClass('text-primary')
96
96
  })
97
97
  })