@startsimpli/ui 0.4.66 → 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 +2 -2
- package/src/__tests__/theme-contract.test.ts +57 -0
- package/src/components/ActivityTimeline.tsx +1 -1
- package/src/components/LogActivityDialog.tsx +4 -4
- package/src/components/collection/ReviewDrawer.tsx +2 -2
- package/src/components/collection/TopicReviewWorkspace.tsx +1 -1
- package/src/components/dashboard/MetricCard.tsx +1 -1
- package/src/components/dashboard/PeriodSelector.tsx +1 -1
- package/src/components/dashboard/PipelineFunnel.tsx +5 -5
- package/src/components/dashboard/TopCampaigns.tsx +6 -6
- package/src/components/dashboard/__tests__/dashboard.test.tsx +3 -3
- package/src/components/enrichment/ApolloEnrichButton.tsx +1 -1
- package/src/components/enrichment/EnrichButton.tsx +1 -1
- package/src/components/health/AttentionFeed.tsx +1 -1
- package/src/components/health/PipelineHealth.tsx +1 -1
- package/src/components/navigation/__tests__/navigation.test.tsx +5 -2
- package/src/components/navigation/app-shell.tsx +13 -3
- package/src/components/navigation/grouped-nav.tsx +2 -2
- package/src/components/navigation/sidebar.tsx +20 -5
- package/src/components/presentations/DeckBuilderWorkspace.tsx +3 -3
- package/src/components/settings/SettingsCard.tsx +16 -8
- package/src/components/team/InviteMemberDialog.tsx +10 -2
- package/src/components/team/MembersTable.tsx +29 -12
- package/src/components/team/__tests__/team-components.test.tsx +44 -0
- package/src/components/team/__tests__/theme-tokens.test.ts +76 -0
- package/src/components/team/domain-claim-card-default-class-names.ts +17 -14
- package/src/components/team/invite-member-dialog-default-class-names.ts +20 -12
- package/src/components/team/leave-team-dialog-default-class-names.ts +13 -11
- package/src/components/team/members-table-default-class-names.ts +21 -13
- package/src/components/team/org-switcher-default-class-names.ts +4 -3
- package/src/components/team/pages/domains-settings-page-default-class-names.ts +13 -13
- package/src/components/team/pages/team-settings-page-default-class-names.ts +17 -17
- package/src/components/team/pending-invitation-callout-default-class-names.ts +8 -6
- package/src/components/team/role-selector-default-class-names.ts +3 -2
- package/src/components/wizard/StepIndicator.tsx +4 -4
- package/src/components/wizard/__tests__/wizard.test.tsx +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/ui",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"@startsimpli/api": "0.5.44",
|
|
46
|
-
"@startsimpli/auth": "0.4.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
48
|
-
'from-primary
|
|
49
|
-
'from-primary
|
|
50
|
-
'from-primary
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
324
|
-
expect(monthBtn).not.toHaveClass('bg-primary
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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">
|
|
@@ -128,7 +128,10 @@ describe('Sidebar', () => {
|
|
|
128
128
|
]
|
|
129
129
|
render(<Sidebar appName="App" sections={customSections} />)
|
|
130
130
|
const activeLink = screen.getByText('Active Link').closest('a')
|
|
131
|
-
|
|
131
|
+
// Semantic token, not a numeric ramp — `bg-primary-600` is undefined in apps
|
|
132
|
+
// that only declare --color-primary, so the active pill vanished (startsim-1f3n).
|
|
133
|
+
expect(activeLink).toHaveClass('bg-primary')
|
|
134
|
+
expect(activeLink).toHaveClass('text-primary-foreground')
|
|
132
135
|
})
|
|
133
136
|
|
|
134
137
|
it('inactive link uses gray text', () => {
|
|
@@ -154,7 +157,7 @@ describe('Sidebar', () => {
|
|
|
154
157
|
]
|
|
155
158
|
render(<Sidebar appName="App" sections={customSections} />)
|
|
156
159
|
const homeLink = screen.getByText('Home').closest('a')
|
|
157
|
-
expect(homeLink).not.toHaveClass('bg-primary
|
|
160
|
+
expect(homeLink).not.toHaveClass('bg-primary')
|
|
158
161
|
})
|
|
159
162
|
})
|
|
160
163
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, type ReactNode } from 'react';
|
|
3
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
4
4
|
import { useAuth, buildCentralAuthUrl } from '@startsimpli/auth';
|
|
5
5
|
import { LogOut, User } from 'lucide-react';
|
|
6
6
|
import { Sidebar, SidebarLayout, type SidebarSection } from './sidebar';
|
|
@@ -52,6 +52,9 @@ export function AppShell({
|
|
|
52
52
|
children,
|
|
53
53
|
}: AppShellProps) {
|
|
54
54
|
const { user, isLoading, logout } = useAuth();
|
|
55
|
+
// Owned here so the content spacer shrinks with the rail — the Sidebar used to
|
|
56
|
+
// keep this private, so "Collapse" narrowed the rail and left a dead 16rem gap.
|
|
57
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
55
58
|
|
|
56
59
|
const signin = (returnTo: string) => buildCentralAuthUrl('signin', { app, returnTo });
|
|
57
60
|
|
|
@@ -112,8 +115,15 @@ export function AppShell({
|
|
|
112
115
|
|
|
113
116
|
return (
|
|
114
117
|
<>
|
|
115
|
-
<Sidebar
|
|
116
|
-
|
|
118
|
+
<Sidebar
|
|
119
|
+
appName={appName}
|
|
120
|
+
sections={sections}
|
|
121
|
+
userDropdown={userDropdown}
|
|
122
|
+
collapsible
|
|
123
|
+
collapsed={collapsed}
|
|
124
|
+
onCollapsedChange={setCollapsed}
|
|
125
|
+
/>
|
|
126
|
+
<SidebarLayout collapsed={collapsed}>{children}</SidebarLayout>
|
|
117
127
|
</>
|
|
118
128
|
);
|
|
119
129
|
}
|
|
@@ -69,7 +69,7 @@ function linkClasses(active: boolean, indented: boolean) {
|
|
|
69
69
|
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
70
70
|
indented && 'pl-9',
|
|
71
71
|
active
|
|
72
|
-
? 'bg-primary
|
|
72
|
+
? 'bg-primary text-primary-foreground'
|
|
73
73
|
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900',
|
|
74
74
|
)
|
|
75
75
|
}
|
|
@@ -175,7 +175,7 @@ function NavGroup({
|
|
|
175
175
|
type="button"
|
|
176
176
|
onClick={() => setOpen((o) => !o)}
|
|
177
177
|
aria-expanded={expanded}
|
|
178
|
-
className="w-full flex items-center justify-between gap-2 px-3 py-1.5 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-
|
|
178
|
+
className="w-full flex items-center justify-between gap-2 px-3 py-1.5 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
179
179
|
>
|
|
180
180
|
{heading}
|
|
181
181
|
<ChevronDown
|
|
@@ -40,9 +40,17 @@ export interface SidebarProps {
|
|
|
40
40
|
*/
|
|
41
41
|
collapsible?: boolean;
|
|
42
42
|
/**
|
|
43
|
-
* Default collapsed state
|
|
43
|
+
* Default collapsed state (uncontrolled)
|
|
44
44
|
*/
|
|
45
45
|
defaultCollapsed?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Controlled collapsed state. Pass with onCollapsedChange when the surrounding
|
|
48
|
+
* layout also needs to know (see AppShell — the content spacer must shrink with
|
|
49
|
+
* the rail, otherwise "Collapse" only narrows the rail and leaves a dead gap).
|
|
50
|
+
*/
|
|
51
|
+
collapsed?: boolean;
|
|
52
|
+
/** Fires whenever the collapse toggle is used. */
|
|
53
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export function Sidebar({
|
|
@@ -52,9 +60,16 @@ export function Sidebar({
|
|
|
52
60
|
className,
|
|
53
61
|
collapsible = true,
|
|
54
62
|
defaultCollapsed = false,
|
|
63
|
+
collapsed: controlledCollapsed,
|
|
64
|
+
onCollapsedChange,
|
|
55
65
|
}: SidebarProps) {
|
|
56
66
|
const pathname = usePathname();
|
|
57
|
-
const [
|
|
67
|
+
const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
|
|
68
|
+
const collapsed = controlledCollapsed ?? internalCollapsed;
|
|
69
|
+
const setCollapsed = (next: boolean) => {
|
|
70
|
+
if (controlledCollapsed === undefined) setInternalCollapsed(next);
|
|
71
|
+
onCollapsedChange?.(next);
|
|
72
|
+
};
|
|
58
73
|
|
|
59
74
|
const isActivePath = (href: string) => {
|
|
60
75
|
if (href === '/') {
|
|
@@ -76,7 +91,7 @@ export function Sidebar({
|
|
|
76
91
|
<Link
|
|
77
92
|
href="/"
|
|
78
93
|
className={cn(
|
|
79
|
-
'font-bold hover:text-primary
|
|
94
|
+
'font-bold transition-colors hover:text-primary',
|
|
80
95
|
collapsed ? 'text-lg' : 'text-xl'
|
|
81
96
|
)}
|
|
82
97
|
>
|
|
@@ -103,7 +118,7 @@ export function Sidebar({
|
|
|
103
118
|
className={cn(
|
|
104
119
|
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
105
120
|
isActive
|
|
106
|
-
? 'bg-primary
|
|
121
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
107
122
|
: 'text-gray-300 hover:bg-gray-800 hover:text-white',
|
|
108
123
|
collapsed && 'justify-center'
|
|
109
124
|
)}
|
|
@@ -163,7 +178,7 @@ export interface SidebarLayoutProps {
|
|
|
163
178
|
|
|
164
179
|
export function SidebarLayout({ children, collapsed = false }: SidebarLayoutProps) {
|
|
165
180
|
return (
|
|
166
|
-
<div className="flex h-screen bg-
|
|
181
|
+
<div className="flex h-screen bg-background">
|
|
167
182
|
{/* Spacer for fixed sidebar */}
|
|
168
183
|
<div className={cn('flex-shrink-0 transition-all duration-200', collapsed ? 'w-20' : 'w-64')} />
|
|
169
184
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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">
|
|
@@ -8,24 +8,32 @@ export interface SettingsCardProps {
|
|
|
8
8
|
children?: React.ReactNode
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A titled settings panel with an optional trailing action.
|
|
13
|
+
*
|
|
14
|
+
* Theme-driven surfaces (startsim-1f3n): it used to hard-code
|
|
15
|
+
* `bg-white text-gray-900`, which ignored every app's palette and was
|
|
16
|
+
* unreadable in dark mode. Now it renders on the `card` token like the rest of
|
|
17
|
+
* the system.
|
|
18
|
+
*/
|
|
11
19
|
export function SettingsCard({ title, description, icon: Icon, action, children }: SettingsCardProps) {
|
|
12
20
|
return (
|
|
13
|
-
<div className="rounded-xl border bg-
|
|
14
|
-
<div className="flex items-start justify-between p-6">
|
|
15
|
-
<div className="flex items-start gap-4">
|
|
21
|
+
<div className="rounded-xl border border-border bg-card text-card-foreground shadow-sm">
|
|
22
|
+
<div className="flex flex-wrap items-start justify-between gap-4 p-6">
|
|
23
|
+
<div className="flex min-w-0 items-start gap-4">
|
|
16
24
|
{Icon && (
|
|
17
|
-
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-
|
|
18
|
-
<Icon className="h-5 w-5 text-
|
|
25
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
|
26
|
+
<Icon className="h-5 w-5 text-primary" />
|
|
19
27
|
</div>
|
|
20
28
|
)}
|
|
21
|
-
<div>
|
|
29
|
+
<div className="min-w-0">
|
|
22
30
|
<h3 className="font-semibold leading-none tracking-tight">{title}</h3>
|
|
23
31
|
{description && (
|
|
24
|
-
<p className="text-sm text-
|
|
32
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{description}</p>
|
|
25
33
|
)}
|
|
26
34
|
</div>
|
|
27
35
|
</div>
|
|
28
|
-
{action && <div className="
|
|
36
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
29
37
|
</div>
|
|
30
38
|
{children && <div className="px-6 pb-6 pt-0">{children}</div>}
|
|
31
39
|
</div>
|
|
@@ -32,6 +32,12 @@ export interface InviteMemberDialogProps {
|
|
|
32
32
|
triggerLabel?: string
|
|
33
33
|
/** Title in the modal header. */
|
|
34
34
|
title?: string
|
|
35
|
+
/** Submit button label. Override when the backend adds directly rather than
|
|
36
|
+
* emailing an invite (e.g. Foundry: "Add member"). */
|
|
37
|
+
submitLabel?: string
|
|
38
|
+
/** Confirmation line after a successful submit. Receives the email so the
|
|
39
|
+
* caller can match its own semantics ("Added ada@x to this foundry"). */
|
|
40
|
+
successMessage?: (email: string) => string
|
|
35
41
|
/** Default role pre-selected. */
|
|
36
42
|
defaultRole?: TeamRole
|
|
37
43
|
/** Available roles in the dropdown. Defaults to admin/member/viewer (no owner). */
|
|
@@ -48,6 +54,8 @@ export function InviteMemberDialog({
|
|
|
48
54
|
onInvited,
|
|
49
55
|
triggerLabel = 'Invite member',
|
|
50
56
|
title = 'Invite a teammate',
|
|
57
|
+
submitLabel = 'Send invitation',
|
|
58
|
+
successMessage = (email) => `Invitation sent to ${email}`,
|
|
51
59
|
defaultRole = 'member',
|
|
52
60
|
availableRoles = ['admin', 'member', 'viewer'],
|
|
53
61
|
classNames,
|
|
@@ -95,7 +103,7 @@ export function InviteMemberDialog({
|
|
|
95
103
|
if (onSubmit) {
|
|
96
104
|
invited = await onSubmit({ email: email.trim(), teamId, role })
|
|
97
105
|
}
|
|
98
|
-
setSuccess(
|
|
106
|
+
setSuccess(successMessage(email.trim()))
|
|
99
107
|
setEmail('')
|
|
100
108
|
onInvited?.(invited)
|
|
101
109
|
} catch (err) {
|
|
@@ -170,7 +178,7 @@ export function InviteMemberDialog({
|
|
|
170
178
|
className={cls.submitButton}
|
|
171
179
|
disabled={submitting}
|
|
172
180
|
>
|
|
173
|
-
{submitting ? '
|
|
181
|
+
{submitting ? 'Saving…' : submitLabel}
|
|
174
182
|
</button>
|
|
175
183
|
</div>
|
|
176
184
|
</form>
|
|
@@ -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>
|
|
@@ -77,7 +88,13 @@ export function MembersTable({
|
|
|
77
88
|
</td>
|
|
78
89
|
</tr>
|
|
79
90
|
)}
|
|
80
|
-
{members.map((m) =>
|
|
91
|
+
{members.map((m) => {
|
|
92
|
+
const displayName = userDisplayName(m.user) || m.userId
|
|
93
|
+
// Members with no name fall back to their email, so printing the
|
|
94
|
+
// email underneath stacked the same string on itself.
|
|
95
|
+
const secondary = m.user?.email && m.user.email !== displayName ? m.user.email : null
|
|
96
|
+
const isSelf = !!currentUserId && m.userId === currentUserId
|
|
97
|
+
return (
|
|
81
98
|
<tr key={m.id} className={cls.row}>
|
|
82
99
|
<td className={cls.cell}>
|
|
83
100
|
<div className="flex items-center gap-3">
|
|
@@ -93,17 +110,16 @@ export function MembersTable({
|
|
|
93
110
|
{userInitials(m.user)}
|
|
94
111
|
</span>
|
|
95
112
|
)}
|
|
96
|
-
<div className="flex flex-col">
|
|
97
|
-
<span className=
|
|
98
|
-
{
|
|
113
|
+
<div className="flex min-w-0 flex-col">
|
|
114
|
+
<span className="flex items-center gap-2">
|
|
115
|
+
<span className={cls.name}>{displayName}</span>
|
|
116
|
+
{isSelf && <span className={cls.selfBadge}>You</span>}
|
|
99
117
|
</span>
|
|
100
|
-
{
|
|
101
|
-
<span className={cls.email}>{m.user.email}</span>
|
|
102
|
-
)}
|
|
118
|
+
{secondary && <span className={cls.email}>{secondary}</span>}
|
|
103
119
|
</div>
|
|
104
120
|
</div>
|
|
105
121
|
</td>
|
|
106
|
-
<td className={cls.cell}>
|
|
122
|
+
<td className={tight(cls.cell, cls.tightColumn)}>
|
|
107
123
|
{canManage && onChangeRole ? (
|
|
108
124
|
<RoleSelector
|
|
109
125
|
value={m.role}
|
|
@@ -115,7 +131,7 @@ export function MembersTable({
|
|
|
115
131
|
)}
|
|
116
132
|
</td>
|
|
117
133
|
{canManage && (
|
|
118
|
-
<td className={cls.cell}>
|
|
134
|
+
<td className={tight(cls.cell, cls.tightColumn)}>
|
|
119
135
|
<div className={cls.actions}>
|
|
120
136
|
{onRemove && (
|
|
121
137
|
<button
|
|
@@ -130,7 +146,8 @@ export function MembersTable({
|
|
|
130
146
|
</td>
|
|
131
147
|
)}
|
|
132
148
|
</tr>
|
|
133
|
-
|
|
149
|
+
)
|
|
150
|
+
})}
|
|
134
151
|
</tbody>
|
|
135
152
|
</table>
|
|
136
153
|
</div>
|
|
@@ -73,6 +73,50 @@ describe('MembersTable', () => {
|
|
|
73
73
|
expect(container.firstChild).toHaveClass('my-root-class')
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
it('does not print the email twice when it is also the display name', () => {
|
|
77
|
+
// Foundry maps control-plane members with an email and no name, so
|
|
78
|
+
// userDisplayName() falls back to the email — the row used to stack the
|
|
79
|
+
// same string on itself. startsim-1f3n.4.
|
|
80
|
+
render(
|
|
81
|
+
<MembersTable
|
|
82
|
+
members={[
|
|
83
|
+
{
|
|
84
|
+
id: 'u1',
|
|
85
|
+
userId: 'u1',
|
|
86
|
+
teamId: 't1',
|
|
87
|
+
role: 'owner',
|
|
88
|
+
joinedAt: '2025-01-01',
|
|
89
|
+
user: { id: 'u1', email: 'qosha@debugg.ai' },
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
expect(screen.getAllByText('qosha@debugg.ai')).toHaveLength(1)
|
|
95
|
+
})
|
|
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
|
+
|
|
76
120
|
it('shows the empty state when no members', () => {
|
|
77
121
|
render(<MembersTable members={[]} />)
|
|
78
122
|
expect(screen.getByText('No members yet.')).toBeInTheDocument()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-token guard for the team/settings surfaces (startsim-1f3n.1).
|
|
3
|
+
*
|
|
4
|
+
* These components ship default Tailwind classes. The only colors an app is
|
|
5
|
+
* guaranteed to have are the semantic tokens from packages/ui/theme/contract.css
|
|
6
|
+
* (--primary, --card, --border, --muted-foreground, …). Apps do NOT all define a
|
|
7
|
+
* numeric palette ramp: apps/foundry defines `--color-primary` with no
|
|
8
|
+
* `--color-primary-600`, so in Tailwind v4 `bg-primary-600` generates no rule at
|
|
9
|
+
* all — the Invite member button rendered as white text on a white card, i.e.
|
|
10
|
+
* invisible.
|
|
11
|
+
*
|
|
12
|
+
* Hard-coded `gray-*` / `bg-white` are the same class of bug one step quieter:
|
|
13
|
+
* the classes exist, but they ignore the app's theme and are unreadable in dark
|
|
14
|
+
* mode. Shared components must be theme-driven, not palette-driven.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { INVITE_DIALOG_DEFAULTS } from '../invite-member-dialog-default-class-names'
|
|
18
|
+
import { MEMBERS_TABLE_DEFAULTS } from '../members-table-default-class-names'
|
|
19
|
+
import { ROLE_SELECTOR_DEFAULTS } from '../role-selector-default-class-names'
|
|
20
|
+
import { PENDING_INVITE_DEFAULTS } from '../pending-invitation-callout-default-class-names'
|
|
21
|
+
import { LEAVE_DIALOG_DEFAULTS } from '../leave-team-dialog-default-class-names'
|
|
22
|
+
import { ORG_SWITCHER_DEFAULTS } from '../org-switcher-default-class-names'
|
|
23
|
+
import { DOMAIN_CLAIM_DEFAULTS } from '../domain-claim-card-default-class-names'
|
|
24
|
+
import { TEAM_SETTINGS_PAGE_DEFAULTS } from '../pages/team-settings-page-default-class-names'
|
|
25
|
+
import { DOMAINS_SETTINGS_PAGE_DEFAULTS } from '../pages/domains-settings-page-default-class-names'
|
|
26
|
+
|
|
27
|
+
const MAPS: Record<string, Record<string, string>> = {
|
|
28
|
+
INVITE_DIALOG_DEFAULTS,
|
|
29
|
+
MEMBERS_TABLE_DEFAULTS,
|
|
30
|
+
ROLE_SELECTOR_DEFAULTS,
|
|
31
|
+
PENDING_INVITE_DEFAULTS,
|
|
32
|
+
LEAVE_DIALOG_DEFAULTS,
|
|
33
|
+
ORG_SWITCHER_DEFAULTS,
|
|
34
|
+
DOMAIN_CLAIM_DEFAULTS,
|
|
35
|
+
TEAM_SETTINGS_PAGE_DEFAULTS,
|
|
36
|
+
DOMAINS_SETTINGS_PAGE_DEFAULTS,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `bg-primary-600`, `text-primary-700`, `focus:border-primary-500`, … */
|
|
40
|
+
const PRIMARY_RAMP = /(^|:)[a-z-]*-primary-\d{2,3}\b/
|
|
41
|
+
/** `text-gray-500`, `bg-white`, `divide-gray-100`, … — theme-blind surfaces. */
|
|
42
|
+
const RAW_SURFACE = /(^|:)(bg|text|border|divide|ring)-(gray|slate|zinc|neutral|stone)-\d{2,3}\b|(^|:)bg-white\b/
|
|
43
|
+
|
|
44
|
+
function offenders(map: Record<string, string>, pattern: RegExp): string[] {
|
|
45
|
+
return Object.entries(map)
|
|
46
|
+
.flatMap(([slot, value]) =>
|
|
47
|
+
String(value)
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter((cls) => pattern.test(cls))
|
|
50
|
+
.map((cls) => `${slot}: ${cls}`),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('team surfaces are theme-driven', () => {
|
|
55
|
+
it.each(Object.keys(MAPS))(
|
|
56
|
+
'%s uses no numeric primary ramp (undefined in token-only apps like foundry)',
|
|
57
|
+
(name) => {
|
|
58
|
+
expect(offenders(MAPS[name], PRIMARY_RAMP)).toEqual([])
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
it.each(Object.keys(MAPS))('%s uses no theme-blind gray/white surfaces', (name) => {
|
|
63
|
+
expect(offenders(MAPS[name], RAW_SURFACE)).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('the invite trigger is a filled primary button that is visible on a card', () => {
|
|
67
|
+
expect(INVITE_DIALOG_DEFAULTS.trigger).toContain('bg-primary')
|
|
68
|
+
expect(INVITE_DIALOG_DEFAULTS.trigger).toContain('text-primary-foreground')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('dialog panels sit on the card surface, not hard-coded white', () => {
|
|
72
|
+
expect(INVITE_DIALOG_DEFAULTS.panel).toContain('bg-card')
|
|
73
|
+
expect(LEAVE_DIALOG_DEFAULTS.panel).toContain('bg-card')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link DomainClaimCard}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link DomainClaimCard}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — theme-driven, dark-mode safe. */
|
|
2
3
|
export interface DomainClaimCardClassNames {
|
|
3
4
|
root?: string
|
|
4
5
|
header?: string
|
|
@@ -20,26 +21,28 @@ export interface DomainClaimCardClassNames {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export const DOMAIN_CLAIM_DEFAULTS: Required<DomainClaimCardClassNames> = {
|
|
23
|
-
root: 'rounded-xl border border-
|
|
24
|
+
root: 'rounded-xl border border-border bg-card p-5 text-card-foreground',
|
|
24
25
|
header: 'flex items-center justify-between gap-3',
|
|
25
|
-
domain: 'text-base font-semibold text-
|
|
26
|
+
domain: 'text-base font-semibold text-foreground',
|
|
26
27
|
statusBadge: 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
27
|
-
statusVerified:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
statusVerified:
|
|
29
|
+
'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
|
|
30
|
+
statusUnverified:
|
|
31
|
+
'bg-amber-500/10 text-amber-700 dark:text-amber-400',
|
|
32
|
+
body: 'mt-4 space-y-3 text-sm text-foreground',
|
|
33
|
+
sectionLabel: 'text-xs font-medium uppercase tracking-wide text-muted-foreground',
|
|
31
34
|
tokenRow: 'flex items-center gap-2',
|
|
32
35
|
tokenCode:
|
|
33
|
-
'flex-1 rounded-md bg-
|
|
34
|
-
helpText: 'text-xs text-
|
|
36
|
+
'flex-1 rounded-md bg-muted px-3 py-2 font-mono text-xs text-foreground',
|
|
37
|
+
helpText: 'text-xs text-muted-foreground',
|
|
35
38
|
actions: 'mt-4 flex flex-wrap items-center gap-2',
|
|
36
39
|
primaryButton:
|
|
37
|
-
'rounded-md bg-primary
|
|
40
|
+
'rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-50',
|
|
38
41
|
secondaryButton:
|
|
39
|
-
'rounded-md border border-
|
|
42
|
+
'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50',
|
|
40
43
|
revokeButton:
|
|
41
|
-
'rounded-md border border-
|
|
42
|
-
errorText: 'mt-2 text-sm text-
|
|
44
|
+
'rounded-md border border-destructive/40 px-3 py-1.5 text-sm font-medium text-destructive transition-colors hover:bg-destructive/10 disabled:opacity-50',
|
|
45
|
+
errorText: 'mt-2 text-sm text-destructive',
|
|
43
46
|
codeInput:
|
|
44
|
-
'w-24 rounded-md border border-
|
|
47
|
+
'w-24 rounded-md border border-input bg-background px-2 py-1.5 text-sm tracking-widest text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring',
|
|
45
48
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Default Tailwind classes for {@link InviteMemberDialog}. startsim-o7s.
|
|
3
|
+
*
|
|
4
|
+
* Semantic tokens only (startsim-1f3n) — see packages/ui/theme/contract.css.
|
|
5
|
+
* A numeric ramp like `bg-primary-600` is NOT part of the contract: apps that
|
|
6
|
+
* define `--color-primary` alone (foundry, gen-ai-consulting, workflow-studio)
|
|
7
|
+
* generate no rule for it, which is how this trigger became an invisible
|
|
8
|
+
* white-on-white button in the Foundry control plane.
|
|
9
|
+
*/
|
|
2
10
|
export interface InviteMemberDialogClassNames {
|
|
3
11
|
trigger?: string
|
|
4
12
|
overlay?: string
|
|
@@ -18,24 +26,24 @@ export interface InviteMemberDialogClassNames {
|
|
|
18
26
|
|
|
19
27
|
export const INVITE_DIALOG_DEFAULTS: Required<InviteMemberDialogClassNames> = {
|
|
20
28
|
trigger:
|
|
21
|
-
'inline-flex items-center rounded-md bg-primary
|
|
29
|
+
'inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
22
30
|
overlay:
|
|
23
31
|
'fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4',
|
|
24
32
|
panel:
|
|
25
|
-
'w-full max-w-md rounded-xl
|
|
26
|
-
header: 'border-b border-
|
|
27
|
-
title: 'text-base font-semibold text-
|
|
33
|
+
'w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-xl',
|
|
34
|
+
header: 'border-b border-border px-6 py-4',
|
|
35
|
+
title: 'text-base font-semibold text-foreground',
|
|
28
36
|
body: 'space-y-4 px-6 py-4',
|
|
29
37
|
fieldRow: '',
|
|
30
|
-
label: 'block text-sm font-medium text-
|
|
38
|
+
label: 'mb-1 block text-sm font-medium text-foreground',
|
|
31
39
|
input:
|
|
32
|
-
'w-full rounded-lg border border-
|
|
33
|
-
errorText: 'text-sm text-
|
|
34
|
-
successText: 'text-sm text-
|
|
40
|
+
'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring',
|
|
41
|
+
errorText: 'text-sm text-destructive',
|
|
42
|
+
successText: 'text-sm text-emerald-600 dark:text-emerald-400',
|
|
35
43
|
footer:
|
|
36
|
-
'flex items-center justify-end gap-2 border-t border-
|
|
44
|
+
'flex items-center justify-end gap-2 border-t border-border px-6 py-3',
|
|
37
45
|
cancelButton:
|
|
38
|
-
'rounded-md border border-
|
|
46
|
+
'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted',
|
|
39
47
|
submitButton:
|
|
40
|
-
'rounded-md bg-primary
|
|
48
|
+
'rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-50',
|
|
41
49
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link LeaveTeamDialog}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link LeaveTeamDialog}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — theme-driven, dark-mode safe. */
|
|
2
3
|
export interface LeaveTeamDialogClassNames {
|
|
3
4
|
trigger?: string
|
|
4
5
|
overlay?: string
|
|
@@ -15,19 +16,20 @@ export interface LeaveTeamDialogClassNames {
|
|
|
15
16
|
|
|
16
17
|
export const LEAVE_DIALOG_DEFAULTS: Required<LeaveTeamDialogClassNames> = {
|
|
17
18
|
trigger:
|
|
18
|
-
'inline-flex items-center rounded-md border border-
|
|
19
|
+
'inline-flex items-center rounded-md border border-destructive/40 px-3 py-1.5 text-sm font-medium text-destructive transition-colors hover:bg-destructive/10',
|
|
19
20
|
overlay:
|
|
20
21
|
'fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4',
|
|
21
|
-
panel:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
panel:
|
|
23
|
+
'w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-xl',
|
|
24
|
+
header: 'border-b border-border px-6 py-4',
|
|
25
|
+
title: 'text-base font-semibold text-foreground',
|
|
26
|
+
body: 'space-y-3 px-6 py-4 text-sm text-foreground',
|
|
27
|
+
warningText: 'text-sm text-muted-foreground',
|
|
28
|
+
errorText: 'text-sm text-destructive',
|
|
27
29
|
footer:
|
|
28
|
-
'flex items-center justify-end gap-2 border-t border-
|
|
30
|
+
'flex items-center justify-end gap-2 border-t border-border px-6 py-3',
|
|
29
31
|
cancelButton:
|
|
30
|
-
'rounded-md border border-
|
|
32
|
+
'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted',
|
|
31
33
|
confirmButton:
|
|
32
|
-
'rounded-md bg-
|
|
34
|
+
'rounded-md bg-destructive px-3 py-1.5 text-sm font-semibold text-destructive-foreground shadow-sm transition-colors hover:bg-destructive/90 disabled:opacity-50',
|
|
33
35
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link MembersTable}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link MembersTable}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — theme-driven, dark-mode safe. */
|
|
2
3
|
export interface MembersTableClassNames {
|
|
3
4
|
root?: string
|
|
4
5
|
table?: string
|
|
@@ -15,25 +16,32 @@ export interface MembersTableClassNames {
|
|
|
15
16
|
actions?: string
|
|
16
17
|
removeButton?: string
|
|
17
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
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const MEMBERS_TABLE_DEFAULTS: Required<MembersTableClassNames> = {
|
|
21
|
-
root: 'w-full overflow-x-auto rounded-lg border border-
|
|
22
|
-
table: 'w-full text-
|
|
23
|
-
thead: 'bg-
|
|
26
|
+
root: 'w-full overflow-x-auto rounded-lg border border-border bg-card',
|
|
27
|
+
table: 'w-full text-left text-sm',
|
|
28
|
+
thead: 'bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground',
|
|
24
29
|
headerRow: '',
|
|
25
|
-
th: 'px-4 py-2 font-medium',
|
|
26
|
-
tbody: 'divide-y divide-
|
|
27
|
-
row: 'hover:bg-
|
|
30
|
+
th: 'px-4 py-2.5 font-medium',
|
|
31
|
+
tbody: 'divide-y divide-border',
|
|
32
|
+
row: 'transition-colors hover:bg-muted/40',
|
|
28
33
|
cell: 'px-4 py-3 align-middle',
|
|
29
34
|
avatar:
|
|
30
|
-
'flex h-8 w-8 items-center justify-center rounded-full bg-
|
|
31
|
-
name: 'font-medium text-
|
|
32
|
-
email: 'text-
|
|
35
|
+
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary',
|
|
36
|
+
name: 'font-medium text-foreground',
|
|
37
|
+
email: 'text-muted-foreground',
|
|
33
38
|
roleBadge:
|
|
34
|
-
'inline-flex items-center rounded-full bg-
|
|
39
|
+
'inline-flex items-center rounded-full border border-border bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground',
|
|
35
40
|
actions: 'flex items-center justify-end gap-2',
|
|
36
41
|
removeButton:
|
|
37
|
-
'rounded-md border border-
|
|
38
|
-
emptyRow: 'px-4 py-6 text-center text-sm text-
|
|
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',
|
|
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',
|
|
39
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link OrgSwitcher}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link OrgSwitcher}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — theme-driven, dark-mode safe. */
|
|
2
3
|
export interface OrgSwitcherClassNames {
|
|
3
4
|
root?: string
|
|
4
5
|
label?: string
|
|
@@ -7,7 +8,7 @@ export interface OrgSwitcherClassNames {
|
|
|
7
8
|
|
|
8
9
|
export const ORG_SWITCHER_DEFAULTS: Required<OrgSwitcherClassNames> = {
|
|
9
10
|
root: 'inline-flex items-center gap-2',
|
|
10
|
-
label: 'text-xs font-medium uppercase tracking-wide text-
|
|
11
|
+
label: 'text-xs font-medium uppercase tracking-wide text-muted-foreground',
|
|
11
12
|
select:
|
|
12
|
-
'rounded-md border border-
|
|
13
|
+
'rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring',
|
|
13
14
|
}
|
|
@@ -60,30 +60,30 @@ export const DOMAINS_SETTINGS_PAGE_DEFAULTS: Required<
|
|
|
60
60
|
> = {
|
|
61
61
|
root: 'space-y-6',
|
|
62
62
|
header: '',
|
|
63
|
-
backLink: 'text-sm text-primary
|
|
63
|
+
backLink: 'text-sm text-primary hover:underline',
|
|
64
64
|
titleRow: 'mt-2 flex items-center justify-between gap-4',
|
|
65
|
-
title: 'text-xl font-semibold text-
|
|
66
|
-
subtitle: 'mt-1 text-sm text-
|
|
65
|
+
title: 'text-xl font-semibold text-foreground',
|
|
66
|
+
subtitle: 'mt-1 text-sm text-muted-foreground',
|
|
67
67
|
|
|
68
68
|
primaryButton:
|
|
69
|
-
'rounded-lg bg-primary
|
|
69
|
+
'rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-50',
|
|
70
70
|
input:
|
|
71
|
-
'w-full rounded-lg border border-
|
|
72
|
-
label: 'block text-sm font-medium text-
|
|
73
|
-
formCard: 'rounded-xl border border-
|
|
71
|
+
'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring',
|
|
72
|
+
label: 'block text-sm font-medium text-foreground',
|
|
73
|
+
formCard: 'space-y-3 rounded-xl border border-border bg-card p-4',
|
|
74
74
|
formActions: 'flex gap-2',
|
|
75
75
|
cancelButton:
|
|
76
|
-
'rounded-lg border border-
|
|
76
|
+
'rounded-lg border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted',
|
|
77
77
|
|
|
78
|
-
errorText: 'text-sm text-
|
|
79
|
-
loadingText: 'text-sm text-
|
|
78
|
+
errorText: 'text-sm text-destructive',
|
|
79
|
+
loadingText: 'text-sm text-muted-foreground',
|
|
80
80
|
|
|
81
81
|
emptyState:
|
|
82
|
-
'rounded-xl border border-dashed border-
|
|
83
|
-
emptyText: 'text-sm text-
|
|
82
|
+
'rounded-xl border border-dashed border-border bg-card p-10 text-center',
|
|
83
|
+
emptyText: 'text-sm text-muted-foreground',
|
|
84
84
|
|
|
85
85
|
retryButton:
|
|
86
|
-
'rounded-lg border border-
|
|
86
|
+
'rounded-lg border border-border px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-muted',
|
|
87
87
|
|
|
88
88
|
list: 'space-y-4',
|
|
89
89
|
}
|
|
@@ -82,35 +82,35 @@ export const TEAM_SETTINGS_PAGE_DEFAULTS: Required<
|
|
|
82
82
|
> = {
|
|
83
83
|
root: 'space-y-8',
|
|
84
84
|
header: '',
|
|
85
|
-
backLink: 'text-sm text-primary
|
|
86
|
-
title: 'mt-2 text-xl font-semibold text-
|
|
87
|
-
subtitle: 'mt-1 text-sm text-
|
|
85
|
+
backLink: 'text-sm text-primary hover:underline',
|
|
86
|
+
title: 'mt-2 text-xl font-semibold text-foreground',
|
|
87
|
+
subtitle: 'mt-1 text-sm text-muted-foreground',
|
|
88
88
|
|
|
89
89
|
section: 'space-y-3',
|
|
90
90
|
sectionHeader: 'flex items-center justify-between',
|
|
91
|
-
sectionHeading: 'text-sm font-semibold uppercase tracking-wide text-
|
|
92
|
-
sectionBody: 'text-sm text-
|
|
91
|
+
sectionHeading: 'text-sm font-semibold uppercase tracking-wide text-muted-foreground',
|
|
92
|
+
sectionBody: 'text-sm text-muted-foreground',
|
|
93
93
|
|
|
94
|
-
dangerDivider: 'space-y-3 border-t border-
|
|
94
|
+
dangerDivider: 'space-y-3 border-t border-border pt-6',
|
|
95
95
|
|
|
96
|
-
errorText: 'text-sm text-
|
|
97
|
-
loadingText: 'text-sm text-
|
|
98
|
-
emptyText: 'text-sm text-
|
|
96
|
+
errorText: 'text-sm text-destructive',
|
|
97
|
+
loadingText: 'text-sm text-muted-foreground',
|
|
98
|
+
emptyText: 'text-sm text-muted-foreground',
|
|
99
99
|
|
|
100
100
|
retryButton:
|
|
101
|
-
'rounded-lg border border-
|
|
101
|
+
'rounded-lg border border-border px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-muted',
|
|
102
102
|
|
|
103
103
|
invitationsTableWrapper:
|
|
104
|
-
'overflow-hidden rounded-xl border border-
|
|
104
|
+
'overflow-hidden rounded-xl border border-border bg-card',
|
|
105
105
|
invitationsTable: 'w-full text-sm',
|
|
106
|
-
invitationsThead: 'bg-
|
|
106
|
+
invitationsThead: 'bg-muted/50 text-left text-xs uppercase text-muted-foreground',
|
|
107
107
|
invitationsTh: 'px-4 py-3 font-medium',
|
|
108
|
-
invitationsTbody: 'divide-y divide-
|
|
109
|
-
invitationsEmailCell: 'px-4 py-3 text-
|
|
108
|
+
invitationsTbody: 'divide-y divide-border',
|
|
109
|
+
invitationsEmailCell: 'px-4 py-3 text-foreground',
|
|
110
110
|
invitationsRoleCell: 'px-4 py-3',
|
|
111
111
|
invitationsRoleBadge:
|
|
112
|
-
'inline-flex items-center rounded-full bg-primary
|
|
113
|
-
invitationsExpiryCell: 'px-4 py-3 text-
|
|
112
|
+
'inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary',
|
|
113
|
+
invitationsExpiryCell: 'px-4 py-3 text-muted-foreground',
|
|
114
114
|
invitationsActionsCell: 'px-4 py-3 text-right',
|
|
115
|
-
invitationsRevokeButton: 'text-
|
|
115
|
+
invitationsRevokeButton: 'text-sm text-destructive hover:underline',
|
|
116
116
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link PendingInvitationCallout}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link PendingInvitationCallout}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — the callout picks up the app's brand
|
|
3
|
+
* color instead of a hard-coded blue that clashes with every non-blue theme. */
|
|
2
4
|
export interface PendingInvitationCalloutClassNames {
|
|
3
5
|
root?: string
|
|
4
6
|
message?: string
|
|
@@ -10,13 +12,13 @@ export interface PendingInvitationCalloutClassNames {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export const PENDING_INVITE_DEFAULTS: Required<PendingInvitationCalloutClassNames> = {
|
|
13
|
-
root: 'flex items-center gap-3 rounded-lg border border-
|
|
14
|
-
message: 'flex-1 text-sm text-
|
|
15
|
+
root: 'flex items-center gap-3 rounded-lg border border-primary/25 bg-primary/5 px-4 py-3',
|
|
16
|
+
message: 'flex-1 text-sm text-foreground',
|
|
15
17
|
emphasized: 'font-semibold',
|
|
16
18
|
actions: 'flex items-center gap-2',
|
|
17
19
|
acceptButton:
|
|
18
|
-
'rounded-md bg-
|
|
20
|
+
'rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-50',
|
|
19
21
|
declineButton:
|
|
20
|
-
'rounded-md border border-
|
|
21
|
-
errorText: 'mt-2 text-sm text-
|
|
22
|
+
'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
|
|
23
|
+
errorText: 'mt-2 text-sm text-destructive',
|
|
22
24
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Default Tailwind classes for {@link RoleSelector}. startsim-o7s.
|
|
1
|
+
/** Default Tailwind classes for {@link RoleSelector}. startsim-o7s.
|
|
2
|
+
* Semantic tokens only (startsim-1f3n) — theme-driven, dark-mode safe. */
|
|
2
3
|
export interface RoleSelectorClassNames {
|
|
3
4
|
root?: string
|
|
4
5
|
select?: string
|
|
@@ -7,5 +8,5 @@ export interface RoleSelectorClassNames {
|
|
|
7
8
|
export const ROLE_SELECTOR_DEFAULTS: Required<RoleSelectorClassNames> = {
|
|
8
9
|
root: 'inline-block',
|
|
9
10
|
select:
|
|
10
|
-
'rounded-md border border-
|
|
11
|
+
'rounded-md border border-input bg-background px-2 py-1 text-sm text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
11
12
|
}
|
|
@@ -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
|
|
30
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
31
31
|
: isActive
|
|
32
|
-
? 'border-primary
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
expect(screen.getByText('Review')).toHaveClass('text-primary')
|
|
96
96
|
})
|
|
97
97
|
})
|