a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Feature/text alignment (#92)

* add text alignment toolbar

* added align icons

---------

Co-authored-by: celine <celine@hyperlink.academy>

authored by

Jared Pereira
celine
and committed by
GitHub
2b2a738b d9f8d60f

+184 -30
+16
components/Blocks/TextBlock/index.tsx
··· 120 120 }) { 121 121 let initialFact = useEntity(props.entityID, "block/text"); 122 122 let headingLevel = useEntity(props.entityID, "block/heading-level"); 123 + let alignment = 124 + useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 125 + let alignmentClass = { 126 + left: "text-left", 127 + right: "text-right", 128 + center: "text-center", 129 + }[alignment]; 123 130 let { permissions } = useEntitySetContext(); 124 131 125 132 if (!initialFact) { ··· 161 168 <pre 162 169 style={{ wordBreak: "break-word" }} // better than tailwind break-all! 163 170 className={` 171 + ${alignmentClass} 164 172 w-full whitespace-pre-wrap outline-none ${props.className} `} 165 173 > 166 174 {nodes.length === 0 && <br />} ··· 188 196 (s) => !!s.selectedBlocks.find((b) => b.value === props.entityID), 189 197 ); 190 198 let headingLevel = useEntity(props.entityID, "block/heading-level"); 199 + let alignment = 200 + useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 201 + let alignmentClass = { 202 + left: "text-left", 203 + right: "text-right", 204 + center: "text-center", 205 + }[alignment]; 191 206 192 207 let [value, factID] = useYJSValue(props.entityID); 193 208 ··· 293 308 // forces break if a single text string (e.g. a url) spans more than a full line 294 309 style={{ wordBreak: "break-word" }} 295 310 className={` 311 + ${alignmentClass} 296 312 grow resize-none align-top whitespace-pre-wrap bg-transparent 297 313 outline-none 298 314 ${props.className}`}
+17 -6
components/Blocks/TextBlock/keymap.ts
··· 271 271 if (propsRef.current.previousBlock) { 272 272 focusBlock(propsRef.current.previousBlock, { type: "end" }); 273 273 } else { 274 - useUIState 275 - .getState() 276 - .setFocusedBlock({ 277 - entityType: "page", 278 - entityID: propsRef.current.parent, 279 - }); 274 + useUIState.getState().setFocusedBlock({ 275 + entityType: "page", 276 + entityID: propsRef.current.parent, 277 + }); 280 278 } 281 279 return true; 282 280 } ··· 497 495 entity: newEntityID, 498 496 attribute: "block/heading-level", 499 497 data: { type: "number", value: headingLevel.data.value || 0 }, 498 + }); 499 + } 500 + let alignment = await repRef.current?.query((tx) => 501 + scanIndex(tx).eav(propsRef.current.entityID, "block/text-alignment"), 502 + ); 503 + if (alignment?.[0] && alignment?.[0].data.value !== "left") { 504 + await repRef.current?.mutate.assertFact({ 505 + entity: newEntityID, 506 + attribute: "block/text-alignment", 507 + data: { 508 + type: "text-alignment-type-union", 509 + value: alignment?.[0].data.value, 510 + }, 500 511 }); 501 512 } 502 513 };
+58
components/Icons.tsx
··· 955 955 ); 956 956 }; 957 957 958 + export const AlignLeftSmall = (props: Props) => { 959 + return ( 960 + <svg 961 + width="24" 962 + height="24" 963 + viewBox="0 0 24 24" 964 + fill="none" 965 + xmlns="http://www.w3.org/2000/svg" 966 + {...props} 967 + > 968 + <path 969 + fillRule="evenodd" 970 + clipRule="evenodd" 971 + d="M3.5 6.19983C3.5 5.64754 3.94772 5.19983 4.5 5.19983H19.6547C20.2069 5.19983 20.6547 5.64754 20.6547 6.19983C20.6547 6.75211 20.2069 7.19983 19.6547 7.19983H4.5C3.94772 7.19983 3.5 6.75211 3.5 6.19983ZM3.5 12C3.5 11.4477 3.94772 11 4.5 11H12.8243C13.3766 11 13.8243 11.4477 13.8243 12C13.8243 12.5523 13.3766 13 12.8243 13H4.5C3.94772 13 3.5 12.5523 3.5 12ZM4.5 16.7999C3.94772 16.7999 3.5 17.2476 3.5 17.7999C3.5 18.3522 3.94772 18.7999 4.5 18.7999H16.1426C16.6949 18.7999 17.1426 18.3522 17.1426 17.7999C17.1426 17.2476 16.6949 16.7999 16.1426 16.7999H4.5Z" 972 + fill="currentColor" 973 + /> 974 + </svg> 975 + ); 976 + }; 977 + export const AlignCenterSmall = (props: Props) => { 978 + return ( 979 + <svg 980 + width="24" 981 + height="24" 982 + viewBox="0 0 24 24" 983 + fill="none" 984 + xmlns="http://www.w3.org/2000/svg" 985 + {...props} 986 + > 987 + <path 988 + fillRule="evenodd" 989 + clipRule="evenodd" 990 + d="M3.5 6.19983C3.5 5.64754 3.94772 5.19983 4.5 5.19983H19.6547C20.2069 5.19983 20.6547 5.64754 20.6547 6.19983C20.6547 6.75211 20.2069 7.19983 19.6547 7.19983H4.5C3.94772 7.19983 3.5 6.75211 3.5 6.19983ZM6.91519 12C6.91519 11.4477 7.36291 11 7.91519 11H16.2395C16.7918 11 17.2395 11.4477 17.2395 12C17.2395 12.5523 16.7918 13 16.2395 13H7.91519C7.36291 13 6.91519 12.5523 6.91519 12ZM6.25601 16.7999C5.70373 16.7999 5.25601 17.2476 5.25601 17.7999C5.25601 18.3522 5.70373 18.7999 6.25601 18.7999H17.8987C18.4509 18.7999 18.8987 18.3522 18.8987 17.7999C18.8987 17.2476 18.4509 16.7999 17.8987 16.7999H6.25601Z" 991 + fill="currentColor" 992 + /> 993 + </svg> 994 + ); 995 + }; 996 + export const AlignRightSmall = (props: Props) => { 997 + return ( 998 + <svg 999 + width="24" 1000 + height="24" 1001 + viewBox="0 0 24 24" 1002 + fill="none" 1003 + xmlns="http://www.w3.org/2000/svg" 1004 + {...props} 1005 + > 1006 + <path 1007 + fillRule="evenodd" 1008 + clipRule="evenodd" 1009 + d="M3.5 6.19983C3.5 5.64754 3.94772 5.19983 4.5 5.19983H19.6547C20.2069 5.19983 20.6547 5.64754 20.6547 6.19983C20.6547 6.75211 20.2069 7.19983 19.6547 7.19983H4.5C3.94772 7.19983 3.5 6.75211 3.5 6.19983ZM10.3304 12C10.3304 11.4477 10.7781 11 11.3304 11H19.6547C20.2069 11 20.6547 11.4477 20.6547 12C20.6547 12.5523 20.2069 13 19.6547 13H11.3304C10.7781 13 10.3304 12.5523 10.3304 12ZM8.01202 16.7999C7.45974 16.7999 7.01202 17.2476 7.01202 17.7999C7.01202 18.3522 7.45974 18.7999 8.01202 18.7999H19.6547C20.2069 18.7999 20.6547 18.3522 20.6547 17.7999C20.6547 17.2476 20.2069 16.7999 19.6547 16.7999H8.01202Z" 1010 + fill="currentColor" 1011 + /> 1012 + </svg> 1013 + ); 1014 + }; 1015 + 958 1016 export const ListUnorderedSmall = (props: Props) => { 959 1017 return ( 960 1018 <svg
+74
components/Toolbar/TextAlignmentToolbar.tsx
··· 1 + import { useUIState } from "src/useUIState"; 2 + import { ToolbarButton } from "."; 3 + import { useCallback } from "react"; 4 + import { useEntity, useReplicache } from "src/replicache"; 5 + import { 6 + AlignCenterSmall, 7 + AlignLeftSmall, 8 + AlignRightSmall, 9 + } from "components/Icons"; 10 + 11 + export function TextAlignmentToolbar() { 12 + let focusedBlock = useUIState((s) => s.focusedEntity); 13 + let { rep } = useReplicache(); 14 + let setAlignment = useCallback( 15 + (alignment: "right" | "center" | "left") => { 16 + if (focusedBlock?.entityType === "page" || !focusedBlock) return null; 17 + rep?.mutate.assertFact({ 18 + entity: focusedBlock?.entityID, 19 + attribute: "block/text-alignment", 20 + data: { type: "text-alignment-type-union", value: alignment }, 21 + }); 22 + }, 23 + [focusedBlock, rep], 24 + ); 25 + return ( 26 + <> 27 + <ToolbarButton 28 + onClick={() => setAlignment("left")} 29 + tooltipContent="Align Text Left" 30 + > 31 + <AlignLeftSmall /> 32 + </ToolbarButton> 33 + <ToolbarButton 34 + onClick={() => setAlignment("center")} 35 + tooltipContent="Align Text Center" 36 + > 37 + <AlignCenterSmall /> 38 + </ToolbarButton> 39 + <ToolbarButton 40 + onClick={() => setAlignment("right")} 41 + tooltipContent="Align Text Right" 42 + > 43 + <AlignRightSmall /> 44 + </ToolbarButton> 45 + </> 46 + ); 47 + } 48 + 49 + export function TextAlignmentButton(props: { 50 + setToolbarState: (s: "text-alignment") => void; 51 + className?: string; 52 + }) { 53 + let focusedBlock = useUIState((s) => s.focusedEntity); 54 + let alignment = 55 + useEntity(focusedBlock?.entityID || null, "block/text-alignment")?.data 56 + .value || "left"; 57 + return ( 58 + <ToolbarButton 59 + tooltipContent={<div>Text Size</div>} 60 + className={`${props.className}`} 61 + onClick={() => { 62 + props.setToolbarState("text-alignment"); 63 + }} 64 + > 65 + {alignment === "left" ? ( 66 + <AlignLeftSmall /> 67 + ) : alignment === "center" ? ( 68 + <AlignCenterSmall /> 69 + ) : ( 70 + <AlignRightSmall /> 71 + )} 72 + </ToolbarButton> 73 + ); 74 + }
+2
components/Toolbar/TextToolbar.tsx
··· 9 9 import { HighlightButton } from "./HighlightToolbar"; 10 10 import { ToolbarTypes } from "."; 11 11 import { schema } from "components/Blocks/TextBlock/schema"; 12 + import { TextAlignmentButton } from "./TextAlignmentToolbar"; 12 13 13 14 export const TextToolbar = (props: { 14 15 lastUsedHighlight: string; ··· 72 73 /> 73 74 <Separator classname="h-6" /> 74 75 <TextBlockTypeButton setToolbarState={props.setToolbarState} /> 76 + <TextAlignmentButton setToolbarState={props.setToolbarState} /> 75 77 <Separator classname="h-6" /> 76 78 <ListButton setToolbarState={props.setToolbarState} /> 77 79 <Separator classname="h-6" />
+6 -10
components/Toolbar/index.tsx
··· 18 18 import { focusPage } from "components/Pages"; 19 19 import { AreYouSure, deleteBlock } from "components/Blocks/DeleteBlock"; 20 20 import { TooltipButton } from "components/Buttons"; 21 + import { TextAlignmentToolbar } from "./TextAlignmentToolbar"; 21 22 22 23 export type ToolbarTypes = 23 24 | "areYouSure" ··· 25 26 | "highlight" 26 27 | "link" 27 28 | "heading" 29 + | "text-alignment" 28 30 | "list" 29 31 | "linkBlock" 30 32 | "block" ··· 109 111 /> 110 112 ) : toolbarState === "heading" ? ( 111 113 <TextBlockTypeToolbar onClose={() => setToolbarState("default")} /> 114 + ) : toolbarState === "text-alignment" ? ( 115 + <TextAlignmentToolbar /> 112 116 ) : toolbarState === "block" ? ( 113 - <BlockToolbar 114 - setToolbarState={(state) => { 115 - setToolbarState(state); 116 - }} 117 - /> 117 + <BlockToolbar setToolbarState={setToolbarState} /> 118 118 ) : toolbarState === "multiselect" ? ( 119 - <MultiselectToolbar 120 - setToolbarState={(state) => { 121 - setToolbarState(state); 122 - }} 123 - /> 119 + <MultiselectToolbar setToolbarState={setToolbarState} /> 124 120 ) : toolbarState === "areYouSure" ? ( 125 121 <AreYouSure 126 122 compact
+8
src/replicache/attributes.ts
··· 48 48 type: "boolean", 49 49 cardinality: "one", 50 50 }, 51 + "block/text-alignment": { 52 + type: "text-alignment-type-union", 53 + cardinality: "one", 54 + }, 51 55 "block/text": { 52 56 type: "text", 53 57 cardinality: "one", ··· 206 210 value: string; 207 211 }; 208 212 reference: { type: "reference"; value: string }; 213 + "text-alignment-type-union": { 214 + type: "text-alignment-type-union"; 215 + value: "right" | "left" | "center"; 216 + }; 209 217 "page-type-union": { type: "page-type-union"; value: "doc" | "canvas" }; 210 218 "block-type-union": { 211 219 type: "block-type-union";
+3 -14
src/replicache/clientMutationContext.ts
··· 1 1 import { WriteTransaction } from "replicache"; 2 2 import * as Y from "yjs"; 3 3 import * as base64 from "base64-js"; 4 - import { FactWithIndexes } from "./utils"; 4 + import { FactWithIndexes, scanIndex } from "./utils"; 5 5 import { Attributes, FilterAttributes } from "./attributes"; 6 6 import { Fact } from "."; 7 7 import { MutationContext } from "./mutations"; ··· 21 21 }, 22 22 scanIndex: { 23 23 async eav(entity, attribute) { 24 - let existingFact = await tx 25 - .scan<Fact<typeof attribute>>({ 26 - indexName: "eav", 27 - prefix: attribute ? `${entity}-${attribute}` : entity, 28 - }) 29 - .toArray(); 30 - return existingFact; 24 + return scanIndex(tx).eav(entity, attribute); 31 25 }, 32 26 }, 33 27 async assertFact(f) { ··· 36 30 let id = f.id || v7(); 37 31 let data = { ...f.data }; 38 32 if (attribute.cardinality === "one") { 39 - let existingFact = await tx 40 - .scan<Fact<typeof f.attribute>>({ 41 - indexName: "eav", 42 - prefix: `${f.entity}-${f.attribute}`, 43 - }) 44 - .toArray(); 33 + let existingFact = await scanIndex(tx).eav(f.entity, f.attribute); 45 34 if (existingFact[0]) { 46 35 id = existingFact[0].id; 47 36 if (attribute.type === "text") {