@startsimpli/ui 0.4.14 → 0.4.16

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 (78) 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/__tests__/team-settings-page.test.tsx +146 -0
  34. package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
  35. package/src/components/team/index.ts +62 -0
  36. package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
  37. package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
  38. package/src/components/team/members-table-default-class-names.ts +39 -0
  39. package/src/components/team/org-switcher-default-class-names.ts +13 -0
  40. package/src/components/team/pages/DomainsSettingsPage.tsx +289 -0
  41. package/src/components/team/pages/TeamSettingsPage.tsx +423 -0
  42. package/src/components/team/pages/domains-settings-page-default-class-names.ts +89 -0
  43. package/src/components/team/pages/index.ts +33 -0
  44. package/src/components/team/pages/team-settings-page-default-class-names.ts +116 -0
  45. package/src/components/team/pages/types.ts +135 -0
  46. package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
  47. package/src/components/team/role-selector-default-class-names.ts +11 -0
  48. package/src/components/team/types.ts +97 -0
  49. package/src/components/workflows/ExecNodeDetails.tsx +83 -0
  50. package/src/components/workflows/ExecutionTimeline.tsx +146 -0
  51. package/src/components/workflows/NodeInspector.tsx +257 -0
  52. package/src/components/workflows/NodePalette.tsx +119 -0
  53. package/src/components/workflows/WorkflowCanvas.tsx +113 -0
  54. package/src/components/workflows/WorkflowEdge.tsx +65 -0
  55. package/src/components/workflows/WorkflowEditor.tsx +130 -0
  56. package/src/components/workflows/WorkflowNode.tsx +198 -0
  57. package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
  58. package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
  59. package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
  60. package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
  61. package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
  62. package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
  63. package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
  64. package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
  65. package/src/components/workflows/__tests__/serialization.test.ts +278 -0
  66. package/src/components/workflows/exec-status.ts +90 -0
  67. package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
  68. package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
  69. package/src/components/workflows/index.ts +78 -0
  70. package/src/components/workflows/layout/auto-layout.ts +142 -0
  71. package/src/components/workflows/node-icons.ts +31 -0
  72. package/src/components/workflows/serialization.ts +171 -0
  73. package/src/components/workflows/theme/categories.ts +96 -0
  74. package/src/components/workflows/types.ts +231 -0
  75. package/src/components/workflows/workflows.css +29 -0
  76. package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
  77. package/src/components/workspace/SplitPane.tsx +174 -0
  78. package/src/components/workspace/index.ts +4 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Narrow API-surface interfaces for the shared TeamSettingsPage /
3
+ * DomainsSettingsPage composers.
4
+ *
5
+ * Apps pass in the @startsimpli/api `api` instance and these interfaces
6
+ * structurally describe the slice the pages actually use. Keeping the slice
7
+ * shape inline here means @startsimpli/ui does NOT depend on @startsimpli/api
8
+ * directly. startsim-o7s.
9
+ */
10
+
11
+ import type {
12
+ InvitationLite,
13
+ MemberRow,
14
+ TeamRole,
15
+ DomainClaimLite,
16
+ } from '../types'
17
+
18
+ /** A page-level `member` row hits the API as-is — keep the shape thin. */
19
+ export interface ApiMember {
20
+ id: string
21
+ userId: string
22
+ teamId: string
23
+ role: TeamRole
24
+ joinedAt: string
25
+ user?: MemberRow['user']
26
+ }
27
+
28
+ /** Paginated wrapper used by @startsimpli/api list endpoints. */
29
+ export interface ApiPaginated<T> {
30
+ count?: number
31
+ next?: string | null
32
+ previous?: string | null
33
+ results: T[]
34
+ }
35
+
36
+ /** A team membership row from /team-members/my-teams/. */
37
+ export interface ApiMyTeamMembership {
38
+ id: string
39
+ userId: string
40
+ teamId: string
41
+ role: TeamRole
42
+ joinedAt: string
43
+ team?: {
44
+ id: string
45
+ slug: string
46
+ name: string
47
+ companyId: string
48
+ }
49
+ }
50
+
51
+ /** A team row from /teams/{idOrSlug}/. */
52
+ export interface ApiTeam {
53
+ id: string
54
+ slug: string
55
+ name: string
56
+ companyId: string
57
+ }
58
+
59
+ /** A company row from /companies/{idOrSlug}/. */
60
+ export interface ApiCompany {
61
+ id: string
62
+ slug: string
63
+ name: string
64
+ }
65
+
66
+ /** An invitation row from /team-invitations/. */
67
+ export interface ApiInvitation extends InvitationLite {
68
+ createdAt: string
69
+ }
70
+
71
+ /** A domain-claim row from /team-domain-claims/. */
72
+ export interface ApiDomainClaim extends DomainClaimLite {}
73
+
74
+ /**
75
+ * Minimum API surface the TeamSettingsPage composer needs. The real
76
+ * @startsimpli/api `api` instance satisfies this structurally.
77
+ */
78
+ export interface TeamSettingsApi {
79
+ teams: {
80
+ myTeams: () => Promise<ApiMyTeamMembership[]>
81
+ retrieve: (idOrSlug: string) => Promise<ApiTeam>
82
+ members: (idOrSlug: string) => Promise<ApiMember[]>
83
+ updateRole: (
84
+ idOrSlug: string,
85
+ userId: string,
86
+ role: TeamRole,
87
+ ) => Promise<unknown>
88
+ removeMember: (idOrSlug: string, userId: string) => Promise<unknown>
89
+ bulkInvite: (
90
+ idOrSlug: string,
91
+ invitations: Array<{ email: string; role: TeamRole }>,
92
+ ) => Promise<{
93
+ invited: ApiInvitation[]
94
+ skipped?: Array<{ email: string; reason: string }>
95
+ }>
96
+ }
97
+ teamInvitations: {
98
+ list: (params: { teamId: string }) => Promise<ApiPaginated<ApiInvitation>>
99
+ create: (input: {
100
+ email: string
101
+ teamId: string
102
+ role: TeamRole
103
+ }) => Promise<ApiInvitation>
104
+ revoke: (id: string) => Promise<void>
105
+ }
106
+ companies: {
107
+ retrieve: (idOrSlug: string) => Promise<ApiCompany>
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Minimum API surface the DomainsSettingsPage composer needs. The real
113
+ * @startsimpli/api `api` instance satisfies this structurally.
114
+ */
115
+ export interface DomainsSettingsApi {
116
+ domainClaims: {
117
+ list: (params: { companyId: string }) => Promise<ApiPaginated<ApiDomainClaim>>
118
+ create: (input: {
119
+ companyId: string
120
+ domain: string
121
+ }) => Promise<ApiDomainClaim>
122
+ verifyDns: (id: string) => Promise<ApiDomainClaim>
123
+ verifyEmailInitiate: (id: string) => Promise<{ detail: string }>
124
+ verifyEmailCode: (id: string, code: string) => Promise<ApiDomainClaim>
125
+ revoke: (id: string) => Promise<void>
126
+ }
127
+ /** Used to pick the default companyId when the prop is omitted. */
128
+ teams: {
129
+ myTeams: () => Promise<ApiMyTeamMembership[]>
130
+ retrieve: (idOrSlug: string) => Promise<ApiTeam>
131
+ }
132
+ companies: {
133
+ retrieve: (idOrSlug: string) => Promise<ApiCompany>
134
+ }
135
+ }
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared types for the team-management components.
3
+ *
4
+ * Mirrors the @startsimpli/api shapes, but redeclared here so the UI
5
+ * package doesn't pull @startsimpli/api as a peer. Each component accepts
6
+ * the data it actually renders — apps pass through whatever @startsimpli/api
7
+ * gave them. startsim-o7s.
8
+ */
9
+
10
+ import type * as React from 'react'
11
+
12
+ export type TeamRole = 'owner' | 'admin' | 'member' | 'viewer'
13
+
14
+ export interface TeamLite {
15
+ id: string
16
+ slug: string
17
+ name: string
18
+ companyId: string
19
+ }
20
+
21
+ export interface CompanyLite {
22
+ id: string
23
+ slug: string
24
+ name: string
25
+ }
26
+
27
+ /** User shape rendered next to each row. Optional fields stay optional. */
28
+ export interface MemberUserLite {
29
+ id: string
30
+ email: string
31
+ firstName?: string
32
+ lastName?: string
33
+ fullName?: string
34
+ /** Optional URL to an avatar image. */
35
+ avatarUrl?: string
36
+ }
37
+
38
+ export interface MemberRow {
39
+ id: string
40
+ userId: string
41
+ teamId: string
42
+ role: TeamRole
43
+ joinedAt: string
44
+ /** Optionally embedded; when absent, the email + role-only render path is used. */
45
+ user?: MemberUserLite
46
+ }
47
+
48
+ export interface InvitationLite {
49
+ id: string
50
+ email: string
51
+ teamId: string
52
+ role: TeamRole
53
+ /** Only present on the create-response; otherwise the backend redacts. */
54
+ token?: string
55
+ expiresAt: string
56
+ acceptedAt?: string | null
57
+ revokedAt?: string | null
58
+ isExpired: boolean
59
+ isAccepted: boolean
60
+ }
61
+
62
+ export type DomainVerificationMethod = 'dns_txt' | 'email_attestation'
63
+
64
+ export interface DomainClaimLite {
65
+ id: string
66
+ companyId: string
67
+ domain: string
68
+ verified: boolean
69
+ verificationMethod?: DomainVerificationMethod | null
70
+ verificationToken?: string | null
71
+ verifiedAt?: string | null
72
+ createdAt: string
73
+ }
74
+
75
+ /**
76
+ * Helper: render a human-friendly label for a user — fullName wins, then
77
+ * firstName + lastName, then email.
78
+ */
79
+ export function userDisplayName(user: MemberUserLite | undefined): string {
80
+ if (!user) return ''
81
+ if (user.fullName) return user.fullName
82
+ const fl = [user.firstName, user.lastName].filter(Boolean).join(' ').trim()
83
+ return fl || user.email
84
+ }
85
+
86
+ /** Helper: render initials for the avatar fallback. */
87
+ export function userInitials(user: MemberUserLite | undefined): string {
88
+ if (!user) return '?'
89
+ const name = userDisplayName(user)
90
+ const parts = name.split(/\s+/).filter(Boolean)
91
+ if (parts.length === 0) return '?'
92
+ if (parts.length === 1) return parts[0][0].toUpperCase()
93
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
94
+ }
95
+
96
+ /** Common React-prop alias used by every team component. */
97
+ export type ChildrenProps = { children?: React.ReactNode }
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../../lib/utils';
5
+ import { StatusBadge } from '../badge/StatusBadge';
6
+ import { EmptyState } from '../states/EmptyState';
7
+ import type { NodeExecView } from './types';
8
+ import { NODE_STATUS_BADGE, formatDuration } from './exec-status';
9
+
10
+ export interface ExecNodeDetailsProps {
11
+ /** The selected node execution, or null when nothing is selected. */
12
+ node: NodeExecView | null | undefined;
13
+ className?: string;
14
+ }
15
+
16
+ function JsonBlock({
17
+ label,
18
+ value,
19
+ }: {
20
+ label: string;
21
+ value: Record<string, unknown> | null | undefined;
22
+ }) {
23
+ const isEmpty = value == null || Object.keys(value).length === 0;
24
+ return (
25
+ <div className="flex min-h-0 flex-col">
26
+ <h4 className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
27
+ {label}
28
+ </h4>
29
+ {isEmpty ? (
30
+ <p className="text-xs italic text-muted-foreground">No {label.toLowerCase()}.</p>
31
+ ) : (
32
+ <pre className="overflow-auto rounded-md border border-border bg-muted/40 p-2 text-xs leading-relaxed">
33
+ {JSON.stringify(value, null, 2)}
34
+ </pre>
35
+ )}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Read-only detail panel for a single node execution: name, type, status
42
+ * badge, duration, error, and pretty-printed input/output JSON. Purpose-built
43
+ * for the execution monitor — deliberately NOT the editor's NodeInspector.
44
+ */
45
+ export function ExecNodeDetails({ node, className }: ExecNodeDetailsProps) {
46
+ if (!node) {
47
+ return (
48
+ <EmptyState
49
+ title="No node selected"
50
+ description="Select a node in the timeline to inspect its input and output."
51
+ className={className}
52
+ />
53
+ );
54
+ }
55
+
56
+ const duration = formatDuration(node.executionTimeMs);
57
+
58
+ return (
59
+ <div className={cn('flex h-full min-h-0 flex-col gap-4 overflow-auto p-4', className)}>
60
+ <header className="flex flex-col gap-2">
61
+ <div className="flex items-center gap-2">
62
+ <h3 className="truncate text-base font-semibold text-foreground">
63
+ {node.name ?? node.nodeId}
64
+ </h3>
65
+ <StatusBadge status={node.status} config={NODE_STATUS_BADGE} size="sm" />
66
+ </div>
67
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
68
+ {node.type && <span>{node.type}</span>}
69
+ {duration && <span className="tabular-nums">{duration}</span>}
70
+ </div>
71
+ </header>
72
+
73
+ {node.status === 'error' && node.error && (
74
+ <p className="break-words rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
75
+ {node.error}
76
+ </p>
77
+ )}
78
+
79
+ <JsonBlock label="Input" value={node.inputData} />
80
+ <JsonBlock label="Output" value={node.outputData} />
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../../lib/utils';
5
+ import { StatusBadge } from '../badge/StatusBadge';
6
+ import { EmptyState } from '../states/EmptyState';
7
+ import type { NodeExecView } from './types';
8
+ import { NODE_STATUS_BADGE, formatDuration, isLiveStatus } from './exec-status';
9
+
10
+ export interface ExecutionTimelineProps {
11
+ /** Per-node execution overlays; rendered sorted by `executionOrder`. */
12
+ nodeExecutions: NodeExecView[];
13
+ /** Currently selected node id (row highlighted as active). */
14
+ activeNodeId?: string;
15
+ /** Called with a node id when its row is activated. */
16
+ onSelectNode?: (nodeId: string) => void;
17
+ className?: string;
18
+ }
19
+
20
+ function byExecutionOrder(a: NodeExecView, b: NodeExecView): number {
21
+ const ao = a.executionOrder ?? 0;
22
+ const bo = b.executionOrder ?? 0;
23
+ return ao - bo;
24
+ }
25
+
26
+ /**
27
+ * Vertical execution timeline: one row per node execution (ordered by
28
+ * `executionOrder`), each showing the node name, a status badge, the run
29
+ * duration, and inline error text for failed nodes. Running nodes show a
30
+ * pulsing live indicator; terminal nodes render statically.
31
+ *
32
+ * Read-only and app-agnostic — selection is surfaced via `onSelectNode`.
33
+ */
34
+ export function ExecutionTimeline({
35
+ nodeExecutions,
36
+ activeNodeId,
37
+ onSelectNode,
38
+ className,
39
+ }: ExecutionTimelineProps) {
40
+ const ordered = React.useMemo(
41
+ () => [...nodeExecutions].sort(byExecutionOrder),
42
+ [nodeExecutions]
43
+ );
44
+
45
+ if (ordered.length === 0) {
46
+ return (
47
+ <EmptyState
48
+ title="No node executions"
49
+ description="This run has not produced any node activity yet."
50
+ className={className}
51
+ />
52
+ );
53
+ }
54
+
55
+ return (
56
+ <ol className={cn('flex flex-col', className)} aria-label="Execution timeline">
57
+ {ordered.map((node) => {
58
+ const live = isLiveStatus(node.status);
59
+ const isActive = activeNodeId != null && node.nodeId === activeNodeId;
60
+ const duration = formatDuration(node.executionTimeMs);
61
+ const selectable = onSelectNode != null;
62
+
63
+ return (
64
+ <li
65
+ key={node.nodeId}
66
+ aria-current={isActive ? 'true' : undefined}
67
+ data-node-id={node.nodeId}
68
+ data-status={node.status}
69
+ className={cn(
70
+ 'group flex items-start gap-3 border-b border-border px-3 py-2.5 text-sm last:border-b-0',
71
+ selectable && 'cursor-pointer hover:bg-accent/50',
72
+ isActive && 'bg-accent'
73
+ )}
74
+ onClick={selectable ? () => onSelectNode!(node.nodeId) : undefined}
75
+ onKeyDown={
76
+ selectable
77
+ ? (e) => {
78
+ if (e.key === 'Enter' || e.key === ' ') {
79
+ e.preventDefault();
80
+ onSelectNode!(node.nodeId);
81
+ }
82
+ }
83
+ : undefined
84
+ }
85
+ tabIndex={selectable ? 0 : undefined}
86
+ >
87
+ {/* Status rail dot */}
88
+ <span className="relative mt-1 flex h-2.5 w-2.5 shrink-0 items-center justify-center">
89
+ {live ? (
90
+ <>
91
+ <span
92
+ data-live="true"
93
+ aria-hidden="true"
94
+ className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75"
95
+ />
96
+ <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-blue-500" />
97
+ </>
98
+ ) : (
99
+ <span
100
+ className={cn(
101
+ 'inline-flex h-2.5 w-2.5 rounded-full',
102
+ node.status === 'success' && 'bg-green-500',
103
+ node.status === 'error' && 'bg-red-500',
104
+ node.status === 'skipped' && 'bg-gray-300',
105
+ node.status === 'pending' && 'bg-amber-400',
106
+ node.status === 'idle' && 'bg-gray-300'
107
+ )}
108
+ />
109
+ )}
110
+ </span>
111
+
112
+ <div className="min-w-0 flex-1">
113
+ <div className="flex items-center gap-2">
114
+ <span className="truncate font-medium text-foreground">
115
+ {node.name ?? node.nodeId}
116
+ </span>
117
+ {duration && (
118
+ <span className="ml-auto shrink-0 tabular-nums text-xs text-muted-foreground">
119
+ {duration}
120
+ </span>
121
+ )}
122
+ </div>
123
+ <div className="mt-1 flex items-center gap-2">
124
+ <StatusBadge
125
+ status={node.status}
126
+ config={NODE_STATUS_BADGE}
127
+ size="sm"
128
+ />
129
+ {node.type && (
130
+ <span className="truncate text-xs text-muted-foreground">
131
+ {node.type}
132
+ </span>
133
+ )}
134
+ </div>
135
+ {node.status === 'error' && node.error && (
136
+ <p className="mt-1.5 break-words text-xs text-red-600">
137
+ {node.error}
138
+ </p>
139
+ )}
140
+ </div>
141
+ </li>
142
+ );
143
+ })}
144
+ </ol>
145
+ );
146
+ }