a tool for shared writing and social publishing

added a slider to adjust page-wdith to leaflets

+173 -14
+19 -6
app/globals.css
··· 107 107 --highlight-3: 255, 205, 195; 108 108 109 109 --list-marker-width: 36px; 110 - --page-width-unitless: min(624, calc(var(--leaflet-width-unitless) - 12)); 111 - --page-width-units: min(624px, calc(100vw - 12px)); 110 + --page-max-width-unitless: 624; 111 + --page-width-unitless: min( 112 + var(--page-max-width-unitless), 113 + calc(var(--leaflet-width-unitless) - 12) 114 + ); 115 + --page-width-units: min( 116 + calc(var(--page-max-width-unitless) * 1px), 117 + calc(100vw - 12px) 118 + ); 112 119 113 120 --gripperSVG: url("/gripperPattern.svg"); 114 121 --gripperSVG2: url("/gripperPattern2.svg"); ··· 126 133 @media (min-width: 640px) { 127 134 :root { 128 135 --page-width-unitless: min( 129 - 624, 136 + var(--page-max-width-unitless), 130 137 calc(var(--leaflet-width-unitless) - 128) 131 138 ); 132 - --page-width-units: min(624px, calc(100vw - 128px)); 139 + --page-width-units: min( 140 + calc(var(--page-max-width-unitless) * 1px), 141 + calc(100vw - 128px) 142 + ); 133 143 } 134 144 } 135 145 136 146 @media (min-width: 1280px) { 137 147 :root { 138 148 --page-width-unitless: min( 139 - 624, 149 + var(--page-max-width-unitless), 140 150 calc((var(--leaflet-width-unitless) / 2) - 32) 141 151 ); 142 - --page-width-units: min(624px, calc((100vw / 2) - 32px)); 152 + --page-width-units: min( 153 + calc(var(--page-max-width-unitless) * 1px), 154 + calc((100vw / 2) - 32px) 155 + ); 143 156 } 144 157 } 145 158
+2 -2
components/ThemeManager/Pickers/ImagePicker.tsx
··· 127 127 <Slider.Thumb 128 128 className={` 129 129 flex w-4 h-4 rounded-full border-2 border-white cursor-pointer 130 - ${repeat ? "bg-[#595959]" : " bg-[#C3C3C3] "} 131 - ${repeat && "shadow-[0_0_0_1px_#8C8C8C,inset_0_0_0_1px_#8C8C8C]"} `} 130 + ${repeat ? "bg-[#595959] shadow-[0_0_0_1px_#8C8C8C,inset_0_0_0_1px_#8C8C8C]" : " bg-[#C3C3C3] "} 131 + `} 132 132 aria-label="Volume" 133 133 /> 134 134 </Slider.Root>
+2 -2
components/ThemeManager/Pickers/PageThemePickers.tsx
··· 51 51 <hr className="border-border-light w-full" /> 52 52 </> 53 53 )} 54 - <PageTextPicker 54 + <TextPickers 55 55 value={primaryValue} 56 56 setValue={set("theme/primary")} 57 57 openPicker={props.openPicker} ··· 347 347 ); 348 348 }; 349 349 350 - export const PageTextPicker = (props: { 350 + export const TextPickers = (props: { 351 351 openPicker: pickers; 352 352 setOpenPicker: (thisPicker: pickers) => void; 353 353 value: Color;
+120
components/ThemeManager/Pickers/PageWidthSetter.tsx
··· 1 + import * as Slider from "@radix-ui/react-slider"; 2 + import { Input } from "components/Input"; 3 + import { useEntity, useReplicache } from "src/replicache"; 4 + import { pickers } from "../ThemeSetter"; 5 + import { useState } from "react"; 6 + 7 + export const PageWidthSetter = (props: { 8 + entityID: string; 9 + openPicker: pickers; 10 + thisPicker: pickers; 11 + setOpenPicker: (thisPicker: pickers) => void; 12 + closePicker: () => void; 13 + }) => { 14 + let { rep } = useReplicache(); 15 + let pageWidth = useEntity(props.entityID, "theme/page-width"); 16 + let currentValue = pageWidth?.data.value || 624; 17 + let [interimValue, setInterimValue] = useState<number>(currentValue); 18 + let min = 324; 19 + let max = 1200; 20 + 21 + let open = props.openPicker == props.thisPicker; 22 + 23 + return ( 24 + <div className="pageWidthSetter flex flex-col gap-2 px-2 py-[6px] border border-[#CCCCCC] rounded-md"> 25 + <div className="flex flex-col gap-2"> 26 + <div className="flex gap-2 items-center"> 27 + <button 28 + className="font-bold text-[#000000] shrink-0 grow-0 w-fit" 29 + onClick={() => { 30 + if (props.openPicker === props.thisPicker) { 31 + props.setOpenPicker("null"); 32 + } else { 33 + props.setOpenPicker(props.thisPicker); 34 + } 35 + }} 36 + > 37 + Max Page Width 38 + </button> 39 + <div className="flex font-normal text-[#969696]"> 40 + <Input 41 + type="number" 42 + className="relative w-10 text-right appearance-none bg-transparent" 43 + max={max} 44 + min={min} 45 + value={interimValue} 46 + onFocus={(e) => { 47 + e.preventDefault(); 48 + props.setOpenPicker(props.thisPicker); 49 + }} 50 + onChange={(e) => { 51 + setInterimValue(parseInt(e.currentTarget.value)); 52 + }} 53 + onKeyDown={(e) => { 54 + if (e.key === "Enter" || e.key === "Escape") { 55 + e.preventDefault(); 56 + let clampedValue = interimValue; 57 + if (!isNaN(interimValue)) { 58 + clampedValue = Math.max(min, Math.min(max, interimValue)); 59 + setInterimValue(clampedValue); 60 + } 61 + rep?.mutate.assertFact({ 62 + entity: props.entityID, 63 + attribute: "theme/page-width", 64 + data: { 65 + type: "number", 66 + value: clampedValue, 67 + }, 68 + }); 69 + } 70 + }} 71 + onBlur={() => { 72 + let clampedValue = interimValue; 73 + if (!isNaN(interimValue)) { 74 + clampedValue = Math.max(min, Math.min(max, interimValue)); 75 + setInterimValue(clampedValue); 76 + } 77 + rep?.mutate.assertFact({ 78 + entity: props.entityID, 79 + attribute: "theme/page-width", 80 + data: { 81 + type: "number", 82 + value: clampedValue, 83 + }, 84 + }); 85 + }} 86 + /> 87 + px 88 + </div> 89 + </div> 90 + {open && ( 91 + <Slider.Root 92 + className="relative grow flex items-center select-none touch-none w-full h-fit px-1 mb-1" 93 + value={[interimValue]} 94 + max={max} 95 + min={min} 96 + step={16} 97 + onValueChange={(value) => { 98 + setInterimValue(value[0]); 99 + }} 100 + onPointerUp={() => { 101 + rep?.mutate.assertFact({ 102 + entity: props.entityID, 103 + attribute: "theme/page-width", 104 + data: { type: "number", value: interimValue }, 105 + }); 106 + }} 107 + > 108 + <Slider.Track className="bg-[#595959] relative grow rounded-full h-[3px]" /> 109 + <Slider.Thumb 110 + className="flex w-4 h-4 outline-none! rounded-full border-2 border-white cursor-pointer bg-[#595959] 111 + focus:shadow-[0_0_0_1px_#8C8C8C,inset_0_0_0_1px_#8C8C8C] 112 + " 113 + aria-label="Max Page Width" 114 + /> 115 + </Slider.Root> 116 + )} 117 + </div> 118 + </div> 119 + ); 120 + };
+2 -2
components/ThemeManager/PubPickers/PubTextPickers.tsx
··· 1 1 import { pickers } from "../ThemeSetter"; 2 - import { PageTextPicker } from "../Pickers/PageThemePickers"; 2 + import { TextPickers } from "../Pickers/PageThemePickers"; 3 3 import { Color } from "react-aria-components"; 4 4 5 5 export const PagePickers = (props: { ··· 20 20 : "transparent", 21 21 }} 22 22 > 23 - <PageTextPicker 23 + <TextPickers 24 24 value={props.primary} 25 25 setValue={props.setPrimary} 26 26 openPicker={props.openPicker}
+13
components/ThemeManager/ThemeProvider.tsx
··· 74 74 let accent1 = useColorAttribute(props.entityID, "theme/accent-background"); 75 75 let accent2 = useColorAttribute(props.entityID, "theme/accent-text"); 76 76 77 + let pageWidth = useEntity(props.entityID, "theme/page-width"); 78 + 77 79 return ( 78 80 <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}> 79 81 <BaseThemeProvider ··· 87 89 accent1={accent1} 88 90 accent2={accent2} 89 91 showPageBackground={showPageBackground} 92 + pageWidth={pageWidth?.data.value} 90 93 > 91 94 {props.children} 92 95 </BaseThemeProvider> ··· 106 109 highlight2, 107 110 highlight3, 108 111 showPageBackground, 112 + pageWidth, 109 113 children, 110 114 }: { 111 115 local?: boolean; ··· 118 122 highlight1?: string; 119 123 highlight2: AriaColor; 120 124 highlight3: AriaColor; 125 + pageWidth?: number; 121 126 children: React.ReactNode; 122 127 }) => { 123 128 // set accent contrast to the accent color that has the highest contrast with the page background ··· 196 201 "--accent-1-is-contrast", 197 202 accentContrast === accent1 ? "1" : "0", 198 203 ); 204 + 205 + // Set page width CSS variable 206 + el?.style.setProperty( 207 + "--page-max-width-unitless", 208 + (pageWidth || 624).toString(), 209 + ); 199 210 }, [ 200 211 local, 201 212 bgLeaflet, ··· 207 218 accent1, 208 219 accent2, 209 220 accentContrast, 221 + pageWidth, 210 222 ]); 211 223 return ( 212 224 <div ··· 226 238 : "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)", 227 239 "--highlight-2": colorToString(highlight2, "rgb"), 228 240 "--highlight-3": colorToString(highlight3, "rgb"), 241 + "--page-max-width-unitless": pageWidth || 624, 229 242 } as CSSProperties 230 243 } 231 244 >
+11 -2
components/ThemeManager/ThemeSetter.tsx
··· 10 10 PageBorderHider, 11 11 PageThemePickers, 12 12 } from "./Pickers/PageThemePickers"; 13 + import { PageWidthSetter } from "./Pickers/PageWidthSetter"; 13 14 import { useMemo, useState } from "react"; 14 15 import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 15 16 import { Replicache } from "replicache"; ··· 35 36 | "highlight-1" 36 37 | "highlight-2" 37 38 | "highlight-3" 38 - | "page-background-image"; 39 + | "page-background-image" 40 + | "page-width"; 39 41 40 42 export function setColorAttribute( 41 43 rep: Replicache<ReplicacheMutators> | null, ··· 75 77 return ( 76 78 <> 77 79 <Popover 78 - className="w-80 bg-white" 80 + className="w-80 bg-white py-3!" 79 81 arrowFill="#FFFFFF" 80 82 asChild 81 83 side={isMobile ? "top" : "right"} ··· 114 116 if (pub?.publications) return null; 115 117 return ( 116 118 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 119 + <PageWidthSetter 120 + entityID={props.entityID} 121 + thisPicker={"page-width"} 122 + openPicker={openPicker} 123 + setOpenPicker={setOpenPicker} 124 + closePicker={() => setOpenPicker("null")} 125 + /> 117 126 <div className="themeBGLeaflet flex"> 118 127 <div className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}> 119 128 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md">
+4
src/replicache/attributes.ts
··· 191 191 type: "boolean", 192 192 cardinality: "one", 193 193 }, 194 + "theme/page-width": { 195 + type: "number", 196 + cardinality: "one", 197 + }, 194 198 "theme/page-background": { 195 199 type: "color", 196 200 cardinality: "one",