@hed-hog/operations 0.0.299 → 0.0.301

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 (97) hide show
  1. package/dist/operations.controller.d.ts +713 -31
  2. package/dist/operations.controller.d.ts.map +1 -1
  3. package/dist/operations.controller.js +157 -0
  4. package/dist/operations.controller.js.map +1 -1
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +5 -1
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.proposal.subscriber.d.ts +11 -0
  9. package/dist/operations.proposal.subscriber.d.ts.map +1 -0
  10. package/dist/operations.proposal.subscriber.js +80 -0
  11. package/dist/operations.proposal.subscriber.js.map +1 -0
  12. package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
  13. package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
  14. package/dist/operations.proposal.subscriber.spec.js +88 -0
  15. package/dist/operations.proposal.subscriber.spec.js.map +1 -0
  16. package/dist/operations.service.d.ts +490 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +3590 -1267
  19. package/dist/operations.service.js.map +1 -1
  20. package/dist/operations.service.spec.d.ts +2 -0
  21. package/dist/operations.service.spec.d.ts.map +1 -0
  22. package/dist/operations.service.spec.js +159 -0
  23. package/dist/operations.service.spec.js.map +1 -0
  24. package/hedhog/data/menu.yaml +232 -198
  25. package/hedhog/data/role.yaml +23 -23
  26. package/hedhog/data/role_route.yaml +39 -0
  27. package/hedhog/data/route.yaml +447 -317
  28. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  29. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  30. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  31. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  32. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  33. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  34. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  35. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  36. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  37. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  38. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  39. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  40. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  41. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  42. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  43. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  44. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  45. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  46. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  48. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  49. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  51. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  52. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  53. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  54. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  55. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  59. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  60. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  61. package/hedhog/frontend/messages/en.json +473 -12
  62. package/hedhog/frontend/messages/pt.json +528 -66
  63. package/hedhog/table/operations_approval.yaml +49 -49
  64. package/hedhog/table/operations_approval_history.yaml +29 -29
  65. package/hedhog/table/operations_collaborator.yaml +87 -67
  66. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
  67. package/hedhog/table/operations_contract.yaml +121 -100
  68. package/hedhog/table/operations_contract_document.yaml +40 -23
  69. package/hedhog/table/operations_contract_financial_term.yaml +40 -40
  70. package/hedhog/table/operations_contract_history.yaml +27 -27
  71. package/hedhog/table/operations_contract_party.yaml +46 -46
  72. package/hedhog/table/operations_contract_revision.yaml +38 -38
  73. package/hedhog/table/operations_contract_signature.yaml +38 -38
  74. package/hedhog/table/operations_contract_template.yaml +58 -0
  75. package/hedhog/table/operations_department.yaml +24 -0
  76. package/hedhog/table/operations_project.yaml +54 -54
  77. package/hedhog/table/operations_project_assignment.yaml +55 -55
  78. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
  79. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
  80. package/hedhog/table/operations_time_off_request.yaml +57 -57
  81. package/hedhog/table/operations_timesheet.yaml +41 -41
  82. package/hedhog/table/operations_timesheet_entry.yaml +40 -40
  83. package/package.json +5 -3
  84. package/src/operations.controller.ts +304 -182
  85. package/src/operations.module.ts +26 -22
  86. package/src/operations.proposal.subscriber.spec.ts +121 -0
  87. package/src/operations.proposal.subscriber.ts +86 -0
  88. package/src/operations.service.spec.ts +210 -0
  89. package/src/operations.service.ts +7317 -3595
  90. package/dist/operations-data.controller.d.ts +0 -139
  91. package/dist/operations-data.controller.d.ts.map +0 -1
  92. package/dist/operations-data.controller.js +0 -113
  93. package/dist/operations-data.controller.js.map +0 -1
  94. package/dist/operations-growth.controller.d.ts +0 -48
  95. package/dist/operations-growth.controller.d.ts.map +0 -1
  96. package/dist/operations-growth.controller.js +0 -90
  97. package/dist/operations-growth.controller.js.map +0 -1
@@ -0,0 +1,256 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Command,
6
+ CommandEmpty,
7
+ CommandGroup,
8
+ CommandInput,
9
+ CommandItem,
10
+ CommandList,
11
+ } from '@/components/ui/command';
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from '@/components/ui/popover';
17
+ import {
18
+ Sheet,
19
+ SheetContent,
20
+ SheetDescription,
21
+ SheetHeader,
22
+ SheetTitle,
23
+ } from '@/components/ui/sheet';
24
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
25
+ import { Check, ChevronsUpDown, Plus, X } from 'lucide-react';
26
+ import { useEffect, useRef, useState } from 'react';
27
+ import type { OperationsCollaborator, OperationsCollaboratorDetails } from '../_lib/types';
28
+ import { fetchOperations } from '../_lib/api';
29
+ import { CollaboratorFormScreen } from './collaborator-form-screen';
30
+
31
+ type CollaboratorSelectWithCreateProps = {
32
+ label: string;
33
+ value?: number | null;
34
+ initialSelectedLabel?: string;
35
+ placeholder: string;
36
+ disabled?: boolean;
37
+ onChange: (option: OperationsCollaborator | null) => void;
38
+ };
39
+
40
+ export function CollaboratorSelectWithCreate({
41
+ label,
42
+ value,
43
+ initialSelectedLabel = '',
44
+ placeholder,
45
+ disabled = false,
46
+ onChange,
47
+ }: CollaboratorSelectWithCreateProps) {
48
+ const { request, currentLocaleCode } = useApp();
49
+ const [open, setOpen] = useState(false);
50
+ const [search, setSearch] = useState('');
51
+ const [createOpen, setCreateOpen] = useState(false);
52
+ const [selectedLabel, setSelectedLabel] = useState(initialSelectedLabel);
53
+ const parentScrollContainerRef = useRef<HTMLElement | null>(null);
54
+ const parentScrollTopRef = useRef(0);
55
+
56
+ const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
57
+ queryKey: ['operations-inline-collaborators', currentLocaleCode],
58
+ queryFn: () =>
59
+ fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
60
+ placeholderData: (old) => old ?? [],
61
+ });
62
+
63
+ useEffect(() => {
64
+ setSelectedLabel(initialSelectedLabel);
65
+ }, [initialSelectedLabel]);
66
+
67
+ useEffect(() => {
68
+ if (value == null) {
69
+ setSelectedLabel('');
70
+ return;
71
+ }
72
+
73
+ const option = collaborators.find((item) => item.id === value);
74
+ if (option) {
75
+ setSelectedLabel(option.displayName);
76
+ }
77
+ }, [collaborators, value]);
78
+
79
+ const filteredOptions = collaborators.filter((item) => {
80
+ const haystack = [
81
+ item.displayName,
82
+ item.code,
83
+ item.title,
84
+ item.department,
85
+ ]
86
+ .filter(Boolean)
87
+ .join(' ')
88
+ .toLowerCase();
89
+ return haystack.includes(search.trim().toLowerCase());
90
+ });
91
+
92
+ const captureParentScrollPosition = (trigger: HTMLElement) => {
93
+ const parentSheetContent = trigger.closest(
94
+ '[data-radix-dialog-content]'
95
+ ) as HTMLElement | null;
96
+
97
+ if (!parentSheetContent) {
98
+ parentScrollContainerRef.current = null;
99
+ parentScrollTopRef.current = 0;
100
+ return;
101
+ }
102
+
103
+ parentScrollContainerRef.current = parentSheetContent;
104
+ parentScrollTopRef.current = parentSheetContent.scrollTop;
105
+ };
106
+
107
+ const restoreParentScrollPosition = () => {
108
+ const container =
109
+ parentScrollContainerRef.current &&
110
+ document.body.contains(parentScrollContainerRef.current)
111
+ ? parentScrollContainerRef.current
112
+ : null;
113
+
114
+ if (!container) {
115
+ return;
116
+ }
117
+
118
+ const restore = () => {
119
+ container.scrollTop = parentScrollTopRef.current;
120
+ };
121
+
122
+ requestAnimationFrame(restore);
123
+ setTimeout(restore, 0);
124
+ setTimeout(restore, 120);
125
+ };
126
+
127
+ return (
128
+ <div className="space-y-1.5">
129
+ <label className="text-xs font-medium text-muted-foreground">
130
+ {label}
131
+ </label>
132
+
133
+ <div className="flex gap-2">
134
+ <Popover open={open} onOpenChange={setOpen}>
135
+ <PopoverTrigger asChild>
136
+ <Button
137
+ type="button"
138
+ variant="outline"
139
+ role="combobox"
140
+ disabled={disabled}
141
+ className="h-9 flex-1 justify-between overflow-hidden"
142
+ >
143
+ <span className="truncate text-left">
144
+ {selectedLabel || placeholder}
145
+ </span>
146
+ <ChevronsUpDown className="size-4 shrink-0 opacity-50" />
147
+ </Button>
148
+ </PopoverTrigger>
149
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
150
+ <Command shouldFilter={false}>
151
+ <CommandInput
152
+ value={search}
153
+ onValueChange={setSearch}
154
+ placeholder="Buscar colaborador..."
155
+ />
156
+ <CommandList>
157
+ <CommandEmpty>Nenhum colaborador encontrado.</CommandEmpty>
158
+ <CommandGroup>
159
+ {filteredOptions.map((option) => (
160
+ <CommandItem
161
+ key={option.id}
162
+ value={String(option.id)}
163
+ onSelect={() => {
164
+ onChange(option);
165
+ setSelectedLabel(option.displayName);
166
+ setOpen(false);
167
+ }}
168
+ >
169
+ <Check
170
+ className={`mr-2 size-4 ${
171
+ value === option.id ? 'opacity-100' : 'opacity-0'
172
+ }`}
173
+ />
174
+ <div className="min-w-0">
175
+ <div className="truncate">{option.displayName}</div>
176
+ <div className="truncate text-xs text-muted-foreground">
177
+ {[option.code, option.title, option.department]
178
+ .filter(Boolean)
179
+ .join(' • ')}
180
+ </div>
181
+ </div>
182
+ </CommandItem>
183
+ ))}
184
+ </CommandGroup>
185
+ </CommandList>
186
+ </Command>
187
+ </PopoverContent>
188
+ </Popover>
189
+
190
+ {value ? (
191
+ <Button
192
+ type="button"
193
+ variant="outline"
194
+ size="icon"
195
+ className="h-9 w-9 shrink-0 cursor-pointer"
196
+ onClick={() => {
197
+ onChange(null);
198
+ setSelectedLabel('');
199
+ }}
200
+ >
201
+ <X className="size-4" />
202
+ </Button>
203
+ ) : null}
204
+
205
+ <Button
206
+ type="button"
207
+ variant="outline"
208
+ size="icon"
209
+ className="h-9 w-9 shrink-0 cursor-pointer"
210
+ disabled={disabled}
211
+ onClick={(event) => {
212
+ captureParentScrollPosition(event.currentTarget);
213
+ setCreateOpen(true);
214
+ }}
215
+ aria-label={`Criar ${label.toLowerCase()}`}
216
+ >
217
+ <Plus className="size-4" />
218
+ </Button>
219
+ </div>
220
+
221
+ <Sheet
222
+ open={createOpen}
223
+ onOpenChange={(nextOpen) => {
224
+ setCreateOpen(nextOpen);
225
+ if (!nextOpen) {
226
+ restoreParentScrollPosition();
227
+ }
228
+ }}
229
+ >
230
+ <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
231
+ <SheetHeader>
232
+ <SheetTitle>Novo colaborador</SheetTitle>
233
+ <SheetDescription>
234
+ Crie o colaborador sem sair do cadastro do contrato.
235
+ </SheetDescription>
236
+ </SheetHeader>
237
+ <CollaboratorFormScreen
238
+ onCancel={() => setCreateOpen(false)}
239
+ onSaved={async (collaborator: OperationsCollaboratorDetails) => {
240
+ onChange({
241
+ id: collaborator.id,
242
+ code: collaborator.code,
243
+ displayName: collaborator.displayName,
244
+ title: collaborator.title,
245
+ department: collaborator.department,
246
+ status: collaborator.status,
247
+ });
248
+ setSelectedLabel(collaborator.displayName);
249
+ setCreateOpen(false);
250
+ }}
251
+ />
252
+ </SheetContent>
253
+ </Sheet>
254
+ </div>
255
+ );
256
+ }
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { RichTextEditor } from '@/components/rich-text-editor';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
+ import { Textarea } from '@/components/ui/textarea';
7
+ import { useApp } from '@hed-hog/next-app-provider';
8
+ import { LoaderCircle, Sparkles } from 'lucide-react';
9
+ import { useTranslations } from 'next-intl';
10
+ import { useMemo, useState } from 'react';
11
+ import { mutateOperations } from '../_lib/api';
12
+ import { SectionCard } from './section-card';
13
+
14
+ type ContractContentEditorProps = {
15
+ value: string;
16
+ onChange: (value: string) => void;
17
+ editorTitle: string;
18
+ editorDescription: string;
19
+ previewTitle: string;
20
+ previewDescription: string;
21
+ previewFallbackHtml?: string;
22
+ promptContext?: Record<string, string | number | boolean | null | undefined>;
23
+ compact?: boolean;
24
+ descriptionMode?: 'inline' | 'tooltip';
25
+ layout?: 'split' | 'tabs';
26
+ showPreview?: boolean;
27
+ chrome?: 'card' | 'plain';
28
+ };
29
+
30
+ export function ContractContentEditor({
31
+ value,
32
+ onChange,
33
+ editorTitle,
34
+ editorDescription,
35
+ previewTitle,
36
+ previewDescription,
37
+ previewFallbackHtml,
38
+ promptContext,
39
+ compact = false,
40
+ descriptionMode = 'inline',
41
+ layout = 'split',
42
+ showPreview = true,
43
+ chrome = 'card',
44
+ }: ContractContentEditorProps) {
45
+ const t = useTranslations('operations.ContractContentEditor');
46
+ const { request, showToastHandler, getSettingValue } = useApp();
47
+ const [prompt, setPrompt] = useState('');
48
+ const [isGenerating, setIsGenerating] = useState(false);
49
+
50
+ const hasOpenAi = Boolean(getSettingValue('ai-openai-api-key-enabled'));
51
+ const hasGemini = Boolean(getSettingValue('ai-gemini-api-key-enabled'));
52
+ const provider = hasOpenAi ? 'openai' : hasGemini ? 'gemini' : null;
53
+ const providerLabel = provider === 'openai' ? 'OpenAI' : 'Gemini';
54
+
55
+ const promptContextSummary = useMemo(
56
+ () =>
57
+ Object.entries(promptContext ?? {})
58
+ .filter(
59
+ ([, rawValue]) =>
60
+ rawValue !== null &&
61
+ rawValue !== undefined &&
62
+ String(rawValue).trim()
63
+ )
64
+ .map(([key, rawValue]) => `${key}: ${String(rawValue)}`)
65
+ .join('\n'),
66
+ [promptContext]
67
+ );
68
+
69
+ const fillSuggestedPrompt = () => {
70
+ setPrompt(
71
+ t('suggestedPrompt', {
72
+ subject: String(
73
+ promptContext?.name ||
74
+ promptContext?.project_name ||
75
+ promptContext?.client_name ||
76
+ 'contrato'
77
+ ),
78
+ })
79
+ );
80
+ };
81
+
82
+ const generateWithAi = async () => {
83
+ if (!provider) {
84
+ showToastHandler?.('error', t('messages.missingConfiguration'));
85
+ return;
86
+ }
87
+
88
+ const trimmedPrompt = prompt.trim();
89
+ if (!trimmedPrompt) {
90
+ showToastHandler?.('error', t('messages.missingPrompt'));
91
+ return;
92
+ }
93
+
94
+ setIsGenerating(true);
95
+
96
+ try {
97
+ const response = await mutateOperations<{ content?: string }>(
98
+ request,
99
+ '/ai/chat',
100
+ 'POST',
101
+ {
102
+ provider,
103
+ message: trimmedPrompt,
104
+ systemPrompt: [
105
+ 'You are drafting professional business contract content for a back-office operations system.',
106
+ 'Return ONLY clean HTML suitable for a rich text editor preview.',
107
+ 'Do not include Markdown fences, explanations, or commentary.',
108
+ 'When useful, keep placeholders like {{client_name}}, {{project_name}}, {{project_code}}, {{start_date}}, {{end_date}}, {{budget_amount}}, and {{monthly_hour_cap}} in the generated content.',
109
+ 'Prefer short sections, headings, bullet points, and concise legal-style language.',
110
+ value?.trim()
111
+ ? `Current HTML content to improve or rewrite:\n${value}`
112
+ : 'There is no current contract content yet.',
113
+ promptContextSummary
114
+ ? `Context:\n${promptContextSummary}`
115
+ : 'No extra context was provided.',
116
+ ].join('\n\n'),
117
+ }
118
+ );
119
+
120
+ const generatedHtml = String(response?.content ?? '').trim();
121
+ if (!generatedHtml) {
122
+ throw new Error('Empty AI response');
123
+ }
124
+
125
+ onChange(generatedHtml);
126
+ showToastHandler?.(
127
+ 'success',
128
+ t('messages.generateSuccess', { provider: providerLabel })
129
+ );
130
+ } catch {
131
+ showToastHandler?.('error', t('messages.generateError'));
132
+ } finally {
133
+ setIsGenerating(false);
134
+ }
135
+ };
136
+
137
+ const renderSection = (
138
+ title: string,
139
+ description: string,
140
+ content: React.ReactNode
141
+ ) => {
142
+ if (chrome === 'plain') {
143
+ return (
144
+ <div className="space-y-3">
145
+ <div className="space-y-1">
146
+ <h3 className="text-lg font-semibold">{title}</h3>
147
+ <p className="text-sm text-muted-foreground">{description}</p>
148
+ </div>
149
+ {content}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ return (
155
+ <SectionCard
156
+ title={title}
157
+ description={description}
158
+ compact={compact}
159
+ descriptionMode={descriptionMode}
160
+ >
161
+ {content}
162
+ </SectionCard>
163
+ );
164
+ };
165
+
166
+ const editorSection = renderSection(
167
+ editorTitle,
168
+ editorDescription,
169
+ <RichTextEditor value={value} onChange={onChange} />
170
+ );
171
+
172
+ const previewSection = renderSection(
173
+ previewTitle,
174
+ previewDescription,
175
+ <div
176
+ className="prose prose-sm max-w-none rounded-lg border p-4"
177
+ dangerouslySetInnerHTML={{
178
+ __html: value || previewFallbackHtml || `<p>${t('emptyPreview')}</p>`,
179
+ }}
180
+ />
181
+ );
182
+
183
+ return (
184
+ <div className="space-y-4">
185
+ {provider ? (
186
+ <div className="rounded-lg border border-dashed px-3 py-3">
187
+ <div className="flex flex-wrap items-start justify-between gap-3">
188
+ <div className="space-y-1">
189
+ <div className="flex items-center gap-2 text-sm font-medium">
190
+ <Sparkles className="size-4 text-primary" />
191
+ <span>{t('assistantTitle')}</span>
192
+ </div>
193
+ <p className="text-xs text-muted-foreground">
194
+ {t('assistantDescription', { provider: providerLabel })}
195
+ </p>
196
+ </div>
197
+ <Button
198
+ type="button"
199
+ variant="outline"
200
+ size="sm"
201
+ className="cursor-pointer"
202
+ onClick={fillSuggestedPrompt}
203
+ >
204
+ {t('actions.useSuggestion')}
205
+ </Button>
206
+ </div>
207
+
208
+ <div className="mt-3 space-y-2">
209
+ <Textarea
210
+ rows={compact ? 2 : 3}
211
+ value={prompt}
212
+ placeholder={t('promptPlaceholder')}
213
+ onChange={(event) => setPrompt(event.target.value)}
214
+ />
215
+ <div className="flex flex-wrap gap-2">
216
+ <Button
217
+ type="button"
218
+ size="sm"
219
+ className="cursor-pointer"
220
+ disabled={isGenerating}
221
+ onClick={() => void generateWithAi()}
222
+ >
223
+ {isGenerating ? (
224
+ <LoaderCircle className="size-4 animate-spin" />
225
+ ) : (
226
+ <Sparkles className="size-4" />
227
+ )}
228
+ {t('actions.generate')}
229
+ </Button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ ) : null}
234
+
235
+ {!showPreview ? (
236
+ editorSection
237
+ ) : layout === 'tabs' ? (
238
+ <Tabs defaultValue="editor" className="w-full">
239
+ <TabsList className="grid w-full grid-cols-2">
240
+ <TabsTrigger value="editor">{editorTitle}</TabsTrigger>
241
+ <TabsTrigger value="preview">{previewTitle}</TabsTrigger>
242
+ </TabsList>
243
+ <TabsContent value="editor" className="mt-4">
244
+ {editorSection}
245
+ </TabsContent>
246
+ <TabsContent value="preview" className="mt-4">
247
+ {previewSection}
248
+ </TabsContent>
249
+ </Tabs>
250
+ ) : (
251
+ <div className="grid gap-4 xl:grid-cols-2">
252
+ {editorSection}
253
+ {previewSection}
254
+ </div>
255
+ )}
256
+ </div>
257
+ );
258
+ }