a tool for shared writing and social publishing

change blockToolbar to imageToolbar, not

+98 -206
+9
.claude/settings.local.json
··· 1 + { 2 + "permissions": { 3 + "allow": [ 4 + "mcp__acp__Edit", 5 + "mcp__acp__Write", 6 + "mcp__acp__Bash" 7 + ] 8 + } 9 + }
+1 -1
components/Blocks/ImageBlock.tsx
··· 147 147 hasAlignment 148 148 isSelected={!!isSelected} 149 149 className={blockClassName} 150 - optionsClassName="top-[-8px]!" 150 + optionsClassName={isFullBleed ? "top-[-8px]!" : ""} 151 151 > 152 152 {isLocalUpload || image.data.local ? ( 153 153 <img
components/Toolbar/BlockToolbar.1.tsx

This is a binary file and will not be displayed.

+10 -129
components/Toolbar/BlockToolbar.tsx
··· 1 - import { useEntity, useReplicache } from "src/replicache"; 2 - import { ToolbarButton } from "."; 3 - import { Separator, ShortcutKey } from "components/Layout"; 4 - import { metaKey } from "src/utils/metaKey"; 1 + import { useEntity } from "src/replicache"; 2 + import { Separator } from "components/Layout"; 5 3 import { useUIState } from "src/useUIState"; 6 - import { LockBlockButton } from "./LockBlockButton"; 7 4 import { TextAlignmentButton } from "./TextAlignmentToolbar"; 8 5 import { 9 6 ImageFullBleedButton, 10 7 ImageAltTextButton, 11 8 ImageCoverButton, 12 9 } from "./ImageToolbar"; 13 - import { DeleteSmall } from "components/Icons/DeleteSmall"; 14 - import { moveBlockUp, moveBlockDown } from "src/utils/moveBlock"; 15 - import { useEntitySetContext } from "components/EntitySetProvider"; 16 10 17 - export const BlockToolbar = (props: { 18 - setToolbarState: ( 19 - state: "areYouSure" | "block" | "text-alignment" | "img-alt-text", 20 - ) => void; 11 + export const ImageToolbar = (props: { 12 + setToolbarState: (state: "image" | "text-alignment") => void; 21 13 }) => { 22 14 let focusedEntity = useUIState((s) => s.focusedEntity); 23 15 let focusedEntityType = useEntity( ··· 34 26 return ( 35 27 <div className="flex items-center gap-2 justify-between w-full"> 36 28 <div className="flex items-center gap-2"> 37 - <ToolbarButton 38 - onClick={() => { 39 - props.setToolbarState("areYouSure"); 40 - }} 41 - tooltipContent="Delete Block" 42 - > 43 - <DeleteSmall /> 44 - </ToolbarButton> 45 - <Separator classname="h-6!" /> 46 - <MoveBlockButtons /> 47 - {blockType === "image" && ( 48 - <> 49 - <TextAlignmentButton setToolbarState={props.setToolbarState} /> 50 - <ImageFullBleedButton /> 51 - <ImageAltTextButton setToolbarState={props.setToolbarState} /> 52 - <ImageCoverButton /> 53 - {focusedEntityType?.data.value !== "canvas" && ( 54 - <Separator classname="h-6!" /> 55 - )} 56 - </> 57 - )} 58 - {(blockType === "button" || blockType === "datetime") && ( 59 - <> 60 - <TextAlignmentButton setToolbarState={props.setToolbarState} /> 61 - {focusedEntityType?.data.value !== "canvas" && ( 62 - <Separator classname="h-6!" /> 63 - )} 64 - </> 29 + <TextAlignmentButton setToolbarState={props.setToolbarState} /> 30 + <ImageFullBleedButton /> 31 + <ImageAltTextButton /> 32 + <ImageCoverButton /> 33 + {focusedEntityType?.data.value !== "canvas" && ( 34 + <Separator classname="h-6!" /> 65 35 )} 66 - 67 - <LockBlockButton /> 68 36 </div> 69 37 </div> 70 38 ); 71 39 }; 72 - 73 - const MoveBlockButtons = () => { 74 - let { rep } = useReplicache(); 75 - let entity_set = useEntitySetContext(); 76 - return ( 77 - <> 78 - <ToolbarButton 79 - hiddenOnCanvas 80 - onClick={async () => { 81 - if (!rep) return; 82 - await moveBlockUp(rep); 83 - }} 84 - tooltipContent={ 85 - <div className="flex flex-col gap-1 justify-center"> 86 - <div className="text-center">Move Up</div> 87 - <div className="flex gap-1"> 88 - <ShortcutKey>Shift</ShortcutKey> +{" "} 89 - <ShortcutKey>{metaKey()}</ShortcutKey> +{" "} 90 - <ShortcutKey> ↑ </ShortcutKey> 91 - </div> 92 - </div> 93 - } 94 - > 95 - <MoveBlockUp /> 96 - </ToolbarButton> 97 - 98 - <ToolbarButton 99 - hiddenOnCanvas 100 - onClick={async () => { 101 - if (!rep) return; 102 - await moveBlockDown(rep, entity_set.set); 103 - }} 104 - tooltipContent={ 105 - <div className="flex flex-col gap-1 justify-center"> 106 - <div className="text-center">Move Down</div> 107 - <div className="flex gap-1"> 108 - <ShortcutKey>Shift</ShortcutKey> +{" "} 109 - <ShortcutKey>{metaKey()}</ShortcutKey> +{" "} 110 - <ShortcutKey> ↓ </ShortcutKey> 111 - </div> 112 - </div> 113 - } 114 - > 115 - <MoveBlockDown /> 116 - </ToolbarButton> 117 - <Separator classname="h-6!" /> 118 - </> 119 - ); 120 - }; 121 - 122 - const MoveBlockDown = () => { 123 - return ( 124 - <svg 125 - width="24" 126 - height="24" 127 - viewBox="0 0 24 24" 128 - fill="none" 129 - xmlns="http://www.w3.org/2000/svg" 130 - > 131 - <path 132 - fillRule="evenodd" 133 - clipRule="evenodd" 134 - d="M18.3444 3.56272L3.89705 5.84775C3.48792 5.91246 3.20871 6.29658 3.27342 6.7057L3.83176 10.2358C3.89647 10.645 4.28058 10.9242 4.68971 10.8595L19.137 8.57444C19.5462 8.50973 19.8254 8.12561 19.7607 7.71649L19.2023 4.18635C19.1376 3.77722 18.7535 3.49801 18.3444 3.56272ZM3.70177 4.61309C2.69864 4.77175 1.9884 5.65049 2.01462 6.63905C1.6067 6.92894 1.37517 7.43373 1.45854 7.96083L2.02167 11.5213C2.19423 12.6123 3.21854 13.3568 4.30955 13.1843L15.5014 11.4142L15.3472 10.4394L16.6131 10.2392L17.2948 13.9166L15.3038 12.4752C14.9683 12.2322 14.4994 12.3073 14.2565 12.6428C14.0135 12.9783 14.0886 13.4472 14.4241 13.6902L18.5417 16.6712L21.5228 12.5536C21.7658 12.2181 21.6907 11.7492 21.3552 11.5063C21.0197 11.2634 20.5508 11.3385 20.3079 11.674L18.7926 13.7669L18.0952 10.0048L19.3323 9.80909C20.4233 9.63654 21.1679 8.61222 20.9953 7.52121L20.437 3.99107C20.2644 2.90007 19.2401 2.15551 18.1491 2.32807L3.70177 4.61309ZM12.5175 14.1726C12.8583 14.118 13.0904 13.7974 13.0358 13.4566C12.9812 13.1157 12.6606 12.8837 12.3198 12.9383L4.48217 14.1937C3.37941 14.3704 2.62785 15.4065 2.80232 16.5096L3.35244 19.9878C3.52716 21.0925 4.56428 21.8463 5.66893 21.6716L20.0583 19.3958C21.1618 19.2212 21.9155 18.186 21.7426 17.0822L21.6508 16.4961C21.5974 16.1551 21.2776 15.922 20.9366 15.9754C20.5956 16.0288 20.3624 16.3486 20.4158 16.6896L20.5077 17.2757C20.5738 17.6981 20.2854 18.0943 19.8631 18.1611L5.47365 20.437C5.05089 20.5038 4.65396 20.2153 4.5871 19.7925L4.03697 16.3143C3.9702 15.8921 4.25783 15.4956 4.67988 15.428L12.5175 14.1726ZM5.48645 8.13141C5.4213 7.72235 5.70009 7.33793 6.10914 7.27278L12.7667 6.21241C13.1757 6.14726 13.5602 6.42605 13.6253 6.83511C13.6905 7.24417 13.4117 7.62859 13.0026 7.69374L6.34508 8.75411C5.93602 8.81926 5.5516 8.54047 5.48645 8.13141Z" 135 - fill="currentColor" 136 - /> 137 - </svg> 138 - ); 139 - }; 140 - 141 - const MoveBlockUp = () => { 142 - return ( 143 - <svg 144 - width="24" 145 - height="24" 146 - viewBox="0 0 24 24" 147 - fill="none" 148 - xmlns="http://www.w3.org/2000/svg" 149 - > 150 - <path 151 - fillRule="evenodd" 152 - clipRule="evenodd" 153 - d="M4.12086 10.3069C3.69777 10.3744 3.30016 10.0858 3.23323 9.66265L2.68364 6.18782C2.61677 5.76506 2.90529 5.36813 3.32805 5.30127L17.7149 3.0258C18.1378 2.95892 18.5348 3.24759 18.6015 3.67049L18.7835 4.82361C18.8373 5.16457 19.1573 5.39736 19.4983 5.34356C19.8392 5.28975 20.072 4.96974 20.0182 4.62878L19.8363 3.47566C19.6619 2.37067 18.6246 1.61639 17.5197 1.79115L3.13278 4.06661C2.02813 4.24133 1.27427 5.27845 1.44899 6.3831L1.99857 9.85793C2.17346 10.9637 3.21238 11.7177 4.31788 11.5413L11.5185 10.392C11.8594 10.3376 12.0916 10.0171 12.0372 9.67628C11.9828 9.33542 11.6624 9.1032 11.3215 9.15761L4.12086 10.3069ZM19.9004 11.6151L5.45305 13.9001C5.04392 13.9649 4.76471 14.349 4.82942 14.7581L5.38775 18.2882C5.45246 18.6974 5.83658 18.9766 6.24571 18.9119L20.6931 16.6268C21.1022 16.5621 21.3814 16.178 21.3167 15.7689L20.7583 12.2388C20.6936 11.8296 20.3095 11.5504 19.9004 11.6151ZM5.25777 12.6655C4.21806 12.8299 3.49299 13.7679 3.57645 14.8C3.17867 15.1511 2.9637 15.6918 3.05264 16.2541L3.57767 19.5737C3.75023 20.6647 4.77455 21.4093 5.86556 21.2367L19.9927 19.0023C20.7197 18.8873 21.2751 18.3524 21.4519 17.6846C22.2223 17.3097 22.6921 16.4638 22.5513 15.5736L21.993 12.0435C21.8204 10.9525 20.7961 10.2079 19.7051 10.3805L17.9019 10.6657L17.3957 7.46986L19.3483 8.96297C19.6773 9.21457 20.148 9.1518 20.3996 8.82276C20.6512 8.49373 20.5885 8.02302 20.2594 7.77141L16.2213 4.68355L13.1334 8.72172C12.8818 9.05076 12.9445 9.52146 13.2736 9.77307C13.6026 10.0247 14.0733 9.96191 14.3249 9.63287L15.8945 7.58034L16.4203 10.9L5.25777 12.6655ZM7.66514 15.3252C7.25609 15.3903 6.97729 15.7748 7.04245 16.1838C7.1076 16.5929 7.49202 16.8717 7.90108 16.8065L14.5586 15.7461C14.9677 15.681 15.2465 15.2966 15.1813 14.8875C15.1162 14.4785 14.7317 14.1997 14.3227 14.2648L7.66514 15.3252Z" 154 - fill="currentColor" 155 - /> 156 - </svg> 157 - ); 158 - };
+3 -5
components/Toolbar/ImageToolbar.tsx
··· 36 36 ); 37 37 }; 38 38 39 - export const ImageAltTextButton = (props: { 40 - setToolbarState: (s: "img-alt-text") => void; 41 - }) => { 39 + export const ImageAltTextButton = (props: {}) => { 42 40 let { rep } = useReplicache(); 43 41 let focusedBlock = useUIState((s) => s.focusedEntity)?.entityID || null; 44 42 ··· 48 46 let altEditorOpen = useUIState((s) => s.openPopover === focusedBlock); 49 47 let hasSrc = useEntity(focusedBlock, "block/image")?.data; 50 48 if (!hasSrc) return null; 51 - 49 + console.log("alt: " + altText); 52 50 return ( 53 51 <ToolbarButton 54 52 active={altText !== undefined} 55 53 onClick={async (e) => { 56 54 e.preventDefault(); 57 55 if (!focusedBlock) return; 58 - if (!altText) { 56 + if (altText === undefined) { 59 57 await rep?.mutate.assertFact({ 60 58 entity: focusedBlock, 61 59 attribute: "image/alt",
+29 -7
components/Toolbar/MultiSelectToolbar.tsx
··· 2 2 import { ReplicacheMutators, useReplicache } from "src/replicache"; 3 3 import { ToolbarButton } from "./index"; 4 4 import { copySelection } from "src/utils/copySelection"; 5 - import { useSmoker } from "components/Toast"; 5 + import { useSmoker, useToaster } from "components/Toast"; 6 6 import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 7 7 import { Replicache } from "replicache"; 8 8 import { LockBlockButton } from "./LockBlockButton"; 9 9 import { Props } from "components/Icons/Props"; 10 10 import { TextAlignmentButton } from "./TextAlignmentToolbar"; 11 11 import { getSortedSelection } from "components/SelectionManager/selectionState"; 12 + import { deleteBlock } from "src/utils/deleteBlock"; 13 + import { ShortcutKey } from "components/Layout"; 12 14 13 15 export const MultiselectToolbar = (props: { 14 - setToolbarState: ( 15 - state: "areYouSure" | "multiselect" | "text-alignment", 16 - ) => void; 16 + setToolbarState: (state: "multiselect" | "text-alignment") => void; 17 17 }) => { 18 18 const { rep } = useReplicache(); 19 19 const smoker = useSmoker(); 20 + const toaster = useToaster(); 20 21 21 22 const handleCopy = async (event: React.MouseEvent) => { 22 23 if (!rep) return; 23 - const [sortedSelection] = await getSortedSelection(rep); 24 + let [sortedSelection] = await getSortedSelection(rep); 24 25 await copySelection(rep, sortedSelection); 25 26 smoker({ 26 27 position: { x: event.clientX, y: event.clientY }, ··· 33 34 <div className="flex items-center gap-2"> 34 35 <ToolbarButton 35 36 tooltipContent="Delete Selected Blocks" 36 - onClick={() => { 37 - props.setToolbarState("areYouSure"); 37 + onClick={async (e) => { 38 + e.stopPropagation(); 39 + if (!rep) return; 40 + let [sortedSelection] = await getSortedSelection(rep); 41 + await deleteBlock( 42 + sortedSelection.map((b) => b.value), 43 + rep, 44 + ); 45 + 46 + toaster({ 47 + content: ( 48 + <div className="font-bold items-center flex"> 49 + {sortedSelection.length} block 50 + {sortedSelection.length === 1 ? "" : "s"} deleted!{" "} 51 + <span className="px-2 flex"> 52 + <ShortcutKey>Ctrl</ShortcutKey> 53 + <ShortcutKey>Z</ShortcutKey>{" "} 54 + </span> 55 + to undo. 56 + </div> 57 + ), 58 + type: "success", 59 + }); 38 60 }} 39 61 > 40 62 <TrashSmall />
+46 -64
components/Toolbar/index.tsx
··· 11 11 import { ListToolbar } from "./ListToolbar"; 12 12 import { HighlightToolbar } from "./HighlightToolbar"; 13 13 import { TextToolbar } from "./TextToolbar"; 14 - import { BlockToolbar } from "./BlockToolbar"; 14 + import { ImageToolbar } from "./BlockToolbar"; 15 15 import { MultiselectToolbar } from "./MultiSelectToolbar"; 16 16 import { AreYouSure } from "components/Blocks/DeleteBlock"; 17 17 import { deleteBlock } from "src/utils/deleteBlock"; ··· 21 21 import { CloseTiny } from "components/Icons/CloseTiny"; 22 22 23 23 export type ToolbarTypes = 24 - | "areYouSure" 25 24 | "default" 26 - | "block" 27 25 | "multiselect" 28 26 | "highlight" 29 27 | "link" ··· 31 29 | "text-alignment" 32 30 | "list" 33 31 | "linkBlock" 34 - | "img-alt-text"; 32 + | "img-alt-text" 33 + | "image"; 35 34 36 35 export const Toolbar = (props: { pageID: string; blockID: string }) => { 37 36 let { rep } = useReplicache(); ··· 64 63 }; 65 64 }, [toolbarState]); 66 65 66 + let isTextBlock = 67 + blockType === "heading" || 68 + blockType === "text" || 69 + blockType === "blockquote"; 70 + 67 71 useEffect(() => { 68 - if (!blockType) return; 69 - if ( 70 - blockType !== "heading" && 71 - blockType !== "text" && 72 - blockType !== "blockquote" 73 - ) { 74 - setToolbarState("block"); 75 - } else { 72 + if (isTextBlock) { 76 73 setToolbarState("default"); 77 74 } 75 + if (blockType === "image") { 76 + setToolbarState("image"); 77 + } 78 + if (blockType === "button" || blockType === "datetime") { 79 + setToolbarState("text-alignment"); 80 + } else return; 78 81 }, [blockType]); 79 82 80 83 useEffect(() => { ··· 125 128 <TextBlockTypeToolbar onClose={() => setToolbarState("default")} /> 126 129 ) : toolbarState === "text-alignment" ? ( 127 130 <TextAlignmentToolbar /> 128 - ) : toolbarState === "block" ? ( 129 - <BlockToolbar setToolbarState={setToolbarState} /> 131 + ) : toolbarState === "image" ? ( 132 + <ImageToolbar setToolbarState={setToolbarState} /> 130 133 ) : toolbarState === "multiselect" ? ( 131 134 <MultiselectToolbar setToolbarState={setToolbarState} /> 132 - ) : toolbarState === "areYouSure" ? ( 133 - <AreYouSure 134 - compact 135 - type={blockType} 136 - entityID={selectedBlocks.map((b) => b.value)} 137 - onClick={() => { 138 - rep && 139 - deleteBlock( 140 - selectedBlocks.map((b) => b.value), 141 - rep, 142 - ); 143 - }} 144 - closeAreYouSure={() => { 145 - setToolbarState( 146 - selectedBlocks.length > 1 147 - ? "multiselect" 148 - : blockType !== "heading" && blockType !== "text" 149 - ? "block" 150 - : "default", 151 - ); 152 - }} 153 - /> 154 135 ) : null} 155 136 </div> 156 137 {/* if the thing is are you sure state, don't show the x... is each thing handling its own are you sure? theres no need for that */} 157 - {toolbarState !== "areYouSure" && ( 158 - <button 159 - className="toolbarBackToDefault hover:text-accent-contrast" 160 - onMouseDown={(e) => { 161 - e.preventDefault(); 162 - if ( 163 - toolbarState === "multiselect" || 164 - toolbarState === "block" || 165 - toolbarState === "default" 166 - ) { 167 - useUIState.setState(() => ({ 168 - focusedEntity: { 169 - entityType: "page", 170 - entityID: props.pageID, 171 - }, 172 - selectedBlocks: [], 173 - })); 174 - } else { 175 - if (blockType !== "heading" && blockType !== "text") { 176 - setToolbarState("block"); 177 - } else { 178 - setToolbarState("default"); 179 - } 138 + 139 + <button 140 + className="toolbarBackToDefault hover:text-accent-contrast" 141 + onMouseDown={(e) => { 142 + e.preventDefault(); 143 + if ( 144 + toolbarState === "multiselect" || 145 + toolbarState === "image" || 146 + toolbarState === "default" 147 + ) { 148 + // close the toolbar 149 + useUIState.setState(() => ({ 150 + focusedEntity: { 151 + entityType: "page", 152 + entityID: props.pageID, 153 + }, 154 + selectedBlocks: [], 155 + })); 156 + } else { 157 + if (blockType === "image") { 158 + setToolbarState("image"); 159 + } 160 + if (isTextBlock) { 161 + setToolbarState("default"); 180 162 } 181 - }} 182 - > 183 - <CloseTiny /> 184 - </button> 185 - )} 163 + } 164 + }} 165 + > 166 + <CloseTiny /> 167 + </button> 186 168 </div> 187 169 </Tooltip.Provider> 188 170 );