Skip to content

Commit 57b04b5

Browse files
committed
feat: Add Google Fonts integration with FontProvider and utility functions
- Implemented FontProvider component for loading and applying Google Fonts. - Created google-fonts utility module for managing font configurations and loading. - Added index file for exporting font utilities and components. - Updated timeline components to utilize new font features. - Enhanced button styles in fullscreen and timeline controls for better UX. - Refactored ContentDisplay component to use CSS classes for styling. - Improved timeline scrolling behavior by disabling auto-navigation during keyboard navigation. - Added responsive font size adjustments and utility styles for typography. - Updated theme popover z-index for better layering. - Introduced TypeScript configuration for development environment. - Enhanced Vite configuration for improved development experience.
1 parent c191122 commit 57b04b5

24 files changed

+941
-153
lines changed

src/components/contexts/TimelineContextProvider.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,19 @@ const TimelineContext = createContext<TimelineContextValue | null>(null);
162162
export interface TimelineContextProviderProps
163163
extends Omit<Partial<TimelinePropsModel>, 'children'> {
164164
children: React.ReactNode;
165+
googleFonts?: {
166+
fontFamily: string;
167+
elements?: {
168+
title?: { weight?: any; style?: any; size?: string; };
169+
cardTitle?: { weight?: any; style?: any; size?: string; };
170+
cardSubtitle?: { weight?: any; style?: any; size?: string; };
171+
cardText?: { weight?: any; style?: any; size?: string; };
172+
controls?: { weight?: any; style?: any; size?: string; };
173+
};
174+
weights?: any[];
175+
display?: string;
176+
preconnect?: boolean;
177+
};
165178
}
166179

167180
export const TimelineContextProvider: FunctionComponent<
@@ -361,6 +374,48 @@ export const TimelineContextProvider: FunctionComponent<
361374
[fontSizes],
362375
);
363376

377+
// Google Fonts integration
378+
const googleFontsConfig = useMemo(() => {
379+
if (!fontSizes && !props.googleFonts) return null;
380+
381+
// If we have both fontSizes and googleFonts, merge them
382+
if (props.googleFonts) {
383+
return {
384+
...props.googleFonts,
385+
elements: {
386+
...props.googleFonts.elements,
387+
// Override sizes from fontSizes if provided
388+
...(memoizedFontSizes.title && {
389+
title: {
390+
...props.googleFonts.elements?.title,
391+
size: memoizedFontSizes.title
392+
}
393+
}),
394+
...(memoizedFontSizes.cardTitle && {
395+
cardTitle: {
396+
...props.googleFonts.elements?.cardTitle,
397+
size: memoizedFontSizes.cardTitle
398+
}
399+
}),
400+
...(memoizedFontSizes.cardSubtitle && {
401+
cardSubtitle: {
402+
...props.googleFonts.elements?.cardSubtitle,
403+
size: memoizedFontSizes.cardSubtitle
404+
}
405+
}),
406+
...(memoizedFontSizes.cardText && {
407+
cardText: {
408+
...props.googleFonts.elements?.cardText,
409+
size: memoizedFontSizes.cardText
410+
}
411+
}),
412+
}
413+
};
414+
}
415+
416+
return null;
417+
}, [props.googleFonts, memoizedFontSizes]);
418+
364419
const memoizedMediaSettings = useMemo(
365420
() => ({
366421
align: computedMediaAlign as 'left' | 'right' | 'center',

src/components/elements/list/list.css.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const listItem = style([
3232
background: tokens.semantic.color.background.elevated,
3333
borderRadius: tokens.semantic.borderRadius.sm,
3434
boxShadow: tokens.semantic.shadow.card,
35-
padding: tokens.semantic.spacing.sm,
35+
padding: `${tokens.semantic.spacing.xs} ${tokens.semantic.spacing.sm}`, // Reduced vertical padding
3636
border: `1px solid ${tokens.semantic.color.border.default}`,
3737
transition: `all ${tokens.semantic.motion.duration.normal} ${tokens.semantic.motion.easing.standard}`,
3838

@@ -42,6 +42,12 @@ export const listItem = style([
4242
borderColor: tokens.semantic.color.border.interactive,
4343
boxShadow: tokens.semantic.shadow.cardHover,
4444
transform: 'translateY(-1px)',
45+
backgroundColor: `${tokens.semantic.color.interactive.primary}05`,
46+
},
47+
'&:active': {
48+
transform: 'translateY(0px) scale(0.98)',
49+
backgroundColor: `${tokens.semantic.color.interactive.primary}10`,
50+
borderColor: tokens.semantic.color.interactive.primary,
4551
},
4652
},
4753
},

src/components/elements/popover/index.tsx

Lines changed: 123 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
memo,
99
useState,
1010
} from 'react';
11+
import ReactDOM from 'react-dom';
1112
import useCloseClickOutside from 'src/components/effects/useCloseClickOutside';
1213
import { ChevronDown, CloseIcon } from 'src/components/icons';
1314
import { PopOverModel } from './popover.model';
@@ -78,6 +79,11 @@ const PopOver: FunctionComponent<PopOverModel> = ({
7879
const [horizontalPosition, setHorizontalPosition] = useState<
7980
'left' | 'right' | 'center'
8081
>('center');
82+
const [popoverPosition, setPopoverPosition] = useState({
83+
top: 0,
84+
left: 0,
85+
width: 0
86+
});
8187

8288
const toggleOpen = useCallback(() => {
8389
dispatch({ type: 'TOGGLE' });
@@ -95,40 +101,91 @@ const PopOver: FunctionComponent<PopOverModel> = ({
95101

96102
useCloseClickOutside(ref, closePopover);
97103

98-
// Calculate optimal horizontal positioning
99-
const calculateHorizontalPosition = useCallback(() => {
100-
if (!ref.current || !popoverRef.current || $isMobile) return;
104+
// Calculate optimal positioning for portal rendering
105+
const calculatePopoverPosition = useCallback(() => {
106+
if (!ref.current) return;
101107

102108
const triggerRect = ref.current.getBoundingClientRect();
103-
const popoverWidth = width;
104109
const viewportWidth = window.innerWidth;
105-
const timelineContainer =
106-
ref.current.closest('[data-testid="timeline"]') || document.body;
107-
const containerRect = timelineContainer.getBoundingClientRect();
108-
109-
// Calculate space available on each side
110-
const spaceLeft = triggerRect.left - containerRect.left;
111-
const spaceRight = containerRect.right - triggerRect.right;
112-
const spaceCenter = Math.min(spaceLeft, spaceRight);
113-
114-
// Choose position based on available space
115-
if (popoverWidth <= spaceCenter * 2) {
116-
setHorizontalPosition('center');
117-
} else if (spaceLeft >= popoverWidth && spaceLeft > spaceRight) {
118-
setHorizontalPosition('left');
119-
} else if (spaceRight >= popoverWidth) {
120-
setHorizontalPosition('right');
110+
const viewportHeight = window.innerHeight;
111+
const popoverWidth = $isMobile ? Math.min(320, viewportWidth * 0.9) : width;
112+
113+
// Calculate horizontal position
114+
let left = triggerRect.left;
115+
let horizontalPos: 'left' | 'right' | 'center' = 'left';
116+
117+
if ($isMobile) {
118+
// Center on mobile
119+
left = (viewportWidth - popoverWidth) / 2;
120+
horizontalPos = 'center';
121121
} else {
122-
setHorizontalPosition('center'); // Fallback to center with potential overflow
122+
// Calculate space available on each side
123+
const spaceLeft = triggerRect.left;
124+
const spaceRight = viewportWidth - triggerRect.right;
125+
126+
if (popoverWidth <= spaceRight) {
127+
// Align with left edge of trigger
128+
left = triggerRect.left;
129+
horizontalPos = 'left';
130+
} else if (popoverWidth <= spaceLeft) {
131+
// Align with right edge of trigger
132+
left = triggerRect.right - popoverWidth;
133+
horizontalPos = 'right';
134+
} else {
135+
// Center horizontally
136+
left = Math.max(16, (viewportWidth - popoverWidth) / 2);
137+
horizontalPos = 'center';
138+
}
139+
140+
// Ensure popover doesn't go off-screen
141+
left = Math.max(16, Math.min(left, viewportWidth - popoverWidth - 16));
123142
}
124-
}, [width, $isMobile]);
143+
144+
// Calculate vertical position
145+
let top = triggerRect.bottom + 8; // 8px gap below trigger
146+
147+
// If position === 'bottom' but there's not enough space below, try above
148+
if (position === 'bottom' && top + 400 > viewportHeight) {
149+
const topPosition = triggerRect.top - 408; // 400px height + 8px gap
150+
if (topPosition > 16) {
151+
top = topPosition;
152+
}
153+
} else if (position === 'top') {
154+
top = Math.max(16, triggerRect.top - 408);
155+
}
156+
157+
// Ensure popover doesn't go above viewport
158+
top = Math.max(16, top);
159+
160+
setPopoverPosition({ top, left, width: popoverWidth });
161+
setHorizontalPosition(horizontalPos);
162+
}, [width, $isMobile, position]);
125163

126-
// Position calculation on layout changes
164+
// Position calculation on layout changes and scroll/resize
127165
useLayoutEffect(() => {
128166
if (state.open) {
129-
calculateHorizontalPosition();
167+
calculatePopoverPosition();
130168
}
131-
}, [state.open, calculateHorizontalPosition]);
169+
}, [state.open, calculatePopoverPosition]);
170+
171+
// Recalculate position on scroll or resize
172+
useEffect(() => {
173+
if (!state.open) return;
174+
175+
const handlePositionUpdate = () => {
176+
if (state.open) {
177+
calculatePopoverPosition();
178+
}
179+
};
180+
181+
window.addEventListener('scroll', handlePositionUpdate, { passive: true });
182+
window.addEventListener('resize', handlePositionUpdate, { passive: true });
183+
184+
return () => {
185+
window.removeEventListener('scroll', handlePositionUpdate);
186+
window.removeEventListener('resize', handlePositionUpdate);
187+
};
188+
}, [state.open, calculatePopoverPosition]);
132189

133190
// Use CSS transition instead of setTimeout
134191
useEffect(() => {
@@ -164,40 +221,47 @@ const PopOver: FunctionComponent<PopOverModel> = ({
164221
{icon || <ChevronDown />}
165222
</span>
166223
</button>
167-
{state.open ? (
168-
<div
169-
ref={popoverRef}
170-
className={[
171-
popoverHolder,
172-
popoverHolderRecipe({
173-
visible: state.isVisible,
174-
position: position === 'bottom' ? 'bottom' : 'top',
175-
leftMobile: !!$isMobile,
176-
}),
177-
].join(' ')}
178-
style={{
179-
...computeCssVarsFromTheme(theme),
180-
width: $isMobile ? '90%' : `${width}px`,
181-
}}
182-
data-position-x={horizontalPosition}
183-
role="dialog"
184-
aria-modal="false"
185-
aria-labelledby={placeholder ? undefined : 'popover-content'}
186-
>
187-
<div className={header}>
188-
<button
189-
className={closeButton}
190-
onClick={closePopover}
191-
type="button"
192-
aria-label="Close menu"
193-
title="Close menu"
194-
>
195-
<CloseIcon />
196-
</button>
197-
</div>
198-
<MemoizedContent>{children}</MemoizedContent>
199-
</div>
200-
) : null}
224+
{state.open &&
225+
ReactDOM.createPortal(
226+
<div
227+
ref={popoverRef}
228+
className={[
229+
popoverHolder,
230+
popoverHolderRecipe({
231+
visible: state.isVisible,
232+
position: position === 'bottom' ? 'bottom' : 'top',
233+
leftMobile: !!$isMobile,
234+
}),
235+
].join(' ')}
236+
style={{
237+
...computeCssVarsFromTheme(theme),
238+
width: `${popoverPosition.width}px`,
239+
position: 'fixed',
240+
zIndex: 99999,
241+
left: `${popoverPosition.left}px`,
242+
top: `${popoverPosition.top}px`,
243+
transform: 'none',
244+
}}
245+
data-position-x={horizontalPosition}
246+
role="dialog"
247+
aria-modal="false"
248+
aria-labelledby={placeholder ? undefined : 'popover-content'}
249+
>
250+
<div className={header}>
251+
<button
252+
className={closeButton}
253+
onClick={closePopover}
254+
type="button"
255+
aria-label="Close menu"
256+
title="Close menu"
257+
>
258+
<CloseIcon />
259+
</button>
260+
</div>
261+
<MemoizedContent>{children}</MemoizedContent>
262+
</div>,
263+
document.body,
264+
)}
201265
</div>
202266
</>
203267
);

0 commit comments

Comments
 (0)