diff --git a/CLAUDE.md b/CLAUDE.md index dd3521e7..a2b07439 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,3 +71,4 @@ To add or modify a lexicon: - **React contexts**: `DocumentProvider`, `LeafletContentProvider` for page-level data - **Inngest functions**: Async jobs in `app/api/inngest/functions/` - **Icons**: Icon components live in `components/Icons/`. Each icon is a named export in its own file (e.g. `RefreshSmall.tsx`), imports `Props` from `./Props`, spreads `{...props}` on the `` element, and uses `fill="currentColor"` instead of hardcoded colors like `fill="black"`. +- **Popovers and menus**: Use the existing `Popover` (`components/Popover`), `Menu`, and `MenuItem` (`components/Menu`) components — do not create new popover/menu primitives diff --git a/components/Menu.tsx b/components/Menu.tsx index 7a2e1fbd..721283dc 100644 --- a/components/Menu.tsx +++ b/components/Menu.tsx @@ -74,6 +74,42 @@ export const Menu = (props: { ); }; +export const RadioMenuGroup = (props: { + value: string; + onValueChange?: (value: string) => void; + children: React.ReactNode; +}) => { + return ( + + {props.children} + + ); +}; + +export const RadioMenuItem = (props: { + value: string; + children?: React.ReactNode; + className?: string; + onSelect?: (e: Event) => void; + selected?: boolean; +}) => { + return ( + + {props.children} + + ); +}; + export const MenuItem = (props: { children?: React.ReactNode; className?: string; diff --git a/components/Popover/index.tsx b/components/Popover/index.tsx index ed259f8c..cf0cc92f 100644 --- a/components/Popover/index.tsx +++ b/components/Popover/index.tsx @@ -64,6 +64,7 @@ export const Popover = (props: { width={16} height={8} viewBox="0 0 16 8" + className="z-30" > void; }) => { let isMobile = useIsMobile(); + let smoker = useSmoker(); + let inputWrapperRef = useRef(null); let [showCustomInput, setShowCustomInput] = useState(false); let [customFontValue, setCustomFontValue] = useState(""); let fontId = props.value || defaultFontId; @@ -31,9 +35,28 @@ export const FontPicker = (props: { a.displayName.localeCompare(b.displayName), ); - const handleCustomSubmit = () => { + let [loading, setLoading] = useState(false); + + const handleCustomSubmit = async () => { const parsed = parseGoogleFontInput(customFontValue); - if (parsed) { + if (!parsed) return; + + setLoading(true); + try { + const url = `https://fonts.googleapis.com/css2?family=${parsed.googleFontsFamily}&display=swap`; + const res = await fetch(url, { method: "HEAD" }); + if (!res.ok) { + const rect = inputWrapperRef.current?.getBoundingClientRect(); + smoker({ + error: true, + position: { + x: rect ? rect.left + rect.width / 2 : 0, + y: rect ? rect.top - 8 : 0, + }, + text: "No font found!", + }); + return; + } const customId = createCustomFontId( parsed.fontName, parsed.googleFontsFamily, @@ -41,6 +64,18 @@ export const FontPicker = (props: { props.onChange(customId); setShowCustomInput(false); setCustomFontValue(""); + } catch { + const rect = inputWrapperRef.current?.getBoundingClientRect(); + smoker({ + error: true, + position: { + x: rect ? rect.left + rect.width / 2 : 0, + y: rect ? rect.top - 8 : 0, + }, + text: "No font found!", + }); + } finally { + setLoading(false); } }; @@ -62,118 +97,95 @@ export const FontPicker = (props: { } side={isMobile ? "bottom" : "right"} align="start" - className="w-[250px] !gap-0 !outline-none max-h-72 " + className="w-fit !gap-0 !outline-none max-h-72 " > {showCustomInput ? (
-
- Paste a Google Font name +
+
+ Paste any Google Font name +
+
This is case sensitive
- setCustomFontValue(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - handleCustomSubmit(); - } else if (e.key === "Escape") { - setShowCustomInput(false); - setCustomFontValue(""); - } - }} - /> -
- +
+ setCustomFontValue(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleCustomSubmit(); + } else if (e.key === "Escape") { + setShowCustomInput(false); + setCustomFontValue(""); + } + }} + /> +
+
+ + {loading ? : "Add Font"} +
) : (
- {fontList.map((fontOption) => { - return ( - { - props.onChange(fontOption.id); - }} - font={fontOption} - selected={fontOption.id === fontId} - /> - ); - })} - {isCustom && ( - {}} - font={font} - selected={true} - /> - )} + + {fontList.map((fontOption) => { + return ( + + ); + })} + {isCustom && ( + + )} +
- { e.preventDefault(); setShowCustomInput(true); }} - className={` - fontOption - z-10 px-1 py-0.5 - text-left text-secondary - data-[highlighted]:bg-border-light data-[highlighted]:text-secondary - hover:bg-border-light hover:text-secondary - outline-none - cursor-pointer - `} > -
Custom Google Font...
-
+ Add a Custom Font +
)} ); }; -const FontOption = (props: { - onSelect: () => void; - font: FontConfig; - selected: boolean; -}) => { +const FontOption = (props: { font: FontConfig; selected: boolean }) => { return ( - -
- {props.font.displayName} -
-
+ {props.font.displayName} + ); };