@startsimpli/ui 0.4.6 → 0.4.7
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 -1
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandPalette.tsx +344 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +3 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/index.ts +14 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +791 -0
- package/src/components/email-editor/email-editor.tsx +886 -0
- package/src/components/email-editor/index.ts +50 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttChart.tsx +25 -25
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/index.ts +6 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface CreateListDialogProps {
|
|
7
|
+
open: boolean
|
|
8
|
+
onClose: () => void
|
|
9
|
+
onSubmit: (data: { name: string; description: string; sourceType: string }) => void
|
|
10
|
+
isSubmitting?: boolean
|
|
11
|
+
sourceTypes?: Array<{ value: string; label: string }>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultSourceTypes = [
|
|
15
|
+
{ value: 'static', label: 'Static' },
|
|
16
|
+
{ value: 'funnel', label: 'Funnel' },
|
|
17
|
+
{ value: 'query', label: 'Query' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function CreateListDialog({
|
|
21
|
+
open,
|
|
22
|
+
onClose,
|
|
23
|
+
onSubmit,
|
|
24
|
+
isSubmitting = false,
|
|
25
|
+
sourceTypes = defaultSourceTypes,
|
|
26
|
+
}: CreateListDialogProps) {
|
|
27
|
+
const [name, setName] = React.useState('')
|
|
28
|
+
const [description, setDescription] = React.useState('')
|
|
29
|
+
const [sourceType, setSourceType] = React.useState(sourceTypes[0]?.value ?? 'static')
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (open) {
|
|
33
|
+
setName('')
|
|
34
|
+
setDescription('')
|
|
35
|
+
setSourceType(sourceTypes[0]?.value ?? 'static')
|
|
36
|
+
}
|
|
37
|
+
}, [open, sourceTypes])
|
|
38
|
+
|
|
39
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
40
|
+
e.preventDefault()
|
|
41
|
+
if (!name.trim()) return
|
|
42
|
+
onSubmit({ name: name.trim(), description: description.trim(), sourceType })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!open) return null
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
49
|
+
{/* Backdrop */}
|
|
50
|
+
<div
|
|
51
|
+
className="fixed inset-0 bg-black/50"
|
|
52
|
+
onClick={isSubmitting ? undefined : onClose}
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Dialog */}
|
|
57
|
+
<div className="relative z-50 w-full max-w-md rounded-lg border bg-background p-6 shadow-lg">
|
|
58
|
+
<h2 className="text-lg font-semibold text-foreground">Create List</h2>
|
|
59
|
+
|
|
60
|
+
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
|
|
61
|
+
{/* Name */}
|
|
62
|
+
<div className="space-y-1.5">
|
|
63
|
+
<label htmlFor="list-name" className="text-sm font-medium text-foreground">
|
|
64
|
+
Name <span className="text-red-500">*</span>
|
|
65
|
+
</label>
|
|
66
|
+
<input
|
|
67
|
+
id="list-name"
|
|
68
|
+
type="text"
|
|
69
|
+
required
|
|
70
|
+
value={name}
|
|
71
|
+
onChange={(e) => setName(e.target.value)}
|
|
72
|
+
placeholder="Enter list name"
|
|
73
|
+
disabled={isSubmitting}
|
|
74
|
+
className={cn(
|
|
75
|
+
'w-full rounded-md border bg-background px-3 py-2 text-sm',
|
|
76
|
+
'placeholder:text-muted-foreground',
|
|
77
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
78
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Description */}
|
|
84
|
+
<div className="space-y-1.5">
|
|
85
|
+
<label htmlFor="list-description" className="text-sm font-medium text-foreground">
|
|
86
|
+
Description
|
|
87
|
+
</label>
|
|
88
|
+
<textarea
|
|
89
|
+
id="list-description"
|
|
90
|
+
value={description}
|
|
91
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
92
|
+
placeholder="Optional description"
|
|
93
|
+
rows={3}
|
|
94
|
+
disabled={isSubmitting}
|
|
95
|
+
className={cn(
|
|
96
|
+
'w-full rounded-md border bg-background px-3 py-2 text-sm resize-none',
|
|
97
|
+
'placeholder:text-muted-foreground',
|
|
98
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
99
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Source Type */}
|
|
105
|
+
<div className="space-y-1.5">
|
|
106
|
+
<label htmlFor="list-source-type" className="text-sm font-medium text-foreground">
|
|
107
|
+
Source Type
|
|
108
|
+
</label>
|
|
109
|
+
<select
|
|
110
|
+
id="list-source-type"
|
|
111
|
+
value={sourceType}
|
|
112
|
+
onChange={(e) => setSourceType(e.target.value)}
|
|
113
|
+
disabled={isSubmitting}
|
|
114
|
+
className={cn(
|
|
115
|
+
'w-full rounded-md border bg-background px-3 py-2 text-sm',
|
|
116
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
117
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{sourceTypes.map((st) => (
|
|
121
|
+
<option key={st.value} value={st.value}>
|
|
122
|
+
{st.label}
|
|
123
|
+
</option>
|
|
124
|
+
))}
|
|
125
|
+
</select>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Actions */}
|
|
129
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={onClose}
|
|
133
|
+
disabled={isSubmitting}
|
|
134
|
+
className={cn(
|
|
135
|
+
'rounded-md border px-4 py-2 text-sm font-medium',
|
|
136
|
+
'hover:bg-muted transition-colors',
|
|
137
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
Cancel
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
type="submit"
|
|
144
|
+
disabled={isSubmitting || !name.trim()}
|
|
145
|
+
className={cn(
|
|
146
|
+
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
|
147
|
+
'hover:bg-primary/90 transition-colors',
|
|
148
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{isSubmitting ? 'Creating...' : 'Create'}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface ListCardProps {
|
|
7
|
+
name: string
|
|
8
|
+
sourceType: string
|
|
9
|
+
memberCount: number
|
|
10
|
+
createdAt: string
|
|
11
|
+
onClick?: () => void
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sourceTypeBadgeClasses: Record<string, string> = {
|
|
16
|
+
static: 'bg-blue-100 text-blue-700 border-blue-200',
|
|
17
|
+
funnel: 'bg-purple-100 text-purple-700 border-purple-200',
|
|
18
|
+
query: 'bg-green-100 text-green-700 border-green-200',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ListCard({
|
|
22
|
+
name,
|
|
23
|
+
sourceType,
|
|
24
|
+
memberCount,
|
|
25
|
+
createdAt,
|
|
26
|
+
onClick,
|
|
27
|
+
className,
|
|
28
|
+
}: ListCardProps) {
|
|
29
|
+
const badgeClass =
|
|
30
|
+
sourceTypeBadgeClasses[sourceType] ??
|
|
31
|
+
'bg-gray-100 text-gray-700 border-gray-200'
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
role={onClick ? 'button' : undefined}
|
|
36
|
+
tabIndex={onClick ? 0 : undefined}
|
|
37
|
+
onClick={onClick}
|
|
38
|
+
onKeyDown={
|
|
39
|
+
onClick
|
|
40
|
+
? (e) => {
|
|
41
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
42
|
+
e.preventDefault()
|
|
43
|
+
onClick()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
: undefined
|
|
47
|
+
}
|
|
48
|
+
className={cn(
|
|
49
|
+
'rounded-lg border bg-card text-card-foreground p-4 transition-all duration-150',
|
|
50
|
+
onClick && 'cursor-pointer hover:shadow-md hover:border-primary/30',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<div className="flex items-start justify-between gap-3">
|
|
55
|
+
<div className="min-w-0 flex-1">
|
|
56
|
+
<h4 className="text-sm font-semibold text-foreground truncate">
|
|
57
|
+
{name}
|
|
58
|
+
</h4>
|
|
59
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
60
|
+
{memberCount.toLocaleString()} {memberCount === 1 ? 'member' : 'members'}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<span
|
|
64
|
+
className={cn(
|
|
65
|
+
'inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium shrink-0',
|
|
66
|
+
badgeClass,
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{sourceType}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
73
|
+
Created {createdAt}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface StageTransitionModalProps {
|
|
7
|
+
open: boolean
|
|
8
|
+
onClose: () => void
|
|
9
|
+
onConfirm: (targetStage: string, notes?: string) => void
|
|
10
|
+
currentStage: string
|
|
11
|
+
stages: Array<{ id: string; label: string }>
|
|
12
|
+
entityName?: string
|
|
13
|
+
isSubmitting?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function StageTransitionModal({
|
|
17
|
+
open,
|
|
18
|
+
onClose,
|
|
19
|
+
onConfirm,
|
|
20
|
+
currentStage,
|
|
21
|
+
stages,
|
|
22
|
+
entityName,
|
|
23
|
+
isSubmitting = false,
|
|
24
|
+
}: StageTransitionModalProps) {
|
|
25
|
+
const [targetStage, setTargetStage] = React.useState('')
|
|
26
|
+
const [notes, setNotes] = React.useState('')
|
|
27
|
+
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (open) {
|
|
30
|
+
// Default to the first stage that isn't the current one
|
|
31
|
+
const firstOther = stages.find((s) => s.id !== currentStage)
|
|
32
|
+
setTargetStage(firstOther?.id ?? '')
|
|
33
|
+
setNotes('')
|
|
34
|
+
}
|
|
35
|
+
}, [open, currentStage, stages])
|
|
36
|
+
|
|
37
|
+
const handleConfirm = () => {
|
|
38
|
+
if (!targetStage) return
|
|
39
|
+
onConfirm(targetStage, notes.trim() || undefined)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const currentLabel = stages.find((s) => s.id === currentStage)?.label ?? currentStage
|
|
43
|
+
|
|
44
|
+
if (!open) return null
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
48
|
+
{/* Backdrop */}
|
|
49
|
+
<div
|
|
50
|
+
className="fixed inset-0 bg-black/50"
|
|
51
|
+
onClick={isSubmitting ? undefined : onClose}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{/* Dialog */}
|
|
56
|
+
<div className="relative z-50 w-full max-w-md rounded-lg border bg-background p-6 shadow-lg">
|
|
57
|
+
<h2 className="text-lg font-semibold text-foreground">
|
|
58
|
+
Move {entityName ? `"${entityName}"` : 'Item'} to Stage
|
|
59
|
+
</h2>
|
|
60
|
+
|
|
61
|
+
<div className="mt-4 space-y-4">
|
|
62
|
+
{/* Current stage indicator */}
|
|
63
|
+
<div className="space-y-1.5">
|
|
64
|
+
<p className="text-sm font-medium text-foreground">Current Stage</p>
|
|
65
|
+
<p className="text-sm text-muted-foreground">{currentLabel}</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Target stage select */}
|
|
69
|
+
<div className="space-y-1.5">
|
|
70
|
+
<label htmlFor="target-stage" className="text-sm font-medium text-foreground">
|
|
71
|
+
New Stage
|
|
72
|
+
</label>
|
|
73
|
+
<select
|
|
74
|
+
id="target-stage"
|
|
75
|
+
value={targetStage}
|
|
76
|
+
onChange={(e) => setTargetStage(e.target.value)}
|
|
77
|
+
disabled={isSubmitting}
|
|
78
|
+
className={cn(
|
|
79
|
+
'w-full rounded-md border bg-background px-3 py-2 text-sm',
|
|
80
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
81
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{stages
|
|
85
|
+
.filter((s) => s.id !== currentStage)
|
|
86
|
+
.map((stage) => (
|
|
87
|
+
<option key={stage.id} value={stage.id}>
|
|
88
|
+
{stage.label}
|
|
89
|
+
</option>
|
|
90
|
+
))}
|
|
91
|
+
</select>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Notes */}
|
|
95
|
+
<div className="space-y-1.5">
|
|
96
|
+
<label htmlFor="transition-notes" className="text-sm font-medium text-foreground">
|
|
97
|
+
Notes <span className="text-muted-foreground">(optional)</span>
|
|
98
|
+
</label>
|
|
99
|
+
<textarea
|
|
100
|
+
id="transition-notes"
|
|
101
|
+
value={notes}
|
|
102
|
+
onChange={(e) => setNotes(e.target.value)}
|
|
103
|
+
placeholder="Add notes about this transition..."
|
|
104
|
+
rows={3}
|
|
105
|
+
disabled={isSubmitting}
|
|
106
|
+
className={cn(
|
|
107
|
+
'w-full rounded-md border bg-background px-3 py-2 text-sm resize-none',
|
|
108
|
+
'placeholder:text-muted-foreground',
|
|
109
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
110
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Actions */}
|
|
116
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={onClose}
|
|
120
|
+
disabled={isSubmitting}
|
|
121
|
+
className={cn(
|
|
122
|
+
'rounded-md border px-4 py-2 text-sm font-medium',
|
|
123
|
+
'hover:bg-muted transition-colors',
|
|
124
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
Cancel
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={handleConfirm}
|
|
132
|
+
disabled={isSubmitting || !targetStage}
|
|
133
|
+
className={cn(
|
|
134
|
+
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
|
135
|
+
'hover:bg-primary/90 transition-colors',
|
|
136
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{isSubmitting ? 'Moving...' : 'Confirm'}
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SettingsCardProps {
|
|
4
|
+
title: string
|
|
5
|
+
description?: string
|
|
6
|
+
icon?: React.ElementType
|
|
7
|
+
action?: React.ReactNode
|
|
8
|
+
children?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SettingsCard({ title, description, icon: Icon, action, children }: SettingsCardProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="rounded-xl border bg-white text-gray-900 shadow-sm">
|
|
14
|
+
<div className="flex items-start justify-between p-6">
|
|
15
|
+
<div className="flex items-start gap-4">
|
|
16
|
+
{Icon && (
|
|
17
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
|
|
18
|
+
<Icon className="h-5 w-5 text-gray-600" />
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
<div>
|
|
22
|
+
<h3 className="font-semibold leading-none tracking-tight">{title}</h3>
|
|
23
|
+
{description && (
|
|
24
|
+
<p className="text-sm text-gray-500 mt-1.5">{description}</p>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{action && <div className="flex-shrink-0 ml-4">{action}</div>}
|
|
29
|
+
</div>
|
|
30
|
+
{children && <div className="px-6 pb-6 pt-0">{children}</div>}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SettingsNavItem {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
href: string
|
|
7
|
+
icon?: React.ElementType
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SettingsLayoutProps {
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
title?: string
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SettingsLayout({ children, title, description }: SettingsLayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="space-y-6 max-w-4xl">
|
|
19
|
+
{title && (
|
|
20
|
+
<div>
|
|
21
|
+
<h1 className="text-3xl font-bold text-gray-900">{title}</h1>
|
|
22
|
+
{description && <p className="text-gray-600 mt-1">{description}</p>}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
export interface SettingsNavItem {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
href: string
|
|
9
|
+
icon?: React.ElementType
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SettingsNavProps {
|
|
13
|
+
items: SettingsNavItem[]
|
|
14
|
+
activeId?: string
|
|
15
|
+
onNavigate: (href: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SettingsNav({ items, activeId, onNavigate }: SettingsNavProps) {
|
|
19
|
+
return (
|
|
20
|
+
<nav className="flex flex-col gap-1">
|
|
21
|
+
{items.map((item) => {
|
|
22
|
+
const isActive = item.id === activeId
|
|
23
|
+
const Icon = item.icon
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
key={item.id}
|
|
28
|
+
onClick={() => onNavigate(item.href)}
|
|
29
|
+
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-left ${
|
|
30
|
+
isActive
|
|
31
|
+
? 'bg-gray-100 text-gray-900'
|
|
32
|
+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
|
33
|
+
}`}
|
|
34
|
+
>
|
|
35
|
+
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
|
|
36
|
+
{item.label}
|
|
37
|
+
</button>
|
|
38
|
+
)
|
|
39
|
+
})}
|
|
40
|
+
</nav>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SettingsLayout } from './SettingsLayout'
|
|
2
|
+
export type { SettingsLayoutProps, SettingsNavItem as SettingsLayoutNavItem } from './SettingsLayout'
|
|
3
|
+
export { SettingsNav } from './SettingsNav'
|
|
4
|
+
export type { SettingsNavProps, SettingsNavItem } from './SettingsNav'
|
|
5
|
+
export { SettingsCard } from './SettingsCard'
|
|
6
|
+
export type { SettingsCardProps } from './SettingsCard'
|