@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.
Files changed (29) hide show
  1. package/dist/cli/index.js +55 -55
  2. package/dist/cli/index.mjs +61 -61
  3. package/package.json +1 -1
  4. package/templates/docs/00-integration/component-checklist.md +1 -0
  5. package/templates/docs/templates/CsatMatrixScale.tsx +86 -33
  6. package/templates/docs/templates/CustomSliderTrack.tsx +6 -4
  7. package/templates/docs/templates/Footer.tsx +69 -26
  8. package/templates/docs/templates/Header.tsx +102 -65
  9. package/templates/docs/templates/LikertMatrixScale.tsx +7 -6
  10. package/templates/docs/templates/Question.tsx +7 -1
  11. package/templates/docs/templates/RankOrderScale.tsx +8 -1
  12. package/templates/docs/templates/RatingScale.tsx +2 -1
  13. package/templates/docs/templates/SliderMatrixScale.tsx +5 -3
  14. package/templates/docs/templates/labelStyles.ts +59 -16
  15. package/templates/docs/templates/selectionStyles.ts +16 -1
  16. package/templates/docs/templates/surveyUiIcons.tsx +1 -1
  17. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +41 -1
  18. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +2 -1
  19. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +3 -2
  20. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +18 -9
  21. package/templates/survey-theme.css +14 -2
  22. package/templates/wizard-dist/assets/{PreviewMock-DbbLpHdF.js → PreviewMock-CC1UlWe-.js} +1 -1
  23. package/templates/wizard-dist/assets/TypePanel-DISCXm0z.js +1 -0
  24. package/templates/wizard-dist/assets/index-Blpq4QjK.js +34 -0
  25. package/templates/wizard-dist/assets/index-CVBd54V0.css +1 -0
  26. package/templates/wizard-dist/index.html +2 -2
  27. package/templates/wizard-dist/assets/TypePanel-DQbV2iCf.js +0 -1
  28. package/templates/wizard-dist/assets/index-BhWM50Yu.css +0 -1
  29. package/templates/wizard-dist/assets/index-CY7WMJ93.js +0 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -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({ scaleColumns }: { scaleColumns: ScaleColumn[] }) {
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 dangerouslySetInnerHTML={{ __html: col.label }} />
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 showLabels = labels && labels.length > 0;
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
- {/* Labels row */}
200
- {showLabels && m > 0 && (
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: '24px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
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
- {label}
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 (shown with or without anchor labels) */}
234
+ {/* Column tick labels for graphics sliders */}
229
235
  {isGraphics && m > 0 && (
230
- <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showLabels ? '8px' : '16px', minHeight: '24px' }}>
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 Headers for non-graphics */}
242
- {!isGraphics && !showLabels && m > 0 && (
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={{ fontSize: '13px', fontWeight: 500, textAlign: 'center', lineHeight: 1.2, ...csatColumnLabelStyle() }}
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', backgroundColor: rowIdx % 2 === 0 ? 'rgba(249, 250, 251, 0.8)' : '#fff',
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 showLabels = labels && labels.length > 0;
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
- {showLabels && scaleColumns.length > 0 && (
434
- <div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '24px', marginBottom: '16px' }}>
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', fontSize: '13px', fontWeight: 600,
446
- color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
474
+ textAlign: 'center',
447
475
  }}>
448
- {label}
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: showLabels ? '8px' : '16px' }}>
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 style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
522
- dangerouslySetInnerHTML={{ __html: col.label }} />
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: 'var(--cfm-slider-track, #e5e7eb)',
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
- * Required: footer.cfm-footer, data-cfm-copyright, data-cfm-footer-links, data-cfm-footer-logo
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 grid w-full max-w-4xl items-end gap-6"
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 1fr 1fr',
39
+ gridTemplateColumns: '1fr auto 1fr',
40
+ gap: 'var(--cfm-footer-inner-gap, 24px)',
23
41
  }}
24
42
  >
25
43
  <div
26
- className="space-y-3"
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
- <img
33
- data-cfm-footer-logo
34
- data-cfm-logo
35
- src="./logo.svg"
36
- alt=""
37
- style={{
38
- display: 'none',
39
- width: 'var(--cfm-footer-logo-width, 120px)',
40
- height: 'var(--cfm-footer-logo-height, 32px)',
41
- objectFit: 'contain',
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
- © Company
87
+ {seedCopyright}
50
88
  </p>
51
89
  </div>
52
90
 
53
91
  <nav
54
92
  data-cfm-footer-links
55
- className="flex flex-wrap gap-x-6 gap-y-2 text-xs"
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
- <a
62
- href="#"
63
- className="transition-colors hover:opacity-80"
64
- style={{ color: 'var(--cfm-footer-link, #9ca3af)' }}
65
- >
66
- Privacy
67
- </a>
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
- * Brand row: [logo image] [company name] company sits after logo when both exist.
5
- * When no logo file: text fallback via data-cfm-logo-text (company name or empty).
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
- export default function Header({ embedded = false }: HeaderProps) {
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, 120px)',
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: `0 var(--cfm-header-padding-x, 40px)`,
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
- {hasLogoFile ? (
51
- <img
52
- data-cfm-logo
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
- <span
75
- data-cfm-logo-text
76
- className="font-semibold"
77
- style={{
78
- display: hasLogoFile ? 'none' : seedCompany ? '' : 'none',
79
- color: 'var(--cfm-header-company-color, var(--cfm-text))',
80
- fontSize: 'var(--cfm-header-company-size, 18px)',
81
- fontWeight: 'var(--cfm-header-company-weight, 600)',
82
- }}
83
- >
84
- {seedCompany}
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
- <span
88
- data-cfm-company
89
- className="font-semibold"
90
- style={{
91
- display:
92
- seedShowCompany && (hasLogoFile || seedCompany)
93
- ? hasLogoFile
94
- ? ''
95
- : 'none'
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>