first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import {useTrans} from '@common/i18n/use-trans';
import {TextField} from '@common/ui/forms/input-field/text-field/text-field';
import {SearchIcon} from '@common/icons/material/Search';
import {message} from '@common/i18n/message';
import {Select} from '@common/ui/forms/select/select';
import {Item} from '@common/ui/forms/listbox/item';
import {Trans} from '@common/i18n/trans';
import React from 'react';
import {FontSelectorState} from '@common/ui/font-selector/font-selector-state';
export interface FontSelectorFilterValue {
query: string;
category: string;
}
interface FiltersHeaderProps {
state: FontSelectorState;
}
export function FontSelectorFilters({
state: {filters, setFilters},
}: FiltersHeaderProps) {
const {trans} = useTrans();
return (
<div className="mb-24 items-center gap-24 @xs:flex">
<TextField
className="mb-12 flex-auto @xs:mb-0"
value={filters.query}
onChange={e => {
setFilters({
...filters,
query: e.target.value,
});
}}
startAdornment={<SearchIcon />}
placeholder={trans(message('Search fonts'))}
/>
<Select
className="flex-auto"
selectionMode="single"
selectedValue={filters.category}
onSelectionChange={value => {
setFilters({
...filters,
category: value as string,
});
}}
>
<Item value="">
<Trans message="All categories" />
</Item>
<Item value="serif">
<Trans message="Serif" />
</Item>
<Item value="sans-serif">
<Trans message="Sans serif" />
</Item>
<Item value="display">
<Trans message="Display" />
</Item>
<Item value="handwriting">
<Trans message="Handwriting" />
</Item>
<Item value="monospace">
<Trans message="Monospace" />
</Item>
</Select>
</div>
);
}

View File

@@ -0,0 +1,50 @@
import {Trans} from '@common/i18n/trans';
import {IconButton} from '@common/ui/buttons/icon-button';
import {KeyboardArrowLeftIcon} from '@common/icons/material/KeyboardArrowLeft';
import {KeyboardArrowRightIcon} from '@common/icons/material/KeyboardArrowRight';
import React from 'react';
import {FontSelectorState} from '@common/ui/font-selector/font-selector-state';
interface FontSelectorPaginationProps {
state: FontSelectorState;
}
export function FontSelectorPagination({
state: {currentPage = 0, setCurrentPage, filteredFonts, pages},
}: FontSelectorPaginationProps) {
const total = filteredFonts?.length || 0;
return (
<div className="flex items-center justify-end gap-24 text-sm mt-30 pt-14 border-t">
{total > 0 && (
<div>
<Trans
message=":from - :to of :total"
values={{
from: currentPage * 20 + 1,
to: Math.min((currentPage + 1) * 20, total),
total,
}}
/>
</div>
)}
<div className="text-muted">
<IconButton
disabled={currentPage < 1}
onClick={() => {
setCurrentPage(Math.max(0, currentPage - 1));
}}
>
<KeyboardArrowLeftIcon />
</IconButton>
<IconButton
disabled={currentPage >= pages.length - 1}
onClick={() => {
setCurrentPage(currentPage + 1);
}}
>
<KeyboardArrowRightIcon />
</IconButton>
</div>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import {useCallback, useEffect, useMemo, useState} from 'react';
import {FontSelectorFilterValue} from '@common/ui/font-selector/font-selector-filters';
import {FontConfig, useValueLists} from '@common/http/value-lists';
import {useFilter} from '@common/i18n/use-filter';
import {BrowserSafeFonts} from '@common/ui/font-picker/browser-safe-fonts';
import {chunkArray} from '@common/utils/array/chunk-array';
import {loadFonts} from '@common/ui/font-picker/load-fonts';
export interface FontSelectorState extends UseFontSelectorProps {
fonts: FontConfig[];
filteredFonts: FontConfig[];
pages: FontConfig[][];
isLoading: boolean;
filters: FontSelectorFilterValue;
setFilters: (filters: FontSelectorFilterValue) => void;
currentPage: number;
setCurrentPage: (page: number) => void;
}
export interface UseFontSelectorProps {
value?: FontConfig;
onChange: (value: FontConfig) => void;
}
export function useFontSelectorState({
value,
onChange,
}: UseFontSelectorProps): FontSelectorState {
const {data, isLoading} = useValueLists(['googleFonts']);
const [currentPage, setCurrentPage] = useState(0);
const [filters, setFilterState] = useState<FontSelectorFilterValue>({
query: '',
category: value?.category ?? '',
});
const {contains} = useFilter({
sensitivity: 'base',
});
const setFilters = useCallback((filters: FontSelectorFilterValue) => {
setFilterState(filters);
// reset to first page when searching or changing category
setCurrentPage(0);
}, []);
const allFonts = useMemo(() => {
return BrowserSafeFonts.concat(data?.googleFonts ?? []);
}, [data?.googleFonts]);
const filteredFonts = useMemo(() => {
return allFonts.filter(font => {
return (
contains(font.family, filters.query) &&
(!filters.category ||
font.category?.toLowerCase() === filters.category.toLowerCase())
);
});
}, [allFonts, filters, contains]);
const pages = useMemo(() => {
return chunkArray(filteredFonts, 20);
}, [filteredFonts]);
const fonts = pages[currentPage];
useEffect(() => {
const id = 'font-selector';
if (fonts?.length) {
loadFonts(fonts, {id});
}
}, [fonts, currentPage]);
return {
fonts: fonts || [],
currentPage,
filteredFonts: filteredFonts || [],
setCurrentPage,
isLoading,
filters,
setFilters,
value,
onChange,
pages,
};
}

View File

@@ -0,0 +1,125 @@
import {Trans} from '@common/i18n/trans';
import React from 'react';
import {ButtonBase} from '@common/ui/buttons/button-base';
import clsx from 'clsx';
import {IllustratedMessage} from '@common/ui/images/illustrated-message';
import fontImage from './font.svg';
import {SvgImage} from '@common/ui/images/svg-image/svg-image';
import {FontSelectorFilters} from '@common/ui/font-selector/font-selector-filters';
import {
FontSelectorState,
UseFontSelectorProps,
useFontSelectorState,
} from '@common/ui/font-selector/font-selector-state';
import {FontSelectorPagination} from '@common/ui/font-selector/font-selector-pagination';
import {FontConfig} from '@common/http/value-lists';
import {Skeleton} from '@common/ui/skeleton/skeleton';
import {AnimatePresence, m} from 'framer-motion';
import {opacityAnimation} from '@common/ui/animation/opacity-animation';
interface FontSelectorProps extends UseFontSelectorProps {
className?: string;
}
export function FontSelector(props: FontSelectorProps) {
const state = useFontSelectorState(props);
return (
<div className={props.className}>
<FontSelectorFilters state={state} />
<AnimatePresence initial={false} mode="wait">
<FontList state={state} />
</AnimatePresence>
<FontSelectorPagination state={state} />
</div>
);
}
interface FontListProps {
state: FontSelectorState;
}
function FontList({state}: FontListProps) {
const {isLoading, fonts} = state;
const gridClassName =
'grid gap-24 grid-cols-[repeat(auto-fill,minmax(90px,1fr))] items-start';
if (isLoading) {
return <FontListSkeleton className={gridClassName} />;
}
if (!fonts?.length) {
return (
<IllustratedMessage
className="mt-60"
size="sm"
image={<SvgImage src={fontImage} />}
title={<Trans message="No matching fonts" />}
description={
<Trans message="Try another search query or different category" />
}
/>
);
}
return (
<m.div key="font-list" {...opacityAnimation} className={gridClassName}>
{fonts?.map(font => (
<FontButton key={font.family} font={font} state={state} />
))}
</m.div>
);
}
interface FontButtonProps {
font: FontConfig;
state: FontSelectorState;
}
function FontButton({font, state: {value, onChange}}: FontButtonProps) {
const isActive = value?.family === font.family;
const displayName = font.family.split(',')[0].replace(/"/g, '');
return (
<ButtonBase
key={font.family}
display="block"
onClick={() => {
onChange(font);
}}
>
<span
className={clsx(
'flex aspect-square items-center justify-center rounded-panel border text-4xl transition-bg-color hover:bg-hover md:text-5xl',
isActive && 'ring-2 ring-primary ring-offset-2',
)}
>
<span style={{fontFamily: font.family}}>Aa</span>
</span>
<span
className={clsx(
'mt-6 block overflow-hidden overflow-ellipsis whitespace-nowrap text-sm',
isActive && 'text-primary',
)}
>
{font.label ? <Trans {...font.label} /> : displayName}
</span>
</ButtonBase>
);
}
interface FontListSkeletonProps {
className: string;
}
function FontListSkeleton({className}: FontListSkeletonProps) {
const items = Array.from(Array(20).keys());
return (
<m.div key="font-list-skeleton" {...opacityAnimation} className={className}>
{items.map(index => (
<div key={index}>
<div className="aspect-square">
<Skeleton display="block" variant="rect" />
</div>
<Skeleton className="mt-6 text-sm" />
</div>
))}
</m.div>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB