@visns-studio/visns-components 5.11.5 → 5.11.6
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/README.md +353 -0
- package/package.json +5 -5
- package/src/components/crm/Field.jsx +88 -60
- package/src/components/crm/columns/ColumnRenderers.jsx +19 -0
- package/src/components/crm/generic/GenericDetail.jsx +40 -0
- package/src/components/crm/generic/GenericEditableTable.jsx +3 -3
- package/src/components/crm/generic/GenericQuote.jsx +353 -1
- package/src/components/crm/generic/styles/GenericEditableTable.module.scss +102 -22
- package/src/components/crm/generic/styles/GenericQuote.module.scss +203 -0
- package/src/components/proposal/ProposalTemplatePreview.css +364 -0
- package/src/components/proposal/ProposalTemplatePreview.jsx +331 -0
- package/src/components/proposal/ProposalTemplateSectionManager.css +321 -0
- package/src/components/proposal/ProposalTemplateSectionManager.jsx +473 -0
- package/src/components/proposal/SectionEditor.css +508 -0
- package/src/components/proposal/SectionEditor.jsx +412 -0
- package/src/components/proposal/SectionTypeSelector.css +277 -0
- package/src/components/proposal/SectionTypeSelector.jsx +158 -0
- package/src/components/proposal/VariableInserter.css +392 -0
- package/src/components/proposal/VariableInserter.jsx +260 -0
- package/src/components/proposal/index.js +5 -0
- package/src/components/styles/global.css +179 -1
- package/src/index.js +13 -0
- package/src/utils/urlBuilder.js +40 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Eye, Download, RefreshCw, ExternalLink } from 'lucide-react';
|
|
3
|
+
import { buildUrl, defaultProposalApiEndpoints } from '../../utils/urlBuilder';
|
|
4
|
+
import './ProposalTemplatePreview.css';
|
|
5
|
+
|
|
6
|
+
// Helper function to extract variables from sections
|
|
7
|
+
const extractVariablesFromSections = (sections) => {
|
|
8
|
+
const variables = new Set();
|
|
9
|
+
sections.forEach(section => {
|
|
10
|
+
if (section.variables && Array.isArray(section.variables)) {
|
|
11
|
+
section.variables.forEach(variable => variables.add(variable));
|
|
12
|
+
}
|
|
13
|
+
// Also extract from content using regex
|
|
14
|
+
if (section.content) {
|
|
15
|
+
const matches = section.content.match(/{{(\w+)}}/g);
|
|
16
|
+
if (matches) {
|
|
17
|
+
matches.forEach(match => {
|
|
18
|
+
const variable = match.replace(/[{}]/g, '');
|
|
19
|
+
variables.add(variable);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return Array.from(variables);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ProposalTemplatePreview = ({
|
|
28
|
+
templateId,
|
|
29
|
+
templateData,
|
|
30
|
+
apiEndpoints = defaultProposalApiEndpoints
|
|
31
|
+
}) => {
|
|
32
|
+
const [previewData, setPreviewData] = useState(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
const [selectedBrandingProfile, setSelectedBrandingProfile] = useState('');
|
|
36
|
+
const [brandingProfiles, setBrandingProfiles] = useState([]);
|
|
37
|
+
|
|
38
|
+
// Helper function to replace template variables in URLs with templateId included
|
|
39
|
+
const buildUrlWithTemplate = (urlTemplate, params = {}) => {
|
|
40
|
+
return buildUrl(urlTemplate, { templateId, ...params });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (templateId) {
|
|
45
|
+
loadBrandingProfiles();
|
|
46
|
+
if (templateData) {
|
|
47
|
+
// Use template data if available
|
|
48
|
+
setPreviewData({
|
|
49
|
+
template: templateData,
|
|
50
|
+
sections: templateData.sections || [],
|
|
51
|
+
variables_used: extractVariablesFromSections(templateData.sections || [])
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
loadPreview();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [templateId, templateData]);
|
|
58
|
+
|
|
59
|
+
const loadBrandingProfiles = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(apiEndpoints.brandingProfiles, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
// Handle different response formats
|
|
71
|
+
let profiles = [];
|
|
72
|
+
if (data && data.success && Array.isArray(data.data)) {
|
|
73
|
+
profiles = data.data;
|
|
74
|
+
} else if (Array.isArray(data)) {
|
|
75
|
+
profiles = data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setBrandingProfiles(profiles);
|
|
79
|
+
if (profiles.length > 0) {
|
|
80
|
+
setSelectedBrandingProfile(profiles[0].id);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error loading branding profiles:', error);
|
|
84
|
+
setBrandingProfiles([]);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const loadPreview = async () => {
|
|
89
|
+
if (!templateId) return;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
setIsLoading(true);
|
|
93
|
+
setError(null);
|
|
94
|
+
|
|
95
|
+
const response = await fetch(`${buildUrlWithTemplate(apiEndpoints.templatePreview)}?branding_profile_id=${selectedBrandingProfile}`);
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error('Failed to load preview');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
if (data.success) {
|
|
103
|
+
setPreviewData(data.data);
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(data.message || 'Failed to load preview');
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Error loading preview:', error);
|
|
109
|
+
setError(error.message);
|
|
110
|
+
} finally {
|
|
111
|
+
setIsLoading(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleBrandingChange = (profileId) => {
|
|
116
|
+
setSelectedBrandingProfile(profileId);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleRefreshPreview = () => {
|
|
120
|
+
loadPreview();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleDownloadPDF = async () => {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(`${buildUrlWithTemplate(apiEndpoints.templatePdf)}?branding_profile_id=${selectedBrandingProfile}`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
const blob = await response.blob();
|
|
135
|
+
const url = window.URL.createObjectURL(blob);
|
|
136
|
+
const a = document.createElement('a');
|
|
137
|
+
a.href = url;
|
|
138
|
+
a.download = `proposal-template-${templateId}.pdf`;
|
|
139
|
+
document.body.appendChild(a);
|
|
140
|
+
a.click();
|
|
141
|
+
window.URL.revokeObjectURL(url);
|
|
142
|
+
document.body.removeChild(a);
|
|
143
|
+
} else {
|
|
144
|
+
throw new Error('Failed to generate PDF');
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Error downloading PDF:', error);
|
|
148
|
+
alert('Failed to download PDF. Please try again.');
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleOpenFullPreview = () => {
|
|
153
|
+
const url = `${buildUrlWithTemplate(apiEndpoints.templateFullPreview)}?branding_profile_id=${selectedBrandingProfile}&full=1`;
|
|
154
|
+
window.open(url, '_blank');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (!templateId) {
|
|
158
|
+
return (
|
|
159
|
+
<div className="proposal-preview-no-template">
|
|
160
|
+
<Eye size={48} className="proposal-preview-no-template-icon" />
|
|
161
|
+
<h3 className="proposal-preview-no-template-title">No Template Selected</h3>
|
|
162
|
+
<p className="proposal-preview-no-template-text">Save the template first to see the preview</p>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="proposal-preview-container">
|
|
169
|
+
{/* Preview Controls */}
|
|
170
|
+
<div className="proposal-preview-controls">
|
|
171
|
+
<div className="proposal-preview-branding-section">
|
|
172
|
+
<div>
|
|
173
|
+
<label className="proposal-preview-branding-label">
|
|
174
|
+
Branding Profile
|
|
175
|
+
</label>
|
|
176
|
+
<select
|
|
177
|
+
value={selectedBrandingProfile}
|
|
178
|
+
onChange={(e) => handleBrandingChange(e.target.value)}
|
|
179
|
+
className="proposal-preview-branding-select"
|
|
180
|
+
>
|
|
181
|
+
<option value="">Default Branding</option>
|
|
182
|
+
{Array.isArray(brandingProfiles) && brandingProfiles.map(profile => (
|
|
183
|
+
<option key={profile.id} value={profile.id}>
|
|
184
|
+
{profile.name}
|
|
185
|
+
</option>
|
|
186
|
+
))}
|
|
187
|
+
</select>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="proposal-preview-actions">
|
|
192
|
+
<button
|
|
193
|
+
onClick={handleRefreshPreview}
|
|
194
|
+
disabled={isLoading}
|
|
195
|
+
className="proposal-preview-btn secondary"
|
|
196
|
+
>
|
|
197
|
+
<RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
|
|
198
|
+
Refresh
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
<button
|
|
202
|
+
onClick={handleOpenFullPreview}
|
|
203
|
+
className="proposal-preview-btn outline"
|
|
204
|
+
>
|
|
205
|
+
<ExternalLink size={16} />
|
|
206
|
+
Full Preview
|
|
207
|
+
</button>
|
|
208
|
+
|
|
209
|
+
<button
|
|
210
|
+
onClick={handleDownloadPDF}
|
|
211
|
+
className="proposal-preview-btn primary"
|
|
212
|
+
>
|
|
213
|
+
<Download size={16} />
|
|
214
|
+
Download PDF
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Preview Content */}
|
|
220
|
+
<div className="proposal-preview-content">
|
|
221
|
+
{isLoading ? (
|
|
222
|
+
<div className="proposal-preview-loading">
|
|
223
|
+
<div className="proposal-preview-loading-content">
|
|
224
|
+
<RefreshCw size={32} className="proposal-preview-loading-icon" />
|
|
225
|
+
<p className="proposal-preview-loading-text">Loading preview...</p>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
) : error ? (
|
|
229
|
+
<div className="proposal-preview-error">
|
|
230
|
+
<div className="proposal-preview-error-content">
|
|
231
|
+
<div className="proposal-preview-error-icon">⚠️</div>
|
|
232
|
+
<h3 className="proposal-preview-error-title">Preview Error</h3>
|
|
233
|
+
<p className="proposal-preview-error-message">{error}</p>
|
|
234
|
+
<button
|
|
235
|
+
onClick={handleRefreshPreview}
|
|
236
|
+
className="proposal-preview-btn primary"
|
|
237
|
+
>
|
|
238
|
+
Try Again
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
) : previewData ? (
|
|
243
|
+
<div>
|
|
244
|
+
{/* Preview Header */}
|
|
245
|
+
<div className="proposal-preview-header">
|
|
246
|
+
<div className="proposal-preview-header-content">
|
|
247
|
+
<div className="proposal-preview-header-info">
|
|
248
|
+
<h3>Template Preview</h3>
|
|
249
|
+
<p className="proposal-preview-header-meta">
|
|
250
|
+
{previewData.sections?.length || 0} sections •
|
|
251
|
+
Variables: {previewData.variables_used?.length || 0}
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="proposal-preview-header-timestamp">
|
|
255
|
+
Last updated: {new Date().toLocaleString()}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Preview Body */}
|
|
261
|
+
<div className="proposal-preview-body">
|
|
262
|
+
<div className="proposal-preview-prose">
|
|
263
|
+
{previewData.html ? (
|
|
264
|
+
<div dangerouslySetInnerHTML={{ __html: previewData.html }} />
|
|
265
|
+
) : previewData.sections && previewData.sections.length > 0 ? (
|
|
266
|
+
<div>
|
|
267
|
+
{previewData.sections.map((section, index) => (
|
|
268
|
+
<div key={section.id || index} className="proposal-preview-section">
|
|
269
|
+
<h3 className="proposal-preview-section-title">
|
|
270
|
+
{section.title}
|
|
271
|
+
</h3>
|
|
272
|
+
<div
|
|
273
|
+
className="proposal-preview-section-content"
|
|
274
|
+
dangerouslySetInnerHTML={{
|
|
275
|
+
__html: section.content || '<em>No content</em>'
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
) : (
|
|
282
|
+
<div className="proposal-preview-empty">
|
|
283
|
+
No content to preview
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{/* Preview Footer */}
|
|
290
|
+
{previewData.sections && previewData.sections.length > 0 && (
|
|
291
|
+
<div className="proposal-preview-footer">
|
|
292
|
+
<div className="proposal-preview-footer-tags">
|
|
293
|
+
{previewData.sections.map((section, index) => (
|
|
294
|
+
<span
|
|
295
|
+
key={index}
|
|
296
|
+
className="proposal-preview-section-tag"
|
|
297
|
+
>
|
|
298
|
+
{section.title}
|
|
299
|
+
</span>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
) : (
|
|
306
|
+
<div className="proposal-preview-no-template">
|
|
307
|
+
<Eye size={48} className="proposal-preview-no-template-icon" />
|
|
308
|
+
<h3 className="proposal-preview-no-template-title">No Preview Available</h3>
|
|
309
|
+
<p className="proposal-preview-no-template-text">Add some sections to see the preview</p>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Variables Summary */}
|
|
315
|
+
{previewData?.variables_used && previewData.variables_used.length > 0 && (
|
|
316
|
+
<div className="proposal-preview-variables">
|
|
317
|
+
<h4 className="proposal-preview-variables-title">Variables Used in This Template</h4>
|
|
318
|
+
<div className="proposal-preview-variables-list">
|
|
319
|
+
{previewData.variables_used.map((variable, index) => (
|
|
320
|
+
<span key={index} className="proposal-preview-variable-tag">
|
|
321
|
+
{`{{${variable}}}`}
|
|
322
|
+
</span>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export default ProposalTemplatePreview;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/* Proposal Template Section Manager Styles */
|
|
2
|
+
.proposal-section-manager {
|
|
3
|
+
padding: 1rem;
|
|
4
|
+
background: #f8fafc;
|
|
5
|
+
min-height: 400px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.section-item {
|
|
9
|
+
background: white;
|
|
10
|
+
border: 1px solid #e2e8f0;
|
|
11
|
+
border-radius: 8px;
|
|
12
|
+
margin-bottom: 0.5rem;
|
|
13
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
14
|
+
transition: all 0.2s ease-in-out;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.section-item:hover {
|
|
19
|
+
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
|
|
20
|
+
border-color: #cbd5e1;
|
|
21
|
+
transform: translateY(-1px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.section-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding: 0.75rem 1rem;
|
|
29
|
+
border-bottom: 1px solid #f1f5f9;
|
|
30
|
+
background: #fcfcfd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.section-info {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 0.75rem;
|
|
37
|
+
flex: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.drag-handle {
|
|
41
|
+
cursor: move;
|
|
42
|
+
padding: 0.5rem;
|
|
43
|
+
color: #9ca3af;
|
|
44
|
+
transition: color 0.2s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.drag-handle:hover {
|
|
48
|
+
color: #6b7280;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.section-title {
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
color: #1e293b;
|
|
54
|
+
margin: 0;
|
|
55
|
+
font-size: 0.95rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.section-type-badge {
|
|
59
|
+
padding: 0.2rem 0.5rem;
|
|
60
|
+
font-size: 0.7rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
background-color: #e0f2fe;
|
|
63
|
+
color: #0c4a6e;
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.025em;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.section-dynamic-badge {
|
|
70
|
+
padding: 0.2rem 0.5rem;
|
|
71
|
+
font-size: 0.7rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
background-color: #d1fae5;
|
|
74
|
+
color: #065f46;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
letter-spacing: 0.025em;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.section-meta {
|
|
81
|
+
font-size: 0.8rem;
|
|
82
|
+
color: #64748b;
|
|
83
|
+
margin-top: 0.25rem;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.section-actions {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 0.5rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.section-action-btn {
|
|
94
|
+
padding: 0.5rem;
|
|
95
|
+
color: #9ca3af;
|
|
96
|
+
background: none;
|
|
97
|
+
border: none;
|
|
98
|
+
border-radius: 0.375rem;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.2s;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.section-action-btn:hover {
|
|
104
|
+
color: #6b7280;
|
|
105
|
+
background-color: #f3f4f6;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.section-action-btn.edit {
|
|
109
|
+
padding: 0.25rem 0.75rem;
|
|
110
|
+
color: #2563eb;
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.section-action-btn.edit:hover {
|
|
115
|
+
color: #1d4ed8;
|
|
116
|
+
background-color: #eff6ff;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.section-action-btn.delete {
|
|
120
|
+
color: #dc2626;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.section-action-btn.delete:hover {
|
|
124
|
+
color: #b91c1c;
|
|
125
|
+
background-color: #fef2f2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.section-content {
|
|
129
|
+
padding: 0.75rem 1rem;
|
|
130
|
+
max-height: 120px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
background: #fafbfc;
|
|
133
|
+
border-top: 1px solid #f1f5f9;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.section-content-preview {
|
|
137
|
+
font-size: 0.75rem;
|
|
138
|
+
line-height: 1.3;
|
|
139
|
+
color: #475569;
|
|
140
|
+
position: relative;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Removed gradient overlay that was causing blurred text */
|
|
144
|
+
|
|
145
|
+
.section-content-preview h1,
|
|
146
|
+
.section-content-preview h2,
|
|
147
|
+
.section-content-preview h3,
|
|
148
|
+
.section-content-preview h4 {
|
|
149
|
+
margin: 0 0 0.25rem 0;
|
|
150
|
+
font-size: 0.875rem;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.section-content-preview p {
|
|
155
|
+
margin: 0 0 0.25rem 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.section-content-preview ul,
|
|
159
|
+
.section-content-preview ol {
|
|
160
|
+
margin: 0 0 0.25rem 1rem;
|
|
161
|
+
padding: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.section-no-content {
|
|
165
|
+
color: #9ca3af;
|
|
166
|
+
font-style: italic;
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: 0.5rem 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.add-section-header {
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: space-between;
|
|
175
|
+
margin-bottom: 1.25rem;
|
|
176
|
+
padding: 1rem;
|
|
177
|
+
background: white;
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
border: 1px solid #e2e8f0;
|
|
180
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.add-section-title {
|
|
184
|
+
font-size: 1.1rem;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
color: #1e293b;
|
|
187
|
+
margin: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.add-section-description {
|
|
191
|
+
font-size: 0.85rem;
|
|
192
|
+
color: #64748b;
|
|
193
|
+
margin-top: 0.25rem;
|
|
194
|
+
font-weight: 500;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.add-section-btn {
|
|
198
|
+
display: inline-flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 0.5rem;
|
|
201
|
+
padding: 0.625rem 1.25rem;
|
|
202
|
+
font-size: 0.875rem;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
color: white;
|
|
205
|
+
background: #3cbf7d;
|
|
206
|
+
border: none;
|
|
207
|
+
border-radius: 5px;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
transition: all 0.2s ease-in-out;
|
|
210
|
+
box-shadow: 0 2px 4px rgba(60, 191, 125, 0.25);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.add-section-btn:hover {
|
|
214
|
+
background: #1b3933;
|
|
215
|
+
transform: translateY(-1px);
|
|
216
|
+
box-shadow: 0 4px 12px rgba(27, 57, 51, 0.35);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.empty-state {
|
|
220
|
+
text-align: center;
|
|
221
|
+
padding: 3rem;
|
|
222
|
+
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
223
|
+
border: 2px dashed #cbd5e1;
|
|
224
|
+
border-radius: 12px;
|
|
225
|
+
margin: 1rem 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.empty-state-icon {
|
|
229
|
+
margin: 0 auto 1rem;
|
|
230
|
+
color: #94a3b8;
|
|
231
|
+
opacity: 0.7;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.empty-state-title {
|
|
235
|
+
font-size: 1.2rem;
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
color: #334155;
|
|
238
|
+
margin: 0 0 0.5rem 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.empty-state-description {
|
|
242
|
+
color: #64748b;
|
|
243
|
+
margin: 0 0 1.5rem 0;
|
|
244
|
+
font-size: 0.95rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.empty-state-btn {
|
|
248
|
+
display: inline-flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 0.5rem;
|
|
251
|
+
padding: 0.75rem 1.5rem;
|
|
252
|
+
font-size: 0.875rem;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: white;
|
|
255
|
+
background: #3cbf7d;
|
|
256
|
+
border: none;
|
|
257
|
+
border-radius: 5px;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
transition: all 0.2s ease-in-out;
|
|
260
|
+
box-shadow: 0 2px 4px rgba(60, 191, 125, 0.25);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.empty-state-btn:hover {
|
|
264
|
+
background: #1b3933;
|
|
265
|
+
transform: translateY(-1px);
|
|
266
|
+
box-shadow: 0 4px 12px rgba(27, 57, 51, 0.35);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
/* Loading state */
|
|
271
|
+
.loading-spinner {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
justify-content: center;
|
|
275
|
+
padding: 3rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.spinner {
|
|
279
|
+
animation: spin 1s linear infinite;
|
|
280
|
+
border: 2px solid #f3f4f6;
|
|
281
|
+
border-top: 2px solid #2563eb;
|
|
282
|
+
border-radius: 50%;
|
|
283
|
+
width: 2rem;
|
|
284
|
+
height: 2rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@keyframes spin {
|
|
288
|
+
0% { transform: rotate(0deg); }
|
|
289
|
+
100% { transform: rotate(360deg); }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Sortable styling */
|
|
293
|
+
.sortable-helper {
|
|
294
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
295
|
+
transform: rotate(5deg);
|
|
296
|
+
z-index: 9999;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.sortable-ghost {
|
|
300
|
+
opacity: 0.4;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Responsive */
|
|
304
|
+
@media (max-width: 768px) {
|
|
305
|
+
.section-header {
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
align-items: flex-start;
|
|
308
|
+
gap: 0.75rem;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.section-actions {
|
|
312
|
+
width: 100%;
|
|
313
|
+
justify-content: space-between;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.add-section-header {
|
|
317
|
+
flex-direction: column;
|
|
318
|
+
align-items: flex-start;
|
|
319
|
+
gap: 1rem;
|
|
320
|
+
}
|
|
321
|
+
}
|