Skip to content

Commit 9d9143f

Browse files
authored
feat(reader): support custom fonts (#161)
* query local font * load font on focus * use datalist to show font lists * style is not supported * fix: dropdown list flash * preload fonts on mouse enter * add placeholder
1 parent 56980a3 commit 9d9143f

4 files changed

Lines changed: 49 additions & 11 deletions

File tree

apps/reader/src/components/Form.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type TextFieldProps<T extends ElementType> = PolymorphicPropsWithoutRef<
2626
hideLabel?: boolean
2727
autoFocus?: boolean
2828
actions?: Action[]
29+
datalist?: React.ReactNode[]
2930
onClear?: () => void
3031
// https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forward_and_create_ref/#generic-forwardrefs
3132
mRef?: RefObject<HTMLInputElement> | null
@@ -39,13 +40,15 @@ export function TextField<T extends ElementType = 'input'>({
3940
hideLabel = false,
4041
autoFocus,
4142
actions = [],
43+
datalist,
4244
onClear,
4345
mRef: outerRef,
4446
...props
4547
}: TextFieldProps<T>) {
4648
const Component = as || 'input'
4749
const isInput = Component === 'input'
4850
const innerRef = useRef<HTMLInputElement>(null)
51+
const datalistId = `${name}-datalist` // TODO: use `useId`
4952
const ref = outerRef || innerRef
5053
const mobile = useMobile()
5154
const t = useTranslation()
@@ -83,8 +86,10 @@ export function TextField<T extends ElementType = 'input'>({
8386
'typescale-body-medium text-on-surface-variant placeholder:text-outline/60 w-0 flex-1 bg-transparent py-1 px-1.5 !text-[13px]',
8487
isInput || 'scroll h-full resize-none',
8588
)}
89+
{...(datalist && { list: datalistId })}
8690
{...props}
8791
/>
92+
{datalist && <datalist id={datalistId}>{datalist}</datalist>}
8893
{!!actions.length && (
8994
<div className="mx-1 flex gap-0.5">
9095
{actions.map(({ onClick, ...a }) => (

apps/reader/src/components/viewlets/TypographyView.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ import { keys } from '@flow/reader/utils'
1515
import { Select, TextField, TextFieldProps } from '../Form'
1616
import { PaneViewProps, PaneView, Pane } from '../base'
1717

18+
// Define an interface for the Font object
19+
1820
enum TypographyScope {
1921
Book,
2022
Global,
2123
}
2224

23-
const typefaces = ['default', 'sans-serif', 'serif']
24-
2525
export const TypographyView: React.FC<PaneViewProps> = (props) => {
2626
const { focusedBookTab } = useReaderSnapshot()
2727
const [settings, setSettings] = useSettings()
2828
const [scope, setScope] = useState(TypographyScope.Book)
2929
const t = useTranslation('typography')
3030

31+
const [localFonts, setLocalFonts] = useState<string[]>()
32+
3133
const { fontFamily, fontSize, fontWeight, lineHeight, zoom, spread } =
3234
scope === TypographyScope.Book
3335
? focusedBookTab?.book.configuration?.typography ?? defaultSettings
@@ -58,6 +60,22 @@ export const TypographyView: React.FC<PaneViewProps> = (props) => {
5860
[scope, setSettings],
5961
)
6062

63+
const queryLocalFonts = useCallback(async () => {
64+
if (localFonts) return
65+
if (!('queryLocalFonts' in window)) {
66+
console.error('queryLocalFonts is not available')
67+
return
68+
}
69+
70+
try {
71+
const fonts = await window.queryLocalFonts()
72+
const uniqueFonts = Array.from(new Set(fonts.map((f) => f.family)))
73+
setLocalFonts(uniqueFonts)
74+
} catch (error) {
75+
console.error('Error querying local fonts:', error)
76+
}
77+
}, [localFonts])
78+
6179
return (
6280
<PaneView {...props}>
6381
<div className="typescale-body-medium flex gap-2 px-5 pb-2 !text-[13px]">
@@ -96,19 +114,26 @@ export const TypographyView: React.FC<PaneViewProps> = (props) => {
96114
{t('page_view.double_page')}
97115
</option>
98116
</Select>
99-
<Select
117+
<TextField
118+
as="input"
100119
name={t('font_family')}
101120
value={fontFamily}
121+
placeholder="default"
122+
// Tips: Datalist only appears on mouse click or keyboard input.
123+
// Does not show when focused via Tab/focus() or triggered by click()
124+
datalist={localFonts?.map((font) => (
125+
<option key={font} value={font}>
126+
{font}
127+
</option>
128+
))}
129+
onFocus={queryLocalFonts}
130+
// Preload fonts to ensure `localFonts` is available on first mouse click.
131+
// Without preloading, datalist dropdown will be empty for the first mouse click.
132+
onMouseEnter={queryLocalFonts}
102133
onChange={(e) => {
103134
setTypography('fontFamily', e.target.value)
104135
}}
105-
>
106-
{typefaces.map((t) => (
107-
<option key={t} value={t} style={{ fontFamily: t }}>
108-
{t}
109-
</option>
110-
))}
111-
</Select>
136+
/>
112137
<NumberField
113138
name={t('font_size')}
114139
min={14}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ interface LaunchQueue {
99
setConsumer(consumer: (launchParams: LaunchParams) => any): void
1010
}
1111

12+
interface LocalFont {
13+
family: string
14+
fullName: string
15+
postscriptName: string
16+
style: string
17+
}
18+
1219
interface Window {
1320
launchQueue: LaunchQueue
21+
queryLocalFonts: () => Promise<LocalFont[]>
1422
}

apps/reader/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
"compilerOptions": {
44
"experimentalDecorators": true
55
},
6-
"include": ["next-env.d.ts", "web.d.ts", "**/*.ts", "**/*.tsx"],
6+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
77
"exclude": ["node_modules"]
88
}

0 commit comments

Comments
 (0)