@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,187 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react'
5
+ import { cn } from '../../lib/utils'
6
+ import { SplitPane } from './SplitPane'
7
+
8
+ export type WorkspaceLayout = 'split' | 'stack' | 'auto'
9
+
10
+ export interface DualPaneWorkspaceProps {
11
+ left: React.ReactNode
12
+ right: React.ReactNode
13
+ /** Optional toolbar rendered above both panes (full width). */
14
+ toolbar?: React.ReactNode
15
+ leftLabel?: string
16
+ rightLabel?: string
17
+ /** Initial size of the left pane as a percentage. Default 42. */
18
+ initialLeftSize?: number
19
+ minLeftSize?: number
20
+ maxLeftSize?: number
21
+ /** Persist the split position under this key. */
22
+ storageKey?: string
23
+ /**
24
+ * `split` = resizable side-by-side, `stack` = vertically stacked,
25
+ * `auto` = stack below `stackBreakpoint`, split above. Default `auto`.
26
+ */
27
+ layout?: WorkspaceLayout
28
+ /** Pixel width below which `auto` stacks. Default 768. */
29
+ stackBreakpoint?: number
30
+ /** Show collapse/expand controls for each pane (split layout only). */
31
+ collapsible?: boolean
32
+ className?: string
33
+ }
34
+
35
+ function useResolvedLayout(layout: WorkspaceLayout, stackBreakpoint: number): 'split' | 'stack' {
36
+ const [isNarrow, setIsNarrow] = React.useState(false)
37
+ React.useEffect(() => {
38
+ if (layout !== 'auto' || typeof window === 'undefined') return
39
+ const mql = window.matchMedia(`(max-width: ${stackBreakpoint - 1}px)`)
40
+ const update = () => setIsNarrow(mql.matches)
41
+ update()
42
+ mql.addEventListener?.('change', update)
43
+ return () => mql.removeEventListener?.('change', update)
44
+ }, [layout, stackBreakpoint])
45
+
46
+ if (layout === 'split') return 'split'
47
+ if (layout === 'stack') return 'stack'
48
+ return isNarrow ? 'stack' : 'split'
49
+ }
50
+
51
+ interface PaneHeaderProps {
52
+ label?: string
53
+ side: 'left' | 'right'
54
+ collapsible?: boolean
55
+ collapsed: boolean
56
+ onToggle: () => void
57
+ }
58
+
59
+ function PaneHeader({ label, side, collapsible, collapsed, onToggle }: PaneHeaderProps) {
60
+ if (!label && !collapsible) return null
61
+ const CollapseIcon = side === 'left' ? PanelLeftClose : PanelRightClose
62
+ const ExpandIcon = side === 'left' ? PanelLeftOpen : PanelRightOpen
63
+ const Icon = collapsed ? ExpandIcon : CollapseIcon
64
+ const verb = collapsed ? 'Expand' : 'Collapse'
65
+ return (
66
+ <div className="flex h-9 shrink-0 items-center justify-between border-b border-border bg-muted/30 px-3">
67
+ {label ? (
68
+ <span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{label}</span>
69
+ ) : (
70
+ <span />
71
+ )}
72
+ {collapsible && (
73
+ <button
74
+ type="button"
75
+ onClick={onToggle}
76
+ aria-label={`${verb} ${label ?? side} pane`}
77
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
78
+ >
79
+ <Icon className="h-4 w-4" aria-hidden="true" />
80
+ </button>
81
+ )}
82
+ </div>
83
+ )
84
+ }
85
+
86
+ /**
87
+ * Opinionated two-pane workspace shell built on {@link SplitPane}: an optional
88
+ * full-width toolbar, two labelled/collapsible panes side-by-side, and
89
+ * automatic vertical stacking on narrow viewports. Fully app-agnostic — used by
90
+ * present-web for chat | slides, but free of any presentation-specific logic.
91
+ */
92
+ export function DualPaneWorkspace({
93
+ left,
94
+ right,
95
+ toolbar,
96
+ leftLabel,
97
+ rightLabel,
98
+ initialLeftSize = 42,
99
+ minLeftSize = 24,
100
+ maxLeftSize = 72,
101
+ storageKey,
102
+ layout = 'auto',
103
+ stackBreakpoint = 768,
104
+ collapsible = false,
105
+ className,
106
+ }: DualPaneWorkspaceProps) {
107
+ const resolved = useResolvedLayout(layout, stackBreakpoint)
108
+ const [collapsed, setCollapsed] = React.useState<null | 'left' | 'right'>(null)
109
+
110
+ const leftHeader = (
111
+ <PaneHeader
112
+ label={leftLabel}
113
+ side="left"
114
+ collapsible={collapsible}
115
+ collapsed={collapsed === 'left'}
116
+ onToggle={() => setCollapsed((c) => (c === 'left' ? null : 'left'))}
117
+ />
118
+ )
119
+ const rightHeader = (
120
+ <PaneHeader
121
+ label={rightLabel}
122
+ side="right"
123
+ collapsible={collapsible}
124
+ collapsed={collapsed === 'right'}
125
+ onToggle={() => setCollapsed((c) => (c === 'right' ? null : 'right'))}
126
+ />
127
+ )
128
+
129
+ const leftPane = (
130
+ <section className="flex h-full min-h-0 flex-col" aria-label={leftLabel}>
131
+ {leftHeader}
132
+ {collapsed !== 'left' && <div className="min-h-0 flex-1 overflow-hidden">{left}</div>}
133
+ </section>
134
+ )
135
+ const rightPane = (
136
+ <section className="flex h-full min-h-0 flex-col" aria-label={rightLabel}>
137
+ {rightHeader}
138
+ {collapsed !== 'right' && <div className="min-h-0 flex-1 overflow-hidden">{right}</div>}
139
+ </section>
140
+ )
141
+
142
+ // When one pane is collapsed, the other takes the full width (no resizer).
143
+ let body: React.ReactNode
144
+ if (resolved === 'stack') {
145
+ body = (
146
+ <div className="flex min-h-0 flex-1 flex-col divide-y divide-border" data-layout="stack">
147
+ <div className="min-h-0 flex-1 overflow-hidden">{leftPane}</div>
148
+ <div className="min-h-0 flex-1 overflow-hidden">{rightPane}</div>
149
+ </div>
150
+ )
151
+ } else if (collapsed === 'left') {
152
+ body = (
153
+ <div className="flex min-h-0 flex-1 flex-row" data-layout="split" data-collapsed="left">
154
+ <div className="shrink-0">{leftHeader}</div>
155
+ <div className="min-h-0 flex-1 overflow-hidden">{rightPane}</div>
156
+ </div>
157
+ )
158
+ } else if (collapsed === 'right') {
159
+ body = (
160
+ <div className="flex min-h-0 flex-1 flex-row" data-layout="split" data-collapsed="right">
161
+ <div className="min-h-0 flex-1 overflow-hidden">{leftPane}</div>
162
+ <div className="shrink-0">{rightHeader}</div>
163
+ </div>
164
+ )
165
+ } else {
166
+ body = (
167
+ <div className="min-h-0 flex-1" data-layout="split">
168
+ <SplitPane
169
+ first={leftPane}
170
+ second={rightPane}
171
+ initialSize={initialLeftSize}
172
+ minSize={minLeftSize}
173
+ maxSize={maxLeftSize}
174
+ storageKey={storageKey}
175
+ aria-label="Resize workspace panes"
176
+ />
177
+ </div>
178
+ )
179
+ }
180
+
181
+ return (
182
+ <div className={cn('flex h-full min-h-0 w-full flex-col', className)}>
183
+ {toolbar && <div className="shrink-0 border-b border-border">{toolbar}</div>}
184
+ {body}
185
+ </div>
186
+ )
187
+ }
@@ -0,0 +1,174 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../lib/utils'
5
+
6
+ export interface SplitPaneProps {
7
+ /** Content of the first (left / top) pane. */
8
+ first: React.ReactNode
9
+ /** Content of the second (right / bottom) pane. */
10
+ second: React.ReactNode
11
+ /** Split direction. `vertical` = side-by-side (a vertical divider). Default `vertical`. */
12
+ orientation?: 'vertical' | 'horizontal'
13
+ /** Initial size of the first pane, as a percentage (0-100). Default 50. */
14
+ initialSize?: number
15
+ /** Minimum size of the first pane as a percentage. Default 20. */
16
+ minSize?: number
17
+ /** Maximum size of the first pane as a percentage. Default 80. */
18
+ maxSize?: number
19
+ /** Keyboard nudge / step size in percentage points. Default 3. */
20
+ step?: number
21
+ /** Persist the size under this key in localStorage. */
22
+ storageKey?: string
23
+ /** Notified with the new first-pane percentage whenever it changes. */
24
+ onResize?: (size: number) => void
25
+ className?: string
26
+ 'aria-label'?: string
27
+ }
28
+
29
+ function clamp(v: number, min: number, max: number) {
30
+ return Math.min(max, Math.max(min, v))
31
+ }
32
+
33
+ /**
34
+ * Generic, app-agnostic resizable two-pane split. Drag the divider with a
35
+ * pointer or nudge it with the arrow keys. Nothing domain-specific lives here.
36
+ */
37
+ export function SplitPane({
38
+ first,
39
+ second,
40
+ orientation = 'vertical',
41
+ initialSize = 50,
42
+ minSize = 20,
43
+ maxSize = 80,
44
+ step = 3,
45
+ storageKey,
46
+ onResize,
47
+ className,
48
+ 'aria-label': ariaLabel = 'Resize panes',
49
+ }: SplitPaneProps) {
50
+ const isVertical = orientation === 'vertical'
51
+ const containerRef = React.useRef<HTMLDivElement>(null)
52
+ const draggingRef = React.useRef(false)
53
+
54
+ const [size, setSizeState] = React.useState<number>(() => {
55
+ if (typeof window !== 'undefined' && storageKey) {
56
+ const saved = window.localStorage.getItem(storageKey)
57
+ if (saved != null) {
58
+ const n = Number(saved)
59
+ if (!Number.isNaN(n)) return clamp(n, minSize, maxSize)
60
+ }
61
+ }
62
+ return clamp(initialSize, minSize, maxSize)
63
+ })
64
+
65
+ const setSize = React.useCallback(
66
+ (next: number) => {
67
+ const clamped = clamp(next, minSize, maxSize)
68
+ setSizeState(clamped)
69
+ onResize?.(clamped)
70
+ if (typeof window !== 'undefined' && storageKey) {
71
+ window.localStorage.setItem(storageKey, String(clamped))
72
+ }
73
+ },
74
+ [minSize, maxSize, onResize, storageKey]
75
+ )
76
+
77
+ const onKeyDown = (e: React.KeyboardEvent) => {
78
+ const grow = isVertical ? 'ArrowRight' : 'ArrowDown'
79
+ const shrink = isVertical ? 'ArrowLeft' : 'ArrowUp'
80
+ if (e.key === grow) {
81
+ e.preventDefault()
82
+ setSize(size + step)
83
+ } else if (e.key === shrink) {
84
+ e.preventDefault()
85
+ setSize(size - step)
86
+ } else if (e.key === 'Home') {
87
+ e.preventDefault()
88
+ setSize(minSize)
89
+ } else if (e.key === 'End') {
90
+ e.preventDefault()
91
+ setSize(maxSize)
92
+ }
93
+ }
94
+
95
+ const onPointerMove = React.useCallback(
96
+ (e: PointerEvent) => {
97
+ if (!draggingRef.current || !containerRef.current) return
98
+ const rect = containerRef.current.getBoundingClientRect()
99
+ const pct = isVertical
100
+ ? ((e.clientX - rect.left) / rect.width) * 100
101
+ : ((e.clientY - rect.top) / rect.height) * 100
102
+ setSize(pct)
103
+ },
104
+ [isVertical, setSize]
105
+ )
106
+
107
+ const stopDragging = React.useCallback(() => {
108
+ draggingRef.current = false
109
+ document.body.style.cursor = ''
110
+ document.body.style.userSelect = ''
111
+ }, [])
112
+
113
+ React.useEffect(() => {
114
+ window.addEventListener('pointermove', onPointerMove)
115
+ window.addEventListener('pointerup', stopDragging)
116
+ return () => {
117
+ window.removeEventListener('pointermove', onPointerMove)
118
+ window.removeEventListener('pointerup', stopDragging)
119
+ }
120
+ }, [onPointerMove, stopDragging])
121
+
122
+ const startDragging = () => {
123
+ draggingRef.current = true
124
+ document.body.style.cursor = isVertical ? 'col-resize' : 'row-resize'
125
+ document.body.style.userSelect = 'none'
126
+ }
127
+
128
+ return (
129
+ <div
130
+ ref={containerRef}
131
+ className={cn(
132
+ 'flex h-full w-full overflow-hidden',
133
+ isVertical ? 'flex-row' : 'flex-col',
134
+ className
135
+ )}
136
+ data-orientation={orientation}
137
+ >
138
+ <div
139
+ className="min-h-0 min-w-0 overflow-hidden"
140
+ style={isVertical ? { width: `${size}%` } : { height: `${size}%` }}
141
+ data-pane="first"
142
+ >
143
+ {first}
144
+ </div>
145
+ <div
146
+ role="separator"
147
+ aria-label={ariaLabel}
148
+ aria-orientation={orientation}
149
+ aria-valuenow={Math.round(size)}
150
+ aria-valuemin={minSize}
151
+ aria-valuemax={maxSize}
152
+ tabIndex={0}
153
+ onKeyDown={onKeyDown}
154
+ onPointerDown={startDragging}
155
+ className={cn(
156
+ 'group relative shrink-0 bg-border transition-colors hover:bg-primary/40 focus-visible:bg-primary/60 focus:outline-none',
157
+ isVertical ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'
158
+ )}
159
+ data-testid="split-pane-resizer"
160
+ >
161
+ <span
162
+ aria-hidden="true"
163
+ className={cn(
164
+ 'absolute bg-transparent',
165
+ isVertical ? '-inset-x-1.5 inset-y-0' : '-inset-y-1.5 inset-x-0'
166
+ )}
167
+ />
168
+ </div>
169
+ <div className="min-h-0 min-w-0 flex-1 overflow-hidden" data-pane="second">
170
+ {second}
171
+ </div>
172
+ </div>
173
+ )
174
+ }
@@ -0,0 +1,4 @@
1
+ export { SplitPane } from './SplitPane'
2
+ export type { SplitPaneProps } from './SplitPane'
3
+ export { DualPaneWorkspace } from './DualPaneWorkspace'
4
+ export type { DualPaneWorkspaceProps, WorkspaceLayout } from './DualPaneWorkspace'