@visns-studio/visns-components 5.11.7 → 5.11.9

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.
@@ -816,3 +816,304 @@ input[type='file']:hover {
816
816
  width: 1.2em;
817
817
  height: 1.2em;
818
818
  }
819
+
820
+ /* Enhanced File Upload Styling */
821
+ .single-file-container {
822
+ display: flex;
823
+ flex-direction: column;
824
+ gap: 0.75em;
825
+ width: 100%;
826
+ padding: 0.5em;
827
+ border: 2px dashed var(--border-color, #ddd);
828
+ border-radius: var(--br, 8px);
829
+ background: var(--tertiary-color, #fafafa);
830
+ transition: all 0.3s ease;
831
+ }
832
+
833
+ .single-file-container:hover {
834
+ border-color: var(--primary-color, #007bff);
835
+ background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.02);
836
+ }
837
+
838
+ .single-file-image {
839
+ border-color: var(--success-color, #28a745);
840
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.05);
841
+ }
842
+
843
+ .single-file-image:hover {
844
+ border-color: var(--success-color, #28a745);
845
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.1);
846
+ }
847
+
848
+ .file-upload-button {
849
+ display: flex;
850
+ align-items: center;
851
+ gap: 0.5em;
852
+ padding: 1em 1.5em;
853
+ border: 1px solid var(--border-color, #ccc);
854
+ border-radius: var(--br, 6px);
855
+ background: var(--tertiary-color, #f8f9fa);
856
+ color: var(--paragraph-color, #333);
857
+ cursor: pointer;
858
+ transition: all 0.3s ease;
859
+ font-size: 0.9em;
860
+ font-weight: 500;
861
+ text-decoration: none;
862
+ outline: none;
863
+ }
864
+
865
+ .file-upload-button:hover {
866
+ background: var(--primary-color, #007bff);
867
+ color: var(--secondary-color, #fff);
868
+ border-color: var(--primary-color, #007bff);
869
+ transform: translateY(-1px);
870
+ box-shadow: 0 2px 8px rgba(var(--primary-color-rgb, 0, 123, 255), 0.3);
871
+ }
872
+
873
+ .file-upload-button-image {
874
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.1);
875
+ border-color: var(--success-color, #28a745);
876
+ color: var(--success-color, #28a745);
877
+ }
878
+
879
+ .file-upload-button-image:hover {
880
+ background: var(--success-color, #28a745);
881
+ color: var(--secondary-color, #fff);
882
+ border-color: var(--success-color, #28a745);
883
+ box-shadow: 0 2px 8px rgba(var(--success-color-rgb, 40, 167, 69), 0.3);
884
+ }
885
+
886
+ .file-button-icon {
887
+ font-size: 1.2em;
888
+ display: flex;
889
+ align-items: center;
890
+ justify-content: center;
891
+ width: 1.5em;
892
+ height: 1.5em;
893
+ }
894
+
895
+ .file-button-text {
896
+ font-weight: 500;
897
+ letter-spacing: 0.01em;
898
+ }
899
+
900
+ .file-info {
901
+ display: flex;
902
+ align-items: center;
903
+ justify-content: space-between;
904
+ gap: 1em;
905
+ padding: 0.75em 1em;
906
+ background: rgba(var(--item-color-rgb, 255, 255, 255), 0.8);
907
+ border: 1px solid var(--border-color, #e9ecef);
908
+ border-radius: var(--br, 6px);
909
+ font-size: 0.85em;
910
+ }
911
+
912
+ .file-name-display {
913
+ flex: 1;
914
+ color: var(--paragraph-color, #495057);
915
+ font-weight: 500;
916
+ overflow: hidden;
917
+ text-overflow: ellipsis;
918
+ white-space: nowrap;
919
+ }
920
+
921
+ .download-button {
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ padding: 0.5em;
926
+ background: transparent;
927
+ border: 1px solid var(--border-color, #ccc);
928
+ border-radius: var(--br, 4px);
929
+ color: var(--primary-color, #007bff);
930
+ cursor: pointer;
931
+ transition: all 0.3s ease;
932
+ outline: none;
933
+ }
934
+
935
+ .download-button:hover {
936
+ background: var(--primary-color, #007bff);
937
+ color: var(--secondary-color, #fff);
938
+ border-color: var(--primary-color, #007bff);
939
+ transform: scale(1.05);
940
+ }
941
+
942
+ /* Enhanced Multiple File List Styling */
943
+ .file-list-item,
944
+ .file-list-item-image {
945
+ display: flex;
946
+ align-items: center;
947
+ gap: 0.75em;
948
+ padding: 0.75em 1em;
949
+ border-bottom: 1px solid var(--border-color, #e9ecef);
950
+ font-size: 0.9rem;
951
+ background: var(--item-color, #fff);
952
+ border-radius: var(--br, 6px);
953
+ transition: all 0.3s ease;
954
+ margin-bottom: 0.5em;
955
+ border: 1px solid transparent;
956
+ }
957
+
958
+ .file-list-item-image {
959
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.05);
960
+ border-color: rgba(var(--success-color-rgb, 40, 167, 69), 0.2);
961
+ }
962
+
963
+ .file-list-item:hover,
964
+ .file-list-item-image:hover {
965
+ background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.05);
966
+ border-color: var(--primary-color, #007bff);
967
+ transform: translateY(-1px);
968
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
969
+ }
970
+
971
+ .file-list-item-image:hover {
972
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.1);
973
+ border-color: var(--success-color, #28a745);
974
+ }
975
+
976
+ .file-icon {
977
+ font-size: 1.5em;
978
+ display: flex;
979
+ align-items: center;
980
+ justify-content: center;
981
+ width: 2em;
982
+ height: 2em;
983
+ flex-shrink: 0;
984
+ }
985
+
986
+ .file-name {
987
+ flex: 1;
988
+ color: var(--paragraph-color, #495057);
989
+ font-weight: 500;
990
+ overflow: hidden;
991
+ text-overflow: ellipsis;
992
+ white-space: nowrap;
993
+ }
994
+
995
+ /* Image Preview Styling */
996
+ .image-preview-container {
997
+ display: flex;
998
+ justify-content: center;
999
+ align-items: center;
1000
+ margin-bottom: 1em;
1001
+ padding: 0.5em;
1002
+ background: rgba(var(--item-color-rgb, 255, 255, 255), 0.9);
1003
+ border: 1px solid var(--border-color, #e9ecef);
1004
+ border-radius: var(--br, 6px);
1005
+ overflow: hidden;
1006
+ }
1007
+
1008
+ .image-preview {
1009
+ max-width: 200px;
1010
+ max-height: 150px;
1011
+ width: auto;
1012
+ height: auto;
1013
+ object-fit: contain;
1014
+ border-radius: var(--br, 4px);
1015
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1016
+ transition: transform 0.3s ease;
1017
+ }
1018
+
1019
+ .image-preview:hover {
1020
+ transform: scale(1.05);
1021
+ cursor: pointer;
1022
+ }
1023
+
1024
+ .file-details {
1025
+ display: flex;
1026
+ align-items: center;
1027
+ justify-content: space-between;
1028
+ gap: 1em;
1029
+ }
1030
+
1031
+ /* Multiple File Image Preview */
1032
+ .file-image-preview {
1033
+ display: flex;
1034
+ align-items: center;
1035
+ justify-content: center;
1036
+ width: 3em;
1037
+ height: 3em;
1038
+ flex-shrink: 0;
1039
+ background: rgba(var(--item-color-rgb, 255, 255, 255), 0.9);
1040
+ border: 1px solid var(--border-color, #e9ecef);
1041
+ border-radius: var(--br, 4px);
1042
+ overflow: hidden;
1043
+ }
1044
+
1045
+ .file-thumbnail {
1046
+ width: 100%;
1047
+ height: 100%;
1048
+ object-fit: cover;
1049
+ border-radius: var(--br, 4px);
1050
+ transition: transform 0.3s ease;
1051
+ }
1052
+
1053
+ .file-thumbnail:hover {
1054
+ transform: scale(1.1);
1055
+ }
1056
+
1057
+ /* Enhanced Multiple File List Layout */
1058
+ .file-list-item,
1059
+ .file-list-item-image {
1060
+ display: grid;
1061
+ grid-template-columns: auto 1fr auto;
1062
+ align-items: center;
1063
+ gap: 1em;
1064
+ padding: 0.75em 1em;
1065
+ border-bottom: 1px solid var(--border-color, #e9ecef);
1066
+ font-size: 0.9rem;
1067
+ background: var(--item-color, #fff);
1068
+ border-radius: var(--br, 6px);
1069
+ transition: all 0.3s ease;
1070
+ margin-bottom: 0.5em;
1071
+ border: 1px solid transparent;
1072
+ }
1073
+
1074
+ .file-list-item-image {
1075
+ background: rgba(var(--success-color-rgb, 40, 167, 69), 0.05);
1076
+ border-color: rgba(var(--success-color-rgb, 40, 167, 69), 0.2);
1077
+ grid-template-columns: auto 1fr auto; /* Consistent grid for images */
1078
+ }
1079
+
1080
+ /* Responsive Design */
1081
+ @media (max-width: 768px) {
1082
+ .image-preview {
1083
+ max-width: 150px;
1084
+ max-height: 100px;
1085
+ }
1086
+
1087
+ .file-image-preview {
1088
+ width: 2.5em;
1089
+ height: 2.5em;
1090
+ }
1091
+
1092
+ .file-info {
1093
+ flex-direction: column;
1094
+ align-items: stretch;
1095
+ gap: 0.75em;
1096
+ }
1097
+
1098
+ .file-details {
1099
+ flex-direction: column;
1100
+ align-items: stretch;
1101
+ gap: 0.5em;
1102
+ }
1103
+ }
1104
+
1105
+ @media (max-width: 480px) {
1106
+ .image-preview {
1107
+ max-width: 120px;
1108
+ max-height: 80px;
1109
+ }
1110
+
1111
+ .single-file-container {
1112
+ padding: 0.25em;
1113
+ }
1114
+
1115
+ .file-upload-button {
1116
+ padding: 0.75em 1em;
1117
+ font-size: 0.85em;
1118
+ }
1119
+ }
@@ -243,16 +243,25 @@
243
243
  .proposal-preview-prose ol {
244
244
  font-size: 0.875rem !important;
245
245
  line-height: 1.4 !important;
246
- margin: 0 0 0.5rem 1rem !important;
247
- padding: 0;
246
+ margin: 0.5rem 0 0.5rem 1rem !important;
247
+ padding-left: 0 !important;
248
248
  color: #374151 !important;
249
+ list-style-type: disc !important;
250
+ list-style-position: outside !important;
251
+ }
252
+
253
+ .proposal-preview-prose ol {
254
+ list-style-type: decimal !important;
249
255
  }
250
256
 
251
257
  .proposal-preview-prose li {
252
- margin: 0 !important;
253
- padding: 0 !important;
258
+ margin: 0.25rem 0 !important;
259
+ padding-left: 1.2rem !important;
260
+ margin-left: 0 !important;
254
261
  font-size: 0.875rem !important;
255
262
  line-height: 1.4 !important;
263
+ display: list-item !important;
264
+ text-indent: 0 !important;
256
265
  }
257
266
 
258
267
  .proposal-preview-section {
@@ -304,16 +313,25 @@
304
313
  .proposal-preview-section-content ol {
305
314
  font-size: 0.875rem !important;
306
315
  line-height: 1.4 !important;
307
- margin: 0 0 0.5rem 1rem !important;
308
- padding: 0;
316
+ margin: 0.5rem 0 0.5rem 1rem !important;
317
+ padding-left: 0 !important;
309
318
  color: #4b5563 !important;
319
+ list-style-type: disc !important;
320
+ list-style-position: outside !important;
321
+ }
322
+
323
+ .proposal-preview-section-content ol {
324
+ list-style-type: decimal !important;
310
325
  }
311
326
 
312
327
  .proposal-preview-section-content li {
313
- margin: 0 !important;
314
- padding: 0 !important;
328
+ margin: 0.25rem 0 !important;
329
+ padding-left: 1.2rem !important;
330
+ margin-left: 0 !important;
315
331
  font-size: 0.875rem !important;
316
332
  line-height: 1.4 !important;
333
+ display: list-item !important;
334
+ text-indent: 0 !important;
317
335
  }
318
336
 
319
337
  .proposal-preview-empty {
@@ -15,10 +15,42 @@
15
15
  overflow: hidden;
16
16
  }
17
17
 
18
- .section-item:hover {
18
+ .section-item:hover:not(.sortable-helper) {
19
19
  box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
20
20
  border-color: #cbd5e1;
21
- transform: translateY(-1px);
21
+ /* Remove transform to prevent conflicts with sortable transforms */
22
+ }
23
+
24
+ /* Sortable Helper Styles - Applied during drag */
25
+ .sortable-helper {
26
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
27
+ border-color: #3b82f6 !important;
28
+ transform: rotate(2deg) !important;
29
+ z-index: 9999 !important;
30
+ background: rgba(255, 255, 255, 0.95) !important;
31
+ backdrop-filter: blur(4px);
32
+ }
33
+
34
+ .sortable-helper .section-header {
35
+ background: rgba(59, 130, 246, 0.1) !important;
36
+ }
37
+
38
+ /* Prevent hover effects during drag */
39
+ .sortable-helper:hover {
40
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
41
+ border-color: #3b82f6 !important;
42
+ }
43
+
44
+ /* Smooth transitions for non-dragged items */
45
+ .section-item:not(.sortable-helper) {
46
+ transition: all 0.2s ease-in-out;
47
+ }
48
+
49
+ /* Ghost/placeholder styling */
50
+ .sortable-ghost {
51
+ opacity: 0.4;
52
+ background: #f8fafc !important;
53
+ border: 2px dashed #cbd5e1 !important;
22
54
  }
23
55
 
24
56
  .section-header {
@@ -38,14 +70,24 @@
38
70
  }
39
71
 
40
72
  .drag-handle {
41
- cursor: move;
73
+ cursor: grab;
42
74
  padding: 0.5rem;
43
75
  color: #9ca3af;
44
- transition: color 0.2s;
76
+ transition: all 0.2s ease;
77
+ border-radius: 4px;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
45
81
  }
46
82
 
47
83
  .drag-handle:hover {
48
- color: #6b7280;
84
+ color: #3b82f6;
85
+ background: rgba(59, 130, 246, 0.05);
86
+ }
87
+
88
+ .drag-handle:active {
89
+ cursor: grabbing;
90
+ background: rgba(59, 130, 246, 0.1);
49
91
  }
50
92
 
51
93
  .section-title {
@@ -90,7 +90,7 @@ const SortableSectionList = SortableContainer(({ sections, onEdit, onDelete, onT
90
90
  <div>
91
91
  {sections.map((section, index) => (
92
92
  <SortableSection
93
- key={`section-${section.id || index}`}
93
+ key={section.id}
94
94
  index={index}
95
95
  section={section}
96
96
  onEdit={onEdit}
@@ -164,11 +164,12 @@ const ProposalTemplateSectionManager = ({
164
164
  sort_order: index + 1
165
165
  }));
166
166
 
167
+ // Immediately update UI for smooth experience
167
168
  setSections(updatedSections);
168
169
 
169
170
  try {
170
171
  // Update sort orders on the backend
171
- await fetch(buildUrlWithTemplate(apiEndpoints.sectionsReorder), {
172
+ const response = await fetch(buildUrlWithTemplate(apiEndpoints.sectionsReorder), {
172
173
  method: 'POST',
173
174
  headers: {
174
175
  'Content-Type': 'application/json',
@@ -178,19 +179,33 @@ const ProposalTemplateSectionManager = ({
178
179
  sections: updatedSections.map(s => ({ id: s.id, sort_order: s.sort_order }))
179
180
  })
180
181
  });
181
-
182
- await loadSections(); // Refetch fresh data from server
183
- toast.success('Section order updated');
182
+
183
+ if (response.ok) {
184
+ // Don't refetch data immediately to prevent UI jumping
185
+ // The local state is already updated and accurate
186
+ toast.success('Section order updated');
187
+ } else {
188
+ throw new Error('Failed to update section order');
189
+ }
184
190
  } catch (error) {
185
191
  console.error('Error updating section order:', error);
186
192
  toast.error('Failed to update section order');
187
- // Revert on error
188
- loadSections();
193
+ // Revert to original order on error
194
+ setSections(sections);
189
195
  }
190
196
  };
191
197
 
192
198
  const handleAddSection = async (sectionType) => {
193
199
  try {
200
+ // Check if a section with this type already exists
201
+ const existingSection = sections.find(section => section.section_type === sectionType);
202
+
203
+ if (existingSection) {
204
+ const sectionTypeName = sectionType.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
205
+ toast.error(`A ${sectionTypeName} section already exists in this template. Each template can only have one section of each type.`);
206
+ return;
207
+ }
208
+
194
209
  const response = await fetch(buildUrlWithTemplate(apiEndpoints.sectionsCreate), {
195
210
  method: 'POST',
196
211
  headers: {
@@ -447,6 +462,16 @@ const ProposalTemplateSectionManager = ({
447
462
  onDelete={handleDeleteSection}
448
463
  onToggleVisibility={handleToggleVisibility}
449
464
  useDragHandle
465
+ lockAxis="y"
466
+ helperClass="sortable-helper"
467
+ transitionDuration={200}
468
+ shouldCancelStart={(e) => {
469
+ // Prevent drag from starting on interactive elements
470
+ if (e.target.closest('button, input, textarea, select, a')) {
471
+ return true;
472
+ }
473
+ return false;
474
+ }}
450
475
  />
451
476
  )}
452
477
 
@@ -454,6 +479,7 @@ const ProposalTemplateSectionManager = ({
454
479
  <SectionTypeSelector
455
480
  onSelect={handleAddSection}
456
481
  onCancel={() => setShowSectionTypeSelector(false)}
482
+ existingSections={sections}
457
483
  />
458
484
  )}
459
485
 
@@ -37,12 +37,38 @@ const SectionEditor = ({ section, availableVariables = [], onSave, onCancel }) =
37
37
  }
38
38
  }, [section]);
39
39
 
40
+ // Function to extract variables from content
41
+ const extractVariablesFromContent = (content) => {
42
+ if (!content || typeof content !== 'string') return [];
43
+
44
+ const variableMatches = content.match(/{{(\w+)}}/g);
45
+ if (!variableMatches) return [];
46
+
47
+ const variables = variableMatches.map(match =>
48
+ match.replace(/[{}]/g, '')
49
+ );
50
+
51
+ // Return unique variables
52
+ return [...new Set(variables)];
53
+ };
54
+
40
55
  const handleInputChange = (field, value) => {
41
56
  console.log(`Field change: ${field} =`, value);
42
- setFormData(prev => ({
43
- ...prev,
44
- [field]: value
45
- }));
57
+
58
+ // If content is being changed, update variables automatically
59
+ if (field === 'content') {
60
+ const extractedVariables = extractVariablesFromContent(value);
61
+ setFormData(prev => ({
62
+ ...prev,
63
+ [field]: value,
64
+ variables: extractedVariables
65
+ }));
66
+ } else {
67
+ setFormData(prev => ({
68
+ ...prev,
69
+ [field]: value
70
+ }));
71
+ }
46
72
  };
47
73
 
48
74
  const handleStylingChange = (property, value) => {
@@ -61,17 +87,21 @@ const SectionEditor = ({ section, availableVariables = [], onSave, onCancel }) =
61
87
  // Try to insert at cursor position in TinyMCE editor
62
88
  if (window.tinymce && window.tinymce.activeEditor) {
63
89
  window.tinymce.activeEditor.insertContent(variableTag);
90
+
91
+ // Get the updated content and extract variables
92
+ setTimeout(() => {
93
+ const updatedContent = window.tinymce.activeEditor.getContent();
94
+ const extractedVariables = extractVariablesFromContent(updatedContent);
95
+ setFormData(prev => ({
96
+ ...prev,
97
+ content: updatedContent,
98
+ variables: extractedVariables
99
+ }));
100
+ }, 100);
64
101
  } else {
65
102
  // Fallback: append to content
66
- handleInputChange('content', formData.content + variableTag);
67
- }
68
-
69
- // Add to variables array if not already present
70
- if (!formData.variables.includes(variable)) {
71
- setFormData(prev => ({
72
- ...prev,
73
- variables: [...prev.variables, variable]
74
- }));
103
+ const updatedContent = formData.content + variableTag;
104
+ handleInputChange('content', updatedContent);
75
105
  }
76
106
 
77
107
  setShowVariableInserter(false);
@@ -105,9 +135,13 @@ const SectionEditor = ({ section, availableVariables = [], onSave, onCancel }) =
105
135
  }
106
136
  }
107
137
 
138
+ // Extract variables from final content before saving
139
+ const extractedVariables = extractVariablesFromContent(currentContent);
140
+
108
141
  const finalFormData = {
109
142
  ...formData,
110
- content: currentContent
143
+ content: currentContent,
144
+ variables: extractedVariables
111
145
  };
112
146
 
113
147
  console.log('Final form data being saved:', finalFormData);
@@ -274,4 +274,58 @@
274
274
  .section-type-modal-body {
275
275
  padding: 1rem;
276
276
  }
277
+ }
278
+
279
+ /* Disabled Section State */
280
+ .section-type-card.disabled {
281
+ opacity: 0.5;
282
+ cursor: not-allowed;
283
+ background: #f8f9fa !important;
284
+ border-color: #e9ecef !important;
285
+ transform: none !important;
286
+ }
287
+
288
+ .section-type-card.disabled:hover {
289
+ transform: none !important;
290
+ box-shadow: none !important;
291
+ border-color: #e9ecef !important;
292
+ }
293
+
294
+ .section-type-card.disabled .section-type-icon {
295
+ opacity: 0.6;
296
+ }
297
+
298
+ .section-type-card.disabled .section-type-name,
299
+ .section-type-card.disabled .section-type-description {
300
+ color: #6c757d !important;
301
+ }
302
+
303
+ /* Already Used Badge */
304
+ .used-badge {
305
+ display: inline-block;
306
+ background: #dc3545;
307
+ color: white;
308
+ font-size: 0.65rem;
309
+ font-weight: 500;
310
+ padding: 0.15rem 0.4rem;
311
+ border-radius: 0.25rem;
312
+ margin-left: 0.5rem;
313
+ text-transform: uppercase;
314
+ letter-spacing: 0.025em;
315
+ vertical-align: middle;
316
+ }
317
+
318
+ /* Dynamic Badge - Enhanced */
319
+ .dynamic-badge {
320
+ display: inline-block;
321
+ background: #28a745;
322
+ color: white;
323
+ font-size: 0.65rem;
324
+ font-weight: 500;
325
+ padding: 0.15rem 0.4rem;
326
+ border-radius: 0.25rem;
327
+ margin-left: 0.5rem;
328
+ text-transform: uppercase;
329
+ letter-spacing: 0.025em;
330
+ vertical-align: middle;
277
331
  }