a tool for shared writing and social publishing
at feature/hard_breaks 420 lines 14 kB view raw
1"use client"; 2 3import { 4 ColorPicker as SpectrumColorPicker, 5 parseColor, 6 Color, 7 ColorThumb, 8 ColorSlider, 9 Input, 10 ColorField, 11 SliderTrack, 12 ColorSwatch, 13} from "react-aria-components"; 14import { Checkbox } from "components/Checkbox"; 15import { useMemo, useState } from "react"; 16import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 17import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 18import { Separator } from "components/Layout"; 19import { onMouseDown } from "src/utils/iosInputMouseDown"; 20import { pickers, setColorAttribute } from "../ThemeSetter"; 21import { ImageInput, ImageSettings } from "./ImagePicker"; 22 23import { ColorPicker, thumbStyle } from "./ColorPicker"; 24import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 25import { Replicache } from "replicache"; 26import { CanvasBackgroundPattern } from "components/Canvas"; 27import { Toggle } from "components/Toggle"; 28import { DeleteSmall } from "components/Icons/DeleteSmall"; 29 30export const PageThemePickers = (props: { 31 entityID: string; 32 openPicker: pickers; 33 setOpenPicker: (thisPicker: pickers) => void; 34}) => { 35 let { rep } = useReplicache(); 36 let set = useMemo(() => { 37 return setColorAttribute(rep, props.entityID); 38 }, [rep, props.entityID]); 39 40 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 41 let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 42 43 return ( 44 <div 45 className="pageThemeBG flex flex-col gap-2 h-full text-primary bg-bg-leaflet p-2 rounded-md border border-primary shadow-[0_0_0_1px_rgb(var(--bg-page))]" 46 style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }} 47 > 48 {pageType === "canvas" && ( 49 <> 50 <CanvasBGPatternPicker entityID={props.entityID} rep={rep} />{" "} 51 <hr className="border-border-light w-full" /> 52 </> 53 )} 54 <PageTextPicker 55 value={primaryValue} 56 setValue={set("theme/primary")} 57 openPicker={props.openPicker} 58 setOpenPicker={props.setOpenPicker} 59 /> 60 </div> 61 ); 62}; 63 64export const PageBackgroundPicker = (props: { 65 entityID: string; 66 setValue: (c: Color) => void; 67 openPicker: pickers; 68 setOpenPicker: (p: pickers) => void; 69 home?: boolean; 70}) => { 71 let pageValue = useColorAttribute(props.entityID, "theme/card-background"); 72 let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 73 let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden"); 74 75 return ( 76 <> 77 {pageBGImage && pageBGImage !== null && ( 78 <PageBackgroundImagePicker 79 disabled={pageBorderHidden?.data.value} 80 entityID={props.entityID} 81 thisPicker={"page-background-image"} 82 openPicker={props.openPicker} 83 setOpenPicker={props.setOpenPicker} 84 closePicker={() => props.setOpenPicker("null")} 85 setValue={props.setValue} 86 home={props.home} 87 /> 88 )} 89 <div className="relative"> 90 <PageBackgroundColorPicker 91 label={pageBorderHidden?.data.value ? "Menus" : "Page"} 92 value={pageValue} 93 setValue={props.setValue} 94 thisPicker={"page"} 95 openPicker={props.openPicker} 96 setOpenPicker={props.setOpenPicker} 97 alpha 98 /> 99 {(pageBGImage === null || 100 (!pageBGImage && !pageBorderHidden?.data.value && !props.home)) && ( 101 <label 102 className={` 103 hover:cursor-pointer text-[#969696] shrink-0 104 absolute top-0 right-0 105 `} 106 > 107 <BlockImageSmall /> 108 <div className="hidden"> 109 <ImageInput 110 entityID={props.entityID} 111 onChange={() => props.setOpenPicker("page-background-image")} 112 card 113 /> 114 </div> 115 </label> 116 )} 117 </div> 118 </> 119 ); 120}; 121 122export const PageBackgroundColorPicker = (props: { 123 disabled?: boolean; 124 label: string; 125 openPicker: pickers; 126 thisPicker: pickers; 127 setOpenPicker: (thisPicker: pickers) => void; 128 setValue: (c: Color) => void; 129 value: Color; 130 alpha?: boolean; 131}) => { 132 return ( 133 <ColorPicker 134 disabled={props.disabled} 135 label={props.label} 136 value={props.value} 137 setValue={props.setValue} 138 thisPicker={"page"} 139 openPicker={props.openPicker} 140 setOpenPicker={props.setOpenPicker} 141 closePicker={() => props.setOpenPicker("null")} 142 alpha={props.alpha} 143 /> 144 ); 145}; 146 147export const PageBackgroundImagePicker = (props: { 148 disabled?: boolean; 149 entityID: string; 150 openPicker: pickers; 151 thisPicker: pickers; 152 setOpenPicker: (thisPicker: pickers) => void; 153 closePicker: () => void; 154 setValue: (c: Color) => void; 155 home?: boolean; 156}) => { 157 let bgImage = useEntity(props.entityID, "theme/card-background-image"); 158 let bgRepeat = useEntity( 159 props.entityID, 160 "theme/card-background-image-repeat", 161 ); 162 let bgColor = useColorAttribute(props.entityID, "theme/card-background"); 163 let bgAlpha = 164 useEntity(props.entityID, "theme/card-background-image-opacity")?.data 165 .value || 1; 166 let alphaColor = useMemo(() => { 167 return parseColor(`rgba(0,0,0,${bgAlpha})`); 168 }, [bgAlpha]); 169 let open = props.openPicker == props.thisPicker; 170 let { rep } = useReplicache(); 171 172 return ( 173 <> 174 <div className="bgPickerColorLabel flex gap-2 items-center"> 175 <button 176 disabled={props.disabled} 177 onClick={() => { 178 if (props.openPicker === props.thisPicker) { 179 props.setOpenPicker("null"); 180 } else { 181 props.setOpenPicker(props.thisPicker); 182 } 183 }} 184 className="flex gap-2 items-center disabled:text-[#969696]" 185 > 186 <ColorSwatch 187 color={bgColor} 188 className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C] ${props.disabled ? "opacity-50" : ""}`} 189 style={{ 190 backgroundImage: bgImage?.data.src 191 ? `url(${bgImage.data.src})` 192 : undefined, 193 backgroundPosition: "center", 194 backgroundSize: "cover", 195 }} 196 /> 197 <strong 198 className={`${props.disabled ? "text-[#969696]" : " text-[#272727] "}`} 199 > 200 Page 201 </strong> 202 <div className="">Image</div> 203 </button> 204 205 <SpectrumColorPicker 206 value={alphaColor} 207 onChange={(c) => { 208 let alpha = c.getChannelValue("alpha"); 209 rep?.mutate.assertFact({ 210 entity: props.entityID, 211 attribute: "theme/card-background-image-opacity", 212 data: { type: "number", value: alpha }, 213 }); 214 }} 215 > 216 <Separator classname="h-4! my-1 border-[#C3C3C3]!" /> 217 <ColorField className="w-fit pl-[6px]" channel="alpha"> 218 <Input 219 disabled={props.disabled} 220 onMouseDown={onMouseDown} 221 onFocus={(e) => { 222 e.currentTarget.setSelectionRange( 223 0, 224 e.currentTarget.value.length - 1, 225 ); 226 }} 227 onKeyDown={(e) => { 228 if (e.key === "Enter") { 229 e.currentTarget.blur(); 230 } else return; 231 }} 232 className={`w-[48px] bg-transparent outline-hidden disabled:text-[#969696]`} 233 /> 234 </ColorField> 235 </SpectrumColorPicker> 236 <div className="flex gap-1 justify-end grow text-[#969696]"> 237 <button 238 onClick={() => { 239 if (bgImage) rep?.mutate.retractFact({ factID: bgImage.id }); 240 if (bgRepeat) rep?.mutate.retractFact({ factID: bgRepeat.id }); 241 }} 242 > 243 <DeleteSmall /> 244 </button> 245 <label> 246 <BlockImageSmall /> 247 <div className="hidden"> 248 <ImageInput 249 entityID={props.entityID} 250 onChange={() => props.setOpenPicker("page-background-image")} 251 card 252 /> 253 </div> 254 </label> 255 </div> 256 </div> 257 {open && ( 258 <div className="pageImagePicker flex flex-col gap-2"> 259 <ImageSettings 260 entityID={props.entityID} 261 card 262 setValue={props.setValue} 263 /> 264 <div className="flex flex-col gap-2 pr-2 pl-8 -mt-2 mb-2"> 265 <hr className="border-[#DBDBDB]" /> 266 <SpectrumColorPicker 267 value={alphaColor} 268 onChange={(c) => { 269 let alpha = c.getChannelValue("alpha"); 270 rep?.mutate.assertFact({ 271 entity: props.entityID, 272 attribute: "theme/card-background-image-opacity", 273 data: { type: "number", value: alpha }, 274 }); 275 }} 276 > 277 <ColorSlider 278 colorSpace="hsb" 279 className="w-full mt-1 rounded-full" 280 style={{ 281 backgroundImage: `url(/transparent-bg.png)`, 282 backgroundRepeat: "repeat", 283 backgroundSize: "8px", 284 }} 285 channel="alpha" 286 > 287 <SliderTrack className="h-2 w-full rounded-md"> 288 <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 289 </SliderTrack> 290 </ColorSlider> 291 </SpectrumColorPicker> 292 </div> 293 </div> 294 )} 295 </> 296 ); 297}; 298 299const CanvasBGPatternPicker = (props: { 300 entityID: string; 301 rep: Replicache<ReplicacheMutators> | null; 302}) => { 303 let selectedPattern = useEntity(props.entityID, "canvas/background-pattern") 304 ?.data.value; 305 return ( 306 <div className="flex gap-2 h-8 "> 307 <button 308 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "grid" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 309 onMouseDown={() => { 310 props.rep && 311 props.rep.mutate.assertFact({ 312 entity: props.entityID, 313 attribute: "canvas/background-pattern", 314 data: { type: "canvas-pattern-union", value: "grid" }, 315 }); 316 }} 317 > 318 <CanvasBackgroundPattern pattern="grid" scale={0.5} /> 319 </button> 320 <button 321 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "dot" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 322 onMouseDown={() => { 323 props.rep && 324 props.rep.mutate.assertFact({ 325 entity: props.entityID, 326 attribute: "canvas/background-pattern", 327 data: { type: "canvas-pattern-union", value: "dot" }, 328 }); 329 }} 330 > 331 <CanvasBackgroundPattern pattern="dot" scale={0.5} /> 332 </button> 333 <button 334 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "plain" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 335 onMouseDown={() => { 336 props.rep && 337 props.rep.mutate.assertFact({ 338 entity: props.entityID, 339 attribute: "canvas/background-pattern", 340 data: { type: "canvas-pattern-union", value: "plain" }, 341 }); 342 }} 343 > 344 <CanvasBackgroundPattern pattern="plain" /> 345 </button> 346 </div> 347 ); 348}; 349 350export const PageTextPicker = (props: { 351 openPicker: pickers; 352 setOpenPicker: (thisPicker: pickers) => void; 353 value: Color; 354 setValue: (c: Color) => void; 355}) => { 356 return ( 357 <ColorPicker 358 label="Text" 359 value={props.value} 360 setValue={props.setValue} 361 thisPicker={"text"} 362 openPicker={props.openPicker} 363 setOpenPicker={props.setOpenPicker} 364 closePicker={() => props.setOpenPicker("null")} 365 /> 366 ); 367}; 368 369export const PageBorderHider = (props: { 370 entityID: string; 371 setOpenPicker: (p: pickers) => void; 372 openPicker: pickers; 373}) => { 374 let { rep, rootEntity } = useReplicache(); 375 let rootPageBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); 376 let entityPageBorderHidden = useEntity( 377 props.entityID, 378 "theme/card-border-hidden", 379 ); 380 let pageBorderHidden = 381 (entityPageBorderHidden || rootPageBorderHidden)?.data.value || false; 382 383 function handleToggle() { 384 rep?.mutate.assertFact({ 385 entity: props.entityID, 386 attribute: "theme/card-border-hidden", 387 data: { type: "boolean", value: !pageBorderHidden }, 388 }); 389 390 (pageBorderHidden && props.openPicker === "page") || 391 (props.openPicker === "page-background-image" && 392 props.setOpenPicker("null")); 393 } 394 395 return ( 396 <> 397 <div className="flex gap-2 items-center"> 398 <Toggle 399 toggleOn={!pageBorderHidden} 400 setToggleOn={() => { 401 handleToggle(); 402 }} 403 disabledColor1="#8C8C8C" 404 disabledColor2="#DBDBDB" 405 /> 406 <button 407 className="flex gap-2 items-center" 408 onClick={() => { 409 handleToggle(); 410 }} 411 > 412 <div className="font-bold">Page Background</div> 413 <div className="italic text-[#8C8C8C]"> 414 {pageBorderHidden ? "hidden" : ""} 415 </div> 416 </button> 417 </div> 418 </> 419 ); 420};