@explorer02/cfm-survey-sdk 0.3.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.3.3",
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)
@@ -16,13 +16,19 @@ import {
16
16
  mcqSelectedAccentVar,
17
17
  mcqSelectedBgVar,
18
18
  } from '@/lib/surveyUi/selectionStyles';
19
- import { csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
19
+ import { columnLabelPillStyle, csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
20
20
 
21
21
  const MATRIX_ROW_LABEL_WIDTH = 'var(--cfm-matrix-row-width, 180px)';
22
22
 
23
23
  type CsatMatrixQuestion = CsatQuestion | RatingMatrixQuestion;
24
24
 
25
- 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
+ }) {
26
32
  if (scaleColumns.length === 0) return null;
27
33
 
28
34
  return (
@@ -39,15 +45,13 @@ function GraphicsColumnLabelsRow({ scaleColumns }: { scaleColumns: ScaleColumn[]
39
45
  transform: 'translateX(-50%)',
40
46
  bottom: 0,
41
47
  textAlign: 'center',
42
- fontSize: '12px',
43
- fontWeight: 500,
44
- color: '#4b5563',
45
- lineHeight: 1.2,
46
- wordBreak: 'break-word',
47
48
  width: '64px',
48
49
  }}
49
50
  >
50
- <span dangerouslySetInnerHTML={{ __html: col.label }} />
51
+ <span
52
+ style={{ fontSize: '12px', fontWeight: 500, ...columnLabelPillStyle(pillScope) }}
53
+ dangerouslySetInnerHTML={{ __html: col.label }}
54
+ />
51
55
  </div>
52
56
  );
53
57
  })}
@@ -189,20 +193,21 @@ function CsatMatrixGrid({
189
193
  isGraphics
190
194
  }: GridProps) {
191
195
  const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
196
+ const isRating = question.type === 'RATING_MATRIX';
192
197
  const [hoveredCell, setHoveredCell] = useState<string | null>(null);
193
198
 
194
- const showLabels = labels && labels.length > 0;
199
+ const showAnchorLabels = isRating && labels && labels.length > 0;
195
200
  const m = scaleColumns.length;
196
201
  const n = labels?.length || 0;
197
202
 
198
203
  return (
199
204
  <div style={{ width: '100%' }}>
200
- {/* Labels row */}
201
- {showLabels && m > 0 && (
205
+ {/* RATING anchor labels (max ~5 along scale) */}
206
+ {showAnchorLabels && m > 0 && (
202
207
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
203
208
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
204
209
  <div style={{ flex: 1, display: 'flex' }}>
205
- <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' } : {}) }}>
206
211
  {labels!.map((label, i) => {
207
212
  const startPercent = isGraphics ? 0 : 0.5 / m;
208
213
  const rangePercent = isGraphics ? 1 : (m - 1) / m;
@@ -213,10 +218,10 @@ function CsatMatrixGrid({
213
218
  position: 'absolute',
214
219
  left: `${percent}%`,
215
220
  transform: 'translateX(-50%)',
216
- textAlign: 'center', fontSize: '13px', fontWeight: 600,
217
- color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
218
221
  }}>
219
- {label}
222
+ <span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
223
+ {label}
224
+ </span>
220
225
  </div>
221
226
  );
222
227
  })}
@@ -226,12 +231,12 @@ function CsatMatrixGrid({
226
231
  </div>
227
232
  )}
228
233
 
229
- {/* Column tick labels for graphics sliders (shown with or without anchor labels) */}
234
+ {/* Column tick labels for graphics sliders */}
230
235
  {isGraphics && m > 0 && (
231
- <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' }}>
232
237
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
233
238
  <div style={{ flex: 1, display: 'flex' }}>
234
- <GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
239
+ <GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
235
240
  {hasNotApplicable && (
236
241
  <div style={{ width: '56px', flexShrink: 0 }} />
237
242
  )}
@@ -239,8 +244,8 @@ function CsatMatrixGrid({
239
244
  </div>
240
245
  )}
241
246
 
242
- {/* Column Headers for non-graphics star format always shows labels above stars */}
243
- {!isGraphics && (!showLabels || isStar) && m > 0 && (
247
+ {/* Column headers CSAT always; RATING numeric headers when not graphics */}
248
+ {!isGraphics && m > 0 && (
244
249
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
245
250
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
246
251
  <div style={{ flex: 1, display: 'flex' }}>
@@ -248,7 +253,13 @@ function CsatMatrixGrid({
248
253
  {scaleColumns.map(col => (
249
254
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
250
255
  <span
251
- 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
+ }}
252
263
  dangerouslySetInnerHTML={{ __html: col.label }}
253
264
  />
254
265
  </div>
@@ -292,6 +303,16 @@ function CsatMatrixGrid({
292
303
  ticks={scaleColumns.length}
293
304
  tickLabels={scaleColumns.map(col => col.label)}
294
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
+ }
295
316
  onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
296
317
  />
297
318
  </div>
@@ -419,20 +440,27 @@ function CsatMatrixCarousel({
419
440
  isGraphics
420
441
  }: CarouselProps) {
421
442
  const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
443
+ const isRating = question.type === 'RATING_MATRIX';
422
444
  const [activeCarouselIndex, setActiveCarouselIndex] = useState(0);
423
445
  const [hoveredCell, setHoveredCell] = useState<string | null>(null);
424
446
 
425
447
  const row = statementRows[activeCarouselIndex];
426
448
  if (!row) return null;
427
-
428
- 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)))';
429
457
 
430
458
  return (
431
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' }}>
432
460
  <h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
433
461
 
434
- {showLabels && scaleColumns.length > 0 && (
435
- <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' }}>
436
464
  {labels!.map((label, i) => {
437
465
  const m = scaleColumns.length;
438
466
  const n = labels!.length;
@@ -443,19 +471,33 @@ function CsatMatrixCarousel({
443
471
  return (
444
472
  <div key={i} style={{
445
473
  position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
446
- textAlign: 'center', fontSize: '13px', fontWeight: 600,
447
- color: '#4b5563', lineHeight: 1.3, whiteSpace: 'nowrap'
474
+ textAlign: 'center',
448
475
  }}>
449
- {label}
476
+ <span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
477
+ {label}
478
+ </span>
450
479
  </div>
451
480
  );
452
481
  })}
453
482
  </div>
454
483
  )}
455
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
+
456
498
  {isGraphics && scaleColumns.length > 0 && (
457
- <div style={{ width: '100%', maxWidth: '800px', marginBottom: showLabels ? '8px' : '16px' }}>
458
- <GraphicsColumnLabelsRow scaleColumns={scaleColumns} />
499
+ <div style={{ width: '100%', maxWidth: '800px', marginBottom: showAnchorLabels ? '8px' : '16px' }}>
500
+ <GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
459
501
  </div>
460
502
  )}
461
503
 
@@ -473,6 +515,8 @@ function CsatMatrixCarousel({
473
515
  sliderType="graphics" ticks={scaleColumns.length}
474
516
  tickLabels={scaleColumns.map(col => col.label)}
475
517
  reverseScaleOrder={reverseScaleOrder}
518
+ trackVar={trackVar}
519
+ thumbVar={thumbVar}
476
520
  onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
477
521
  />
478
522
  </div>
@@ -519,8 +563,16 @@ function CsatMatrixCarousel({
519
563
  >
520
564
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
521
565
  </button>
522
- <span style={{ fontSize: '13px', fontWeight: 500, color: '#4b5563', textAlign: 'center', lineHeight: 1.2 }}
523
- 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
+ />
524
576
  </div>
525
577
  );
526
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>