a tool for shared writing and social publishing
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};