Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
24 kB · 730 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731"use client";
import { ColorPicker as SpectrumColorPicker, parseColor, Color, ColorThumb, ColorSlider, Input, ColorField, SliderTrack, ColorSwatch,} from "react-aria-components";import { Checkbox } from "components/Checkbox";import { useMemo, useState } from "react";import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache";import { useColorAttribute } from "components/ThemeManager/useColorAttribute";import { Separator } from "components/Layout";import { onMouseDown } from "src/utils/iosInputMouseDown";import { pickers, setColorAttribute } from "../ThemeSetter";import { ImageInput, ImageSettings } from "./ImagePicker";
import { ColorPicker, thumbStyle } from "./ColorPicker";import { BlockImageSmall } from "components/Icons/BlockImageSmall";import { Replicache } from "replicache";import { CanvasBackgroundPattern } from "components/Canvas";import { Toggle } from "components/Toggle";import { DeleteSmall } from "components/Icons/DeleteSmall";
export const PageThemePickers = (props: { entityID: string; openPicker: pickers; setOpenPicker: (thisPicker: pickers) => void;}) => { let { rep } = useReplicache(); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]);
let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; let primaryValue = useColorAttribute(props.entityID, "theme/primary");
return ( <div 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))]" style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }} > {pageType === "canvas" && ( <> <CanvasBGPatternPicker entityID={props.entityID} rep={rep} />{" "} <hr className="border-border-light w-full" /> </> )} <TextPickers value={primaryValue} setValue={set("theme/primary")} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} /> </div> );};
// Page background picker for subpages - shows Page/Containers color with optional background imageexport const SubpageBackgroundPicker = (props: { entityID: string; openPicker: pickers; setOpenPicker: (p: pickers) => void;}) => { let { rep, rootEntity } = useReplicache(); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]);
let pageValue = useColorAttribute(props.entityID, "theme/card-background"); let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); let rootPageBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); let entityPageBorderHidden = useEntity( props.entityID, "theme/card-border-hidden", ); let pageBorderHidden = (entityPageBorderHidden || rootPageBorderHidden)?.data.value || false; let hasPageBackground = !pageBorderHidden;
// Label is "Page" when page background is visible, "Containers" when hidden let label = hasPageBackground ? "Page" : "Containers";
// If root page border is hidden, only show color picker (no image support) if (!hasPageBackground) { return ( <ColorPicker label={label} helpText={"Affects menus, tooltips and some block backgrounds"} value={pageValue} setValue={set("theme/card-background")} thisPicker="page" openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} alpha /> ); }
return ( <> {pageBGImage && ( <SubpageBackgroundImagePicker entityID={props.entityID} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} setValue={set("theme/card-background")} /> )} <div className="relative"> <ColorPicker label={label} value={pageValue} setValue={set("theme/card-background")} thisPicker="page" openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} alpha /> {!pageBGImage && ( <label className="text-[#969696] hover:cursor-pointer shrink-0 absolute top-0 right-0"> <BlockImageSmall /> <div className="hidden"> <ImageInput entityID={props.entityID} onChange={() => props.setOpenPicker("page-background-image")} card /> </div> </label> )} </div> </> );};
const SubpageBackgroundImagePicker = (props: { entityID: string; openPicker: pickers; setOpenPicker: (p: pickers) => void; setValue: (c: Color) => void;}) => { let { rep } = useReplicache(); let bgImage = useEntity(props.entityID, "theme/card-background-image"); let bgRepeat = useEntity( props.entityID, "theme/card-background-image-repeat", ); let bgColor = useColorAttribute(props.entityID, "theme/card-background"); let bgAlpha = useEntity(props.entityID, "theme/card-background-image-opacity")?.data .value || 1; let alphaColor = useMemo(() => { return parseColor(`rgba(0,0,0,${bgAlpha})`); }, [bgAlpha]); let open = props.openPicker === "page-background-image";
return ( <> <div className="bgPickerColorLabel flex gap-2 items-center"> <button onClick={() => { props.setOpenPicker(open ? "null" : "page-background-image"); }} className="flex gap-2 items-center grow" > <ColorSwatch color={bgColor} className="w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]" style={{ backgroundImage: bgImage?.data.src ? `url(${bgImage.data.src})` : undefined, backgroundPosition: "center", backgroundSize: "cover", }} /> <strong className="text-[#595959]">Page</strong> <div className="italic text-[#8C8C8C]">image</div> </button>
<SpectrumColorPicker value={alphaColor} onChange={(c) => { let alpha = c.getChannelValue("alpha"); rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/card-background-image-opacity", data: { type: "number", value: alpha }, }); }} > <Separator classname="h-4! my-1 border-[#C3C3C3]!" /> <ColorField className="w-fit pl-[6px]" channel="alpha"> <Input onMouseDown={onMouseDown} onFocus={(e) => { e.currentTarget.setSelectionRange( 0, e.currentTarget.value.length - 1, ); }} onKeyDown={(e) => { if (e.key === "Enter") { e.currentTarget.blur(); } else return; }} className="w-[48px] bg-transparent outline-hidden" /> </ColorField> </SpectrumColorPicker>
<div className="flex gap-1 text-[#8C8C8C]"> <button onClick={() => { if (bgImage) rep?.mutate.retractFact({ factID: bgImage.id }); if (bgRepeat) rep?.mutate.retractFact({ factID: bgRepeat.id }); }} > <DeleteSmall /> </button> <label className="hover:cursor-pointer"> <BlockImageSmall /> <div className="hidden"> <ImageInput entityID={props.entityID} onChange={() => props.setOpenPicker("page-background-image")} card /> </div> </label> </div> </div> {open && ( <div className="pageImagePicker flex flex-col gap-2"> <ImageSettings entityID={props.entityID} card setValue={props.setValue} /> <div className="flex flex-col gap-2 pr-2 pl-8 -mt-2 mb-2"> <hr className="border-[#DBDBDB]" /> <SpectrumColorPicker value={alphaColor} onChange={(c) => { let alpha = c.getChannelValue("alpha"); rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/card-background-image-opacity", data: { type: "number", value: alpha }, }); }} > <ColorSlider colorSpace="hsb" className="w-full mt-1 rounded-full" style={{ backgroundImage: `url(/transparent-bg.png)`, backgroundRepeat: "repeat", backgroundSize: "8px", }} channel="alpha" > <SliderTrack className="h-2 w-full rounded-md"> <ColorThumb className={`${thumbStyle} mt-[4px]`} /> </SliderTrack> </ColorSlider> </SpectrumColorPicker> </div> </div> )} </> );};
// Unified background picker for leaflets - matches structure of BackgroundPicker for publicationsexport const LeafletBackgroundPicker = (props: { entityID: string; openPicker: pickers; setOpenPicker: (p: pickers) => void;}) => { let { rep } = useReplicache(); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]);
let leafletBgValue = useColorAttribute( props.entityID, "theme/page-background", ); let pageValue = useColorAttribute(props.entityID, "theme/card-background"); let leafletBGImage = useEntity(props.entityID, "theme/background-image"); let leafletBGRepeat = useEntity( props.entityID, "theme/background-image-repeat", ); let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden"); let hasPageBackground = !pageBorderHidden?.data.value;
// When page background is hidden and no background image, only show the Background picker let showPagePicker = hasPageBackground || !!leafletBGImage;
return ( <> {/* Background color/image picker */} {leafletBGImage ? ( <LeafletBackgroundImagePicker entityID={props.entityID} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} /> ) : ( <div className="relative"> <ColorPicker label="Background" value={leafletBgValue} setValue={set("theme/page-background")} thisPicker="leaflet" openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} /> <label className="text-[#969696] hover:cursor-pointer shrink-0 absolute top-0 right-0"> <BlockImageSmall /> <div className="hidden"> <ImageInput entityID={props.entityID} onChange={() => props.setOpenPicker("leaflet")} /> </div> </label> </div> )}
{/* Page/Containers color picker - only shown when page background is visible OR there's a bg image */} {showPagePicker && ( <ColorPicker label={hasPageBackground ? "Page" : "Containers"} helpText={ hasPageBackground ? undefined : "Affects menus, tooltips and some block backgrounds" } value={pageValue} setValue={set("theme/card-background")} thisPicker="page" openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} alpha /> )}
<hr className="border-[#CCCCCC]" />
{/* Page Background toggle */} <PageBorderHider entityID={props.entityID} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} /> </> );};
const LeafletBackgroundImagePicker = (props: { entityID: string; openPicker: pickers; setOpenPicker: (p: pickers) => void;}) => { let { rep } = useReplicache(); let bgImage = useEntity(props.entityID, "theme/background-image"); let bgRepeat = useEntity(props.entityID, "theme/background-image-repeat"); let bgColor = useColorAttribute(props.entityID, "theme/page-background"); let open = props.openPicker === "leaflet";
return ( <> <div className="bgPickerColorLabel flex gap-2 items-center"> <button onClick={() => { props.setOpenPicker(open ? "null" : "leaflet"); }} className="flex gap-2 items-center grow" > <ColorSwatch color={bgColor} className="w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]" style={{ backgroundImage: bgImage?.data.src ? `url(${bgImage.data.src})` : undefined, backgroundPosition: "center", backgroundSize: "cover", }} /> <strong className="text-[#595959]">Background</strong> <div className="italic text-[#8C8C8C]">image</div> </button> <div className="flex gap-1 text-[#8C8C8C]"> <button onClick={() => { if (bgImage) rep?.mutate.retractFact({ factID: bgImage.id }); if (bgRepeat) rep?.mutate.retractFact({ factID: bgRepeat.id }); }} > <DeleteSmall /> </button> <label className="hover:cursor-pointer"> <BlockImageSmall /> <div className="hidden"> <ImageInput entityID={props.entityID} onChange={() => props.setOpenPicker("leaflet")} /> </div> </label> </div> </div> {open && ( <div className="pageImagePicker flex flex-col gap-2"> <ImageSettings entityID={props.entityID} setValue={() => {}} /> </div> )} </> );};
export const PageBackgroundColorPicker = (props: { disabled?: boolean; label: string; openPicker: pickers; thisPicker: pickers; setOpenPicker: (thisPicker: pickers) => void; setValue: (c: Color) => void; value: Color; alpha?: boolean; helpText?: string;}) => { return ( <ColorPicker disabled={props.disabled} label={props.label} helpText={props.helpText} value={props.value} setValue={props.setValue} thisPicker={"page"} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} alpha={props.alpha} /> );};
export const PageBackgroundImagePicker = (props: { disabled?: boolean; entityID: string; openPicker: pickers; thisPicker: pickers; setOpenPicker: (thisPicker: pickers) => void; closePicker: () => void; setValue: (c: Color) => void; home?: boolean;}) => { let bgImage = useEntity(props.entityID, "theme/card-background-image"); let bgRepeat = useEntity( props.entityID, "theme/card-background-image-repeat", ); let bgColor = useColorAttribute(props.entityID, "theme/card-background"); let bgAlpha = useEntity(props.entityID, "theme/card-background-image-opacity")?.data .value || 1; let alphaColor = useMemo(() => { return parseColor(`rgba(0,0,0,${bgAlpha})`); }, [bgAlpha]); let open = props.openPicker == props.thisPicker; let { rep } = useReplicache();
return ( <> <div className="bgPickerColorLabel flex gap-2 items-center"> <button disabled={props.disabled} onClick={() => { if (props.openPicker === props.thisPicker) { props.setOpenPicker("null"); } else { props.setOpenPicker(props.thisPicker); } }} className="flex gap-2 items-center disabled:text-[#969696]" > <ColorSwatch color={bgColor} className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C] ${props.disabled ? "opacity-50" : ""}`} style={{ backgroundImage: bgImage?.data.src ? `url(${bgImage.data.src})` : undefined, backgroundPosition: "center", backgroundSize: "cover", }} /> <strong className={`${props.disabled ? "text-[#969696]" : " text-[#272727] "}`} > Page </strong> <div className="">Image</div> </button>
<SpectrumColorPicker value={alphaColor} onChange={(c) => { let alpha = c.getChannelValue("alpha"); rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/card-background-image-opacity", data: { type: "number", value: alpha }, }); }} > <Separator classname="h-4! my-1 border-[#C3C3C3]!" /> <ColorField className="w-fit pl-[6px]" channel="alpha"> <Input disabled={props.disabled} onMouseDown={onMouseDown} onFocus={(e) => { e.currentTarget.setSelectionRange( 0, e.currentTarget.value.length - 1, ); }} onKeyDown={(e) => { if (e.key === "Enter") { e.currentTarget.blur(); } else return; }} className={`w-[48px] bg-transparent outline-hidden disabled:text-[#969696]`} /> </ColorField> </SpectrumColorPicker> <div className="flex gap-1 justify-end grow text-[#969696]"> <button onClick={() => { if (bgImage) rep?.mutate.retractFact({ factID: bgImage.id }); if (bgRepeat) rep?.mutate.retractFact({ factID: bgRepeat.id }); }} > <DeleteSmall /> </button> <label> <BlockImageSmall /> <div className="hidden"> <ImageInput entityID={props.entityID} onChange={() => props.setOpenPicker("page-background-image")} card /> </div> </label> </div> </div> {open && ( <div className="pageImagePicker flex flex-col gap-2"> <ImageSettings entityID={props.entityID} card setValue={props.setValue} /> <div className="flex flex-col gap-2 pr-2 pl-8 -mt-2 mb-2"> <hr className="border-[#DBDBDB]" /> <SpectrumColorPicker value={alphaColor} onChange={(c) => { let alpha = c.getChannelValue("alpha"); rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/card-background-image-opacity", data: { type: "number", value: alpha }, }); }} > <ColorSlider colorSpace="hsb" className="w-full mt-1 rounded-full" style={{ backgroundImage: `url(/transparent-bg.png)`, backgroundRepeat: "repeat", backgroundSize: "8px", }} channel="alpha" > <SliderTrack className="h-2 w-full rounded-md"> <ColorThumb className={`${thumbStyle} mt-[4px]`} /> </SliderTrack> </ColorSlider> </SpectrumColorPicker> </div> </div> )} </> );};
const CanvasBGPatternPicker = (props: { entityID: string; rep: Replicache<ReplicacheMutators> | null;}) => { let selectedPattern = useEntity(props.entityID, "canvas/background-pattern") ?.data.value; return ( <div className="flex gap-2 h-8 "> <button className={`w-full rounded-md bg-bg-page border ${selectedPattern === "grid" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} onMouseDown={() => { props.rep && props.rep.mutate.assertFact({ entity: props.entityID, attribute: "canvas/background-pattern", data: { type: "canvas-pattern-union", value: "grid" }, }); }} > <CanvasBackgroundPattern pattern="grid" scale={0.5} /> </button> <button className={`w-full rounded-md bg-bg-page border ${selectedPattern === "dot" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} onMouseDown={() => { props.rep && props.rep.mutate.assertFact({ entity: props.entityID, attribute: "canvas/background-pattern", data: { type: "canvas-pattern-union", value: "dot" }, }); }} > <CanvasBackgroundPattern pattern="dot" scale={0.5} /> </button> <button className={`w-full rounded-md bg-bg-page border ${selectedPattern === "plain" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} onMouseDown={() => { props.rep && props.rep.mutate.assertFact({ entity: props.entityID, attribute: "canvas/background-pattern", data: { type: "canvas-pattern-union", value: "plain" }, }); }} > <CanvasBackgroundPattern pattern="plain" /> </button> </div> );};
export const TextPickers = (props: { openPicker: pickers; setOpenPicker: (thisPicker: pickers) => void; value: Color; setValue: (c: Color) => void;}) => { return ( <ColorPicker label="Text" value={props.value} setValue={props.setValue} thisPicker={"text"} openPicker={props.openPicker} setOpenPicker={props.setOpenPicker} closePicker={() => props.setOpenPicker("null")} /> );};
export const PageBorderHider = (props: { entityID: string; setOpenPicker: (p: pickers) => void; openPicker: pickers;}) => { let { rep, rootEntity } = useReplicache(); let rootPageBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); let entityPageBorderHidden = useEntity( props.entityID, "theme/card-border-hidden", ); let pageBorderHidden = (entityPageBorderHidden || rootPageBorderHidden)?.data.value || false;
function handleToggle() { rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/card-border-hidden", data: { type: "boolean", value: !pageBorderHidden }, });
(pageBorderHidden && props.openPicker === "page") || (props.openPicker === "page-background-image" && props.setOpenPicker("null")); }
return ( <> <Toggle toggle={!pageBorderHidden} onToggle={() => { handleToggle(); }} disabledColor1="#8C8C8C" disabledColor2="#DBDBDB" > <div className="flex gap-2"> <div className="font-bold">Page Background</div> <div className="italic text-[#8C8C8C]"> {pageBorderHidden ? "none" : ""} </div> </div> </Toggle> </> );};