@explorer02/cfm-survey-sdk 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -117,7 +117,6 @@ 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
121
120
  - [ ] `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
122
121
  - [ ] **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)
123
122
  - [ ] Per-type scale vars per `wizard-question-type-styling.md` for **all 11 types** (wizard exports all; survey inventory may be smaller)
@@ -1,7 +1,7 @@
1
1
  import React, { useState } from 'react';
2
2
  import type { CsatQuestion, RatingMatrixQuestion, MatrixRowAnswers, ScaleColumn } from '@explorer02/cfm-survey-sdk';
3
3
  import { getEmojiForIndex, CsatStarIcons } from './surveyUiIcons';
4
- import { columnSubmitValue, nonNaScaleColumns } from './surveyUiScaleUtils';
4
+ import { columnSubmitValue, nonNaScaleColumns, anchorLabelPercent } from './surveyUiScaleUtils';
5
5
  import { CustomSliderTrack } from './CustomSliderTrack';
6
6
  import MatrixDropdown from './MatrixDropdown';
7
7
  import {
@@ -16,20 +16,34 @@ import {
16
16
  mcqSelectedAccentVar,
17
17
  mcqSelectedBgVar,
18
18
  } from '@/lib/surveyUi/selectionStyles';
19
- import { columnLabelPillStyle, csatColumnLabelStyle } from '@/lib/surveyUi/labelStyles';
19
+ import { csatColumnLabelStyle, ratingColumnLabelStyle } 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 anchorLabelStyle(question: CsatMatrixQuestion) {
26
+ return question.type === 'RATING_MATRIX' ? ratingColumnLabelStyle() : csatColumnLabelStyle();
27
+ }
28
+
29
+ function columnHeaderLabelStyle(question: CsatMatrixQuestion) {
30
+ if (question.type === 'RATING_MATRIX') {
31
+ return { fontSize: '13px', fontWeight: 500, textAlign: 'center' as const, lineHeight: 1.2, color: '#4b5563' };
32
+ }
33
+ return { fontSize: '13px', fontWeight: 500, textAlign: 'center' as const, lineHeight: 1.2, ...csatColumnLabelStyle() };
34
+ }
35
+
25
36
  function GraphicsColumnLabelsRow({
26
37
  scaleColumns,
27
- pillScope = 'csat' as 'csat' | 'rating',
38
+ question,
28
39
  }: {
29
40
  scaleColumns: ScaleColumn[];
30
- pillScope?: 'csat' | 'rating';
41
+ question: CsatMatrixQuestion;
31
42
  }) {
32
43
  if (scaleColumns.length === 0) return null;
44
+ const labelStyle = question.type === 'RATING_MATRIX'
45
+ ? { fontSize: '12px', fontWeight: 500, lineHeight: 1.2, wordBreak: 'break-word' as const, color: '#4b5563' }
46
+ : { fontSize: '12px', fontWeight: 500, lineHeight: 1.2, wordBreak: 'break-word' as const, ...csatColumnLabelStyle() };
33
47
 
34
48
  return (
35
49
  <div style={{ flex: 1, padding: '0 24px' }}>
@@ -48,10 +62,7 @@ function GraphicsColumnLabelsRow({
48
62
  width: '64px',
49
63
  }}
50
64
  >
51
- <span
52
- style={{ fontSize: '12px', fontWeight: 500, ...columnLabelPillStyle(pillScope) }}
53
- dangerouslySetInnerHTML={{ __html: col.label }}
54
- />
65
+ <span style={labelStyle} dangerouslySetInnerHTML={{ __html: col.label }} />
55
66
  </div>
56
67
  );
57
68
  })}
@@ -193,35 +204,33 @@ function CsatMatrixGrid({
193
204
  isGraphics
194
205
  }: GridProps) {
195
206
  const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
196
- const isRating = question.type === 'RATING_MATRIX';
197
207
  const [hoveredCell, setHoveredCell] = useState<string | null>(null);
198
208
 
199
- const showAnchorLabels = isRating && labels && labels.length > 0;
209
+ const showLabels = labels && labels.length > 0;
200
210
  const m = scaleColumns.length;
201
211
  const n = labels?.length || 0;
202
212
 
203
213
  return (
204
214
  <div style={{ width: '100%' }}>
205
- {/* RATING anchor labels (max ~5 along scale) */}
206
- {showAnchorLabels && m > 0 && (
215
+ {/* Labels row */}
216
+ {showLabels && m > 0 && (
207
217
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '12px' }}>
208
218
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
209
219
  <div style={{ flex: 1, display: 'flex' }}>
210
- <div style={{ flex: 1, position: 'relative', height: '28px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
220
+ <div style={{ flex: 1, position: 'relative', height: '24px', ...(isGraphics ? { margin: '0 40px' } : {}) }}>
211
221
  {labels!.map((label, i) => {
212
- const startPercent = isGraphics ? 0 : 0.5 / m;
213
- const rangePercent = isGraphics ? 1 : (m - 1) / m;
214
- const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
222
+ const percent = anchorLabelPercent(i, n, m, isGraphics);
215
223
 
216
224
  return (
217
225
  <div key={i} style={{
218
226
  position: 'absolute',
219
227
  left: `${percent}%`,
220
228
  transform: 'translateX(-50%)',
229
+ textAlign: 'center', fontSize: '13px', fontWeight: 600,
230
+ lineHeight: 1.3, whiteSpace: 'nowrap',
231
+ ...anchorLabelStyle(question),
221
232
  }}>
222
- <span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
223
- {label}
224
- </span>
233
+ {label}
225
234
  </div>
226
235
  );
227
236
  })}
@@ -231,12 +240,12 @@ function CsatMatrixGrid({
231
240
  </div>
232
241
  )}
233
242
 
234
- {/* Column tick labels for graphics sliders */}
243
+ {/* Column tick labels for graphics sliders (shown with or without anchor labels) */}
235
244
  {isGraphics && m > 0 && (
236
- <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showAnchorLabels ? '8px' : '16px', minHeight: '24px' }}>
245
+ <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: showLabels ? '8px' : '16px', minHeight: '24px' }}>
237
246
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
238
247
  <div style={{ flex: 1, display: 'flex' }}>
239
- <GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
248
+ <GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
240
249
  {hasNotApplicable && (
241
250
  <div style={{ width: '56px', flexShrink: 0 }} />
242
251
  )}
@@ -244,8 +253,8 @@ function CsatMatrixGrid({
244
253
  </div>
245
254
  )}
246
255
 
247
- {/* Column headersCSAT always; RATING numeric headers when not graphics */}
248
- {!isGraphics && m > 0 && (
256
+ {/* Column Headers for non-graphics star format always shows labels above stars */}
257
+ {!isGraphics && (!showLabels || isStar) && m > 0 && (
249
258
  <div style={{ display: 'flex', alignItems: 'flex-end', marginBottom: '16px', minHeight: '24px' }}>
250
259
  <div style={{ width: MATRIX_ROW_LABEL_WIDTH, flexShrink: 0 }} />
251
260
  <div style={{ flex: 1, display: 'flex' }}>
@@ -253,13 +262,7 @@ function CsatMatrixGrid({
253
262
  {scaleColumns.map(col => (
254
263
  <div key={col.id} style={{ display: 'flex', justifyContent: 'center', padding: '0 4px' }}>
255
264
  <span
256
- style={{
257
- fontSize: '13px',
258
- fontWeight: 500,
259
- ...(isRating
260
- ? { color: 'var(--cfm-text, #374151)', textAlign: 'center' as const }
261
- : columnLabelPillStyle('csat')),
262
- }}
265
+ style={columnHeaderLabelStyle(question)}
263
266
  dangerouslySetInnerHTML={{ __html: col.label }}
264
267
  />
265
268
  </div>
@@ -303,16 +306,6 @@ function CsatMatrixGrid({
303
306
  ticks={scaleColumns.length}
304
307
  tickLabels={scaleColumns.map(col => col.label)}
305
308
  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
- }
316
309
  onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
317
310
  />
318
311
  </div>
@@ -367,9 +360,17 @@ function CsatMatrixGrid({
367
360
  fontSize: '14px', fontWeight: 500,
368
361
  ...(isSelected
369
362
  ? scaleCellSelectedStyle(true)
370
- : { border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#111827' }),
363
+ : {
364
+ border: '1px solid #d1d5db',
365
+ backgroundColor: '#fff',
366
+ color: '#111827',
367
+ ...unselectedOpacityStyle(),
368
+ }),
371
369
  transition: 'all 0.15s',
372
- } : { padding: '8px' }),
370
+ } : {
371
+ padding: '8px',
372
+ ...(isSelected ? {} : unselectedOpacityStyle()),
373
+ }),
373
374
  }}
374
375
  >
375
376
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
@@ -440,64 +441,42 @@ function CsatMatrixCarousel({
440
441
  isGraphics
441
442
  }: CarouselProps) {
442
443
  const { statementRows, scaleAnchorLabels: labels, hasNotApplicableOption: hasNotApplicable, reverseScaleOrder } = question;
443
- const isRating = question.type === 'RATING_MATRIX';
444
444
  const [activeCarouselIndex, setActiveCarouselIndex] = useState(0);
445
445
  const [hoveredCell, setHoveredCell] = useState<string | null>(null);
446
446
 
447
447
  const row = statementRows[activeCarouselIndex];
448
448
  if (!row) return null;
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)))';
449
+
450
+ const showLabels = labels && labels.length > 0;
457
451
 
458
452
  return (
459
453
  <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' }}>
460
454
  <h3 style={{ fontSize: '18px', fontWeight: 500, color: '#111827', marginBottom: '32px', textAlign: 'center' }} dangerouslySetInnerHTML={{ __html: row.statementText }} />
461
455
 
462
- {showAnchorLabels && scaleColumns.length > 0 && (
463
- <div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '28px', marginBottom: '16px' }}>
456
+ {showLabels && scaleColumns.length > 0 && (
457
+ <div style={{ width: '100%', maxWidth: '800px', display: 'flex', position: 'relative', height: '24px', marginBottom: '16px' }}>
464
458
  {labels!.map((label, i) => {
465
459
  const m = scaleColumns.length;
466
460
  const n = labels!.length;
467
- const startPercent = isGraphics ? 0 : 0.5 / m;
468
- const rangePercent = isGraphics ? 1 : (m - 1) / m;
469
- const percent = n === 1 ? 50 : (startPercent + (i / (n - 1)) * rangePercent) * 100;
461
+ const percent = anchorLabelPercent(i, n, m, isGraphics);
470
462
 
471
463
  return (
472
464
  <div key={i} style={{
473
465
  position: 'absolute', left: `${percent}%`, transform: 'translateX(-50%)',
474
- textAlign: 'center',
466
+ textAlign: 'center', fontSize: '13px', fontWeight: 600,
467
+ lineHeight: 1.3, whiteSpace: 'nowrap',
468
+ ...anchorLabelStyle(question),
475
469
  }}>
476
- <span style={{ fontSize: '13px', fontWeight: 600, ...columnLabelPillStyle('rating') }}>
477
- {label}
478
- </span>
470
+ {label}
479
471
  </div>
480
472
  );
481
473
  })}
482
474
  </div>
483
475
  )}
484
476
 
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
-
498
477
  {isGraphics && scaleColumns.length > 0 && (
499
- <div style={{ width: '100%', maxWidth: '800px', marginBottom: showAnchorLabels ? '8px' : '16px' }}>
500
- <GraphicsColumnLabelsRow scaleColumns={scaleColumns} pillScope={isRating ? 'rating' : 'csat'} />
478
+ <div style={{ width: '100%', maxWidth: '800px', marginBottom: showLabels ? '8px' : '16px' }}>
479
+ <GraphicsColumnLabelsRow scaleColumns={scaleColumns} question={question} />
501
480
  </div>
502
481
  )}
503
482
 
@@ -515,8 +494,6 @@ function CsatMatrixCarousel({
515
494
  sliderType="graphics" ticks={scaleColumns.length}
516
495
  tickLabels={scaleColumns.map(col => col.label)}
517
496
  reverseScaleOrder={reverseScaleOrder}
518
- trackVar={trackVar}
519
- thumbVar={thumbVar}
520
497
  onChange={v => onCellSelect(row.id, columnSubmitValue(scaleColumns[v]))}
521
498
  />
522
499
  </div>
@@ -550,29 +527,27 @@ function CsatMatrixCarousel({
550
527
  cursor: 'pointer', borderRadius: '6px', pointerEvents: 'none',
551
528
  ...(isEmoji ? {
552
529
  padding: '6px', transform: isSelected ? 'scale(1.1)' : isHovered ? 'scale(1.25)' : 'scale(1)',
553
- ...(isSelected ? { backgroundColor: 'rgba(252, 231, 243, 0.6)', boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)', borderRadius: '50%' } : {}),
530
+ ...(isSelected ? { backgroundColor: 'rgba(252, 231, 243, 0.6)', boxShadow: '0 0 0 2px rgba(226, 0, 116, 0.3)', borderRadius: '50%' } : unselectedOpacityStyle()),
554
531
  } : isStar ? {
555
- padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)', opacity: isSelected ? 1 : isHovered ? 1 : 0.5,
532
+ padding: '4px', transform: isHovered ? 'scale(1.1)' : 'scale(1)', color: isSelected ? cellSelectedVar : focusRingVar,
533
+ ...(isSelected ? {} : unselectedOpacityStyle()),
556
534
  } : isNumbered ? {
557
535
  width: '40px', height: '40px', borderRadius: '8px', fontSize: '14px', fontWeight: 500,
558
536
  ...(isSelected
559
537
  ? scaleCellSelectedStyle(true)
560
- : { border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#111827' }),
561
- } : { padding: '8px' }),
538
+ : {
539
+ border: '1px solid #d1d5db',
540
+ backgroundColor: '#fff',
541
+ color: '#111827',
542
+ ...unselectedOpacityStyle(),
543
+ }),
544
+ } : { padding: '8px', ...(isSelected ? {} : unselectedOpacityStyle()) }),
562
545
  }}
563
546
  >
564
547
  {(EmojiNode ?? StarNode ?? (isNumbered ? (displayIdx + 1) : RadioNode)) as React.ReactNode}
565
548
  </button>
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
- />
549
+ <span style={columnHeaderLabelStyle(question)}
550
+ dangerouslySetInnerHTML={{ __html: col.label }} />
576
551
  </div>
577
552
  );
578
553
  })}
@@ -22,8 +22,6 @@ 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;
27
25
  onChange: (v: number) => void;
28
26
  };
29
27
 
@@ -37,8 +35,6 @@ export function CustomSliderTrack({
37
35
  ticks,
38
36
  tickLabels,
39
37
  reverseScaleOrder,
40
- trackVar = 'var(--cfm-slider-track, #e5e7eb)',
41
- thumbVar = 'var(--cfm-slider-thumb, var(--cfm-input-focus-ring, var(--cfm-primary)))',
42
38
  onChange,
43
39
  }: CustomSliderTrackProps) {
44
40
  const [isHovered, setIsHovered] = React.useState(false);
@@ -54,7 +50,9 @@ export function CustomSliderTrack({
54
50
  ? tooltipLabel.replace(/<[^>]*>/g, '')
55
51
  : String(Math.round(value));
56
52
 
57
- const themeColor = disabled ? '#9ca3af' : thumbVar;
53
+ const themeColor = disabled
54
+ ? '#9ca3af'
55
+ : 'var(--cfm-slider-thumb, var(--cfm-input-focus-ring, var(--cfm-primary)))';
58
56
 
59
57
  return (
60
58
  <div
@@ -64,7 +62,7 @@ export function CustomSliderTrack({
64
62
  >
65
63
  <div style={{
66
64
  position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
67
- height: '4px', borderRadius: '2px', backgroundColor: trackVar,
65
+ height: '4px', borderRadius: '2px', backgroundColor: 'var(--cfm-slider-track, #e5e7eb)',
68
66
  }} />
69
67
 
70
68
  <div style={{
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * Wizard-ready Footer — copy to src/components/Footer.tsx
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.
3
+ * Required: footer.cfm-footer, data-cfm-copyright, data-cfm-footer-links, data-cfm-footer-logo
6
4
  */
7
5
  'use client';
8
6
 
@@ -31,84 +29,56 @@ export default function Footer() {
31
29
  }}
32
30
  >
33
31
  <div
34
- className="cfm-footer-inner mx-auto w-full items-end"
35
- style={{
36
- maxWidth: 'var(--cfm-max-width, 48rem)',
37
- display: 'var(--cfm-footer-inner-display, grid)',
38
- flexDirection: 'var(--cfm-footer-inner-direction, row)' as React.CSSProperties['flexDirection'],
39
- gridTemplateColumns: '1fr auto 1fr',
40
- gap: 'var(--cfm-footer-inner-gap, 24px)',
41
- }}
32
+ className="cfm-footer-inner mx-auto w-full"
33
+ style={{ maxWidth: 'var(--cfm-max-width, 48rem)' }}
42
34
  >
43
- <div
44
- className="cfm-footer-logo-column"
45
- style={{
46
- gridColumn: 'var(--cfm-footer-logo-col, 1)',
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)',
52
- }}
53
- >
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
- ) : (
35
+ {showLogo ? (
36
+ <div className="cfm-footer-logo-slot">
74
37
  <img
75
38
  data-cfm-footer-logo
76
39
  data-cfm-logo
77
- src=""
40
+ src={seedLogoSrc!}
78
41
  alt=""
79
- style={{ display: 'none' }}
42
+ className="object-contain"
43
+ style={{
44
+ width: 'var(--cfm-footer-logo-width, 120px)',
45
+ height: 'var(--cfm-footer-logo-height, 32px)',
46
+ }}
80
47
  />
81
- )}
82
- <p
83
- data-cfm-copyright
84
- className="cfm-footer-copyright m-0 text-xs"
85
- style={{ color: 'var(--cfm-footer-text, #9ca3af)' }}
86
- >
48
+ </div>
49
+ ) : (
50
+ <img
51
+ data-cfm-footer-logo
52
+ data-cfm-logo
53
+ src=""
54
+ alt=""
55
+ style={{ display: 'none' }}
56
+ />
57
+ )}
58
+
59
+ {seedCopyright ? (
60
+ <p data-cfm-copyright className="cfm-footer-copyright text-xs">
87
61
  {seedCopyright}
88
62
  </p>
89
- </div>
63
+ ) : (
64
+ <p data-cfm-copyright className="cfm-footer-copyright text-xs" style={{ display: 'none' }} />
65
+ )}
90
66
 
91
- <nav
92
- data-cfm-footer-links
93
- className="cfm-footer-links flex flex-wrap text-xs"
94
- style={{
95
- gridColumn: 'var(--cfm-footer-links-col, 3)',
96
- justifySelf: 'var(--cfm-footer-links-justify, end)',
97
- gap: 'var(--cfm-footer-inner-gap, 24px)',
98
- alignSelf: 'end',
99
- }}
100
- >
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
- ))}
111
- </nav>
67
+ {seedLinks.length > 0 ? (
68
+ <nav data-cfm-footer-links className="cfm-footer-links flex flex-wrap gap-x-6 gap-y-2 text-xs">
69
+ {seedLinks.map((link, i) => (
70
+ <a
71
+ key={`${link.label}-${i}`}
72
+ href={link.url || '#'}
73
+ className="cfm-footer-link transition-colors hover:opacity-80"
74
+ >
75
+ {link.label}
76
+ </a>
77
+ ))}
78
+ </nav>
79
+ ) : (
80
+ <nav data-cfm-footer-links className="cfm-footer-links" style={{ display: 'none' }} />
81
+ )}
112
82
  </div>
113
83
  </footer>
114
84
  );