@startsimpli/ui 0.4.14 → 0.4.15

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.
Files changed (71) hide show
  1. package/README.md +457 -398
  2. package/package.json +18 -13
  3. package/src/components/__tests__/calendar-view-popup.test.tsx +42 -0
  4. package/src/components/__tests__/chat.test.tsx +129 -0
  5. package/src/components/__tests__/meetings-list.test.tsx +114 -0
  6. package/src/components/__tests__/slide-deck-viewer.test.tsx +82 -0
  7. package/src/components/__tests__/workspace.test.tsx +106 -0
  8. package/src/components/account/__tests__/account.test.tsx +5 -32
  9. package/src/components/account/change-password-form.tsx +1 -28
  10. package/src/components/calendar/calendar-view.tsx +31 -0
  11. package/src/components/calendar/index.ts +7 -0
  12. package/src/components/calendar/meetings-list.tsx +202 -0
  13. package/src/components/calendar/upcoming-meetings.tsx +5 -5
  14. package/src/components/chat/ChatComposer.tsx +113 -0
  15. package/src/components/chat/ChatMessage.tsx +81 -0
  16. package/src/components/chat/ChatThread.tsx +57 -0
  17. package/src/components/chat/index.ts +12 -0
  18. package/src/components/chat/types.ts +20 -0
  19. package/src/components/index.ts +13 -0
  20. package/src/components/slide-deck/SlideCanvas.tsx +68 -0
  21. package/src/components/slide-deck/SlideDeckViewer.tsx +144 -0
  22. package/src/components/slide-deck/SlideFilmstrip.tsx +73 -0
  23. package/src/components/slide-deck/index.ts +7 -0
  24. package/src/components/slide-deck/types.ts +18 -0
  25. package/src/components/team/DomainClaimCard.tsx +170 -0
  26. package/src/components/team/InviteMemberDialog.tsx +182 -0
  27. package/src/components/team/LeaveTeamDialog.tsx +130 -0
  28. package/src/components/team/MembersTable.tsx +138 -0
  29. package/src/components/team/OrgSwitcher.tsx +68 -0
  30. package/src/components/team/PendingInvitationCallout.tsx +106 -0
  31. package/src/components/team/RoleSelector.tsx +68 -0
  32. package/src/components/team/__tests__/team-components.test.tsx +352 -0
  33. package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
  34. package/src/components/team/index.ts +57 -0
  35. package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
  36. package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
  37. package/src/components/team/members-table-default-class-names.ts +39 -0
  38. package/src/components/team/org-switcher-default-class-names.ts +13 -0
  39. package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
  40. package/src/components/team/role-selector-default-class-names.ts +11 -0
  41. package/src/components/team/types.ts +97 -0
  42. package/src/components/workflows/ExecNodeDetails.tsx +83 -0
  43. package/src/components/workflows/ExecutionTimeline.tsx +146 -0
  44. package/src/components/workflows/NodeInspector.tsx +257 -0
  45. package/src/components/workflows/NodePalette.tsx +119 -0
  46. package/src/components/workflows/WorkflowCanvas.tsx +113 -0
  47. package/src/components/workflows/WorkflowEdge.tsx +65 -0
  48. package/src/components/workflows/WorkflowEditor.tsx +130 -0
  49. package/src/components/workflows/WorkflowNode.tsx +198 -0
  50. package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
  51. package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
  52. package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
  53. package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
  54. package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
  55. package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
  56. package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
  57. package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
  58. package/src/components/workflows/__tests__/serialization.test.ts +278 -0
  59. package/src/components/workflows/exec-status.ts +90 -0
  60. package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
  61. package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
  62. package/src/components/workflows/index.ts +78 -0
  63. package/src/components/workflows/layout/auto-layout.ts +142 -0
  64. package/src/components/workflows/node-icons.ts +31 -0
  65. package/src/components/workflows/serialization.ts +171 -0
  66. package/src/components/workflows/theme/categories.ts +96 -0
  67. package/src/components/workflows/types.ts +231 -0
  68. package/src/components/workflows/workflows.css +29 -0
  69. package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
  70. package/src/components/workspace/SplitPane.tsx +174 -0
  71. package/src/components/workspace/index.ts +4 -0
@@ -0,0 +1,352 @@
1
+ import { render, screen, fireEvent, act } from '@testing-library/react'
2
+ import {
3
+ MembersTable,
4
+ RoleSelector,
5
+ InviteMemberDialog,
6
+ PendingInvitationCallout,
7
+ DomainClaimCard,
8
+ LeaveTeamDialog,
9
+ OrgSwitcher,
10
+ } from '../index'
11
+ import type { MemberRow, InvitationLite, DomainClaimLite, TeamLite, CompanyLite } from '../types'
12
+
13
+ // ---------- MembersTable ---------------------------------------------------
14
+
15
+ describe('MembersTable', () => {
16
+ const baseMembers: MemberRow[] = [
17
+ {
18
+ id: 'm1',
19
+ userId: 'u1',
20
+ teamId: 't1',
21
+ role: 'owner',
22
+ joinedAt: '2025-01-01',
23
+ user: { id: 'u1', email: 'owner@x.com', firstName: 'Olivia', lastName: 'Owner' },
24
+ },
25
+ {
26
+ id: 'm2',
27
+ userId: 'u2',
28
+ teamId: 't1',
29
+ role: 'member',
30
+ joinedAt: '2025-01-02',
31
+ user: { id: 'u2', email: 'mem@x.com', firstName: 'Mike', lastName: 'Member' },
32
+ },
33
+ ]
34
+
35
+ it('renders a row per member with name + email + role', () => {
36
+ render(<MembersTable members={baseMembers} />)
37
+ expect(screen.getByText('Olivia Owner')).toBeInTheDocument()
38
+ expect(screen.getByText('owner@x.com')).toBeInTheDocument()
39
+ expect(screen.getAllByText('Owner').length).toBeGreaterThan(0)
40
+ expect(screen.getByText('Mike Member')).toBeInTheDocument()
41
+ })
42
+
43
+ it('hides the actions column when canManage is false', () => {
44
+ render(<MembersTable members={baseMembers} />)
45
+ expect(screen.queryByText('Remove')).not.toBeInTheDocument()
46
+ })
47
+
48
+ it('shows RoleSelector + Remove when canManage is true', () => {
49
+ const onChangeRole = jest.fn()
50
+ const onRemove = jest.fn()
51
+ render(
52
+ <MembersTable
53
+ members={baseMembers}
54
+ canManage
55
+ onChangeRole={onChangeRole}
56
+ onRemove={onRemove}
57
+ />
58
+ )
59
+ const removes = screen.getAllByText('Remove')
60
+ expect(removes).toHaveLength(2)
61
+ fireEvent.click(removes[1])
62
+ expect(onRemove).toHaveBeenCalledWith('u2')
63
+
64
+ const selects = screen.getAllByRole('combobox') as HTMLSelectElement[]
65
+ fireEvent.change(selects[1], { target: { value: 'admin' } })
66
+ expect(onChangeRole).toHaveBeenCalledWith('u2', 'admin')
67
+ })
68
+
69
+ it('respects classNames overrides', () => {
70
+ const { container } = render(
71
+ <MembersTable members={baseMembers} classNames={{ root: 'my-root-class' }} />
72
+ )
73
+ expect(container.firstChild).toHaveClass('my-root-class')
74
+ })
75
+
76
+ it('shows the empty state when no members', () => {
77
+ render(<MembersTable members={[]} />)
78
+ expect(screen.getByText('No members yet.')).toBeInTheDocument()
79
+ })
80
+ })
81
+
82
+ // ---------- RoleSelector ---------------------------------------------------
83
+
84
+ describe('RoleSelector', () => {
85
+ it('renders the supplied roles and fires onChange', () => {
86
+ const onChange = jest.fn()
87
+ render(
88
+ <RoleSelector
89
+ value="member"
90
+ onChange={onChange}
91
+ availableRoles={['admin', 'member', 'viewer']}
92
+ />
93
+ )
94
+ const select = screen.getByRole('combobox') as HTMLSelectElement
95
+ expect(select.value).toBe('member')
96
+ // Only the three supplied roles should be rendered.
97
+ const options = Array.from(select.querySelectorAll('option')).map((o) => o.value)
98
+ expect(options).toEqual(['admin', 'member', 'viewer'])
99
+ fireEvent.change(select, { target: { value: 'admin' } })
100
+ expect(onChange).toHaveBeenCalledWith('admin')
101
+ })
102
+
103
+ it('honors disabled', () => {
104
+ render(<RoleSelector value="viewer" onChange={() => {}} disabled />)
105
+ expect(screen.getByRole('combobox')).toBeDisabled()
106
+ })
107
+ })
108
+
109
+ // ---------- InviteMemberDialog --------------------------------------------
110
+
111
+ describe('InviteMemberDialog', () => {
112
+ it('does not render the panel until the trigger is clicked', () => {
113
+ render(<InviteMemberDialog teamId="t1" onSubmit={jest.fn()} />)
114
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
115
+ fireEvent.click(screen.getByRole('button', { name: /invite member/i }))
116
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
117
+ })
118
+
119
+ it('submits email + role + teamId via onSubmit and shows a success line', async () => {
120
+ const onSubmit = jest.fn().mockResolvedValue({ id: 'i1' } as InvitationLite)
121
+ const onInvited = jest.fn()
122
+ render(
123
+ <InviteMemberDialog
124
+ teamId="t1"
125
+ onSubmit={onSubmit}
126
+ onInvited={onInvited}
127
+ />
128
+ )
129
+ fireEvent.click(screen.getByRole('button', { name: /invite member/i }))
130
+
131
+ fireEvent.change(screen.getByLabelText('Email'), {
132
+ target: { value: 'newhire@x.com' },
133
+ })
134
+ fireEvent.change(screen.getByLabelText('Invite role'), {
135
+ target: { value: 'admin' },
136
+ })
137
+
138
+ await act(async () => {
139
+ fireEvent.submit(screen.getByRole('dialog').querySelector('form')!)
140
+ })
141
+
142
+ expect(onSubmit).toHaveBeenCalledWith({
143
+ email: 'newhire@x.com',
144
+ teamId: 't1',
145
+ role: 'admin',
146
+ })
147
+ expect(onInvited).toHaveBeenCalled()
148
+ expect(screen.getByText(/Invitation sent to newhire@x.com/)).toBeInTheDocument()
149
+ })
150
+
151
+ it('surfaces submit errors inline', async () => {
152
+ const onSubmit = jest.fn().mockRejectedValue(new Error('domain not allowed'))
153
+ render(<InviteMemberDialog teamId="t1" onSubmit={onSubmit} />)
154
+ fireEvent.click(screen.getByRole('button', { name: /invite member/i }))
155
+ fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'a@x.com' } })
156
+
157
+ await act(async () => {
158
+ fireEvent.submit(screen.getByRole('dialog').querySelector('form')!)
159
+ })
160
+ expect(screen.getByRole('alert')).toHaveTextContent('domain not allowed')
161
+ })
162
+
163
+ it('honors classNames slots', () => {
164
+ render(
165
+ <InviteMemberDialog
166
+ teamId="t1"
167
+ onSubmit={jest.fn()}
168
+ classNames={{ trigger: 'my-trigger' }}
169
+ />
170
+ )
171
+ expect(screen.getByRole('button', { name: /invite member/i })).toHaveClass(
172
+ 'my-trigger'
173
+ )
174
+ })
175
+ })
176
+
177
+ // ---------- PendingInvitationCallout --------------------------------------
178
+
179
+ describe('PendingInvitationCallout', () => {
180
+ const invitation: InvitationLite = {
181
+ id: 'i1',
182
+ email: 'a@x.com',
183
+ teamId: 't1',
184
+ role: 'admin',
185
+ expiresAt: '2099-01-01',
186
+ isExpired: false,
187
+ isAccepted: false,
188
+ }
189
+
190
+ it('renders the message + team name + role', () => {
191
+ render(
192
+ <PendingInvitationCallout
193
+ invitation={invitation}
194
+ teamName="Acme Eng"
195
+ />
196
+ )
197
+ expect(screen.getByText(/You.*invited/i)).toBeInTheDocument()
198
+ expect(screen.getByText('Acme Eng')).toBeInTheDocument()
199
+ expect(screen.getByText('admin')).toBeInTheDocument()
200
+ })
201
+
202
+ it('falls back to "team #<id>" when no team name is supplied', () => {
203
+ render(<PendingInvitationCallout invitation={invitation} />)
204
+ expect(screen.getByText('team #t1')).toBeInTheDocument()
205
+ })
206
+
207
+ it('calls onAccept + onAccepted', async () => {
208
+ const onAccept = jest.fn().mockResolvedValue(undefined)
209
+ const onAccepted = jest.fn()
210
+ render(
211
+ <PendingInvitationCallout
212
+ invitation={invitation}
213
+ teamName="Acme"
214
+ onAccept={onAccept}
215
+ onAccepted={onAccepted}
216
+ />
217
+ )
218
+ await act(async () => {
219
+ fireEvent.click(screen.getByRole('button', { name: /accept/i }))
220
+ })
221
+ expect(onAccept).toHaveBeenCalledWith(invitation)
222
+ expect(onAccepted).toHaveBeenCalledWith(invitation)
223
+ })
224
+
225
+ it('only renders Decline when an onDecline is provided', () => {
226
+ const { rerender } = render(
227
+ <PendingInvitationCallout invitation={invitation} teamName="Acme" />
228
+ )
229
+ expect(screen.queryByRole('button', { name: /decline/i })).not.toBeInTheDocument()
230
+ rerender(
231
+ <PendingInvitationCallout
232
+ invitation={invitation}
233
+ teamName="Acme"
234
+ onDecline={jest.fn()}
235
+ />
236
+ )
237
+ expect(screen.getByRole('button', { name: /decline/i })).toBeInTheDocument()
238
+ })
239
+ })
240
+
241
+ // ---------- DomainClaimCard ------------------------------------------------
242
+
243
+ describe('DomainClaimCard', () => {
244
+ const baseClaim: DomainClaimLite = {
245
+ id: 'd1',
246
+ companyId: 'c1',
247
+ domain: 'acme.com',
248
+ verified: false,
249
+ verificationToken: 'startsim-verify=xyz',
250
+ createdAt: '2025-01-01',
251
+ }
252
+
253
+ it('renders the DNS TXT block when a verification token is present', () => {
254
+ render(<DomainClaimCard claim={baseClaim} />)
255
+ expect(screen.getByText('acme.com')).toBeInTheDocument()
256
+ expect(screen.getByText('startsim-verify=xyz')).toBeInTheDocument()
257
+ expect(screen.getByRole('button', { name: /verify dns/i })).toBeInTheDocument()
258
+ })
259
+
260
+ it('hides verification UI once verified=true', () => {
261
+ render(<DomainClaimCard claim={{ ...baseClaim, verified: true }} />)
262
+ expect(screen.getByText('Verified')).toBeInTheDocument()
263
+ expect(screen.queryByRole('button', { name: /verify dns/i })).not.toBeInTheDocument()
264
+ })
265
+
266
+ it('initiate-email then submit-code calls the verify handler', async () => {
267
+ const onInitiateEmail = jest.fn().mockResolvedValue(undefined)
268
+ const onVerifyEmail = jest.fn().mockResolvedValue(undefined)
269
+ render(
270
+ <DomainClaimCard
271
+ claim={baseClaim}
272
+ onInitiateEmail={onInitiateEmail}
273
+ onVerifyEmail={onVerifyEmail}
274
+ />
275
+ )
276
+ await act(async () => {
277
+ fireEvent.click(screen.getByRole('button', { name: /email attestation/i }))
278
+ })
279
+ expect(onInitiateEmail).toHaveBeenCalledWith(baseClaim)
280
+
281
+ const code = screen.getByLabelText('Attestation code')
282
+ fireEvent.change(code, { target: { value: '123456' } })
283
+ await act(async () => {
284
+ fireEvent.click(screen.getByRole('button', { name: /submit code/i }))
285
+ })
286
+ expect(onVerifyEmail).toHaveBeenCalledWith(baseClaim, '123456')
287
+ })
288
+
289
+ it('renders Revoke when onRevoke is provided', () => {
290
+ render(<DomainClaimCard claim={baseClaim} onRevoke={jest.fn()} />)
291
+ expect(screen.getByRole('button', { name: /revoke/i })).toBeInTheDocument()
292
+ })
293
+ })
294
+
295
+ // ---------- LeaveTeamDialog ------------------------------------------------
296
+
297
+ describe('LeaveTeamDialog', () => {
298
+ const team: TeamLite = { id: 't1', slug: 'eng', name: 'Engineering', companyId: 'c1' }
299
+
300
+ it('opens on trigger click, fires onSubmit + onLeft on confirm', async () => {
301
+ const onSubmit = jest.fn().mockResolvedValue(undefined)
302
+ const onLeft = jest.fn()
303
+ render(<LeaveTeamDialog team={team} onSubmit={onSubmit} onLeft={onLeft} />)
304
+ fireEvent.click(screen.getByRole('button', { name: /leave team/i }))
305
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
306
+ await act(async () => {
307
+ // Click the confirm button (red); it's the second 'Leave team' on screen.
308
+ fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1])
309
+ })
310
+ expect(onSubmit).toHaveBeenCalledWith(team)
311
+ expect(onLeft).toHaveBeenCalledWith(team)
312
+ })
313
+
314
+ it('surfaces backend "last owner" errors inline', async () => {
315
+ const onSubmit = jest.fn().mockRejectedValue(new Error('Cannot remove the last owner'))
316
+ render(<LeaveTeamDialog team={team} onSubmit={onSubmit} />)
317
+ fireEvent.click(screen.getByRole('button', { name: /leave team/i }))
318
+ await act(async () => {
319
+ fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1])
320
+ })
321
+ expect(screen.getByRole('alert')).toHaveTextContent(
322
+ /cannot remove the last owner/i
323
+ )
324
+ })
325
+ })
326
+
327
+ // ---------- OrgSwitcher ----------------------------------------------------
328
+
329
+ describe('OrgSwitcher', () => {
330
+ const companies: CompanyLite[] = [
331
+ { id: 'c1', slug: 'acme', name: 'Acme' },
332
+ { id: 'c2', slug: 'beta', name: 'Beta' },
333
+ ]
334
+
335
+ it('renders an option per company + fires onSwitch on change', () => {
336
+ const onSwitch = jest.fn()
337
+ render(
338
+ <OrgSwitcher companies={companies} currentCompanyId="c1" onSwitch={onSwitch} />
339
+ )
340
+ const select = screen.getByRole('combobox') as HTMLSelectElement
341
+ expect(select.value).toBe('c1')
342
+ fireEvent.change(select, { target: { value: 'c2' } })
343
+ expect(onSwitch).toHaveBeenCalledWith('c2')
344
+ })
345
+
346
+ it('renders nothing when there are no companies', () => {
347
+ const { container } = render(
348
+ <OrgSwitcher companies={[]} currentCompanyId={null} onSwitch={() => {}} />
349
+ )
350
+ expect(container.firstChild).toBeNull()
351
+ })
352
+ })
@@ -0,0 +1,45 @@
1
+ /** Default Tailwind classes for {@link DomainClaimCard}. startsim-o7s. */
2
+ export interface DomainClaimCardClassNames {
3
+ root?: string
4
+ header?: string
5
+ domain?: string
6
+ statusBadge?: string
7
+ statusVerified?: string
8
+ statusUnverified?: string
9
+ body?: string
10
+ sectionLabel?: string
11
+ tokenRow?: string
12
+ tokenCode?: string
13
+ helpText?: string
14
+ actions?: string
15
+ primaryButton?: string
16
+ secondaryButton?: string
17
+ revokeButton?: string
18
+ errorText?: string
19
+ codeInput?: string
20
+ }
21
+
22
+ export const DOMAIN_CLAIM_DEFAULTS: Required<DomainClaimCardClassNames> = {
23
+ root: 'rounded-xl border border-gray-200 bg-white p-5',
24
+ header: 'flex items-center justify-between gap-3',
25
+ domain: 'text-base font-semibold text-gray-900',
26
+ statusBadge: 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
27
+ statusVerified: 'bg-green-100 text-green-800',
28
+ statusUnverified: 'bg-yellow-100 text-yellow-800',
29
+ body: 'mt-4 space-y-3 text-sm text-gray-700',
30
+ sectionLabel: 'text-xs font-medium uppercase tracking-wide text-gray-500',
31
+ tokenRow: 'flex items-center gap-2',
32
+ tokenCode:
33
+ 'flex-1 rounded-md bg-gray-50 px-3 py-2 font-mono text-xs text-gray-900',
34
+ helpText: 'text-xs text-gray-500',
35
+ actions: 'mt-4 flex flex-wrap items-center gap-2',
36
+ primaryButton:
37
+ 'rounded-md bg-primary-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-50',
38
+ secondaryButton:
39
+ 'rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50',
40
+ revokeButton:
41
+ 'rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 disabled:opacity-50',
42
+ errorText: 'mt-2 text-sm text-red-600',
43
+ codeInput:
44
+ 'w-24 rounded-md border border-gray-300 px-2 py-1.5 text-sm tracking-widest outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500',
45
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Headless team-management UI components (startsim-o7s).
3
+ *
4
+ * Each component accepts a `classNames` slot map with sensible Tailwind
5
+ * defaults so each app's `/settings/team` and `/settings/domains` page
6
+ * is a thin wrapper, not a re-implementation. Same pattern as the
7
+ * SignupForm/ResetPasswordForm/SignInForm quartet shipped earlier.
8
+ */
9
+
10
+ export { MembersTable } from './MembersTable'
11
+ export type { MembersTableProps } from './MembersTable'
12
+ export type { MembersTableClassNames } from './members-table-default-class-names'
13
+ export { MEMBERS_TABLE_DEFAULTS } from './members-table-default-class-names'
14
+
15
+ export { RoleSelector } from './RoleSelector'
16
+ export type { RoleSelectorProps } from './RoleSelector'
17
+ export type { RoleSelectorClassNames } from './role-selector-default-class-names'
18
+ export { ROLE_SELECTOR_DEFAULTS } from './role-selector-default-class-names'
19
+
20
+ export { InviteMemberDialog } from './InviteMemberDialog'
21
+ export type { InviteMemberDialogProps } from './InviteMemberDialog'
22
+ export type { InviteMemberDialogClassNames } from './invite-member-dialog-default-class-names'
23
+ export { INVITE_DIALOG_DEFAULTS } from './invite-member-dialog-default-class-names'
24
+
25
+ export { PendingInvitationCallout } from './PendingInvitationCallout'
26
+ export type { PendingInvitationCalloutProps } from './PendingInvitationCallout'
27
+ export type { PendingInvitationCalloutClassNames } from './pending-invitation-callout-default-class-names'
28
+ export { PENDING_INVITE_DEFAULTS } from './pending-invitation-callout-default-class-names'
29
+
30
+ export { DomainClaimCard } from './DomainClaimCard'
31
+ export type { DomainClaimCardProps } from './DomainClaimCard'
32
+ export type { DomainClaimCardClassNames } from './domain-claim-card-default-class-names'
33
+ export { DOMAIN_CLAIM_DEFAULTS } from './domain-claim-card-default-class-names'
34
+
35
+ export { LeaveTeamDialog } from './LeaveTeamDialog'
36
+ export type { LeaveTeamDialogProps } from './LeaveTeamDialog'
37
+ export type { LeaveTeamDialogClassNames } from './leave-team-dialog-default-class-names'
38
+ export { LEAVE_DIALOG_DEFAULTS } from './leave-team-dialog-default-class-names'
39
+
40
+ export { OrgSwitcher } from './OrgSwitcher'
41
+ export type { OrgSwitcherProps } from './OrgSwitcher'
42
+ export type { OrgSwitcherClassNames } from './org-switcher-default-class-names'
43
+ export { ORG_SWITCHER_DEFAULTS } from './org-switcher-default-class-names'
44
+
45
+ // Shared types
46
+ export {
47
+ userDisplayName,
48
+ userInitials,
49
+ type TeamRole,
50
+ type TeamLite,
51
+ type CompanyLite,
52
+ type MemberUserLite,
53
+ type MemberRow,
54
+ type InvitationLite,
55
+ type DomainVerificationMethod,
56
+ type DomainClaimLite,
57
+ } from './types'
@@ -0,0 +1,41 @@
1
+ /** Default Tailwind classes for {@link InviteMemberDialog}. startsim-o7s. */
2
+ export interface InviteMemberDialogClassNames {
3
+ trigger?: string
4
+ overlay?: string
5
+ panel?: string
6
+ header?: string
7
+ title?: string
8
+ body?: string
9
+ fieldRow?: string
10
+ label?: string
11
+ input?: string
12
+ errorText?: string
13
+ successText?: string
14
+ footer?: string
15
+ cancelButton?: string
16
+ submitButton?: string
17
+ }
18
+
19
+ export const INVITE_DIALOG_DEFAULTS: Required<InviteMemberDialogClassNames> = {
20
+ trigger:
21
+ 'inline-flex items-center rounded-md bg-primary-600 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-700',
22
+ overlay:
23
+ 'fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4',
24
+ panel:
25
+ 'w-full max-w-md rounded-xl bg-white shadow-xl ring-1 ring-black/5',
26
+ header: 'border-b border-gray-100 px-6 py-4',
27
+ title: 'text-base font-semibold text-gray-900',
28
+ body: 'space-y-4 px-6 py-4',
29
+ fieldRow: '',
30
+ label: 'block text-sm font-medium text-gray-700 mb-1',
31
+ input:
32
+ 'w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500',
33
+ errorText: 'text-sm text-red-600',
34
+ successText: 'text-sm text-green-600',
35
+ footer:
36
+ 'flex items-center justify-end gap-2 border-t border-gray-100 px-6 py-3',
37
+ cancelButton:
38
+ 'rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50',
39
+ submitButton:
40
+ 'rounded-md bg-primary-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-50',
41
+ }
@@ -0,0 +1,33 @@
1
+ /** Default Tailwind classes for {@link LeaveTeamDialog}. startsim-o7s. */
2
+ export interface LeaveTeamDialogClassNames {
3
+ trigger?: string
4
+ overlay?: string
5
+ panel?: string
6
+ header?: string
7
+ title?: string
8
+ body?: string
9
+ warningText?: string
10
+ errorText?: string
11
+ footer?: string
12
+ cancelButton?: string
13
+ confirmButton?: string
14
+ }
15
+
16
+ export const LEAVE_DIALOG_DEFAULTS: Required<LeaveTeamDialogClassNames> = {
17
+ trigger:
18
+ 'inline-flex items-center rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50',
19
+ overlay:
20
+ 'fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4',
21
+ panel: 'w-full max-w-md rounded-xl bg-white shadow-xl ring-1 ring-black/5',
22
+ header: 'border-b border-gray-100 px-6 py-4',
23
+ title: 'text-base font-semibold text-gray-900',
24
+ body: 'space-y-3 px-6 py-4 text-sm text-gray-700',
25
+ warningText: 'text-sm text-gray-600',
26
+ errorText: 'text-sm text-red-600',
27
+ footer:
28
+ 'flex items-center justify-end gap-2 border-t border-gray-100 px-6 py-3',
29
+ cancelButton:
30
+ 'rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50',
31
+ confirmButton:
32
+ 'rounded-md bg-red-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50',
33
+ }
@@ -0,0 +1,39 @@
1
+ /** Default Tailwind classes for {@link MembersTable}. startsim-o7s. */
2
+ export interface MembersTableClassNames {
3
+ root?: string
4
+ table?: string
5
+ thead?: string
6
+ headerRow?: string
7
+ th?: string
8
+ tbody?: string
9
+ row?: string
10
+ cell?: string
11
+ avatar?: string
12
+ name?: string
13
+ email?: string
14
+ roleBadge?: string
15
+ actions?: string
16
+ removeButton?: string
17
+ emptyRow?: string
18
+ }
19
+
20
+ export const MEMBERS_TABLE_DEFAULTS: Required<MembersTableClassNames> = {
21
+ root: 'w-full overflow-x-auto rounded-lg border border-gray-200',
22
+ table: 'w-full text-sm text-left',
23
+ thead: 'bg-gray-50 text-xs uppercase tracking-wide text-gray-500',
24
+ headerRow: '',
25
+ th: 'px-4 py-2 font-medium',
26
+ tbody: 'divide-y divide-gray-100 bg-white',
27
+ row: 'hover:bg-gray-50',
28
+ cell: 'px-4 py-3 align-middle',
29
+ avatar:
30
+ 'flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-xs font-medium text-gray-600',
31
+ name: 'font-medium text-gray-900',
32
+ email: 'text-gray-500',
33
+ roleBadge:
34
+ 'inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700',
35
+ actions: 'flex items-center justify-end gap-2',
36
+ removeButton:
37
+ 'rounded-md border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-100 disabled:opacity-50',
38
+ emptyRow: 'px-4 py-6 text-center text-sm text-gray-500',
39
+ }
@@ -0,0 +1,13 @@
1
+ /** Default Tailwind classes for {@link OrgSwitcher}. startsim-o7s. */
2
+ export interface OrgSwitcherClassNames {
3
+ root?: string
4
+ label?: string
5
+ select?: string
6
+ }
7
+
8
+ export const ORG_SWITCHER_DEFAULTS: Required<OrgSwitcherClassNames> = {
9
+ root: 'inline-flex items-center gap-2',
10
+ label: 'text-xs font-medium uppercase tracking-wide text-gray-500',
11
+ select:
12
+ 'rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900 outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500',
13
+ }
@@ -0,0 +1,22 @@
1
+ /** Default Tailwind classes for {@link PendingInvitationCallout}. startsim-o7s. */
2
+ export interface PendingInvitationCalloutClassNames {
3
+ root?: string
4
+ message?: string
5
+ emphasized?: string
6
+ actions?: string
7
+ acceptButton?: string
8
+ declineButton?: string
9
+ errorText?: string
10
+ }
11
+
12
+ export const PENDING_INVITE_DEFAULTS: Required<PendingInvitationCalloutClassNames> = {
13
+ root: 'flex items-center gap-3 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3',
14
+ message: 'flex-1 text-sm text-blue-900',
15
+ emphasized: 'font-semibold',
16
+ actions: 'flex items-center gap-2',
17
+ acceptButton:
18
+ 'rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50',
19
+ declineButton:
20
+ 'rounded-md border border-blue-300 px-3 py-1.5 text-sm font-medium text-blue-700 hover:bg-blue-100',
21
+ errorText: 'mt-2 text-sm text-red-600',
22
+ }
@@ -0,0 +1,11 @@
1
+ /** Default Tailwind classes for {@link RoleSelector}. startsim-o7s. */
2
+ export interface RoleSelectorClassNames {
3
+ root?: string
4
+ select?: string
5
+ }
6
+
7
+ export const ROLE_SELECTOR_DEFAULTS: Required<RoleSelectorClassNames> = {
8
+ root: 'inline-block',
9
+ select:
10
+ 'rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-900 outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50',
11
+ }