a tool for shared writing and social publishing

refactoring our theme setting modal it was such a mess

+1097 -983
+19 -7
components/Blocks/PageLinkBlock.tsx
··· 138 138 export function PagePreview(props: { entityID: string }) { 139 139 let blocks = useBlocks(props.entityID); 140 140 let previewRef = useRef<HTMLDivElement | null>(null); 141 + let { rootEntity } = useReplicache(); 141 142 142 - let cardBackgroundImage = useEntity( 143 - props.entityID, 143 + let rootBackgroundImage = useEntity( 144 + rootEntity, 144 145 "theme/card-background-image", 145 146 ); 146 - let cardBackgroundImageRepeat = useEntity( 147 - props.entityID, 147 + let rootBackgroundRepeat = useEntity( 148 + rootEntity, 148 149 "theme/card-background-image-repeat", 150 + ); 151 + let rootBackgroundOpacity = useEntity( 152 + rootEntity, 153 + "theme/card-background-image-opacity", 149 154 ); 150 155 156 + let cardBackgroundImage = 157 + useEntity(props.entityID, "theme/card-background-image") || 158 + rootBackgroundImage; 159 + let cardBackgroundImageRepeat = 160 + useEntity(props.entityID, "theme/card-background-image-repeat") || 161 + rootBackgroundRepeat; 151 162 let cardBackgroundImageOpacity = 152 163 useEntity(props.entityID, "theme/card-background-image-opacity")?.data 153 - .value || 1; 154 - 164 + .value || 165 + rootBackgroundOpacity?.data.value || 166 + 1; 155 167 let pageWidth = `var(--page-width-unitless)`; 156 168 return ( 157 169 <div ··· 168 180 }} 169 181 > 170 182 <div 171 - className={`pageBackground 183 + className={`pageLinkBlockBackground 172 184 absolute top-0 left-0 right-0 bottom-0 173 185 pointer-events-none 174 186 `}
+19 -5
components/Pages/index.tsx
··· 161 161 }; 162 162 163 163 const DocContent = (props: { entityID: string }) => { 164 + let { rootEntity } = useReplicache(); 164 165 let isFocused = useUIState((s) => { 165 166 let focusedElement = s.focusedEntity; 166 167 let focusedPageID = ··· 169 170 : focusedElement?.parent; 170 171 return focusedPageID === props.entityID; 171 172 }); 172 - let cardBackgroundImage = useEntity( 173 - props.entityID, 173 + let rootBackgroundImage = useEntity( 174 + rootEntity, 174 175 "theme/card-background-image", 175 176 ); 176 - let cardBackgroundImageRepeat = useEntity( 177 - props.entityID, 177 + let rootBackgroundRepeat = useEntity( 178 + rootEntity, 178 179 "theme/card-background-image-repeat", 179 180 ); 181 + let rootBackgroundOpacity = useEntity( 182 + rootEntity, 183 + "theme/card-background-image-opacity", 184 + ); 185 + 186 + let cardBackgroundImage = 187 + useEntity(props.entityID, "theme/card-background-image") || 188 + rootBackgroundImage; 189 + let cardBackgroundImageRepeat = 190 + useEntity(props.entityID, "theme/card-background-image-repeat") || 191 + rootBackgroundRepeat; 180 192 let cardBackgroundImageOpacity = 181 193 useEntity(props.entityID, "theme/card-background-image-opacity")?.data 182 - .value || 1; 194 + .value || 195 + rootBackgroundOpacity?.data.value || 196 + 1; 183 197 return ( 184 198 <> 185 199 <div
+6 -3
components/Popover.tsx
··· 16 16 open?: boolean; 17 17 onOpenChange?: (open: boolean) => void; 18 18 asChild?: boolean; 19 + arrowFill?: string; 19 20 }) => { 20 21 let [open, setOpen] = useState(props.open || false); 21 22 return ( ··· 55 56 > 56 57 <PopoverArrow 57 58 arrowFill={ 58 - props.background 59 - ? props.background 60 - : theme.colors["bg-page"] 59 + props.arrowFill 60 + ? props.arrowFill 61 + : props.background 62 + ? props.background 63 + : theme.colors["bg-page"] 61 64 } 62 65 arrowStroke={ 63 66 props.border ? props.border : theme.colors["border"]
+54
components/ThemeManager/AccentThemePickers.tsx
··· 1 + "use client"; 2 + 3 + import { useMemo } from "react"; 4 + import { pickers, setColorAttribute } from "./ThemeSetter"; 5 + import { ColorPicker } from "./ColorPicker"; 6 + import { useReplicache } from "src/replicache"; 7 + import { useColorAttribute } from "./useColorAttribute"; 8 + 9 + export const AccentThemePickers = (props: { 10 + entityID: string; 11 + openPicker: pickers; 12 + setOpenPicker: (thisPicker: pickers) => void; 13 + }) => { 14 + let { rep } = useReplicache(); 15 + let set = useMemo(() => { 16 + return setColorAttribute(rep, props.entityID); 17 + }, [rep, props.entityID]); 18 + 19 + let accent1Value = useColorAttribute( 20 + props.entityID, 21 + "theme/accent-background", 22 + ); 23 + let accent2Value = useColorAttribute(props.entityID, "theme/accent-text"); 24 + 25 + return ( 26 + <> 27 + <div 28 + className="themeLeafletControls text-accent-2 flex flex-col gap-2 h-full bg-bg-leaflet p-2 rounded-md border border-accent-2 shadow-[0_0_0_1px_rgb(var(--accent-1))]" 29 + style={{ 30 + backgroundColor: "rgba(var(--accent-contrast), 0.5)", 31 + }} 32 + > 33 + <ColorPicker 34 + label="Accent" 35 + value={accent1Value} 36 + setValue={set("theme/accent-background")} 37 + thisPicker={"accent-1"} 38 + openPicker={props.openPicker} 39 + setOpenPicker={props.setOpenPicker} 40 + closePicker={() => props.setOpenPicker("null")} 41 + /> 42 + <ColorPicker 43 + label="Text on Accent" 44 + value={accent2Value} 45 + setValue={set("theme/accent-text")} 46 + thisPicker={"accent-2"} 47 + openPicker={props.openPicker} 48 + setOpenPicker={props.setOpenPicker} 49 + closePicker={() => props.setOpenPicker("null")} 50 + /> 51 + </div> 52 + </> 53 + ); 54 + };
+148
components/ThemeManager/ColorPicker.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + ColorPicker as SpectrumColorPicker, 5 + parseColor, 6 + Color, 7 + ColorArea, 8 + ColorThumb, 9 + ColorSlider, 10 + Input, 11 + ColorField, 12 + SliderTrack, 13 + ColorSwatch, 14 + } from "react-aria-components"; 15 + import { pickers } from "./ThemeSetter"; 16 + import { Separator } from "components/Layout"; 17 + import { onMouseDown } from "src/utils/iosInputMouseDown"; 18 + 19 + export let thumbStyle = 20 + "w-4 h-4 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C,_inset_0_0_0_1px_#8C8C8C]"; 21 + 22 + export const ColorPicker = (props: { 23 + label?: string; 24 + value: Color | undefined; 25 + alpha?: boolean; 26 + image?: boolean; 27 + setValue: (c: Color) => void; 28 + openPicker: pickers; 29 + thisPicker: pickers; 30 + setOpenPicker: (thisPicker: pickers) => void; 31 + closePicker: () => void; 32 + children?: React.ReactNode; 33 + }) => { 34 + return ( 35 + <SpectrumColorPicker value={props.value} onChange={props.setValue}> 36 + <div className="flex flex-col w-full gap-2"> 37 + <div className="colorPickerLabel flex gap-2 items-center "> 38 + <button 39 + className="flex gap-2 items-center " 40 + onClick={() => { 41 + if (props.openPicker === props.thisPicker) { 42 + props.setOpenPicker("null"); 43 + } else { 44 + props.setOpenPicker(props.thisPicker); 45 + } 46 + }} 47 + > 48 + <ColorSwatch 49 + color={props.value} 50 + className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 51 + style={{ 52 + backgroundSize: "cover", 53 + }} 54 + /> 55 + <strong className="">{props.label}</strong> 56 + </button> 57 + 58 + <div className="flex gap-1"> 59 + {props.value === undefined ? ( 60 + <div>default</div> 61 + ) : ( 62 + <ColorField className="w-fit gap-1"> 63 + <Input 64 + onMouseDown={onMouseDown} 65 + onFocus={(e) => { 66 + e.currentTarget.setSelectionRange( 67 + 1, 68 + e.currentTarget.value.length, 69 + ); 70 + }} 71 + onKeyDown={(e) => { 72 + if (e.key === "Enter") { 73 + e.currentTarget.blur(); 74 + } else return; 75 + }} 76 + onBlur={(e) => { 77 + props.setValue(parseColor(e.currentTarget.value)); 78 + }} 79 + className="w-[72px] bg-transparent outline-none" 80 + /> 81 + </ColorField> 82 + )} 83 + {props.alpha && ( 84 + <> 85 + <Separator classname="my-1" /> 86 + <ColorField className="w-fit pl-[6px]" channel="alpha"> 87 + <Input 88 + onMouseDown={onMouseDown} 89 + onFocus={(e) => { 90 + e.currentTarget.setSelectionRange( 91 + 0, 92 + e.currentTarget.value.length - 1, 93 + ); 94 + }} 95 + onKeyDown={(e) => { 96 + if (e.key === "Enter") { 97 + e.currentTarget.blur(); 98 + } else return; 99 + }} 100 + className="w-[72px] bg-transparent outline-none text-primary" 101 + /> 102 + </ColorField> 103 + </> 104 + )} 105 + </div> 106 + </div> 107 + {props.openPicker === props.thisPicker && ( 108 + <div className="w-full flex flex-col gap-2 px-1 pb-2"> 109 + { 110 + <> 111 + <ColorArea 112 + className="w-full h-[128px] rounded-md" 113 + colorSpace="hsb" 114 + xChannel="saturation" 115 + yChannel="brightness" 116 + > 117 + <ColorThumb className={thumbStyle} /> 118 + </ColorArea> 119 + <ColorSlider colorSpace="hsb" className="w-full" channel="hue"> 120 + <SliderTrack className="h-2 w-full rounded-md"> 121 + <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 122 + </SliderTrack> 123 + </ColorSlider> 124 + {props.alpha && ( 125 + <ColorSlider 126 + colorSpace="hsb" 127 + className="w-full mt-1 rounded-full" 128 + style={{ 129 + backgroundImage: `url(./transparent-bg.png)`, 130 + backgroundRepeat: "repeat", 131 + backgroundSize: "8px", 132 + }} 133 + channel="alpha" 134 + > 135 + <SliderTrack className="h-2 w-full rounded-md"> 136 + <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 137 + </SliderTrack> 138 + </ColorSlider> 139 + )} 140 + {props.children} 141 + </> 142 + } 143 + </div> 144 + )} 145 + </div> 146 + </SpectrumColorPicker> 147 + ); 148 + };
+176
components/ThemeManager/ImageSetters.tsx
··· 1 + import * as Slider from "@radix-ui/react-slider"; 2 + import { theme } from "../../tailwind.config"; 3 + 4 + import { Color } from "react-aria-components"; 5 + 6 + import { useEntity, useReplicache } from "src/replicache"; 7 + import { addImage } from "src/utils/addImage"; 8 + import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 9 + import { CloseContrastSmall } from "components/Icons/CloseContrastSmall"; 10 + 11 + export const ImageSettings = (props: { 12 + entityID: string; 13 + card?: boolean; 14 + setValue: (c: Color) => void; 15 + }) => { 16 + let image = useEntity( 17 + props.entityID, 18 + props.card ? "theme/card-background-image" : "theme/background-image", 19 + ); 20 + let repeat = useEntity( 21 + props.entityID, 22 + props.card 23 + ? "theme/card-background-image-repeat" 24 + : "theme/background-image-repeat", 25 + ); 26 + let pageType = useEntity(props.entityID, "page/type")?.data.value; 27 + let { rep } = useReplicache(); 28 + return ( 29 + <> 30 + <div 31 + style={{ 32 + backgroundImage: image?.data.src 33 + ? `url(${image.data.src})` 34 + : undefined, 35 + backgroundPosition: "center", 36 + backgroundSize: "cover", 37 + }} 38 + className="themeBGImagePreview flex gap-2 place-items-center justify-center w-full h-[128px] bg-cover bg-center bg-no-repeat" 39 + > 40 + <label className="hover:cursor-pointer "> 41 + <div 42 + className="flex gap-2 rounded-md px-2 py-1 text-accent-contrast font-bold" 43 + style={{ backgroundColor: "rgba(var(--bg-page), .8" }} 44 + > 45 + <BlockImageSmall /> Change Image 46 + </div> 47 + <div className="hidden"> 48 + <ImageInput {...props} /> 49 + </div> 50 + </label> 51 + <button 52 + onClick={() => { 53 + if (image) rep?.mutate.retractFact({ factID: image.id }); 54 + if (repeat) rep?.mutate.retractFact({ factID: repeat.id }); 55 + }} 56 + > 57 + <CloseContrastSmall 58 + fill={theme.colors["accent-1"]} 59 + stroke={theme.colors["accent-2"]} 60 + /> 61 + </button> 62 + </div> 63 + <div className="themeBGImageControls font-bold flex gap-2 items-center"> 64 + {pageType !== "canvas" && ( 65 + <label htmlFor="cover" className="flex shrink-0"> 66 + <input 67 + className="appearance-none" 68 + type="radio" 69 + id="cover" 70 + name="bg-image-options" 71 + value="cover" 72 + checked={!repeat} 73 + onChange={async (e) => { 74 + if (!e.currentTarget.checked) return; 75 + if (!repeat) return; 76 + if (repeat) 77 + await rep?.mutate.retractFact({ factID: repeat.id }); 78 + }} 79 + /> 80 + <div 81 + className={`shink-0 grow-0 w-fit border border-accent-1 rounded-md px-1 py-0.5 cursor-pointer ${!repeat ? "bg-accent-1 text-accent-2" : "bg-transparent text-accent-1"}`} 82 + > 83 + cover 84 + </div> 85 + </label> 86 + )} 87 + <label htmlFor="repeat" className="flex shrink-0"> 88 + <input 89 + className={`appearance-none `} 90 + type="radio" 91 + id="repeat" 92 + name="bg-image-options" 93 + value="repeat" 94 + checked={!!repeat} 95 + onChange={async (e) => { 96 + if (!e.currentTarget.checked) return; 97 + if (repeat) return; 98 + await rep?.mutate.assertFact({ 99 + entity: props.entityID, 100 + attribute: props.card 101 + ? "theme/card-background-image-repeat" 102 + : "theme/background-image-repeat", 103 + data: { type: "number", value: 500 }, 104 + }); 105 + }} 106 + /> 107 + <div 108 + className={`shink-0 grow-0 w-fit z-10 border border-accent-1 rounded-md px-1 py-0.5 cursor-pointer ${repeat ? "bg-accent-1 text-accent-2" : "bg-transparent text-accent-1"}`} 109 + > 110 + repeat 111 + </div> 112 + </label> 113 + {(repeat || pageType === "canvas") && ( 114 + <Slider.Root 115 + className="relative grow flex items-center select-none touch-none w-full h-fit" 116 + value={[repeat?.data.value || 500]} 117 + max={3000} 118 + min={10} 119 + step={10} 120 + onValueChange={(value) => { 121 + rep?.mutate.assertFact({ 122 + entity: props.entityID, 123 + attribute: props.card 124 + ? "theme/card-background-image-repeat" 125 + : "theme/background-image-repeat", 126 + data: { type: "number", value: value[0] }, 127 + }); 128 + }} 129 + > 130 + <Slider.Track className="bg-accent-1 relative grow rounded-full h-[3px]"></Slider.Track> 131 + <Slider.Thumb 132 + className="flex w-4 h-4 rounded-full border-2 border-white bg-accent-1 shadow-[0_0_0_1px_#8C8C8C,_inset_0_0_0_1px_#8C8C8C] cursor-pointer" 133 + aria-label="Volume" 134 + /> 135 + </Slider.Root> 136 + )} 137 + </div> 138 + </> 139 + ); 140 + }; 141 + 142 + export const ImageInput = (props: { 143 + entityID: string; 144 + onChange?: () => void; 145 + card?: boolean; 146 + }) => { 147 + let pageType = useEntity(props.entityID, "page/type")?.data.value; 148 + let { rep } = useReplicache(); 149 + return ( 150 + <input 151 + type="file" 152 + accept="image/*" 153 + onChange={async (e) => { 154 + let file = e.currentTarget.files?.[0]; 155 + if (!file || !rep) return; 156 + 157 + await addImage(file, rep, { 158 + entityID: props.entityID, 159 + attribute: props.card 160 + ? "theme/card-background-image" 161 + : "theme/background-image", 162 + }); 163 + props.onChange?.(); 164 + 165 + if (pageType === "canvas") { 166 + rep && 167 + rep.mutate.assertFact({ 168 + entity: props.entityID, 169 + attribute: "canvas/background-pattern", 170 + data: { type: "canvas-pattern-union", value: "plain" }, 171 + }); 172 + } 173 + }} 174 + /> 175 + ); 176 + };
+223
components/ThemeManager/LeafletBGPicker.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + ColorPicker as SpectrumColorPicker, 5 + parseColor, 6 + Color, 7 + ColorArea, 8 + ColorThumb, 9 + ColorSlider, 10 + Input, 11 + ColorField, 12 + SliderTrack, 13 + ColorSwatch, 14 + } from "react-aria-components"; 15 + import { pickers, setColorAttribute } from "./ThemeSetter"; 16 + import { thumbStyle } from "./ColorPicker"; 17 + import { ImageInput, ImageSettings } from "./ImageSetters"; 18 + import { useEntity, useReplicache } from "src/replicache"; 19 + import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 20 + import { Separator } from "components/Layout"; 21 + import { onMouseDown } from "src/utils/iosInputMouseDown"; 22 + import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 23 + 24 + export const LeafletBGPicker = (props: { 25 + entityID: string; 26 + openPicker: pickers; 27 + thisPicker: pickers; 28 + setOpenPicker: (thisPicker: pickers) => void; 29 + closePicker: () => void; 30 + setValue: (c: Color) => void; 31 + card?: boolean; 32 + }) => { 33 + let bgImage = useEntity( 34 + props.entityID, 35 + props.card ? "theme/card-background-image" : "theme/background-image", 36 + ); 37 + let bgColor = useColorAttribute( 38 + props.entityID, 39 + props.card ? "theme/card-background" : "theme/page-background", 40 + ); 41 + let open = props.openPicker == props.thisPicker; 42 + let { rep } = useReplicache(); 43 + 44 + return ( 45 + <> 46 + <div className="bgPickerLabel flex justify-between place-items-center "> 47 + <div className="bgPickerColorLabel flex gap-2 items-center"> 48 + <button 49 + onClick={() => { 50 + if (props.openPicker === props.thisPicker) { 51 + props.setOpenPicker("null"); 52 + } else { 53 + props.setOpenPicker(props.thisPicker); 54 + } 55 + }} 56 + className="flex gap-2 items-center" 57 + > 58 + <ColorSwatch 59 + color={bgColor} 60 + className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 61 + style={{ 62 + backgroundImage: bgImage?.data.src 63 + ? `url(${bgImage.data.src})` 64 + : undefined, 65 + backgroundSize: "cover", 66 + }} 67 + /> 68 + <strong 69 + className={`${props.card ? "text-primary" : "text-[#595959]"}`} 70 + > 71 + {props.card ? "Page" : "Background"} 72 + </strong> 73 + </button> 74 + 75 + <div className="flex"> 76 + {bgImage ? ( 77 + <div 78 + className={`${props.card ? "text-secondary" : "text-[#969696]"}`} 79 + > 80 + Image 81 + </div> 82 + ) : ( 83 + <> 84 + <ColorField className="w-fit gap-1" value={bgColor}> 85 + <Input 86 + onMouseDown={onMouseDown} 87 + onFocus={(e) => { 88 + e.currentTarget.setSelectionRange( 89 + 1, 90 + e.currentTarget.value.length, 91 + ); 92 + }} 93 + onPaste={(e) => { 94 + console.log(e); 95 + }} 96 + onKeyDown={(e) => { 97 + if (e.key === "Enter") { 98 + e.currentTarget.blur(); 99 + } else return; 100 + }} 101 + onBlur={(e) => { 102 + props.setValue(parseColor(e.currentTarget.value)); 103 + }} 104 + className={`w-[72px] bg-transparent outline-none ${props.card ? "text-primary" : "text-[#595959]"}`} 105 + /> 106 + </ColorField> 107 + {props.card && ( 108 + <> 109 + <Separator classname="my-1" /> 110 + 111 + <SpectrumColorPicker 112 + value={bgColor} 113 + onChange={setColorAttribute( 114 + rep, 115 + props.entityID, 116 + )( 117 + props.card 118 + ? "theme/card-background" 119 + : "theme/page-background", 120 + )} 121 + > 122 + <ColorField className="w-fit pl-[6px]" channel="alpha"> 123 + <Input 124 + onMouseDown={onMouseDown} 125 + onFocus={(e) => { 126 + e.currentTarget.setSelectionRange( 127 + 0, 128 + e.currentTarget.value.length - 1, 129 + ); 130 + }} 131 + onKeyDown={(e) => { 132 + if (e.key === "Enter") { 133 + e.currentTarget.blur(); 134 + } else return; 135 + }} 136 + className="w-[48px] bg-transparent outline-none text-primary" 137 + /> 138 + </ColorField> 139 + </SpectrumColorPicker> 140 + </> 141 + )} 142 + </> 143 + )} 144 + </div> 145 + </div> 146 + <label className="hover:cursor-pointer h-fit"> 147 + <div 148 + className={ 149 + props.card 150 + ? "text-tertiary hover:text-accent-contrast" 151 + : "text-[#8C8C8C] hover:text-[#0000FF]" 152 + } 153 + > 154 + <BlockImageSmall /> 155 + </div> 156 + <div className="hidden"> 157 + <ImageInput 158 + {...props} 159 + onChange={() => { 160 + props.setOpenPicker(props.thisPicker); 161 + }} 162 + /> 163 + </div> 164 + </label> 165 + </div> 166 + {open && ( 167 + <div className="bgImageAndColorPicker w-full flex flex-col gap-2 "> 168 + <SpectrumColorPicker 169 + value={bgColor} 170 + onChange={setColorAttribute( 171 + rep, 172 + props.entityID, 173 + )(props.card ? "theme/card-background" : "theme/page-background")} 174 + > 175 + {bgImage ? ( 176 + <ImageSettings 177 + entityID={props.entityID} 178 + card={props.card} 179 + setValue={props.setValue} 180 + /> 181 + ) : ( 182 + <> 183 + <ColorArea 184 + className="w-full h-[128px] rounded-md" 185 + colorSpace="hsb" 186 + xChannel="saturation" 187 + yChannel="brightness" 188 + > 189 + <ColorThumb className={thumbStyle} /> 190 + </ColorArea> 191 + <ColorSlider 192 + colorSpace="hsb" 193 + className="w-full " 194 + channel="hue" 195 + > 196 + <SliderTrack className="h-2 w-full rounded-md"> 197 + <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 198 + </SliderTrack> 199 + </ColorSlider> 200 + </> 201 + )} 202 + {props.card && ( 203 + <ColorSlider 204 + colorSpace="hsb" 205 + className="w-full mt-1 rounded-full" 206 + style={{ 207 + backgroundImage: `url(./transparent-bg.png)`, 208 + backgroundRepeat: "repeat", 209 + backgroundSize: "8px", 210 + }} 211 + channel="alpha" 212 + > 213 + <SliderTrack className="h-2 w-full rounded-md"> 214 + <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 215 + </SliderTrack> 216 + </ColorSlider> 217 + )} 218 + </SpectrumColorPicker> 219 + </div> 220 + )} 221 + </> 222 + ); 223 + };
+280
components/ThemeManager/PageThemePickers.tsx
··· 1 + "use client"; 2 + 3 + import { 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"; 14 + import { Checkbox } from "components/Checkbox"; 15 + import { useMemo } from "react"; 16 + import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 17 + import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 18 + import { Separator } from "components/Layout"; 19 + import { onMouseDown } from "src/utils/iosInputMouseDown"; 20 + import { pickers, setColorAttribute } from "./ThemeSetter"; 21 + import { ImageInput, ImageSettings } from "./ImageSetters"; 22 + 23 + import { ColorPicker, thumbStyle } from "./ColorPicker"; 24 + import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 25 + import { Replicache } from "replicache"; 26 + import { CanvasBackgroundPattern } from "components/Canvas"; 27 + 28 + export const PageThemePickers = (props: { 29 + entityID: string; 30 + home?: boolean; 31 + openPicker: pickers; 32 + setOpenPicker: (thisPicker: pickers) => void; 33 + }) => { 34 + let { rep } = useReplicache(); 35 + let set = useMemo(() => { 36 + return setColorAttribute(rep, props.entityID); 37 + }, [rep, props.entityID]); 38 + 39 + let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 40 + let pageValue = useColorAttribute(props.entityID, "theme/card-background"); 41 + let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 42 + let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 43 + 44 + return ( 45 + <div 46 + 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))]" 47 + style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }} 48 + > 49 + {pageType === "canvas" && ( 50 + <> 51 + <CanvasBGPatternPicker entityID={props.entityID} rep={rep} />{" "} 52 + <hr className="border-border-light w-full" /> 53 + </> 54 + )} 55 + <ColorPicker 56 + label="Page" 57 + value={pageValue} 58 + setValue={set("theme/card-background")} 59 + thisPicker={"page"} 60 + openPicker={props.openPicker} 61 + setOpenPicker={props.setOpenPicker} 62 + closePicker={() => props.setOpenPicker("null")} 63 + alpha 64 + > 65 + {(pageBGImage === null || !pageBGImage) && ( 66 + <label 67 + className={`m-0 h-max w-full py-0.5 px-1 68 + bg-accent-1 outline-transparent 69 + rounded-md text-base font-bold text-accent-2 70 + hover:cursor-pointer 71 + flex gap-2 items-center justify-center shrink-0 72 + transparent-outline hover:outline-accent-1 outline-offset-1 73 + `} 74 + > 75 + <BlockImageSmall /> Add Background Image 76 + <div className="hidden"> 77 + <ImageInput 78 + entityID={props.entityID} 79 + onChange={() => props.setOpenPicker("page-background-image")} 80 + card 81 + /> 82 + </div> 83 + </label> 84 + )} 85 + </ColorPicker> 86 + {pageBGImage && pageBGImage !== null && ( 87 + <PageBGPicker 88 + entityID={props.entityID} 89 + thisPicker={"page-background-image"} 90 + openPicker={props.openPicker} 91 + setOpenPicker={props.setOpenPicker} 92 + closePicker={() => props.setOpenPicker("null")} 93 + setValue={set("theme/card-background")} 94 + /> 95 + )} 96 + <ColorPicker 97 + label="Text" 98 + value={primaryValue} 99 + setValue={set("theme/primary")} 100 + thisPicker={"text"} 101 + openPicker={props.openPicker} 102 + setOpenPicker={props.setOpenPicker} 103 + closePicker={() => props.setOpenPicker("null")} 104 + /> 105 + <hr /> 106 + 107 + <Checkbox checked={true} onChange={(e) => {}}> 108 + Hide Page Borders 109 + </Checkbox> 110 + </div> 111 + ); 112 + }; 113 + 114 + export const PageBGPicker = (props: { 115 + entityID: string; 116 + openPicker: pickers; 117 + thisPicker: pickers; 118 + setOpenPicker: (thisPicker: pickers) => void; 119 + closePicker: () => void; 120 + setValue: (c: Color) => void; 121 + }) => { 122 + let bgImage = useEntity(props.entityID, "theme/card-background-image"); 123 + let bgColor = useColorAttribute(props.entityID, "theme/card-background"); 124 + let bgAlpha = 125 + useEntity(props.entityID, "theme/card-background-image-opacity")?.data 126 + .value || 1; 127 + let alphaColor = useMemo(() => { 128 + return parseColor(`rgba(0,0,0,${bgAlpha})`); 129 + }, [bgAlpha]); 130 + let open = props.openPicker == props.thisPicker; 131 + let { rep } = useReplicache(); 132 + 133 + return ( 134 + <> 135 + <div className="bgPickerColorLabel flex gap-2 items-center"> 136 + <button 137 + onClick={() => { 138 + if (props.openPicker === props.thisPicker) { 139 + props.setOpenPicker("null"); 140 + } else { 141 + props.setOpenPicker(props.thisPicker); 142 + } 143 + }} 144 + className="flex gap-2 items-center" 145 + > 146 + <ColorSwatch 147 + color={bgColor} 148 + className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 149 + style={{ 150 + backgroundImage: bgImage?.data.src 151 + ? `url(${bgImage.data.src})` 152 + : undefined, 153 + backgroundPosition: "center", 154 + backgroundSize: "cover", 155 + }} 156 + /> 157 + <strong className={`text-primary`}>BG Image</strong> 158 + </button> 159 + 160 + <SpectrumColorPicker 161 + value={alphaColor} 162 + onChange={(c) => { 163 + let alpha = c.getChannelValue("alpha"); 164 + rep?.mutate.assertFact({ 165 + entity: props.entityID, 166 + attribute: "theme/card-background-image-opacity", 167 + data: { type: "number", value: alpha }, 168 + }); 169 + }} 170 + > 171 + <Separator classname="h-5 my-1" /> 172 + <ColorField className="w-fit pl-[6px]" channel="alpha"> 173 + <Input 174 + onMouseDown={onMouseDown} 175 + onFocus={(e) => { 176 + e.currentTarget.setSelectionRange( 177 + 0, 178 + e.currentTarget.value.length - 1, 179 + ); 180 + }} 181 + onKeyDown={(e) => { 182 + if (e.key === "Enter") { 183 + e.currentTarget.blur(); 184 + } else return; 185 + }} 186 + className="w-[48px] bg-transparent outline-none text-primary" 187 + /> 188 + </ColorField> 189 + </SpectrumColorPicker> 190 + </div> 191 + {open && ( 192 + <div className="pageImagePicker flex flex-col gap-2"> 193 + <ImageSettings 194 + entityID={props.entityID} 195 + card 196 + setValue={props.setValue} 197 + /> 198 + 199 + <SpectrumColorPicker 200 + value={alphaColor} 201 + onChange={(c) => { 202 + let alpha = c.getChannelValue("alpha"); 203 + rep?.mutate.assertFact({ 204 + entity: props.entityID, 205 + attribute: "theme/card-background-image-opacity", 206 + data: { type: "number", value: alpha }, 207 + }); 208 + }} 209 + > 210 + <ColorSlider 211 + colorSpace="hsb" 212 + className="w-full mt-1 rounded-full" 213 + style={{ 214 + backgroundImage: `url(./transparent-bg.png)`, 215 + backgroundRepeat: "repeat", 216 + backgroundSize: "8px", 217 + }} 218 + channel="alpha" 219 + > 220 + <SliderTrack className="h-2 w-full rounded-md"> 221 + <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 222 + </SliderTrack> 223 + </ColorSlider> 224 + </SpectrumColorPicker> 225 + </div> 226 + )} 227 + </> 228 + ); 229 + }; 230 + 231 + const CanvasBGPatternPicker = (props: { 232 + entityID: string; 233 + rep: Replicache<ReplicacheMutators> | null; 234 + }) => { 235 + let selectedPattern = useEntity(props.entityID, "canvas/background-pattern") 236 + ?.data.value; 237 + return ( 238 + <div className="flex gap-2 h-8 "> 239 + <button 240 + className={`w-full rounded-md bg-bg-page border ${selectedPattern === "grid" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 241 + onMouseDown={() => { 242 + props.rep && 243 + props.rep.mutate.assertFact({ 244 + entity: props.entityID, 245 + attribute: "canvas/background-pattern", 246 + data: { type: "canvas-pattern-union", value: "grid" }, 247 + }); 248 + }} 249 + > 250 + <CanvasBackgroundPattern pattern="grid" scale={0.5} /> 251 + </button> 252 + <button 253 + className={`w-full rounded-md bg-bg-page border ${selectedPattern === "dot" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 254 + onMouseDown={() => { 255 + props.rep && 256 + props.rep.mutate.assertFact({ 257 + entity: props.entityID, 258 + attribute: "canvas/background-pattern", 259 + data: { type: "canvas-pattern-union", value: "dot" }, 260 + }); 261 + }} 262 + > 263 + <CanvasBackgroundPattern pattern="dot" scale={0.5} /> 264 + </button> 265 + <button 266 + className={`w-full rounded-md bg-bg-page border ${selectedPattern === "plain" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 267 + onMouseDown={() => { 268 + props.rep && 269 + props.rep.mutate.assertFact({ 270 + entity: props.entityID, 271 + attribute: "canvas/background-pattern", 272 + data: { type: "canvas-pattern-union", value: "plain" }, 273 + }); 274 + }} 275 + > 276 + <CanvasBackgroundPattern pattern="plain" /> 277 + </button> 278 + </div> 279 + ); 280 + };
+90 -225
components/ThemeManager/PageThemeSetter.tsx
··· 1 - import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 2 - import { useColorAttribute } from "./useColorAttribute"; 1 + import { useEntity, useReplicache } from "src/replicache"; 3 2 import { useEntitySetContext } from "components/EntitySetProvider"; 4 - import { 5 - LeafletBGPicker, 6 - ColorPicker, 7 - pickers, 8 - SectionArrow, 9 - setColorAttribute, 10 - PageBGPicker, 11 - ImageInput, 12 - } from "./ThemeSetter"; 13 - import { useMemo, useState } from "react"; 14 - import { CanvasBackgroundPattern } from "components/Canvas"; 15 - import { Replicache } from "replicache"; 3 + import { pickers, SectionArrow } from "./ThemeSetter"; 4 + 5 + import { PageThemePickers } from "./PageThemePickers"; 6 + import { useState } from "react"; 16 7 import { theme } from "tailwind.config"; 17 8 import { ButtonPrimary } from "components/Buttons"; 18 - import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 19 9 import { PaintSmall } from "components/Icons/PaintSmall"; 10 + import { AccentThemePickers } from "./AccentThemePickers"; 20 11 21 12 export const PageThemeSetter = (props: { entityID: string }) => { 22 - let { rep, rootEntity } = useReplicache(); 13 + let { rootEntity } = useReplicache(); 23 14 let permission = useEntitySetContext().permissions.write; 24 15 let [openPicker, setOpenPicker] = useState<pickers>("null"); 25 16 26 - let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 27 - 28 - let accent1Value = useColorAttribute( 29 - props.entityID, 30 - "theme/accent-background", 31 - ); 32 - let accent2Value = useColorAttribute(props.entityID, "theme/accent-text"); 33 - let pageValue = useColorAttribute(props.entityID, "theme/card-background"); 34 - let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 35 - 36 17 let leafletBGImage = useEntity(rootEntity, "theme/background-image"); 37 18 let leafletBGRepeat = useEntity(rootEntity, "theme/background-image-repeat"); 38 - let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 39 - let pageBGRepeat = useEntity( 40 - props.entityID, 41 - "theme/card-background-image-repeat", 42 - ); 43 - let pageBGOpacity = useEntity( 44 - props.entityID, 45 - "theme/card-background-image-opacity", 46 - ); 47 - 48 - let set = useMemo(() => { 49 - return setColorAttribute(rep, props.entityID); 50 - }, [rep, props.entityID]); 51 19 52 20 if (!permission) return null; 53 21 ··· 57 25 <div className="gap-2 flex font-bold "> 58 26 <PaintSmall /> Theme Page 59 27 </div> 60 - <ButtonPrimary 61 - compact 62 - onClick={() => { 63 - if (!rep) return; 64 - rep.mutate.retractAttribute({ 65 - entity: props.entityID, 66 - attribute: [ 67 - "theme/primary", 68 - "theme/card-background", 69 - "theme/accent-background", 70 - "theme/accent-text", 71 - "theme/card-background-image", 72 - "theme/card-background-image-repeat", 73 - "theme/card-background-image-opacity", 74 - "canvas/background-pattern", 75 - ], 76 - }); 77 - }} 78 - > 79 - reset 80 - </ButtonPrimary> 28 + <ResetButton entityID={props.entityID} /> 81 29 </div> 82 30 <div 83 31 className="pageThemeSetterContent bg-bg-leaflet w-80 p-3 pb-0 flex flex-col gap-2 rounded-md -mb-1" ··· 92 40 : `calc(${leafletBGRepeat.data.value}px / 2 )`, 93 41 }} 94 42 > 95 - <div 96 - className="pageAccentControls text-accent-2 flex flex-col gap-2 h-full bg-bg-leaflet mt-4 p-2 rounded-md border border-accent-2 shadow-[0_0_0_1px_rgb(var(--accent-1))]" 97 - style={{ 98 - backgroundColor: "rgba(var(--accent-1), 0.6)", 99 - }} 100 - > 101 - <ColorPicker 102 - label="Accent" 103 - value={accent1Value} 104 - setValue={set("theme/accent-background")} 105 - thisPicker={"accent-1"} 106 - openPicker={openPicker} 107 - setOpenPicker={setOpenPicker} 108 - closePicker={() => setOpenPicker("null")} 109 - /> 110 - <ColorPicker 111 - label="Text on Accent" 112 - value={accent2Value} 113 - setValue={set("theme/accent-text")} 114 - thisPicker={"accent-2"} 43 + <AccentThemePickers 44 + entityID={props.entityID} 45 + openPicker={openPicker} 46 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 47 + /> 48 + <div className="flex flex-col -mb-[14px] mt-4 z-10"> 49 + <PageThemePickers 50 + entityID={props.entityID} 115 51 openPicker={openPicker} 116 - setOpenPicker={setOpenPicker} 117 - closePicker={() => setOpenPicker("null")} 52 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 118 53 /> 119 - </div> 120 - <div className="flex flex-col -mb-[14px] mt-4 z-10"> 121 - <div 122 - 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))]" 123 - style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }} 124 - > 125 - {pageType === "canvas" && ( 126 - <> 127 - <BackgroundPatternPicker entityID={props.entityID} rep={rep} />{" "} 128 - <hr className="border-border-light w-full" /> 129 - </> 130 - )} 131 - <ColorPicker 132 - label="Page" 133 - value={pageValue} 134 - setValue={set("theme/card-background")} 135 - thisPicker={"page"} 136 - openPicker={openPicker} 137 - setOpenPicker={setOpenPicker} 138 - closePicker={() => setOpenPicker("null")} 139 - alpha 140 - > 141 - {(pageBGImage === null || !pageBGImage) && ( 142 - <label 143 - className={`m-0 h-max w-full py-0.5 px-1 144 - bg-accent-1 outline-transparent 145 - rounded-md text-base font-bold text-accent-2 146 - hover:cursor-pointer 147 - flex gap-2 items-center justify-center shrink-0 148 - transparent-outline hover:outline-accent-1 outline-offset-1 149 - `} 150 - > 151 - <BlockImageSmall /> Add Background Image 152 - <div className="hidden"> 153 - <ImageInput 154 - entityID={props.entityID} 155 - onChange={() => setOpenPicker("page-background-image")} 156 - card 157 - /> 158 - </div> 159 - </label> 160 - )} 161 - </ColorPicker> 162 - {pageBGImage && pageBGImage !== null && ( 163 - <PageBGPicker 164 - entityID={props.entityID} 165 - thisPicker={"page-background-image"} 166 - openPicker={openPicker} 167 - setOpenPicker={setOpenPicker} 168 - closePicker={() => setOpenPicker("null")} 169 - setValue={set("theme/card-background")} 170 - /> 171 - )} 172 - <ColorPicker 173 - label="Text" 174 - value={primaryValue} 175 - setValue={set("theme/primary")} 176 - thisPicker={"text"} 177 - openPicker={openPicker} 178 - setOpenPicker={setOpenPicker} 179 - closePicker={() => setOpenPicker("null")} 180 - /> 181 - </div> 182 54 <SectionArrow 183 55 fill={theme.colors["primary"]} 184 56 stroke={theme.colors["bg-page"]} 185 57 className="ml-2" 186 58 /> 187 59 </div> 188 - <div 189 - className="relative rounded-t-lg p-2 shadow-md text-primary border border-border border-b-transparent" 190 - style={{ 191 - backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 192 - }} 193 - > 194 - <div 195 - className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 196 - style={{ 197 - backgroundImage: pageBGImage 198 - ? `url(${pageBGImage.data.src})` 199 - : undefined, 200 - 201 - backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 202 - opacity: pageBGOpacity?.data.value || 1, 203 - backgroundSize: !pageBGRepeat 204 - ? "cover" 205 - : `calc(${pageBGRepeat.data.value}px / 2 )`, 206 - }} 207 - /> 208 - <div className="relative"> 209 - <p className="font-bold">Theme Each Page!</p> 210 - <small className=""> 211 - OMG! You can theme each page individually in{" "} 212 - <span className="font-bold text-accent-contrast">Leaflet</span>! 213 - <br /> Buttons and sections appear like: 214 - </small> 215 - <div className="p-2 mt-2 border border-border bg-bg-page rounded-md text-sm flex justify-between items-center font-bold text-secondary"> 216 - Happy Theming! 217 - <div className="bg-accent-1 text-accent-2 py-0.5 px-2 w-fit text-center text-sm font-bold rounded-md"> 218 - Button 219 - </div> 220 - </div> 221 - </div> 222 - </div> 60 + <SamplePage entityID={props.entityID} /> 223 61 </div> 224 62 </> 225 63 ); 226 64 }; 227 65 228 - const BackgroundPatternPicker = (props: { 229 - entityID: string; 230 - rep: Replicache<ReplicacheMutators> | null; 231 - }) => { 232 - let selectedPattern = useEntity(props.entityID, "canvas/background-pattern") 233 - ?.data.value; 66 + const ResetButton = (props: { entityID: string }) => { 67 + let { rep } = useReplicache(); 68 + 69 + return ( 70 + <ButtonPrimary 71 + compact 72 + onClick={() => { 73 + if (!rep) return; 74 + rep.mutate.retractAttribute({ 75 + entity: props.entityID, 76 + attribute: [ 77 + "theme/primary", 78 + "theme/card-background", 79 + "theme/accent-background", 80 + "theme/accent-text", 81 + "theme/card-background-image", 82 + "theme/card-background-image-repeat", 83 + "theme/card-background-image-opacity", 84 + "canvas/background-pattern", 85 + ], 86 + }); 87 + }} 88 + > 89 + reset 90 + </ButtonPrimary> 91 + ); 92 + }; 93 + 94 + const SamplePage = (props: { entityID: string }) => { 95 + let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 96 + let pageBGRepeat = useEntity( 97 + props.entityID, 98 + "theme/card-background-image-repeat", 99 + ); 100 + let pageBGOpacity = useEntity( 101 + props.entityID, 102 + "theme/card-background-image-opacity", 103 + ); 104 + 234 105 return ( 235 - <div className="flex gap-2 h-8 "> 236 - <button 237 - className={`w-full rounded-md bg-bg-page border ${selectedPattern === "grid" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 238 - onMouseDown={() => { 239 - props.rep && 240 - props.rep.mutate.assertFact({ 241 - entity: props.entityID, 242 - attribute: "canvas/background-pattern", 243 - data: { type: "canvas-pattern-union", value: "grid" }, 244 - }); 245 - }} 246 - > 247 - <CanvasBackgroundPattern pattern="grid" scale={0.5} /> 248 - </button> 249 - <button 250 - className={`w-full rounded-md bg-bg-page border ${selectedPattern === "dot" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 251 - onMouseDown={() => { 252 - props.rep && 253 - props.rep.mutate.assertFact({ 254 - entity: props.entityID, 255 - attribute: "canvas/background-pattern", 256 - data: { type: "canvas-pattern-union", value: "dot" }, 257 - }); 258 - }} 259 - > 260 - <CanvasBackgroundPattern pattern="dot" scale={0.5} /> 261 - </button> 262 - <button 263 - className={`w-full rounded-md bg-bg-page border ${selectedPattern === "plain" ? "outline outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`} 264 - onMouseDown={() => { 265 - props.rep && 266 - props.rep.mutate.assertFact({ 267 - entity: props.entityID, 268 - attribute: "canvas/background-pattern", 269 - data: { type: "canvas-pattern-union", value: "plain" }, 270 - }); 106 + <div 107 + className="relative rounded-t-lg p-2 shadow-md text-primary border border-border border-b-transparent" 108 + style={{ 109 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 110 + }} 111 + > 112 + <div 113 + className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 114 + style={{ 115 + backgroundImage: pageBGImage 116 + ? `url(${pageBGImage.data.src})` 117 + : undefined, 118 + 119 + backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 120 + opacity: pageBGOpacity?.data.value || 1, 121 + backgroundSize: !pageBGRepeat 122 + ? "cover" 123 + : `calc(${pageBGRepeat.data.value}px / 2 )`, 271 124 }} 272 - > 273 - <CanvasBackgroundPattern pattern="plain" /> 274 - </button> 125 + /> 126 + <div className="relative"> 127 + <p className="font-bold">Theme Each Page!</p> 128 + <small className=""> 129 + OMG! You can theme each page individually in{" "} 130 + <span className="font-bold text-accent-contrast">Leaflet</span>! 131 + <br /> Buttons and sections appear like: 132 + </small> 133 + <div className="p-2 mt-2 border border-border bg-bg-page rounded-md text-sm flex justify-between items-center font-bold text-secondary"> 134 + Happy Theming! 135 + <div className="bg-accent-1 text-accent-2 py-0.5 px-2 w-fit text-center text-sm font-bold rounded-md"> 136 + Button 137 + </div> 138 + </div> 139 + </div> 275 140 </div> 276 141 ); 277 142 };
+82 -743
components/ThemeManager/ThemeSetter.tsx
··· 1 1 "use client"; 2 2 import { Popover } from "components/Popover"; 3 - import * as Slider from "@radix-ui/react-slider"; 4 3 import { theme } from "../../tailwind.config"; 5 4 6 - import { 7 - ColorPicker as SpectrumColorPicker, 8 - parseColor, 9 - Color, 10 - ColorArea, 11 - ColorThumb, 12 - ColorSlider, 13 - Input, 14 - ColorField, 15 - SliderTrack, 16 - ColorSwatch, 17 - } from "react-aria-components"; 5 + import { Color } from "react-aria-components"; 18 6 19 - import { useEffect, useMemo, useState } from "react"; 7 + import { LeafletBGPicker } from "./LeafletBGPicker"; 8 + import { PageThemePickers } from "./PageThemePickers"; 9 + import { useMemo, useState } from "react"; 20 10 import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 21 11 import { Replicache } from "replicache"; 22 12 import { FilterAttributes } from "src/replicache/attributes"; 23 - import { 24 - colorToString, 25 - useColorAttribute, 26 - } from "components/ThemeManager/useColorAttribute"; 27 - import { addImage } from "src/utils/addImage"; 28 - import { Separator } from "components/Layout"; 13 + import { colorToString } from "components/ThemeManager/useColorAttribute"; 29 14 import { useEntitySetContext } from "components/EntitySetProvider"; 30 - import { isIOS, useViewportSize } from "@react-aria/utils"; 31 - import { onMouseDown } from "src/utils/iosInputMouseDown"; 32 15 import { ActionButton } from "components/ActionBar/ActionButton"; 33 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 34 - import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 35 16 import { CheckboxChecked } from "components/Icons/CheckboxChecked"; 36 17 import { CheckboxEmpty } from "components/Icons/CheckboxEmpty"; 37 - import { CloseContrastSmall } from "components/Icons/CloseContrastSmall"; 38 18 import { PaintSmall } from "components/Icons/PaintSmall"; 19 + import { AccentThemePickers } from "./AccentThemePickers"; 39 20 40 21 export type pickers = 41 22 | "null" ··· 63 44 } 64 45 export const ThemePopover = (props: { entityID: string; home?: boolean }) => { 65 46 let { rep } = useReplicache(); 66 - let pageLoaded = useInitialPageLoad(); 67 - // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. 68 - let leafletValue = useColorAttribute(props.entityID, "theme/page-background"); 69 - let pageValue = useColorAttribute(props.entityID, "theme/card-background"); 70 - let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 71 - let accent1Value = useColorAttribute( 72 - props.entityID, 73 - "theme/accent-background", 74 - ); 75 - let accent2Value = useColorAttribute(props.entityID, "theme/accent-text"); 76 47 48 + // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. 77 49 let permission = useEntitySetContext().permissions.write; 78 50 let leafletBGImage = useEntity(props.entityID, "theme/background-image"); 79 51 let leafletBGRepeat = useEntity( 80 52 props.entityID, 81 53 "theme/background-image-repeat", 82 54 ); 83 - let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 84 - let pageBGRepeat = useEntity( 85 - props.entityID, 86 - "theme/card-background-image-repeat", 87 - ); 88 55 89 56 let [openPicker, setOpenPicker] = useState<pickers>( 90 57 props.home === true ? "leaflet" : "null", ··· 93 60 return setColorAttribute(rep, props.entityID); 94 61 }, [rep, props.entityID]); 95 62 96 - let randomPositions = useMemo(() => { 97 - let values = [] as string[]; 98 - for (let i = 0; i < 3; i++) { 99 - if (!pageLoaded) values.push(`100% 100%`); 100 - else 101 - values.push( 102 - `${Math.floor(Math.random() * 100)}% ${Math.floor(Math.random() * 100)}%`, 103 - ); 104 - } 105 - return values; 106 - }, [pageLoaded]); 107 - 108 - let gradient = [ 109 - `radial-gradient(at ${randomPositions[0]}, ${accent1Value.toString("hex")}80 2px, transparent 70%)`, 110 - `radial-gradient(at ${randomPositions[1]}, ${pageValue.toString("hex")}66 2px, transparent 60%)`, 111 - `radial-gradient(at ${randomPositions[2]}, ${primaryValue.toString("hex")}B3 2px, transparent 100%)`, 112 - ].join(", "); 113 - let viewheight = useViewportSize().height; 114 63 if (!permission) return null; 115 64 116 65 return ( 117 66 <> 118 67 <Popover 119 - className="w-80" 68 + className="w-80 bg-white" 69 + arrowFill="#FFFFFF" 120 70 asChild 121 71 trigger={<ActionButton icon={<PaintSmall />} label="Theme" />} 122 72 > 123 73 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 124 74 <div className="themeBGLeaflet flex"> 125 75 <div 126 - className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full px-2 pt-3`} 76 + className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 127 77 > 128 - <div className="bgPickerBody w-full flex flex-col gap-2 p-2 border border-[#CCCCCC] rounded-md"> 78 + <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md"> 129 79 <LeafletBGPicker 130 80 entityID={props.entityID} 131 81 thisPicker={"leaflet"} ··· 158 108 ? "cover" 159 109 : `calc(${leafletBGRepeat.data.value}px / 2 )`, 160 110 }} 161 - className={`bg-bg-leaflet mx-2 p-3 mb-2 flex flex-col rounded-md border border-border pb-0`} 111 + className={`bg-bg-leaflet p-3 mb-2 flex flex-col rounded-md border border-border pb-0`} 162 112 > 163 113 <div className={`flex flex-col z-10 mt-4 -mb-[6px] `}> 164 - <div 165 - className="themeLeafletControls text-accent-2 flex flex-col gap-2 h-full bg-bg-leaflet p-2 rounded-md border border-accent-2 shadow-[0_0_0_1px_rgb(var(--accent-1))]" 166 - style={{ 167 - backgroundColor: "rgba(var(--accent-1), 0.6)", 168 - }} 169 - > 170 - <ColorPicker 171 - label="Accent" 172 - value={accent1Value} 173 - setValue={set("theme/accent-background")} 174 - thisPicker={"accent-1"} 175 - openPicker={openPicker} 176 - setOpenPicker={setOpenPicker} 177 - closePicker={() => setOpenPicker("null")} 178 - /> 179 - <ColorPicker 180 - label="Text on Accent" 181 - value={accent2Value} 182 - setValue={set("theme/accent-text")} 183 - thisPicker={"accent-2"} 184 - openPicker={openPicker} 185 - setOpenPicker={setOpenPicker} 186 - closePicker={() => setOpenPicker("null")} 187 - /> 188 - </div> 114 + <AccentThemePickers 115 + entityID={props.entityID} 116 + openPicker={openPicker} 117 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 118 + /> 189 119 <SectionArrow 190 120 fill={theme.colors["accent-2"]} 191 121 stroke={theme.colors["accent-1"]} ··· 193 123 /> 194 124 </div> 195 125 196 - <div 197 - onClick={(e) => { 198 - e.target === e.currentTarget && setOpenPicker("accent-1"); 199 - }} 200 - className="pointer-cursor font-bold relative text-center text-lg py-2 rounded-md bg-accent-1 text-accent-2 shadow-md flex items-center justify-center" 201 - > 202 - <div 203 - className="cursor-pointer w-fit" 204 - onClick={() => { 205 - setOpenPicker("accent-2"); 206 - }} 207 - > 208 - Example Button 209 - </div> 210 - </div> 126 + <SampleButton 127 + entityID={props.entityID} 128 + setOpenPicker={setOpenPicker} 129 + /> 211 130 212 131 <div className="flex flex-col mt-8 -mb-[6px] z-10"> 213 - <div 214 - className="themeLeafletControls 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))]" 215 - style={{ backgroundColor: "rgba(var(--bg-page, 0.6)" }} 216 - > 217 - <ColorPicker 218 - label={props.home ? "Menu" : "Page"} 219 - alpha 220 - value={pageValue} 221 - setValue={set("theme/card-background")} 222 - thisPicker={"page"} 223 - openPicker={openPicker} 224 - setOpenPicker={setOpenPicker} 225 - closePicker={() => setOpenPicker("null")} 226 - /> 227 - <ColorPicker 228 - label={props.home ? "Menu Text" : "Text"} 229 - value={primaryValue} 230 - setValue={set("theme/primary")} 231 - thisPicker={"text"} 232 - openPicker={openPicker} 233 - setOpenPicker={setOpenPicker} 234 - closePicker={() => setOpenPicker("null")} 235 - /> 236 - </div> 132 + <PageThemePickers 133 + home 134 + entityID={props.entityID} 135 + openPicker={openPicker} 136 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 137 + /> 237 138 <SectionArrow 238 139 fill={theme.colors["primary"]} 239 140 stroke={theme.colors["bg-page"]} ··· 241 142 /> 242 143 </div> 243 144 244 - <SamplePage setOpenPicker={setOpenPicker} home={props.home} /> 145 + <SamplePage 146 + setOpenPicker={setOpenPicker} 147 + home={props.home} 148 + entityID={props.entityID} 149 + /> 245 150 </div> 246 151 {!props.home && <WatermarkSetter entityID={props.entityID} />} 247 152 </div> ··· 249 154 </> 250 155 ); 251 156 }; 157 + 252 158 function WatermarkSetter(props: { entityID: string }) { 253 159 let { rep } = useReplicache(); 254 160 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark"); ··· 283 189 ); 284 190 } 285 191 286 - const SamplePage = (props: { 287 - home: boolean | undefined; 288 - setOpenPicker: (picker: "page" | "text") => void; 192 + const SampleButton = (props: { 193 + entityID: string; 194 + setOpenPicker: (thisPicker: pickers) => void; 289 195 }) => { 290 196 return ( 291 197 <div 292 198 onClick={(e) => { 293 - e.currentTarget === e.target && props.setOpenPicker("page"); 294 - }} 295 - className={`${props.home ? "rounded-md " : "rounded-t-lg "} cursor-pointer p-2 border border-border border-b-transparent shadow-md text-primary`} 296 - style={{ 297 - backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 199 + e.target === e.currentTarget && props.setOpenPicker("accent-1"); 298 200 }} 201 + className="pointer-cursor font-bold relative text-center text-lg py-2 rounded-md bg-accent-1 text-accent-2 shadow-md flex items-center justify-center" 299 202 > 300 - <p 203 + <div 204 + className="cursor-pointer w-fit" 301 205 onClick={() => { 302 - props.setOpenPicker("text"); 206 + props.setOpenPicker("accent-2"); 303 207 }} 304 - className=" cursor-pointer font-bold w-fit" 305 208 > 306 - Hello! 307 - </p> 308 - <small onClick={() => props.setOpenPicker("text")}> 309 - Welcome to{" "} 310 - <span className="font-bold text-accent-contrast">Leaflet</span>. 311 - It&apos;s a super easy and fun way to make, share, and collab on little 312 - bits of paper 313 - </small> 209 + Example Button 210 + </div> 314 211 </div> 315 212 ); 316 213 }; 317 - 318 - let thumbStyle = 319 - "w-4 h-4 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C,_inset_0_0_0_1px_#8C8C8C]"; 320 - 321 - export const ColorPicker = (props: { 322 - label?: string; 323 - value: Color | undefined; 324 - alpha?: boolean; 325 - image?: boolean; 326 - setValue: (c: Color) => void; 327 - openPicker: pickers; 328 - thisPicker: pickers; 329 - setOpenPicker: (thisPicker: pickers) => void; 330 - closePicker: () => void; 331 - children?: React.ReactNode; 332 - }) => { 333 - return ( 334 - <SpectrumColorPicker value={props.value} onChange={props.setValue}> 335 - <div className="flex flex-col w-full gap-2"> 336 - <div className="colorPickerLabel flex gap-2 items-center "> 337 - <button 338 - className="flex gap-2 items-center " 339 - onClick={() => { 340 - if (props.openPicker === props.thisPicker) { 341 - props.setOpenPicker("null"); 342 - } else { 343 - props.setOpenPicker(props.thisPicker); 344 - } 345 - }} 346 - > 347 - <ColorSwatch 348 - color={props.value} 349 - className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 350 - style={{ 351 - backgroundSize: "cover", 352 - }} 353 - /> 354 - <strong className="">{props.label}</strong> 355 - </button> 356 - 357 - <div className="flex gap-1"> 358 - {props.value === undefined ? ( 359 - <div>default</div> 360 - ) : ( 361 - <ColorField className="w-fit gap-1"> 362 - <Input 363 - onMouseDown={onMouseDown} 364 - onFocus={(e) => { 365 - e.currentTarget.setSelectionRange( 366 - 1, 367 - e.currentTarget.value.length, 368 - ); 369 - }} 370 - onKeyDown={(e) => { 371 - if (e.key === "Enter") { 372 - e.currentTarget.blur(); 373 - } else return; 374 - }} 375 - onBlur={(e) => { 376 - props.setValue(parseColor(e.currentTarget.value)); 377 - }} 378 - className="w-[72px] bg-transparent outline-none" 379 - /> 380 - </ColorField> 381 - )} 382 - {props.alpha && ( 383 - <> 384 - <Separator classname="my-1" /> 385 - <ColorField className="w-fit pl-[6px]" channel="alpha"> 386 - <Input 387 - onMouseDown={onMouseDown} 388 - onFocus={(e) => { 389 - e.currentTarget.setSelectionRange( 390 - 0, 391 - e.currentTarget.value.length - 1, 392 - ); 393 - }} 394 - onKeyDown={(e) => { 395 - if (e.key === "Enter") { 396 - e.currentTarget.blur(); 397 - } else return; 398 - }} 399 - className="w-[72px] bg-transparent outline-none text-primary" 400 - /> 401 - </ColorField> 402 - </> 403 - )} 404 - </div> 405 - </div> 406 - {props.openPicker === props.thisPicker && ( 407 - <div className="w-full flex flex-col gap-2 px-1 pb-2"> 408 - { 409 - <> 410 - <ColorArea 411 - className="w-full h-[128px] rounded-md" 412 - colorSpace="hsb" 413 - xChannel="saturation" 414 - yChannel="brightness" 415 - > 416 - <ColorThumb className={thumbStyle} /> 417 - </ColorArea> 418 - <ColorSlider colorSpace="hsb" className="w-full" channel="hue"> 419 - <SliderTrack className="h-2 w-full rounded-md"> 420 - <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 421 - </SliderTrack> 422 - </ColorSlider> 423 - {props.alpha && ( 424 - <ColorSlider 425 - colorSpace="hsb" 426 - className="w-full mt-1 rounded-full" 427 - style={{ 428 - backgroundImage: `url(./transparent-bg.png)`, 429 - backgroundRepeat: "repeat", 430 - backgroundSize: "8px", 431 - }} 432 - channel="alpha" 433 - > 434 - <SliderTrack className="h-2 w-full rounded-md"> 435 - <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 436 - </SliderTrack> 437 - </ColorSlider> 438 - )} 439 - {props.children} 440 - </> 441 - } 442 - </div> 443 - )} 444 - </div> 445 - </SpectrumColorPicker> 446 - ); 447 - }; 448 - 449 - export const LeafletBGPicker = (props: { 214 + const SamplePage = (props: { 450 215 entityID: string; 451 - openPicker: pickers; 452 - thisPicker: pickers; 453 - setOpenPicker: (thisPicker: pickers) => void; 454 - closePicker: () => void; 455 - setValue: (c: Color) => void; 456 - card?: boolean; 216 + home: boolean | undefined; 217 + setOpenPicker: (picker: "page" | "text") => void; 457 218 }) => { 458 - let bgImage = useEntity( 219 + let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 220 + let pageBGRepeat = useEntity( 459 221 props.entityID, 460 - props.card ? "theme/card-background-image" : "theme/background-image", 222 + "theme/card-background-image-repeat", 461 223 ); 462 - let bgColor = useColorAttribute( 224 + let pageBGOpacity = useEntity( 463 225 props.entityID, 464 - props.card ? "theme/card-background" : "theme/page-background", 226 + "theme/card-background-image-opacity", 465 227 ); 466 - let open = props.openPicker == props.thisPicker; 467 - let { rep } = useReplicache(); 468 228 469 229 return ( 470 - <> 471 - <div className="bgPickerLabel flex justify-between place-items-center "> 472 - <div className="bgPickerColorLabel flex gap-2 items-center"> 473 - <button 474 - onClick={() => { 475 - if (props.openPicker === props.thisPicker) { 476 - props.setOpenPicker("null"); 477 - } else { 478 - props.setOpenPicker(props.thisPicker); 479 - } 480 - }} 481 - className="flex gap-2 items-center" 482 - > 483 - <ColorSwatch 484 - color={bgColor} 485 - className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 486 - style={{ 487 - backgroundImage: bgImage?.data.src 488 - ? `url(${bgImage.data.src})` 489 - : undefined, 490 - backgroundSize: "cover", 491 - }} 492 - /> 493 - <strong 494 - className={`${props.card ? "text-primary" : "text-[#595959]"}`} 495 - > 496 - {props.card ? "Page" : "Background"} 497 - </strong> 498 - </button> 499 - 500 - <div className="flex"> 501 - {bgImage ? ( 502 - <div 503 - className={`${props.card ? "text-secondary" : "text-[#969696]"}`} 504 - > 505 - Image 506 - </div> 507 - ) : ( 508 - <> 509 - <ColorField className="w-fit gap-1" value={bgColor}> 510 - <Input 511 - onMouseDown={onMouseDown} 512 - onFocus={(e) => { 513 - e.currentTarget.setSelectionRange( 514 - 1, 515 - e.currentTarget.value.length, 516 - ); 517 - }} 518 - onPaste={(e) => { 519 - console.log(e); 520 - }} 521 - onKeyDown={(e) => { 522 - if (e.key === "Enter") { 523 - e.currentTarget.blur(); 524 - } else return; 525 - }} 526 - onBlur={(e) => { 527 - props.setValue(parseColor(e.currentTarget.value)); 528 - }} 529 - className={`w-[72px] bg-transparent outline-none ${props.card ? "text-primary" : "text-[#595959]"}`} 530 - /> 531 - </ColorField> 532 - {props.card && ( 533 - <> 534 - <Separator classname="my-1" /> 535 - 536 - <SpectrumColorPicker 537 - value={bgColor} 538 - onChange={setColorAttribute( 539 - rep, 540 - props.entityID, 541 - )( 542 - props.card 543 - ? "theme/card-background" 544 - : "theme/page-background", 545 - )} 546 - > 547 - <ColorField className="w-fit pl-[6px]" channel="alpha"> 548 - <Input 549 - onMouseDown={onMouseDown} 550 - onFocus={(e) => { 551 - e.currentTarget.setSelectionRange( 552 - 0, 553 - e.currentTarget.value.length - 1, 554 - ); 555 - }} 556 - onKeyDown={(e) => { 557 - if (e.key === "Enter") { 558 - e.currentTarget.blur(); 559 - } else return; 560 - }} 561 - className="w-[48px] bg-transparent outline-none text-primary" 562 - /> 563 - </ColorField> 564 - </SpectrumColorPicker> 565 - </> 566 - )} 567 - </> 568 - )} 569 - </div> 570 - </div> 571 - <label className="hover:cursor-pointer h-fit"> 572 - <div 573 - className={ 574 - props.card 575 - ? "text-tertiary hover:text-accent-contrast" 576 - : "text-[#8C8C8C] hover:text-[#0000FF]" 577 - } 578 - > 579 - <BlockImageSmall /> 580 - </div> 581 - <div className="hidden"> 582 - <ImageInput 583 - {...props} 584 - onChange={() => { 585 - props.setOpenPicker(props.thisPicker); 586 - }} 587 - /> 588 - </div> 589 - </label> 590 - </div> 591 - {open && ( 592 - <div className="bgImageAndColorPicker w-full flex flex-col gap-2 "> 593 - <SpectrumColorPicker 594 - value={bgColor} 595 - onChange={setColorAttribute( 596 - rep, 597 - props.entityID, 598 - )(props.card ? "theme/card-background" : "theme/page-background")} 599 - > 600 - {bgImage ? ( 601 - <ImageSettings 602 - entityID={props.entityID} 603 - card={props.card} 604 - setValue={props.setValue} 605 - /> 606 - ) : ( 607 - <> 608 - <ColorArea 609 - className="w-full h-[128px] rounded-md" 610 - colorSpace="hsb" 611 - xChannel="saturation" 612 - yChannel="brightness" 613 - > 614 - <ColorThumb className={thumbStyle} /> 615 - </ColorArea> 616 - <ColorSlider 617 - colorSpace="hsb" 618 - className="w-full " 619 - channel="hue" 620 - > 621 - <SliderTrack className="h-2 w-full rounded-md"> 622 - <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 623 - </SliderTrack> 624 - </ColorSlider> 625 - </> 626 - )} 627 - {props.card && ( 628 - <ColorSlider 629 - colorSpace="hsb" 630 - className="w-full mt-1 rounded-full" 631 - style={{ 632 - backgroundImage: `url(./transparent-bg.png)`, 633 - backgroundRepeat: "repeat", 634 - backgroundSize: "8px", 635 - }} 636 - channel="alpha" 637 - > 638 - <SliderTrack className="h-2 w-full rounded-md"> 639 - <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 640 - </SliderTrack> 641 - </ColorSlider> 642 - )} 643 - </SpectrumColorPicker> 644 - </div> 645 - )} 646 - </> 647 - ); 648 - }; 649 - 650 - export const PageBGPicker = (props: { 651 - entityID: string; 652 - openPicker: pickers; 653 - thisPicker: pickers; 654 - setOpenPicker: (thisPicker: pickers) => void; 655 - closePicker: () => void; 656 - setValue: (c: Color) => void; 657 - }) => { 658 - let bgImage = useEntity(props.entityID, "theme/card-background-image"); 659 - let bgColor = useColorAttribute(props.entityID, "theme/card-background"); 660 - let bgAlpha = 661 - useEntity(props.entityID, "theme/card-background-image-opacity")?.data 662 - .value || 1; 663 - let alphaColor = useMemo(() => { 664 - return parseColor(`rgba(0,0,0,${bgAlpha})`); 665 - }, [bgAlpha]); 666 - let open = props.openPicker == props.thisPicker; 667 - let { rep } = useReplicache(); 668 - 669 - return ( 670 - <> 671 - <div className="bgPickerColorLabel flex gap-2 items-center"> 672 - <button 673 - onClick={() => { 674 - if (props.openPicker === props.thisPicker) { 675 - props.setOpenPicker("null"); 676 - } else { 677 - props.setOpenPicker(props.thisPicker); 678 - } 679 - }} 680 - className="flex gap-2 items-center" 681 - > 682 - <ColorSwatch 683 - color={bgColor} 684 - className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`} 685 - style={{ 686 - backgroundImage: bgImage?.data.src 687 - ? `url(${bgImage.data.src})` 688 - : undefined, 689 - backgroundPosition: "center", 690 - backgroundSize: "cover", 691 - }} 692 - /> 693 - <strong className={`text-primary`}>Background Image</strong> 694 - </button> 695 - 696 - <SpectrumColorPicker 697 - value={alphaColor} 698 - onChange={(c) => { 699 - let alpha = c.getChannelValue("alpha"); 700 - rep?.mutate.assertFact({ 701 - entity: props.entityID, 702 - attribute: "theme/card-background-image-opacity", 703 - data: { type: "number", value: alpha }, 704 - }); 705 - }} 706 - > 707 - <Separator classname="h-5 my-1" /> 708 - <ColorField className="w-fit pl-[6px]" channel="alpha"> 709 - <Input 710 - onMouseDown={onMouseDown} 711 - onFocus={(e) => { 712 - e.currentTarget.setSelectionRange( 713 - 0, 714 - e.currentTarget.value.length - 1, 715 - ); 716 - }} 717 - onKeyDown={(e) => { 718 - if (e.key === "Enter") { 719 - e.currentTarget.blur(); 720 - } else return; 721 - }} 722 - className="w-[48px] bg-transparent outline-none text-primary" 723 - /> 724 - </ColorField> 725 - </SpectrumColorPicker> 726 - </div> 727 - {open && ( 728 - <div className="pageImagePicker flex flex-col gap-2"> 729 - <ImageSettings 730 - entityID={props.entityID} 731 - card 732 - setValue={props.setValue} 733 - /> 734 - 735 - <SpectrumColorPicker 736 - value={alphaColor} 737 - onChange={(c) => { 738 - let alpha = c.getChannelValue("alpha"); 739 - rep?.mutate.assertFact({ 740 - entity: props.entityID, 741 - attribute: "theme/card-background-image-opacity", 742 - data: { type: "number", value: alpha }, 743 - }); 744 - }} 745 - > 746 - <ColorSlider 747 - colorSpace="hsb" 748 - className="w-full mt-1 rounded-full" 749 - style={{ 750 - backgroundImage: `url(./transparent-bg.png)`, 751 - backgroundRepeat: "repeat", 752 - backgroundSize: "8px", 753 - }} 754 - channel="alpha" 755 - > 756 - <SliderTrack className="h-2 w-full rounded-md"> 757 - <ColorThumb className={`${thumbStyle} mt-[4px]`} /> 758 - </SliderTrack> 759 - </ColorSlider> 760 - </SpectrumColorPicker> 761 - </div> 762 - )} 763 - </> 764 - ); 765 - }; 766 - 767 - export const ImageInput = (props: { 768 - entityID: string; 769 - onChange?: () => void; 770 - card?: boolean; 771 - }) => { 772 - let pageType = useEntity(props.entityID, "page/type")?.data.value; 773 - let { rep } = useReplicache(); 774 - return ( 775 - <input 776 - type="file" 777 - accept="image/*" 778 - onChange={async (e) => { 779 - let file = e.currentTarget.files?.[0]; 780 - if (!file || !rep) return; 781 - 782 - await addImage(file, rep, { 783 - entityID: props.entityID, 784 - attribute: props.card 785 - ? "theme/card-background-image" 786 - : "theme/background-image", 787 - }); 788 - props.onChange?.(); 789 - 790 - if (pageType === "canvas") { 791 - rep && 792 - rep.mutate.assertFact({ 793 - entity: props.entityID, 794 - attribute: "canvas/background-pattern", 795 - data: { type: "canvas-pattern-union", value: "plain" }, 796 - }); 797 - } 230 + <div 231 + onClick={(e) => { 232 + e.currentTarget === e.target && props.setOpenPicker("page"); 233 + }} 234 + className={`${props.home ? "rounded-md " : "rounded-t-lg "} relative cursor-pointer p-2 border border-border border-b-transparent shadow-md text-primary`} 235 + style={{ 236 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 798 237 }} 799 - /> 800 - ); 801 - }; 802 - 803 - export const ImageSettings = (props: { 804 - entityID: string; 805 - card?: boolean; 806 - setValue: (c: Color) => void; 807 - }) => { 808 - let image = useEntity( 809 - props.entityID, 810 - props.card ? "theme/card-background-image" : "theme/background-image", 811 - ); 812 - let repeat = useEntity( 813 - props.entityID, 814 - props.card 815 - ? "theme/card-background-image-repeat" 816 - : "theme/background-image-repeat", 817 - ); 818 - let pageType = useEntity(props.entityID, "page/type")?.data.value; 819 - let { rep } = useReplicache(); 820 - return ( 821 - <> 238 + > 822 239 <div 240 + className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 823 241 style={{ 824 - backgroundImage: image?.data.src 825 - ? `url(${image.data.src})` 242 + backgroundImage: pageBGImage 243 + ? `url(${pageBGImage.data.src})` 826 244 : undefined, 827 - backgroundPosition: "center", 828 - backgroundSize: "cover", 245 + 246 + backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 247 + opacity: pageBGOpacity?.data.value || 1, 248 + backgroundSize: !pageBGRepeat 249 + ? "cover" 250 + : `calc(${pageBGRepeat.data.value}px / 2 )`, 829 251 }} 830 - className="themeBGImagePreview flex gap-2 place-items-center justify-center w-full h-[128px] bg-cover bg-center bg-no-repeat" 831 - > 832 - <label className="hover:cursor-pointer "> 833 - <div 834 - className="flex gap-2 rounded-md px-2 py-1 text-accent-contrast font-bold" 835 - style={{ backgroundColor: "rgba(var(--bg-page), .6" }} 836 - > 837 - <BlockImageSmall /> Change Image 838 - </div> 839 - <div className="hidden"> 840 - <ImageInput {...props} /> 841 - </div> 842 - </label> 843 - <button 252 + /> 253 + <div> 254 + <p 844 255 onClick={() => { 845 - if (image) rep?.mutate.retractFact({ factID: image.id }); 846 - if (repeat) rep?.mutate.retractFact({ factID: repeat.id }); 256 + props.setOpenPicker("text"); 847 257 }} 258 + className=" cursor-pointer font-bold w-fit" 848 259 > 849 - <CloseContrastSmall 850 - fill={theme.colors["accent-1"]} 851 - stroke={theme.colors["accent-2"]} 852 - /> 853 - </button> 260 + Hello! 261 + </p> 262 + <small onClick={() => props.setOpenPicker("text")}> 263 + Welcome to{" "} 264 + <span className="font-bold text-accent-contrast">Leaflet</span>. 265 + It&apos;s a super easy and fun way to make, share, and collab on 266 + little bits of paper 267 + </small> 854 268 </div> 855 - <div className="themeBGImageControls font-bold flex gap-2 items-center"> 856 - {pageType !== "canvas" && ( 857 - <label htmlFor="cover" className="flex shrink-0"> 858 - <input 859 - className="appearance-none" 860 - type="radio" 861 - id="cover" 862 - name="bg-image-options" 863 - value="cover" 864 - checked={!repeat} 865 - onChange={async (e) => { 866 - if (!e.currentTarget.checked) return; 867 - if (!repeat) return; 868 - if (repeat) 869 - await rep?.mutate.retractFact({ factID: repeat.id }); 870 - }} 871 - /> 872 - <div 873 - className={`shink-0 grow-0 w-fit border border-accent-1 rounded-md px-1 py-0.5 cursor-pointer ${!repeat ? "bg-accent-1 text-accent-2" : "bg-transparent text-accent-1"}`} 874 - > 875 - cover 876 - </div> 877 - </label> 878 - )} 879 - <label htmlFor="repeat" className="flex shrink-0"> 880 - <input 881 - className={`appearance-none `} 882 - type="radio" 883 - id="repeat" 884 - name="bg-image-options" 885 - value="repeat" 886 - checked={!!repeat} 887 - onChange={async (e) => { 888 - if (!e.currentTarget.checked) return; 889 - if (repeat) return; 890 - await rep?.mutate.assertFact({ 891 - entity: props.entityID, 892 - attribute: props.card 893 - ? "theme/card-background-image-repeat" 894 - : "theme/background-image-repeat", 895 - data: { type: "number", value: 500 }, 896 - }); 897 - }} 898 - /> 899 - <div 900 - className={`shink-0 grow-0 w-fit z-10 border border-accent-1 rounded-md px-1 py-0.5 cursor-pointer ${repeat ? "bg-accent-1 text-accent-2" : "bg-transparent text-accent-1"}`} 901 - > 902 - repeat 903 - </div> 904 - </label> 905 - {(repeat || pageType === "canvas") && ( 906 - <Slider.Root 907 - className="relative grow flex items-center select-none touch-none w-full h-fit" 908 - value={[repeat?.data.value || 500]} 909 - max={3000} 910 - min={10} 911 - step={10} 912 - onValueChange={(value) => { 913 - rep?.mutate.assertFact({ 914 - entity: props.entityID, 915 - attribute: props.card 916 - ? "theme/card-background-image-repeat" 917 - : "theme/background-image-repeat", 918 - data: { type: "number", value: value[0] }, 919 - }); 920 - }} 921 - > 922 - <Slider.Track className="bg-accent-1 relative grow rounded-full h-[3px]"></Slider.Track> 923 - <Slider.Thumb 924 - className="flex w-4 h-4 rounded-full border-2 border-white bg-accent-1 shadow-[0_0_0_1px_#8C8C8C,_inset_0_0_0_1px_#8C8C8C] cursor-pointer" 925 - aria-label="Volume" 926 - /> 927 - </Slider.Root> 928 - )} 929 - </div> 930 - </> 269 + </div> 931 270 ); 932 271 }; 933 272