a tool for shared writing and social publishing
at feature/fonts 214 lines 6.4 kB view raw
1"use client"; 2 3import { Color } from "react-aria-components"; 4import { Input } from "components/Input"; 5import { useState } from "react"; 6import { useEntity, useReplicache } from "src/replicache"; 7import { Menu } from "components/Menu"; 8import { pickers } from "../ThemeSetter"; 9import { ColorPicker } from "./ColorPicker"; 10import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 11import { useIsMobile } from "src/hooks/isMobile"; 12import { 13 fonts, 14 defaultFontId, 15 FontConfig, 16 isCustomFontId, 17 parseGoogleFontInput, 18 createCustomFontId, 19 getFontConfig, 20} from "src/fonts"; 21 22export const TextColorPicker = (props: { 23 openPicker: pickers; 24 setOpenPicker: (thisPicker: pickers) => void; 25 value: Color; 26 setValue: (c: Color) => void; 27}) => { 28 return ( 29 <ColorPicker 30 label="Text" 31 value={props.value} 32 setValue={props.setValue} 33 thisPicker={"text"} 34 openPicker={props.openPicker} 35 setOpenPicker={props.setOpenPicker} 36 closePicker={() => props.setOpenPicker("null")} 37 /> 38 ); 39}; 40 41type FontAttribute = "theme/heading-font" | "theme/body-font"; 42 43export const FontPicker = (props: { 44 label: string; 45 entityID: string; 46 attribute: FontAttribute; 47}) => { 48 let isMobile = useIsMobile(); 49 let { rep } = useReplicache(); 50 let [showCustomInput, setShowCustomInput] = useState(false); 51 let [customFontValue, setCustomFontValue] = useState(""); 52 let currentFont = useEntity(props.entityID, props.attribute); 53 let fontId = currentFont?.data.value || defaultFontId; 54 let font = getFontConfig(fontId); 55 let isCustom = isCustomFontId(fontId); 56 57 let fontList = Object.values(fonts).sort((a, b) => 58 a.displayName.localeCompare(b.displayName), 59 ); 60 61 const handleCustomSubmit = () => { 62 const parsed = parseGoogleFontInput(customFontValue); 63 if (parsed) { 64 const customId = createCustomFontId( 65 parsed.fontName, 66 parsed.googleFontsFamily, 67 ); 68 rep?.mutate.assertFact({ 69 entity: props.entityID, 70 attribute: props.attribute, 71 data: { type: "string", value: customId }, 72 }); 73 setShowCustomInput(false); 74 setCustomFontValue(""); 75 } 76 }; 77 78 return ( 79 <Menu 80 asChild 81 trigger={ 82 <button className="flex gap-2 items-center w-full !outline-none min-w-0"> 83 <div 84 className={`w-6 h-6 rounded-md border border-border relative text-sm bg-bg-page shrink-0 ${props.label === "Heading" ? "font-bold" : "text-secondary"}`} 85 > 86 <div className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 "> 87 Aa 88 </div> 89 </div> 90 <div className="font-bold shrink-0">{props.label}</div> 91 <div className="truncate">{font.displayName}</div> 92 </button> 93 } 94 side={isMobile ? "bottom" : "right"} 95 align="start" 96 className="w-[250px] !gap-0 !outline-none max-h-72 " 97 > 98 {showCustomInput ? ( 99 <div className="p-2 flex flex-col gap-2"> 100 <div className="text-sm text-secondary"> 101 Paste a Google Font name 102 </div> 103 <Input 104 value={customFontValue} 105 className="w-full" 106 placeholder="e.g. Roboto, Open Sans, Playfair Display" 107 autoFocus 108 onChange={(e) => setCustomFontValue(e.currentTarget.value)} 109 onKeyDown={(e) => { 110 if (e.key === "Enter") { 111 e.preventDefault(); 112 handleCustomSubmit(); 113 } else if (e.key === "Escape") { 114 setShowCustomInput(false); 115 setCustomFontValue(""); 116 } 117 }} 118 /> 119 <div className="flex gap-2"> 120 <button 121 className="flex-1 px-2 py-1 text-sm rounded-md bg-accent-1 text-accent-2 hover:opacity-80" 122 onClick={handleCustomSubmit} 123 > 124 Add Font 125 </button> 126 <button 127 className="px-2 py-1 text-sm rounded-md text-secondary hover:bg-border-light" 128 onClick={() => { 129 setShowCustomInput(false); 130 setCustomFontValue(""); 131 }} 132 > 133 Cancel 134 </button> 135 </div> 136 </div> 137 ) : ( 138 <div className="flex flex-col h-full overflow-auto gap-0 py-1"> 139 {fontList.map((fontOption) => { 140 return ( 141 <FontOption 142 key={fontOption.id} 143 onSelect={() => { 144 rep?.mutate.assertFact({ 145 entity: props.entityID, 146 attribute: props.attribute, 147 data: { type: "string", value: fontOption.id }, 148 }); 149 }} 150 font={fontOption} 151 selected={fontOption.id === fontId} 152 /> 153 ); 154 })} 155 {isCustom && ( 156 <FontOption 157 key={fontId} 158 onSelect={() => {}} 159 font={font} 160 selected={true} 161 /> 162 )} 163 <hr className="mx-2 my-1 border-border" /> 164 <DropdownMenu.Item 165 onSelect={(e) => { 166 e.preventDefault(); 167 setShowCustomInput(true); 168 }} 169 className={` 170 fontOption 171 z-10 px-1 py-0.5 172 text-left text-secondary 173 data-[highlighted]:bg-border-light data-[highlighted]:text-secondary 174 hover:bg-border-light hover:text-secondary 175 outline-none 176 cursor-pointer 177 `} 178 > 179 <div className="px-2 py-0 rounded-md">Custom Google Font...</div> 180 </DropdownMenu.Item> 181 </div> 182 )} 183 </Menu> 184 ); 185}; 186 187const FontOption = (props: { 188 onSelect: () => void; 189 font: FontConfig; 190 selected: boolean; 191}) => { 192 return ( 193 <DropdownMenu.RadioItem 194 value={props.font.id} 195 onSelect={props.onSelect} 196 className={` 197 fontOption 198 z-10 px-1 py-0.5 199 text-left text-secondary 200 data-[highlighted]:bg-border-light data-[highlighted]:text-secondary 201 hover:bg-border-light hover:text-secondary 202 outline-none 203 cursor-pointer 204 205 `} 206 > 207 <div 208 className={`px-2 py-0 rounded-md ${props.selected && "bg-accent-1 text-accent-2"}`} 209 > 210 {props.font.displayName} 211 </div> 212 </DropdownMenu.RadioItem> 213 ); 214};