@explorer02/cfm-survey-sdk 0.3.2 → 0.3.4
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/dist/cli/index.js +55 -55
- package/dist/cli/index.mjs +61 -61
- package/package.json +1 -1
- package/templates/docs/00-integration/component-checklist.md +1 -0
- package/templates/docs/templates/CsatMatrixScale.tsx +86 -33
- package/templates/docs/templates/CustomSliderTrack.tsx +6 -4
- package/templates/docs/templates/Footer.tsx +69 -26
- package/templates/docs/templates/Header.tsx +102 -65
- package/templates/docs/templates/LikertMatrixScale.tsx +7 -6
- package/templates/docs/templates/Question.tsx +7 -1
- package/templates/docs/templates/RankOrderScale.tsx +8 -1
- package/templates/docs/templates/RatingScale.tsx +2 -1
- package/templates/docs/templates/SliderMatrixScale.tsx +5 -3
- package/templates/docs/templates/labelStyles.ts +59 -16
- package/templates/docs/templates/selectionStyles.ts +16 -1
- package/templates/docs/templates/surveyUiIcons.tsx +1 -1
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +41 -1
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +2 -1
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +3 -2
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +18 -9
- package/templates/survey-theme.css +14 -2
- package/templates/wizard-dist/assets/{PreviewMock-DbbLpHdF.js → PreviewMock-CC1UlWe-.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DISCXm0z.js +1 -0
- package/templates/wizard-dist/assets/index-Blpq4QjK.js +34 -0
- package/templates/wizard-dist/assets/index-CVBd54V0.css +1 -0
- package/templates/wizard-dist/index.html +2 -2
- package/templates/wizard-dist/assets/TypePanel-DQbV2iCf.js +0 -1
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +0 -1
- package/templates/wizard-dist/assets/index-CY7WMJ93.js +0 -34
package/package.json
CHANGED
|
@@ -117,6 +117,7 @@ Read: [`wizard-chrome-contract.md`](wizard-chrome-contract.md), [`agent-executio
|
|
|
117
117
|
- [ ] Draft `./survey-ui-config.json` synced with mockup branding
|
|
118
118
|
- [ ] `previewBridge.ts` + `PreviewBridgeInit` in root layout
|
|
119
119
|
- [ ] Header/Footer/ProgressBar/SurveyPage use `var(--cfm-*)` — no hardcoded brand hex for wizard-controlled tokens
|
|
120
|
+
- [ ] **Footer logo:** `Footer.tsx` must call `getLogoSrc()` (same `global.logo.fileName` as header) — never hardcode `./logo.svg` or a separate footer upload path
|
|
120
121
|
- [ ] `data-cfm-logo` / `data-cfm-logo-text`, `data-cfm-company`, `data-cfm-survey-title`, `data-cfm-btn-next`, `data-cfm-btn-back`, `data-cfm-thank-you` on chrome
|
|
121
122
|
- [ ] **Layout toggles:** `data-cfm-progress`, `data-cfm-question-number`, `data-cfm-required`, `cfm-footer`, `cfm-header` — see [`wizard-chrome-contract.md`](wizard-chrome-contract.md)
|
|
122
123
|
- [ ] Per-type scale vars per `wizard-question-type-styling.md` for **all 11 types** (wizard exports all; survey inventory may be smaller)
|
|
@@ -7,6 +7,7 @@ import MatrixDropdown from './MatrixDropdown';
|
|
|
7
7
|
import {
|
|
8
8
|
matrixRadioRingStyle,
|
|
9
9
|
matrixRadioDotStyle,
|
|
10
|
+
matrixRowBackgroundStyle,
|
|
10
11
|
scaleCellSelectedStyle,
|
|
11
12
|
unselectedOpacityStyle,
|
|
12
13
|
emojiSizeStyle,
|
|
@@ -15,13 +16,19 @@ import {
|
|
|
15
16
|
mcqSelectedAccentVar,
|
|
16
17
|
mcqSelectedBgVar,
|
|
17
18
|
} from '@/lib/surveyUi/selectionStyles';
|
|
18
|
-
import { csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
19
|
+
import { columnLabelPillStyle, csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
|
|
19
20
|
|
|
20
21
|
const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
|
|
21
22
|
|
|
22
23
|
type CsatMatrixQuestion = CsatQuestion | RatingMatrixQuestion;
|
|
23
24
|
|
|
24
|
-
function GraphicsColumnLabelsRow({
|
|
25
|
+
function GraphicsColumnLabelsRow({
|
|
26
|
+
scaleColumns,
|
|
27
|
+
pillScope = 'csat' as 'csat' | 'rating',
|
|
28
|
+
}: {
|
|
29
|
+
scaleColumns: ScaleColumn[];
|
|
30
|
+
pillScope?: 'csat' | 'rating';
|
|
31
|
+
}) {
|
|
25
32
|
if (scaleColumns.length === 0) return null;
|
|
26
33
|
|
|
27
34
|
return (
|
|
@@ -38,15 +45,13 @@ function GraphicsColumnLabelsRow({ scaleColumns }: { scaleColumns: ScaleColumn[]
|
|
|
38
45
|
transform: 'translateX(-50%)',
|
|
39
46
|
bottom: 0,
|
|
40
47
|
textAlign: 'center',
|
|
41
|
-
fontSize: '12px',
|
|
42
|
-
fontWeight: 500,
|
|
43
|
-
color: '#4b5563',
|
|
44
|
-
lineHeight: 1.2,
|
|
45
|
-
wordBreak: 'break-word',
|
|
46
48
|
width: '64px',
|
|
47
49
|
}}
|
|
48
50
|
>
|
|
49
|
-
<span
|
|
51
|
+
<span
|
|
52
|
+
style={{ fontSize: '12px', fontWeight: 500, ...columnLabelPillStyle(pillScope) }}
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: col.label }}
|
|
54
|
+
/>
|
|
50
55
|
</div>
|
|
51
56
|
);
|
|
52
57
|
})}
|
|
@@ -188,20 +193,21 @@ function CsatMatrixGrid({
|
|
|
188
193
|
isGraphics
|
|
189
194
|
}: GridProps) {
|
|
190
195
|
const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
|
|
196
|
+
const isRating = question.type === 'RATING_MATRIX';
|
|
191
197
|
const [hoveredCell, setHoveredCell] = useState<string | null>(null);
|
|
192
198
|
|
|
193
|
-
const
|
|
199
|
+
const showAnchorLabels = isRating && labels && labels.length > 0;
|
|
194
200
|
const m = scaleColumns.length;
|
|
195
201
|
const n = labels?.length || 0;
|
|
196
202
|
|
|
197
203
|
return (
|
|
198
204
|
<div style={{ width: '100%' }}>
|
|
199
|
-
{/*
|
|
200
|
-
{
|
|
205
|
+
{/* RATING anchor labels (max ~5 along scale) */}
|
|
206
|
+
{showAnchorLabels && m > 0 && (
|
|
201
207
|
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
|
|
202
208
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
|
|
203
209
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
204
|
-
<div style={{ flex: 1, position: 'relative', height: '
|
|
210
|
+
<div style={{ flex: 1, position: 'relative', height: '28px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
|
|
205
211
|
{labels!.map((label, i) => {
|
|
206
212
|
const startPercent = isGraphics ? 0 : 0.5 / m;
|
|
207
213
|
const rangePercent = isGraphics ? 1 : (m - 1) / m;
|
|
@@ -212,10 +218,10 @@ function CsatMatrixGrid({
|
|
|
212
218
|
position: 'absolute',
|
|
213
219
|
left: `${percent}%`,
|
|
214
220
|
transform: 'translateX(-50%)',
|
|
215
|
-
textAlign: 'center', fontSize: '13px', fontWeight: 600,
|
|
216
|
-
color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
|
|
217
221
|
}}>
|
|
218
|
-
{
|
|
222
|
+
<span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
|
|
223
|
+
{label}
|
|
224
|
+
</span>
|
|
219
225
|
</div>
|
|
220
226
|
);
|
|
221
227
|
})}
|
|
@@ -225,12 +231,12 @@ function CsatMatrixGrid({
|
|
|
225
231
|
</div>
|
|
226
232
|
)}
|
|
227
233
|
|
|
228
|
-
{/* Column tick labels for graphics sliders
|
|
234
|
+
{/* Column tick labels for graphics sliders */}
|
|
229
235
|
{isGraphics && m > 0 && (
|
|
230
|
-
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom:
|
|
236
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showAnchorLabels ? '8px' : '16px', minHeight: '24px' }}>
|
|
231
237
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
|
|
232
238
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
233
|
-
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
|
|
239
|
+
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
|
|
234
240
|
{hasNotApplicable && (
|
|
235
241
|
<div style={{ width: '56px', flexShrink: 0 }} />
|
|
236
242
|
)}
|
|
@@ -238,8 +244,8 @@ function CsatMatrixGrid({
|
|
|
238
244
|
</div>
|
|
239
245
|
)}
|
|
240
246
|
|
|
241
|
-
{/* Column
|
|
242
|
-
{!isGraphics &&
|
|
247
|
+
{/* Column headers — CSAT always; RATING numeric headers when not graphics */}
|
|
248
|
+
{!isGraphics && m > 0 && (
|
|
243
249
|
<div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
|
|
244
250
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
|
|
245
251
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
@@ -247,7 +253,13 @@ function CsatMatrixGrid({
|
|
|
247
253
|
{scaleColumns.map(col => (
|
|
248
254
|
<div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
|
|
249
255
|
<span
|
|
250
|
-
style={{
|
|
256
|
+
style={{
|
|
257
|
+
fontSize: '13px',
|
|
258
|
+
fontWeight: 500,
|
|
259
|
+
...(isRating
|
|
260
|
+
? { color: 'var(--cfm-text, #374151)', textAlign: 'center' as const }
|
|
261
|
+
: columnLabelPillStyle('csat')),
|
|
262
|
+
}}
|
|
251
263
|
dangerouslySetInnerHTML={{ __html: col.label }}
|
|
252
264
|
/>
|
|
253
265
|
</div>
|
|
@@ -267,7 +279,7 @@ function CsatMatrixGrid({
|
|
|
267
279
|
{statementRows.map((row, rowIdx) => (
|
|
268
280
|
<div key={row.id} style={{
|
|
269
281
|
display: 'flex', alignItems: 'center', borderRadius: '8px',
|
|
270
|
-
padding: '12px 0',
|
|
282
|
+
padding: '12px 0', ...matrixRowBackgroundStyle(rowIdx),
|
|
271
283
|
}}>
|
|
272
284
|
<div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0, padding: '0 16px' }}>
|
|
273
285
|
<span style={{ fontSize: '14px', fontWeight: 500, color: '#111827', lineHeight: 1.4 }}
|
|
@@ -291,6 +303,16 @@ function CsatMatrixGrid({
|
|
|
291
303
|
ticks={scaleColumns.length}
|
|
292
304
|
tickLabels={scaleColumns.map(col => col.label)}
|
|
293
305
|
reverseScaleOrder={reverseScaleOrder}
|
|
306
|
+
trackVar={
|
|
307
|
+
isRating
|
|
308
|
+
? 'var(--cfm-rating-track, var(--cfm-slider-track, #e5e7eb))'
|
|
309
|
+
: 'var(--cfm-csat-track, var(--cfm-slider-track, #e5e7eb))'
|
|
310
|
+
}
|
|
311
|
+
thumbVar={
|
|
312
|
+
isRating
|
|
313
|
+
? 'var(--cfm-rating-thumb, var(--cfm-slider-thumb, var(--cfm-primary)))'
|
|
314
|
+
: 'var(--cfm-csat-thumb, var(--cfm-slider-thumb, var(--cfm-primary)))'
|
|
315
|
+
}
|
|
294
316
|
onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
|
|
295
317
|
/>
|
|
296
318
|
</div>
|
|
@@ -418,20 +440,27 @@ function CsatMatrixCarousel({
|
|
|
418
440
|
isGraphics
|
|
419
441
|
}: CarouselProps) {
|
|
420
442
|
const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
|
|
443
|
+
const isRating = question.type === 'RATING_MATRIX';
|
|
421
444
|
const [activeCarouselIndex, setActiveCarouselIndex] = useState(0);
|
|
422
445
|
const [hoveredCell, setHoveredCell] = useState<string | null>(null);
|
|
423
446
|
|
|
424
447
|
const row = statementRows[activeCarouselIndex];
|
|
425
448
|
if (!row) return null;
|
|
426
|
-
|
|
427
|
-
const
|
|
449
|
+
|
|
450
|
+
const showAnchorLabels = isRating && labels && labels.length > 0;
|
|
451
|
+
const trackVar = isRating
|
|
452
|
+
? 'var(--cfm-rating-track, var(--cfm-slider-track, #e5e7eb))'
|
|
453
|
+
: 'var(--cfm-csat-track, var(--cfm-slider-track, #e5e7eb))';
|
|
454
|
+
const thumbVar = isRating
|
|
455
|
+
? 'var(--cfm-rating-thumb, var(--cfm-slider-thumb, var(--cfm-primary)))'
|
|
456
|
+
: 'var(--cfm-csat-thumb, var(--cfm-slider-thumb, var(--cfm-primary)))';
|
|
428
457
|
|
|
429
458
|
return (
|
|
430
459
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', backgroundColor: '#fff', borderRadius: '12px', border: '1px solid #e5e5e5', padding: '32px', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.05)', width: '100%', boxSizing: 'border-box' }}>
|
|
431
460
|
<h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
|
|
432
461
|
|
|
433
|
-
{
|
|
434
|
-
<div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '
|
|
462
|
+
{showAnchorLabels && scaleColumns.length > 0 && (
|
|
463
|
+
<div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '28px', marginBottom: '16px' }}>
|
|
435
464
|
{labels!.map((label, i) => {
|
|
436
465
|
const m = scaleColumns.length;
|
|
437
466
|
const n = labels!.length;
|
|
@@ -442,19 +471,33 @@ function CsatMatrixCarousel({
|
|
|
442
471
|
return (
|
|
443
472
|
<div key={i} style={{
|
|
444
473
|
position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
|
|
445
|
-
textAlign: 'center',
|
|
446
|
-
color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
|
|
474
|
+
textAlign: 'center',
|
|
447
475
|
}}>
|
|
448
|
-
{
|
|
476
|
+
<span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
|
|
477
|
+
{label}
|
|
478
|
+
</span>
|
|
449
479
|
</div>
|
|
450
480
|
);
|
|
451
481
|
})}
|
|
452
482
|
</div>
|
|
453
483
|
)}
|
|
454
484
|
|
|
485
|
+
{!isGraphics && !isRating && scaleColumns.length > 0 && (
|
|
486
|
+
<div style={{ width: '100%', maxWidth: '800px', display: 'grid', gridTemplateColumns: `repeat(${scaleColumns.length}, 1fr)`, gap: '8px', marginBottom: '16px' }}>
|
|
487
|
+
{scaleColumns.map(col => (
|
|
488
|
+
<div key={col.id} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
489
|
+
<span
|
|
490
|
+
style={{ fontSize: '13px', fontWeight: 500, ...columnLabelPillStyle('csat') }}
|
|
491
|
+
dangerouslySetInnerHTML={{ __html: col.label }}
|
|
492
|
+
/>
|
|
493
|
+
</div>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
)}
|
|
497
|
+
|
|
455
498
|
{isGraphics && scaleColumns.length > 0 && (
|
|
456
|
-
<div style={{ width: '100%', maxWidth: '800px', marginBottom:
|
|
457
|
-
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
|
|
499
|
+
<div style={{ width: '100%', maxWidth: '800px', marginBottom: showAnchorLabels ? '8px' : '16px' }}>
|
|
500
|
+
<GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
|
|
458
501
|
</div>
|
|
459
502
|
)}
|
|
460
503
|
|
|
@@ -472,6 +515,8 @@ function CsatMatrixCarousel({
|
|
|
472
515
|
sliderType="graphics" ticks={scaleColumns.length}
|
|
473
516
|
tickLabels={scaleColumns.map(col => col.label)}
|
|
474
517
|
reverseScaleOrder={reverseScaleOrder}
|
|
518
|
+
trackVar={trackVar}
|
|
519
|
+
thumbVar={thumbVar}
|
|
475
520
|
onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
|
|
476
521
|
/>
|
|
477
522
|
</div>
|
|
@@ -518,8 +563,16 @@ function CsatMatrixCarousel({
|
|
|
518
563
|
>
|
|
519
564
|
{(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
|
|
520
565
|
</button>
|
|
521
|
-
<span
|
|
522
|
-
|
|
566
|
+
<span
|
|
567
|
+
style={{
|
|
568
|
+
fontSize: '13px',
|
|
569
|
+
fontWeight: 500,
|
|
570
|
+
...(isRating
|
|
571
|
+
? { color: 'var(--cfm-text, #374151)', textAlign: 'center' as const }
|
|
572
|
+
: columnLabelPillStyle('csat')),
|
|
573
|
+
}}
|
|
574
|
+
dangerouslySetInnerHTML={{ __html: col.label }}
|
|
575
|
+
/>
|
|
523
576
|
</div>
|
|
524
577
|
);
|
|
525
578
|
})}
|
|
@@ -22,6 +22,8 @@ type CustomSliderTrackProps = {
|
|
|
22
22
|
/** Pass scaleColumns.map(c => c.label) for visible column labels under slider */
|
|
23
23
|
tickLabels?: string[];
|
|
24
24
|
reverseScaleOrder?: boolean;
|
|
25
|
+
trackVar?: string;
|
|
26
|
+
thumbVar?: string;
|
|
25
27
|
onChange: (v: number) => void;
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -35,6 +37,8 @@ export function CustomSliderTrack({
|
|
|
35
37
|
ticks,
|
|
36
38
|
tickLabels,
|
|
37
39
|
reverseScaleOrder,
|
|
40
|
+
trackVar = 'var(--cfm-slider-track, #e5e7eb)',
|
|
41
|
+
thumbVar = 'var(--cfm-slider-thumb, var(--cfm-input-focus-ring, var(--cfm-primary)))',
|
|
38
42
|
onChange,
|
|
39
43
|
}: CustomSliderTrackProps) {
|
|
40
44
|
const [isHovered, setIsHovered] = React.useState(false);
|
|
@@ -50,9 +54,7 @@ export function CustomSliderTrack({
|
|
|
50
54
|
? tooltipLabel.replace(/<[^>]*>/g, '')
|
|
51
55
|
: String(Math.round(value));
|
|
52
56
|
|
|
53
|
-
const themeColor = disabled
|
|
54
|
-
? '#9ca3af'
|
|
55
|
-
: 'var(--cfm-slider-thumb, var(--cfm-input-focus-ring, var(--cfm-primary)))';
|
|
57
|
+
const themeColor = disabled ? '#9ca3af' : thumbVar;
|
|
56
58
|
|
|
57
59
|
return (
|
|
58
60
|
<div
|
|
@@ -62,7 +64,7 @@ export function CustomSliderTrack({
|
|
|
62
64
|
>
|
|
63
65
|
<div style={{
|
|
64
66
|
position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
|
|
65
|
-
height: '4px', borderRadius: '2px', backgroundColor:
|
|
67
|
+
height: '4px', borderRadius: '2px', backgroundColor: trackVar,
|
|
66
68
|
}} />
|
|
67
69
|
|
|
68
70
|
<div style={{
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wizard-ready Footer — copy to src/components/Footer.tsx
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Uses the same logo as Header via getLogoSrc(). Layout, alignment, and spacing
|
|
5
|
+
* are driven entirely by --cfm-footer-* CSS vars from survey-ui-config.
|
|
4
6
|
*/
|
|
5
7
|
'use client';
|
|
6
8
|
|
|
9
|
+
import {
|
|
10
|
+
getFooterCopyright,
|
|
11
|
+
getFooterLinks,
|
|
12
|
+
getLayoutFlags,
|
|
13
|
+
getLogoSrc,
|
|
14
|
+
} from '@/lib/uiConfig';
|
|
15
|
+
|
|
16
|
+
const seedLogoSrc = getLogoSrc({ staticExport: true });
|
|
17
|
+
const seedCopyright = getFooterCopyright();
|
|
18
|
+
const seedLinks = getFooterLinks();
|
|
19
|
+
const { showFooterLogo } = getLayoutFlags();
|
|
20
|
+
|
|
7
21
|
export default function Footer() {
|
|
22
|
+
const showLogo = showFooterLogo && Boolean(seedLogoSrc);
|
|
23
|
+
|
|
8
24
|
return (
|
|
9
25
|
<footer
|
|
10
26
|
className="cfm-footer w-full"
|
|
@@ -15,56 +31,83 @@ export default function Footer() {
|
|
|
15
31
|
}}
|
|
16
32
|
>
|
|
17
33
|
<div
|
|
18
|
-
className="cfm-footer-inner mx-auto
|
|
34
|
+
className="cfm-footer-inner mx-auto w-full items-end"
|
|
19
35
|
style={{
|
|
36
|
+
maxWidth: 'var(--cfm-max-width, 48rem)',
|
|
20
37
|
display: 'var(--cfm-footer-inner-display, grid)',
|
|
21
38
|
flexDirection: 'var(--cfm-footer-inner-direction, row)' as React.CSSProperties['flexDirection'],
|
|
22
|
-
gridTemplateColumns: '1fr
|
|
39
|
+
gridTemplateColumns: '1fr auto 1fr',
|
|
40
|
+
gap: 'var(--cfm-footer-inner-gap, 24px)',
|
|
23
41
|
}}
|
|
24
42
|
>
|
|
25
43
|
<div
|
|
26
|
-
className="
|
|
44
|
+
className="cfm-footer-logo-column"
|
|
27
45
|
style={{
|
|
28
46
|
gridColumn: 'var(--cfm-footer-logo-col, 1)',
|
|
29
47
|
justifySelf: 'var(--cfm-footer-logo-justify, start)',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'column',
|
|
50
|
+
alignItems: 'var(--cfm-footer-logo-justify, start)',
|
|
51
|
+
gap: 'var(--cfm-footer-logo-copyright-gap, 12px)',
|
|
30
52
|
}}
|
|
31
53
|
>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
{showLogo ? (
|
|
55
|
+
<div
|
|
56
|
+
className="cfm-footer-logo-slot"
|
|
57
|
+
style={{
|
|
58
|
+
width: 'var(--cfm-footer-logo-width, 120px)',
|
|
59
|
+
height: 'var(--cfm-footer-logo-height, 32px)',
|
|
60
|
+
display: 'flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'var(--cfm-footer-logo-justify, start)',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<img
|
|
66
|
+
data-cfm-footer-logo
|
|
67
|
+
data-cfm-logo
|
|
68
|
+
src={seedLogoSrc!}
|
|
69
|
+
alt=""
|
|
70
|
+
className="h-full w-full object-contain"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
<img
|
|
75
|
+
data-cfm-footer-logo
|
|
76
|
+
data-cfm-logo
|
|
77
|
+
src=""
|
|
78
|
+
alt=""
|
|
79
|
+
style={{ display: 'none' }}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
44
82
|
<p
|
|
45
83
|
data-cfm-copyright
|
|
46
|
-
className="text-xs"
|
|
84
|
+
className="cfm-footer-copyright m-0 text-xs"
|
|
47
85
|
style={{ color: 'var(--cfm-footer-text, #9ca3af)' }}
|
|
48
86
|
>
|
|
49
|
-
|
|
87
|
+
{seedCopyright}
|
|
50
88
|
</p>
|
|
51
89
|
</div>
|
|
52
90
|
|
|
53
91
|
<nav
|
|
54
92
|
data-cfm-footer-links
|
|
55
|
-
className="flex flex-wrap
|
|
93
|
+
className="cfm-footer-links flex flex-wrap text-xs"
|
|
56
94
|
style={{
|
|
57
95
|
gridColumn: 'var(--cfm-footer-links-col, 3)',
|
|
58
96
|
justifySelf: 'var(--cfm-footer-links-justify, end)',
|
|
97
|
+
gap: 'var(--cfm-footer-inner-gap, 24px)',
|
|
98
|
+
alignSelf: 'end',
|
|
59
99
|
}}
|
|
60
100
|
>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
{seedLinks.map((link, i) => (
|
|
102
|
+
<a
|
|
103
|
+
key={`${link.label}-${i}`}
|
|
104
|
+
href={link.url || '#'}
|
|
105
|
+
className="cfm-footer-link transition-colors hover:opacity-80"
|
|
106
|
+
style={{ color: 'var(--cfm-footer-link, #9ca3af)' }}
|
|
107
|
+
>
|
|
108
|
+
{link.label}
|
|
109
|
+
</a>
|
|
110
|
+
))}
|
|
68
111
|
</nav>
|
|
69
112
|
</div>
|
|
70
113
|
</footer>
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wizard-ready Header — copy to src/components/Header.tsx
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* After wizard save, logo src MUST come from survey-ui-config.json global.logo.fileName
|
|
8
|
-
* via uiConfig.ts — never hardcode telekom-logo.svg or change fileName manually.
|
|
4
|
+
* Logo + company placement driven entirely by --cfm-header-* CSS vars from survey-ui-config.
|
|
5
|
+
* Logo src from getLogoSrc() — never hardcode asset paths.
|
|
9
6
|
*/
|
|
10
7
|
'use client';
|
|
11
8
|
|
|
@@ -19,91 +16,131 @@ const seedCompany = getCompanyName();
|
|
|
19
16
|
const seedLogoSrc = getLogoSrc({ staticExport: true });
|
|
20
17
|
const seedShowCompany = shouldShowCompanyName();
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
function LogoSlot() {
|
|
23
20
|
const hasLogoFile = Boolean(seedLogoSrc);
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="cfm-header-logo-slot shrink-0"
|
|
24
|
+
style={{
|
|
25
|
+
width: 'var(--cfm-header-logo-width, 80px)',
|
|
26
|
+
height: 'var(--cfm-header-logo-height, 80px)',
|
|
27
|
+
padding: 'var(--cfm-header-logo-padding, 8px)',
|
|
28
|
+
boxSizing: 'border-box',
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{hasLogoFile ? (
|
|
35
|
+
<img
|
|
36
|
+
data-cfm-logo
|
|
37
|
+
src={seedLogoSrc!}
|
|
38
|
+
alt={seedCompany || 'Logo'}
|
|
39
|
+
className="h-full w-full object-contain"
|
|
40
|
+
style={{ background: 'transparent' }}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<img data-cfm-logo src="" alt="Logo" className="h-full w-full object-contain" style={{ display: 'none' }} />
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CompanyName({ inBrandRow = false }: { inBrandRow?: boolean }) {
|
|
50
|
+
const hasLogoFile = Boolean(seedLogoSrc);
|
|
51
|
+
const show =
|
|
52
|
+
seedShowCompany && (hasLogoFile || seedCompany) && (hasLogoFile || !inBrandRow);
|
|
24
53
|
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<span
|
|
57
|
+
data-cfm-logo-text
|
|
58
|
+
className="font-semibold"
|
|
59
|
+
style={{
|
|
60
|
+
display: hasLogoFile ? 'none' : seedCompany ? '' : 'none',
|
|
61
|
+
color: 'var(--cfm-header-company-color, var(--cfm-text))',
|
|
62
|
+
fontSize: 'var(--cfm-header-company-size, 18px)',
|
|
63
|
+
fontWeight: 'var(--cfm-header-company-weight, 600)',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{seedCompany}
|
|
67
|
+
</span>
|
|
68
|
+
<span
|
|
69
|
+
data-cfm-company
|
|
70
|
+
className="font-semibold"
|
|
71
|
+
style={{
|
|
72
|
+
display: show ? (hasLogoFile ? '' : 'none') : 'none',
|
|
73
|
+
marginTop: 'var(--cfm-header-company-margin-top, 0)',
|
|
74
|
+
marginLeft: inBrandRow ? 0 : 'var(--cfm-header-company-margin-left, 0)',
|
|
75
|
+
marginRight: inBrandRow ? 0 : 'var(--cfm-header-company-margin-right, 0)',
|
|
76
|
+
color: 'var(--cfm-header-company-color, var(--cfm-text))',
|
|
77
|
+
fontSize: 'var(--cfm-header-company-size, 18px)',
|
|
78
|
+
fontWeight: 'var(--cfm-header-company-weight, 600)',
|
|
79
|
+
lineHeight: 1.2,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{seedCompany}
|
|
83
|
+
</span>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default function Header({ embedded = false }: HeaderProps) {
|
|
25
89
|
return (
|
|
26
90
|
<header
|
|
27
91
|
className={`cfm-header relative w-full ${embedded ? '' : 'z-10 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'}`}
|
|
28
92
|
style={{
|
|
29
|
-
height: 'var(--cfm-header-height,
|
|
93
|
+
height: 'var(--cfm-header-height, 140px)',
|
|
30
94
|
background: 'var(--cfm-header-bg, #fff)',
|
|
31
95
|
borderBottom: '1px solid var(--cfm-header-border, #e5e7eb)',
|
|
32
96
|
boxShadow: embedded ? undefined : 'var(--cfm-header-shadow, 0 2px 8px rgba(0,0,0,0.06))',
|
|
33
97
|
}}
|
|
34
98
|
>
|
|
35
99
|
<div
|
|
36
|
-
className="cfm-header-inner grid h-full w-full items-center"
|
|
100
|
+
className="cfm-header-inner mx-auto grid h-full w-full items-center"
|
|
37
101
|
style={{
|
|
102
|
+
maxWidth: 'var(--cfm-max-width, 48rem)',
|
|
38
103
|
gridTemplateColumns: '1fr 1fr 1fr',
|
|
39
|
-
padding:
|
|
104
|
+
padding:
|
|
105
|
+
'var(--cfm-header-padding-y, 12px) var(--cfm-header-padding-x, 40px)',
|
|
106
|
+
boxSizing: 'border-box',
|
|
40
107
|
}}
|
|
41
108
|
>
|
|
109
|
+
{/* Brand row: logo + company adjacent (left+left or right+right) */}
|
|
42
110
|
<div
|
|
43
|
-
className="cfm-header-brand flex items-center"
|
|
111
|
+
className="cfm-header-brand-row flex items-center"
|
|
44
112
|
style={{
|
|
45
113
|
gridColumn: 'var(--cfm-header-logo-col, 1)',
|
|
46
114
|
justifySelf: 'var(--cfm-header-logo-justify, start)',
|
|
47
115
|
gap: 'var(--cfm-header-brand-gap, 16px)',
|
|
116
|
+
display: 'var(--cfm-header-brand-row-display, none)',
|
|
48
117
|
}}
|
|
49
118
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
src={seedLogoSrc!}
|
|
54
|
-
alt={seedCompany || 'Logo'}
|
|
55
|
-
className="shrink-0 object-contain"
|
|
56
|
-
style={{
|
|
57
|
-
width: 'var(--cfm-header-logo-width, 120px)',
|
|
58
|
-
height: 'var(--cfm-header-logo-height, 120px)',
|
|
59
|
-
maxHeight: 'calc(var(--cfm-header-height, 120px) - 8px)',
|
|
60
|
-
padding: 'var(--cfm-header-logo-padding, 10px)',
|
|
61
|
-
background: 'transparent',
|
|
62
|
-
}}
|
|
63
|
-
/>
|
|
64
|
-
) : (
|
|
65
|
-
<img
|
|
66
|
-
data-cfm-logo
|
|
67
|
-
src=""
|
|
68
|
-
alt="Logo"
|
|
69
|
-
className="shrink-0 object-contain"
|
|
70
|
-
style={{ display: 'none' }}
|
|
71
|
-
/>
|
|
72
|
-
)}
|
|
119
|
+
<LogoSlot />
|
|
120
|
+
<CompanyName inBrandRow />
|
|
121
|
+
</div>
|
|
73
122
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
</span>
|
|
123
|
+
{/* Separate slots when company is not adjacent to logo */}
|
|
124
|
+
<div
|
|
125
|
+
className="cfm-header-logo-slot-only"
|
|
126
|
+
style={{
|
|
127
|
+
gridColumn: 'var(--cfm-header-logo-col, 1)',
|
|
128
|
+
justifySelf: 'var(--cfm-header-logo-justify, start)',
|
|
129
|
+
display: 'var(--cfm-header-logo-slot-display, flex)',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<LogoSlot />
|
|
133
|
+
</div>
|
|
86
134
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: 'none',
|
|
97
|
-
marginTop: 'var(--cfm-header-company-margin-top, 0)',
|
|
98
|
-
marginLeft: 'var(--cfm-header-company-margin-left, 0)',
|
|
99
|
-
marginRight: 'var(--cfm-header-company-margin-right, 0)',
|
|
100
|
-
color: 'var(--cfm-header-company-color, var(--cfm-text))',
|
|
101
|
-
fontSize: 'var(--cfm-header-company-size, 18px)',
|
|
102
|
-
fontWeight: 'var(--cfm-header-company-weight, 600)',
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
105
|
-
{seedCompany}
|
|
106
|
-
</span>
|
|
135
|
+
<div
|
|
136
|
+
className="cfm-header-company-slot flex items-center"
|
|
137
|
+
style={{
|
|
138
|
+
gridColumn: 'var(--cfm-header-company-col, 2)',
|
|
139
|
+
justifySelf: 'var(--cfm-header-company-justify, center)',
|
|
140
|
+
display: 'var(--cfm-header-company-slot-display, flex)',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<CompanyName />
|
|
107
144
|
</div>
|
|
108
145
|
</div>
|
|
109
146
|
</header>
|