a tool for shared writing and social publishing
0

Configure Feed

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

use styling from previous branch

Jared Pereira (Mar 14, 2026, 9:36 PM EDT) b76eeafe 38239bec

+408 -183
+185 -149
app/[leaflet_id]/actions/ShareOptions/DomainOptions.tsx
··· 15 15 import { useReplicache } from "src/replicache"; 16 16 import { AddDomainForm } from "components/Domains/AddDomainForm"; 17 17 import { DomainSettingsView } from "components/Domains/DomainSettingsView"; 18 - import { AddTiny } from "components/Icons/AddTiny"; 19 18 import { DotLoader } from "components/utils/DotLoader"; 19 + import { GoToArrow } from "components/Icons/GoToArrow"; 20 + import { LoadingTiny } from "components/Icons/LoadingTiny"; 21 + import { UnlinkTiny } from "components/Icons/UnlinkTiny"; 22 + import Link from "next/link"; 20 23 21 24 type DomainMenuState = 22 25 | { state: "default" } ··· 73 76 }) => { 74 77 let { data: domains, mutate: mutateDomains } = useLeafletDomains(); 75 78 let [selectedDomain, setSelectedDomain] = useState<string | undefined>( 76 - domains?.[0]?.domain, 77 - ); 78 - let [selectedRoute, setSelectedRoute] = useState( 79 - domains?.[0]?.route.slice(1) || "", 79 + undefined, 80 80 ); 81 + let [selectedRoute, setSelectedRoute] = useState(""); 81 82 let { identity, mutate: mutateIdentity } = useIdentityData(); 82 83 let { permission_token } = useReplicache(); 83 84 ··· 85 86 let toaster = useToaster(); 86 87 let publishLink = useReadOnlyShareLink(); 87 88 88 - // Filter out domains assigned to publications — they can't be used for leaflets 89 - let availableDomains = (identity?.custom_domains || []).filter( 89 + // Filter out domains assigned to publications 90 + let allDomains = (identity?.custom_domains || []).filter( 90 91 (d: CustomDomain) => d.publication_domains.length === 0, 91 92 ); 92 93 94 + // Categorize domains 95 + let linkedDomains = allDomains.filter((d: CustomDomain) => 96 + d.custom_domain_routes.some( 97 + (r) => r.edit_permission_token === permission_token.id, 98 + ), 99 + ); 100 + let pendingDomainsList: CustomDomain[] = []; 101 + let availableDomainsList: CustomDomain[] = []; 102 + 103 + // We'll categorize in the render since pending requires a hook per domain 104 + let nonLinkedDomains = allDomains.filter( 105 + (d: CustomDomain) => 106 + !d.custom_domain_routes.some( 107 + (r) => r.edit_permission_token === permission_token.id, 108 + ), 109 + ); 110 + 93 111 // Check if the selected route is already assigned to a different leaflet 94 112 let routeConflict = (() => { 95 113 if (!selectedDomain) return false; 96 - let domain = availableDomains.find( 114 + let domain = allDomains.find( 97 115 (d: CustomDomain) => d.domain === selectedDomain, 98 116 ); 99 117 if (!domain) return false; ··· 110 128 setLoading(true); 111 129 let route = "/" + selectedRoute; 112 130 113 - // Optimistic update 114 131 mutateIdentityData(mutateIdentity, (draft) => { 115 132 let domain = draft.custom_domains.find( 116 133 (d) => d.domain === selectedDomain, 117 134 ); 118 135 if (domain) { 119 - // Remove existing route for this leaflet, add new one 120 136 domain.custom_domain_routes = [ 121 137 ...domain.custom_domain_routes.filter( 122 138 (r) => r.edit_permission_token !== permission_token.id, ··· 160 176 } 161 177 162 178 return ( 163 - <div className="px-3 py-1 flex flex-col gap-3 max-w-full w-[600px]"> 164 - <h3 className="text-secondary">Choose a Domain</h3> 165 - <div className="flex flex-col gap-1 text-secondary"> 166 - {availableDomains.map((domain: CustomDomain) => { 167 - return ( 168 - <DomainOption 169 - selectedRoute={selectedRoute} 170 - setSelectedRoute={setSelectedRoute} 171 - key={domain.domain} 172 - domain={domain} 173 - checked={selectedDomain === domain.domain} 174 - setChecked={(d) => setSelectedDomain(d)} 175 - setDomainMenuState={props.setDomainMenuState} 176 - /> 177 - ); 178 - })} 179 + <div className="px-3 py-1 flex flex-col gap-2 max-w-full w-[600px]"> 180 + <div className="flex justify-between"> 181 + <h4>Choose a Domain</h4> 179 182 <button 180 - onMouseDown={() => { 181 - props.setDomainMenuState({ state: "add-domain" }); 182 - }} 183 - className="text-accent-contrast flex gap-2 items-center px-1 py-0.5" 183 + className="text-accent-contrast rotate-180" 184 + onClick={() => props.setShareMenuState("default")} 184 185 > 185 - <AddTiny /> Add a New Domain 186 + <GoToArrow /> 186 187 </button> 187 188 </div> 189 + <hr className="border-border-light -mx-3" /> 190 + <div className="flex flex-col gap-3 text-secondary"> 191 + {linkedDomains.length > 0 && ( 192 + <div className="flex flex-col gap-0.5"> 193 + <strong className="text-sm">Currently Linked</strong> 194 + {linkedDomains.map((domain: CustomDomain) => ( 195 + <LinkedDomainOption 196 + key={domain.domain} 197 + domain={domain} 198 + permission_token_id={permission_token.id} 199 + onUnlink={async (routeId: string) => { 200 + mutateIdentityData(mutateIdentity, (draft) => { 201 + let d = draft.custom_domains.find( 202 + (dd) => dd.domain === domain.domain, 203 + ); 204 + if (d) { 205 + d.custom_domain_routes = 206 + d.custom_domain_routes.filter( 207 + (r) => r.id !== routeId, 208 + ); 209 + } 210 + }); 211 + mutateDomains(); 212 + toaster({ 213 + content: ( 214 + <div className="font-bold"> 215 + Unpublished from custom domain! 216 + </div> 217 + ), 218 + type: "success", 219 + }); 220 + await removeDomainRoute({ routeId }); 221 + }} 222 + /> 223 + ))} 224 + </div> 225 + )} 226 + {nonLinkedDomains.length > 0 && ( 227 + <NonLinkedDomains 228 + domains={nonLinkedDomains} 229 + selectedDomain={selectedDomain} 230 + setSelectedDomain={setSelectedDomain} 231 + selectedRoute={selectedRoute} 232 + setSelectedRoute={setSelectedRoute} 233 + setDomainMenuState={props.setDomainMenuState} 234 + /> 235 + )} 236 + <div className="text-sm text-tertiary"> 237 + Add or delete domains from your profile on{" "} 238 + <Link href="/home" className="text-accent-contrast"> 239 + home 240 + </Link> 241 + </div> 242 + </div> 188 243 189 244 {routeConflict && ( 190 245 <p className="text-error text-sm">Route already assigned</p> 191 246 )} 192 - <div className="flex gap-3 items-center justify-end"> 193 - {domains?.[0] && ( 194 - <button 195 - onMouseDown={async () => { 196 - let route = domains[0]; 197 - // Optimistic update 198 - mutateIdentityData(mutateIdentity, (draft) => { 199 - let domain = draft.custom_domains.find( 200 - (d) => d.domain === route.domain, 201 - ); 202 - if (domain) { 203 - domain.custom_domain_routes = 204 - domain.custom_domain_routes.filter( 205 - (r) => r.edit_permission_token !== permission_token.id, 206 - ); 207 - } 208 - }); 209 - mutateDomains(); 210 - props.setShareMenuState("default"); 211 - toaster({ 212 - content: ( 213 - <div className="font-bold"> 214 - Unpublished from custom domain! 215 - </div> 216 - ), 217 - type: "success", 218 - }); 219 - await removeDomainRoute({ routeId: route.id }); 220 - }} 221 - > 222 - Unpublish 223 - </button> 224 - )} 247 + <ButtonPrimary 248 + id="publish-to-domain" 249 + className="place-self-end" 250 + disabled={loading || routeConflict || !selectedDomain} 251 + onClick={doPublish} 252 + > 253 + {loading ? <DotLoader /> : "Publish!"} 254 + </ButtonPrimary> 255 + </div> 256 + ); 257 + }; 225 258 226 - <ButtonPrimary 227 - id="publish-to-domain" 228 - disabled={ 229 - loading || 230 - routeConflict || 231 - (domains?.[0] 232 - ? domains[0].domain === selectedDomain && 233 - domains[0].route.slice(1) === selectedRoute 234 - : !selectedDomain) 235 - } 236 - onClick={doPublish} 237 - > 238 - {loading ? <DotLoader /> : "Publish!"} 239 - </ButtonPrimary> 259 + const LinkedDomainOption = (props: { 260 + domain: CustomDomain; 261 + permission_token_id: string; 262 + onUnlink: (routeId: string) => void; 263 + }) => { 264 + let route = props.domain.custom_domain_routes.find( 265 + (r) => r.edit_permission_token === props.permission_token_id, 266 + ); 267 + if (!route) return null; 268 + 269 + return ( 270 + <div className="text-sm flex justify-between px-1 py-0.5 bg-accent-1 text-accent-2 rounded-md"> 271 + <div className="font-bold w-full truncate"> 272 + {props.domain.domain} 273 + <span className="font-normal">{route.route}</span> 240 274 </div> 275 + <button onClick={() => props.onUnlink(route.id)}> 276 + <UnlinkTiny /> 277 + </button> 241 278 </div> 242 279 ); 243 280 }; 244 281 245 - const DomainOption = (props: { 282 + function NonLinkedDomains(props: { 283 + domains: CustomDomain[]; 284 + selectedDomain: string | undefined; 285 + setSelectedDomain: (d: string) => void; 246 286 selectedRoute: string; 247 - setSelectedRoute: (s: string) => void; 248 - checked: boolean; 249 - setChecked: (checked: string) => void; 287 + setSelectedRoute: (r: string) => void; 288 + setDomainMenuState: (state: DomainMenuState) => void; 289 + }) { 290 + // Separate pending vs available using hooks rendered per-domain 291 + return ( 292 + <> 293 + {props.domains.map((domain) => ( 294 + <NonLinkedDomainRow 295 + key={domain.domain} 296 + domain={domain} 297 + selectedDomain={props.selectedDomain} 298 + setSelectedDomain={props.setSelectedDomain} 299 + selectedRoute={props.selectedRoute} 300 + setSelectedRoute={props.setSelectedRoute} 301 + setDomainMenuState={props.setDomainMenuState} 302 + /> 303 + ))} 304 + </> 305 + ); 306 + } 307 + 308 + function NonLinkedDomainRow(props: { 250 309 domain: CustomDomain; 310 + selectedDomain: string | undefined; 311 + setSelectedDomain: (d: string) => void; 312 + selectedRoute: string; 313 + setSelectedRoute: (r: string) => void; 251 314 setDomainMenuState: (state: DomainMenuState) => void; 252 - }) => { 253 - let [value, setValue] = useState(""); 315 + }) { 254 316 let { pending } = useDomainStatus(props.domain.domain); 255 317 318 + if (pending) { 319 + return ( 320 + <div className="text-sm flex px-1 py-0.5 border block-border !rounded-md text-secondary"> 321 + <div className="font-bold">{props.domain.domain}/</div> 322 + <Input 323 + disabled 324 + className="w-full bg-transparent appearance-none focus:!outline-none" 325 + placeholder="" 326 + /> 327 + <button 328 + className="text-accent-contrast text-sm" 329 + onMouseDown={() => 330 + props.setDomainMenuState({ 331 + state: "domain-settings", 332 + domain: props.domain.domain, 333 + }) 334 + } 335 + > 336 + <LoadingTiny className="animate-spin text-accent-contrast" /> 337 + </button> 338 + </div> 339 + ); 340 + } 341 + 342 + let isSelected = props.selectedDomain === props.domain.domain; 343 + 256 344 return ( 257 - <label htmlFor={props.domain.domain}> 258 - <input 259 - type="radio" 260 - name={props.domain.domain} 261 - id={props.domain.domain} 262 - value={props.domain.domain} 263 - checked={props.checked} 264 - className="hidden appearance-none" 265 - onChange={() => { 266 - if (pending) return; 267 - props.setChecked(props.domain.domain); 268 - }} 345 + <div 346 + className={`text-sm flex px-1 py-0.5 border block-border !rounded-md cursor-pointer ${isSelected ? "!outline-accent-contrast !border-accent-contrast" : ""}`} 347 + onClick={() => props.setSelectedDomain(props.domain.domain)} 348 + > 349 + <div className="font-bold">{props.domain.domain}/</div> 350 + <Input 351 + value={isSelected ? props.selectedRoute : ""} 352 + onChange={(e) => props.setSelectedRoute(e.currentTarget.value)} 353 + className="w-full bg-transparent appearance-none focus:!outline-none" 354 + placeholder={isSelected ? "optional path" : ""} 355 + onFocus={() => props.setSelectedDomain(props.domain.domain)} 269 356 /> 270 - <div 271 - className={` 272 - px-[6px] py-1 273 - flex 274 - border rounded-md 275 - ${ 276 - pending 277 - ? "border-border-light text-secondary justify-between gap-2 items-center " 278 - : !props.checked 279 - ? "flex-wrap border-border-light" 280 - : "flex-wrap border-accent-1 bg-accent-1 text-accent-2 font-bold" 281 - } `} 282 - > 283 - <div className="flex justify-between w-full items-center"> 284 - <div className={`w-max truncate ${pending ? "animate-pulse" : ""}`}> 285 - {props.domain.domain} 286 - </div> 287 - </div> 288 - {props.checked && ( 289 - <div className="flex gap-0 w-full"> 290 - <span 291 - className="font-normal" 292 - style={value === "" ? { opacity: "0.5" } : {}} 293 - > 294 - / 295 - </span> 296 - 297 - <Input 298 - type="text" 299 - autoFocus 300 - className="appearance-none focus:outline-hidden font-normal text-accent-2 w-full bg-transparent placeholder:text-accent-2 placeholder:opacity-50" 301 - placeholder="add-optional-path" 302 - onChange={(e) => props.setSelectedRoute(e.target.value)} 303 - value={props.selectedRoute} 304 - /> 305 - </div> 306 - )} 307 - {pending && ( 308 - <button 309 - className="text-accent-contrast text-sm" 310 - onMouseDown={() => { 311 - props.setDomainMenuState({ 312 - state: "domain-settings", 313 - domain: props.domain.domain, 314 - }); 315 - }} 316 - > 317 - pending 318 - </button> 319 - )} 320 - </div> 321 - </label> 357 + </div> 322 358 ); 323 - }; 359 + }
+4 -3
components/ActionBar/ProfileButton.tsx
··· 13 13 import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal"; 14 14 import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription"; 15 15 import { ManageDomains } from "components/Domains/ManageDomains"; 16 + import { WebSmall } from "components/Icons/WebSmall"; 16 17 import { useIsPro, useCanSeePro } from "src/hooks/useEntitlement"; 17 18 import { useState } from "react"; 18 19 ··· 104 105 )} 105 106 106 107 <button 107 - className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!" 108 + className="menuItem -mx-[8px] text-left flex items-center gap-2 hover:no-underline!" 108 109 type="button" 109 110 onClick={() => setState("manage-domains")} 110 111 > 111 - My Domains 112 - <ArrowRightTiny /> 112 + <WebSmall /> 113 + Connected Domains 113 114 </button> 114 115 115 116 <hr className="border-border-light border-dashed" />
+141 -26
components/Domains/DomainList.tsx
··· 1 1 "use client"; 2 - import { useIdentityData, Identity } from "components/IdentityProvider"; 2 + import { useState } from "react"; 3 + import { useIdentityData } from "components/IdentityProvider"; 3 4 import { useDomainStatus } from "./useDomainStatus"; 4 - import { getDomainAssignment, describeAssignment } from "./domainAssignment"; 5 + import { getDomainAssignment } from "./domainAssignment"; 5 6 import { AddTiny } from "components/Icons/AddTiny"; 7 + import { UnlinkTiny } from "components/Icons/UnlinkTiny"; 8 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 9 + import { useToaster } from "components/Toast"; 10 + import { Separator } from "components/Layout"; 11 + import { Identity } from "components/IdentityProvider"; 6 12 7 13 export type CustomDomain = NonNullable<Identity>["custom_domains"][number]; 8 14 ··· 15 21 let domains = identity?.custom_domains || []; 16 22 if (props.filter) domains = domains.filter(props.filter); 17 23 24 + let pubDomains = domains.filter( 25 + (d) => getDomainAssignment(d).type === "publication", 26 + ); 27 + let leafletDomains = domains.filter( 28 + (d) => getDomainAssignment(d).type === "document", 29 + ); 30 + let unassignedDomains = domains.filter( 31 + (d) => getDomainAssignment(d).type === "unassigned", 32 + ); 33 + 18 34 return ( 19 - <div className="flex flex-col gap-1 text-secondary"> 20 - {domains.map((domain) => ( 21 - <DomainRow 22 - key={domain.domain} 23 - domain={domain} 24 - onSelect={() => props.onSelectDomain(domain.domain)} 25 - /> 26 - ))} 35 + <div className="flex flex-col gap-2 text-secondary"> 36 + {pubDomains.length > 0 && ( 37 + <div className="flex flex-col gap-0.5"> 38 + <div className="font-bold -mb-1 text-secondary">Publications</div> 39 + {pubDomains.map((domain) => ( 40 + <DomainGroup 41 + key={domain.domain} 42 + domain={domain} 43 + onSelect={() => props.onSelectDomain(domain.domain)} 44 + showBase={false} 45 + /> 46 + ))} 47 + </div> 48 + )} 49 + {pubDomains.length > 0 && leafletDomains.length > 0 && ( 50 + <hr className="border-border-light" /> 51 + )} 52 + {leafletDomains.length > 0 && ( 53 + <div className="flex flex-col gap-0.5"> 54 + <div className="font-bold -mb-1 text-secondary">Leaflets</div> 55 + {leafletDomains.map((domain) => ( 56 + <DomainGroup 57 + key={domain.domain} 58 + domain={domain} 59 + onSelect={() => props.onSelectDomain(domain.domain)} 60 + showBase 61 + /> 62 + ))} 63 + </div> 64 + )} 65 + {unassignedDomains.length > 0 && ( 66 + <> 67 + {(pubDomains.length > 0 || leafletDomains.length > 0) && ( 68 + <hr className="border-border-light" /> 69 + )} 70 + <div className="flex flex-col gap-0.5"> 71 + <div className="font-bold -mb-1 text-secondary">Unassigned</div> 72 + {unassignedDomains.map((domain) => ( 73 + <UnassignedDomainRow 74 + key={domain.domain} 75 + domain={domain} 76 + onSelect={() => props.onSelectDomain(domain.domain)} 77 + /> 78 + ))} 79 + </div> 80 + </> 81 + )} 27 82 <button 28 83 onMouseDown={() => props.onAddDomain()} 29 - className="text-accent-contrast flex gap-2 items-center px-1 py-0.5" 84 + className="text-accent-contrast font-bold text-sm flex gap-2 items-center px-1 py-0.5" 30 85 type="button" 31 86 > 32 87 <AddTiny /> Add a New Domain ··· 35 90 ); 36 91 } 37 92 38 - function DomainRow(props: { domain: CustomDomain; onSelect: () => void }) { 93 + function DomainGroup(props: { 94 + domain: CustomDomain; 95 + onSelect: () => void; 96 + showBase: boolean; 97 + }) { 39 98 let { pending } = useDomainStatus(props.domain.domain); 99 + let assignment = getDomainAssignment(props.domain); 40 100 41 - let assignment = getDomainAssignment(props.domain); 42 - let assignmentLabel = describeAssignment(assignment); 101 + return ( 102 + <div className="flex flex-col gap-0.5"> 103 + {props.showBase && ( 104 + <button 105 + type="button" 106 + className="text-secondary font-bold text-sm text-left" 107 + onClick={props.onSelect} 108 + > 109 + {props.domain.domain} 110 + {pending && ( 111 + <span className="text-accent-contrast text-xs ml-2 font-normal"> 112 + unverified 113 + </span> 114 + )} 115 + </button> 116 + )} 117 + {assignment.type === "publication" && ( 118 + <SubDomainRow 119 + path="/" 120 + label={props.domain.domain} 121 + onSelect={props.onSelect} 122 + isPub 123 + /> 124 + )} 125 + {props.domain.custom_domain_routes.map((route) => ( 126 + <SubDomainRow 127 + key={route.id} 128 + path={route.route} 129 + label={route.route} 130 + onSelect={props.onSelect} 131 + /> 132 + ))} 133 + </div> 134 + ); 135 + } 136 + 137 + function SubDomainRow(props: { 138 + path: string; 139 + label: string; 140 + onSelect: () => void; 141 + isPub?: boolean; 142 + }) { 143 + return ( 144 + <button 145 + type="button" 146 + className="opaque-container py-0.5 px-1 flex gap-1 items-center w-full h-[28px] text-left" 147 + onClick={props.onSelect} 148 + > 149 + <div className="grow flex gap-2 items-center w-full truncate"> 150 + <span className="text-sm flex-shrink-0"> 151 + {props.isPub ? props.label : props.path} 152 + </span> 153 + </div> 154 + </button> 155 + ); 156 + } 157 + 158 + function UnassignedDomainRow(props: { 159 + domain: CustomDomain; 160 + onSelect: () => void; 161 + }) { 162 + let { pending } = useDomainStatus(props.domain.domain); 43 163 44 164 return ( 45 165 <button 46 166 type="button" 47 - className="px-[6px] py-1 flex border rounded-md border-border-light justify-between gap-2 items-center hover:border-accent-1 text-left w-full" 48 - onMouseDown={() => props.onSelect()} 167 + className="px-1 py-0.5 flex justify-between gap-2 items-center text-left w-full text-sm" 168 + onClick={props.onSelect} 49 169 > 50 - <div className={`truncate ${pending ? "animate-pulse" : ""}`}> 170 + <span className={pending ? "animate-pulse" : ""}> 51 171 {props.domain.domain} 52 - </div> 53 - <div className="flex gap-2 items-center shrink-0"> 54 - {assignmentLabel && ( 55 - <span className="text-xs text-tertiary">{assignmentLabel}</span> 56 - )} 57 - {pending && ( 58 - <span className="text-accent-contrast text-sm">unverified</span> 59 - )} 60 - </div> 172 + </span> 173 + {pending && ( 174 + <span className="text-accent-contrast text-xs">unverified</span> 175 + )} 61 176 </button> 62 177 ); 63 178 }
+11 -5
components/Domains/ManageDomains.tsx
··· 4 4 import { DomainList } from "./DomainList"; 5 5 import { AddDomainForm } from "./AddDomainForm"; 6 6 import { DomainSettingsView } from "./DomainSettingsView"; 7 + import { GoToArrow } from "components/Icons/GoToArrow"; 7 8 8 9 type State = 9 10 | "list" ··· 40 41 } 41 42 42 43 return ( 43 - <div> 44 - <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1 flex-shrink-0"> 45 - My Domains 46 - <button type="button" onClick={props.backToMenu}> 47 - <GoBackSmall className="text-accent-contrast" /> 44 + <div className="flex flex-col gap-2 px-3 pt-1 pb-3"> 45 + <div className="flex justify-between"> 46 + <h4>Connected Domains</h4> 47 + <button 48 + className="text-accent-contrast rotate-180" 49 + onClick={props.backToMenu} 50 + type="button" 51 + > 52 + <GoToArrow /> 48 53 </button> 49 54 </div> 55 + <hr className="border-border-light -mx-3" /> 50 56 <DomainList 51 57 onSelectDomain={(domain) => 52 58 setState({ type: "domain-settings", domain })
+18
components/Icons/UnlinkTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + export const UnlinkTiny = (props: Props) => { 3 + return ( 4 + <svg 5 + width="16" 6 + height="16" 7 + viewBox="0 0 16 16" 8 + fill="none" 9 + xmlns="http://www.w3.org/2000/svg" 10 + {...props} 11 + > 12 + <path 13 + d="M8.73287 4.06775C10.1751 2.55464 12.5709 2.49555 14.0854 3.93689C15.6002 5.37888 15.6589 7.77631 14.2172 9.29138L12.434 11.1654C11.5058 12.1407 10.1815 12.5112 8.95162 12.2679L8.74752 12.7855V12.7875C7.97434 14.7306 5.77189 15.6793 3.82857 14.9066C1.88511 14.1334 0.935308 11.9302 1.70846 9.9867L2.37252 8.31873C2.5257 7.93407 2.96237 7.74583 3.34713 7.89881C3.73156 8.05194 3.91963 8.48783 3.76705 8.87244L3.10299 10.5414C2.63611 11.715 3.20883 13.045 4.38228 13.5121C5.55595 13.9789 6.88594 13.4063 7.35299 12.2328L7.56685 11.6908C7.39668 11.5753 7.23345 11.4447 7.07955 11.2982C6.50753 10.7538 6.14207 10.0703 5.98775 9.35095C5.90116 8.94609 6.15906 8.54716 6.56393 8.46033C6.96868 8.37377 7.36759 8.63181 7.45455 9.0365C7.54761 9.4702 7.76708 9.8814 8.11373 10.2113C9.02873 11.0819 10.4763 11.0459 11.3471 10.1312L13.1313 8.2572C14.0017 7.34226 13.9657 5.89465 13.0512 5.02381C12.1362 4.15297 10.6878 4.18901 9.81685 5.10388L9.8149 5.10584L8.75826 6.20642C9.41841 6.81875 9.8375 7.64617 9.94478 8.52966C9.99466 8.94079 9.70156 9.31463 9.29049 9.36463C8.87944 9.41428 8.50539 9.1214 8.45553 8.71033C8.35757 7.90421 7.83428 7.17789 7.02975 6.85779C6.82583 6.7767 6.61766 6.72718 6.41158 6.70642C5.9997 6.6647 5.69922 6.2971 5.74068 5.88513C5.78232 5.47328 6.15007 5.17293 6.56197 5.21424C6.85699 5.24394 7.15221 5.31037 7.44185 5.41248L8.73287 4.06775ZM1.91646 0.929079C2.10216 0.785149 2.3631 0.790118 2.54342 0.940798L4.59908 2.65759L5.47799 1.44177C5.59959 1.27334 5.81354 1.1974 6.01412 1.25134C6.2147 1.30555 6.36189 1.47815 6.38228 1.68494L6.59908 3.89685C6.626 4.17132 6.42616 4.41531 6.15182 4.44275C5.87704 4.46982 5.63207 4.26926 5.60494 3.99451L5.5151 3.09705L5.09713 3.67908C5.01596 3.7915 4.89094 3.86489 4.75338 3.8822C4.61577 3.89945 4.47701 3.85879 4.37057 3.7699L3.66451 3.17908L4.27486 4.4574C4.3406 4.59485 4.34053 4.75495 4.27389 4.89197C4.20723 5.02892 4.08173 5.12836 3.93307 5.1615L2.90475 5.39002L4.4565 6.33142C4.68707 6.48339 4.75105 6.79324 4.59908 7.02381C4.44708 7.25418 4.13719 7.31824 3.9067 7.16638L1.36861 5.57654C1.2014 5.46602 1.11658 5.26591 1.15279 5.06873C1.1891 4.87149 1.33989 4.71501 1.5356 4.67127L3.10103 4.32068L1.77096 1.53943C1.66983 1.32742 1.73098 1.07325 1.91646 0.929079Z" 14 + fill="currentColor" 15 + /> 16 + </svg> 17 + ); 18 + };
+19
components/Icons/WebSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const WebSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="25" 8 + viewBox="0 0 24 25" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M12.2546 2.08102C14.3608 0.971554 16.5868 0.630551 18.2985 1.72946C20.1168 2.89726 20.7002 5.2651 20.4596 7.83199C20.2499 10.0691 19.4078 12.6314 17.9968 15.1484C18.8353 14.6474 19.5703 14.106 20.1784 13.5429C21.7885 12.0519 22.3631 10.5853 22.0475 9.44332C21.922 8.98963 21.6583 8.57412 21.2507 8.20309C20.9444 7.92439 20.9213 7.44892 21.1999 7.14254C21.4786 6.83623 21.9541 6.81401 22.2604 7.09274C22.8483 7.62765 23.2829 8.28082 23.4938 9.04391C24.0351 11.0027 22.9547 13.0156 21.1979 14.6425C19.9638 15.7854 18.3172 16.8196 16.4001 17.6318C14.7029 19.9493 12.7311 21.7713 10.7917 22.8886C8.55782 24.1754 6.16195 24.6309 4.34344 23.4638C3.03938 22.6265 2.36964 21.1586 2.18523 19.4785C2.14035 19.067 2.43775 18.6965 2.8493 18.6513C3.26076 18.6065 3.63116 18.904 3.67644 19.3154C3.83121 20.7248 4.3608 21.6928 5.15398 22.2021C6.25946 22.9112 7.98241 22.7757 10.0427 21.5888C11.2499 20.8934 12.5017 19.8692 13.6911 18.5742C13.6829 18.5764 13.6748 18.5797 13.6667 18.582C10.5778 19.4358 7.56627 19.5727 5.18133 19.0703C2.83847 18.5765 0.878193 17.4029 0.3366 15.4443C-0.180497 13.5725 0.784999 11.6495 2.39617 10.0703C2.69197 9.78036 3.16677 9.78526 3.45672 10.081C3.48313 10.108 3.50649 10.1373 3.52801 10.167C4.49687 7.38475 6.80796 5.13241 9.88152 4.37399C13.0016 3.60426 16.1432 4.56206 18.2966 6.6357C18.5944 6.92307 18.6023 7.39806 18.3151 7.69625C18.0278 7.99403 17.5537 8.00277 17.2555 7.71578C16.2812 6.77754 15.0653 6.11646 13.7429 5.80758C13.7217 5.86594 13.6901 5.92162 13.6462 5.97067C13.4617 6.1761 13.1446 6.19325 12.9391 6.00875C12.6345 5.73517 12.3171 5.63011 11.946 5.61031C11.3826 5.61791 10.8113 5.68933 10.2409 5.83004C9.80842 5.93676 9.39465 6.08104 9.00066 6.25485C9.00584 6.31624 9.00112 6.37985 8.98211 6.44235C8.70725 7.34445 8.79682 8.395 9.2409 9.48043C10.4569 8.5539 11.6356 8.05851 12.9186 7.79977C13.1891 7.74567 13.452 7.92094 13.5065 8.19137C13.5608 8.46176 13.3862 8.72551 13.1159 8.78024C11.9445 9.01646 10.8598 9.47376 9.69598 10.3955C10.3529 11.5185 11.3786 12.6463 12.7624 13.6552C13.0921 13.8956 13.4238 14.1123 13.7536 14.3076C14.404 13.1163 14.8884 11.7348 15.0905 10.6738C15.1422 10.4026 15.4042 10.2247 15.6755 10.2763C15.9465 10.3282 16.1245 10.5902 16.0729 10.8613C15.8542 12.0096 15.3396 13.4866 14.6354 14.7783C14.763 14.8392 14.8898 14.8988 15.0163 14.9531C15.2695 15.0622 15.3867 15.3558 15.278 15.6093C15.169 15.8626 14.8753 15.9796 14.6218 15.8711C14.4542 15.7992 14.2864 15.7208 14.1179 15.6386C14.0515 15.7379 13.9853 15.836 13.9167 15.9306C13.7186 16.2037 13.7499 16.1748 13.5407 16.4404C13.3698 16.6569 13.0554 16.694 12.8386 16.5234C12.6221 16.3526 12.5852 16.039 12.7555 15.8222C12.9496 15.5758 12.8809 15.6546 13.1071 15.3427C13.15 15.2837 13.1911 15.2224 13.2331 15.1611C12.8789 14.951 12.5238 14.7189 12.1725 14.4629C10.7877 13.453 9.68055 12.2798 8.92937 11.0586C8.70604 11.2687 8.47894 11.4968 8.24676 11.7441C7.40477 12.6409 6.9459 13.6112 6.73894 14.5927C7.05357 15.0957 7.40819 15.5897 7.79461 16.0556C7.97038 16.2681 7.9413 16.5835 7.72918 16.7597C7.51671 16.9357 7.20034 16.9055 7.0241 16.6933C6.87978 16.5193 6.73989 16.3402 6.6032 16.1591C6.60396 16.1906 6.60402 16.2224 6.60515 16.2539C6.61475 16.5295 6.39937 16.7624 6.12371 16.7724C5.84795 16.7823 5.61632 16.5657 5.60613 16.29C5.58842 15.7994 5.61306 15.2982 5.68914 14.7949C5.3123 14.1529 4.99703 13.4927 4.76629 12.8388C4.69652 12.6411 4.63414 12.4419 4.57976 12.2431C4.47705 13.1143 4.52518 14.0189 4.74773 14.9209C4.80414 15.1494 4.87049 15.3731 4.94598 15.5908C5.08136 15.9818 4.87487 16.409 4.48406 16.5449C4.09282 16.6805 3.66481 16.4731 3.52898 16.082C3.4385 15.821 3.35909 15.5533 3.29168 15.2802C2.96707 13.9647 2.95095 12.645 3.195 11.3974C1.93099 12.7348 1.49878 14.0197 1.78191 15.0449C2.09773 16.1867 3.34385 17.15 5.4909 17.6025C7.59655 18.0461 10.3611 17.939 13.2673 17.1357C14.0238 16.9266 14.7473 16.6805 15.4323 16.4072C15.6193 16.1416 15.8055 15.8703 15.985 15.5908C17.7485 12.8443 18.7474 10.0176 18.9655 7.69235C19.1874 5.32472 18.5931 3.70207 17.488 2.99215C16.4485 2.32474 14.8656 2.40111 12.9538 3.40817C12.5875 3.60097 12.1343 3.46069 11.9411 3.09469C11.7481 2.72828 11.8882 2.27412 12.2546 2.08102ZM19.1735 16.8798C19.2891 16.4823 19.7046 16.2539 20.1022 16.3691C20.4998 16.4847 20.7283 16.9002 20.613 17.2978C19.8477 19.9322 17.6652 21.5524 15.3796 21.9316C14.9713 21.9993 14.5855 21.7234 14.5173 21.3154C14.4495 20.9068 14.7258 20.5199 15.1345 20.4521C16.9112 20.1573 18.5856 18.9034 19.1735 16.8798ZM8.00066 20.0976C8.20918 19.7401 8.66836 19.6191 9.02605 19.8271C9.10812 19.875 9.1721 19.9012 9.26726 19.913C9.6779 19.9645 9.96959 20.3392 9.91863 20.75C9.86726 21.1606 9.49242 21.4532 9.08172 21.4023C8.69175 21.3538 8.47453 21.2416 8.27117 21.123C7.91364 20.9144 7.79231 20.4553 8.00066 20.0976ZM7.86687 6.87496C6.92198 7.5062 6.14314 8.33916 5.57586 9.29781C5.56979 9.33275 5.5615 9.36812 5.54754 9.40231C5.21338 10.2191 5.28455 11.3009 5.70965 12.5058C5.80926 12.7881 5.92825 13.0734 6.06219 13.3593C6.36312 12.5692 6.83018 11.7924 7.51726 11.0605C7.83012 10.7273 8.13763 10.4255 8.44012 10.1513C7.93212 9.04939 7.723 7.93039 7.86687 6.87496ZM14.9753 6.47457C15.2506 6.46197 15.4845 6.67483 15.4977 6.95016C15.5097 7.2067 15.3255 7.42491 15.0778 7.46481V7.46871C15.0909 7.53397 15.1197 7.62085 15.1677 7.72555C15.3659 8.15813 15.8192 8.7195 16.3698 9.12106C16.5926 9.28375 16.6416 9.59725 16.4792 9.82028C16.3165 10.0429 16.0039 10.092 15.7809 9.92965C15.1107 9.44097 14.5373 8.75013 14.2585 8.14156C14.126 7.85205 14.0178 7.49011 14.0964 7.15328C14.1398 6.96747 14.242 6.78789 14.4177 6.65817C14.587 6.53341 14.7859 6.4835 14.9753 6.47457Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+30
components/utils/CoordDebugger.tsx
··· 1 + "use client"; 2 + import { create } from "zustand"; 3 + 4 + export let useCoordState = create(() => ({ 5 + coords: [] as { x: number; y: number; color?: string }[], 6 + })); 7 + 8 + export function CoordDebugger() { 9 + let coords = useCoordState((s) => s.coords); 10 + return ( 11 + <> 12 + {coords.map((coord, index) => ( 13 + <div 14 + key={index} 15 + style={{ 16 + position: "fixed", 17 + left: coord.x, 18 + top: coord.y, 19 + pointerEvents: "none", 20 + height: "16px", 21 + width: "16px", 22 + backgroundColor: coord.color || "red", 23 + borderRadius: "50%", 24 + zIndex: 9999, 25 + }} 26 + ></div> 27 + ))} 28 + </> 29 + ); 30 + }