a tool for shared writing and social publishing

merged in main, moved the domain settings to a modal

+431 -354
+9
CLAUDE.md
··· 45 45 - **Appview** (`appview/`) consumes the firehose to index published content 46 46 - **Feeds** (`feeds/`) provides subscription feeds for publications 47 47 48 + ### Lexicon Workflow 49 + 50 + The source of truth for lexicon schemas is the TypeScript files in `lexicons/src/` (e.g. `facet.ts`, `publication.ts`, `blocks.ts`). The JSON files in `lexicons/pub/` are **generated output** — do not edit them directly, as `npm run lexgen` will overwrite them. 51 + 52 + To add or modify a lexicon: 53 + 1. Edit the relevant source file in `lexicons/src/` 54 + 2. Run `npm run lexgen` to regenerate both the JSON schemas and the TypeScript API types in `lexicons/api/` 55 + 48 56 ### Key Directories 49 57 50 58 - `app/` - Next.js App Router pages and API routes ··· 62 70 - **Replicache mutations**: Named handlers in `src/replicache/mutations.ts`, keep server mutations idempotent 63 71 - **React contexts**: `DocumentProvider`, `LeafletContentProvider` for page-level data 64 72 - **Inngest functions**: Async jobs in `app/api/inngest/functions/` 73 + - **Icons**: Icon components live in `components/Icons/`. Each icon is a named export in its own file (e.g. `RefreshSmall.tsx`), imports `Props` from `./Props`, spreads `{...props}` on the `<svg>` element, and uses `fill="currentColor"` instead of hardcoded colors like `fill="black"`.
+1 -1
app/[leaflet_id]/actions/HelpButton.tsx
··· 30 30 <HelpLink text="💡 Make with Leaflet" url="https://make.leaflet.pub" /> 31 31 <HelpLink 32 32 text="✨ Explore Publications" 33 - url="https://leaflet.pub/discover" 33 + url="https://leaflet.pub/reader" 34 34 /> 35 35 <HelpLink text="📣 Newsletter" url="https://buttondown.com/leaflet" /> 36 36 {/* contact links */}
+1 -1
app/[leaflet_id]/actions/PublishButton.tsx
··· 190 190 <ActionButton 191 191 primary 192 192 icon={<PublishSmall className="shrink-0" />} 193 - label={"Publish on ATP"} 193 + label={"Publish"} 194 194 /> 195 195 } 196 196 >
+11 -15
app/[leaflet_id]/actions/ShareOptions/DomainOptions.tsx
··· 47 47 ); 48 48 case "domain-settings": 49 49 return ( 50 - <div className="px-3 py-1 max-w-full w-[600px]"> 50 + <div className=""> 51 51 <DomainSettingsView 52 52 domain={state.domain} 53 53 onBack={() => setState({ state: "default" })} ··· 58 58 ); 59 59 case "add-domain": 60 60 return ( 61 - <div className="px-3 py-1 max-w-full w-[600px]"> 62 - <AddDomainForm 63 - onDomainAdded={(domain) => 64 - setState({ state: "domain-settings", domain }) 65 - } 66 - onBack={() => setState({ state: "default" })} 67 - /> 68 - </div> 61 + <AddDomainForm 62 + onDomainAdded={(domain) => 63 + setState({ state: "domain-settings", domain }) 64 + } 65 + onBack={() => setState({ state: "default" })} 66 + /> 69 67 ); 70 68 } 71 69 } ··· 118 116 let route = "/" + selectedRoute; 119 117 return domain.custom_domain_routes.some( 120 118 (r) => 121 - r.route === route && 122 - r.edit_permission_token !== permission_token.id, 119 + r.route === route && r.edit_permission_token !== permission_token.id, 123 120 ); 124 121 })(); 125 122 ··· 202 199 (dd) => dd.domain === domain.domain, 203 200 ); 204 201 if (d) { 205 - d.custom_domain_routes = 206 - d.custom_domain_routes.filter( 207 - (r) => r.id !== routeId, 208 - ); 202 + d.custom_domain_routes = d.custom_domain_routes.filter( 203 + (r) => r.id !== routeId, 204 + ); 209 205 } 210 206 }); 211 207 mutateDomains();
+2 -2
app/lish/Subscribe.tsx
··· 262 262 <Dialog.Root open={open} onOpenChange={setOpen}> 263 263 <Dialog.Trigger asChild></Dialog.Trigger> 264 264 <Dialog.Portal> 265 - <Dialog.Overlay className="fixed inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-10 blur-xs" /> 265 + <Dialog.Overlay className="fixed z-[100] inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-10 blur-xs" /> 266 266 <Dialog.Content 267 267 className={` 268 - z-20 opaque-container 268 + z-[100] opaque-container 269 269 fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 270 270 w-96 px-3 py-4 271 271 max-w-(--radix-popover-content-available-width)
+19 -3
app/lish/[did]/[publication]/UpgradeModal.tsx
··· 67 67 > 68 68 {loading ? <DotLoader /> : "Get it!"} 69 69 </ButtonPrimary> 70 - {error && ( 71 - <div className="text-sm text-red-500 mt-2">{error}</div> 72 - )} 70 + {error && <div className="text-sm text-red-500 mt-2">{error}</div>} 73 71 </div> 74 72 </div> 75 73 </div> ··· 91 89 </Modal> 92 90 ); 93 91 }; 92 + 93 + export const InlineUpgrade = () => { 94 + return ( 95 + <div className="text-center p-2 accent-container text-secondary"> 96 + <UpgradeModal 97 + asChild 98 + trigger={ 99 + <ButtonPrimary compact fullWidth className="text-sm"> 100 + Upgrade to Leaflet Pro! 101 + </ButtonPrimary> 102 + } 103 + /> 104 + <div className="text-sm leading-snug text-tertiary pt-1"> 105 + Analytics for all your pubs! <br /> Emails and membership coming soon. 106 + </div> 107 + </div> 108 + ); 109 + };
+68 -72
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 34 34 import { PollData } from "./fetchPollData"; 35 35 import { ButtonPrimary } from "components/Buttons"; 36 36 import { blockTextSize } from "src/utils/blockTextSize"; 37 + import { slugify } from "src/utils/slugify"; 37 38 import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 38 39 39 40 export function PostContent({ ··· 391 392 ); 392 393 393 394 case PubLeafletBlocksHeader.isMain(b.block): { 395 + let slug = slugify(b.block.plaintext); 396 + let headingProps = { 397 + ...blockProps, 398 + id: preview ? undefined : slug || blockProps.id, 399 + }; 400 + let textBlockProps = { 401 + ...b.block, 402 + index, 403 + preview, 404 + pageId, 405 + footnoteIndexMap, 406 + }; 407 + let href = pageId ? `?page=${pageId}#${slug}` : `#${slug}`; 408 + let link = (children: React.ReactNode) => 409 + preview || !slug ? ( 410 + children 411 + ) : ( 412 + <a href={href} className="no-underline text-inherit cursor-pointer"> 413 + {children} 414 + </a> 415 + ); 394 416 if (b.block.level === 1) 395 417 return ( 396 - <h1 className={`h1Block ${className}`} {...blockProps} style={{ ...blockProps.style, fontSize: blockTextSize.h1 }}> 397 - <TextBlock 398 - {...b.block} 399 - index={index} 400 - preview={preview} 401 - pageId={pageId} 402 - /> 418 + <h1 className={`h1Block ${className}`} {...headingProps} style={{ ...headingProps.style, fontSize: blockTextSize.h1 }}> 419 + {link(<TextBlock {...textBlockProps} />)} 403 420 </h1> 404 421 ); 405 422 if (b.block.level === 2) 406 423 return ( 407 - <h2 className={`h2Block ${className}`} {...blockProps} style={{ ...blockProps.style, fontSize: blockTextSize.h2 }}> 408 - <TextBlock 409 - {...b.block} 410 - index={index} 411 - preview={preview} 412 - pageId={pageId} 413 - footnoteIndexMap={footnoteIndexMap} 414 - /> 424 + <h2 className={`h2Block ${className}`} {...headingProps} style={{ ...headingProps.style, fontSize: blockTextSize.h2 }}> 425 + {link(<TextBlock {...textBlockProps} />)} 415 426 </h2> 416 427 ); 417 428 if (b.block.level === 3) 418 429 return ( 419 - <h3 className={`h3Block ${className}`} {...blockProps} style={{ ...blockProps.style, fontSize: blockTextSize.h3 }}> 420 - <TextBlock 421 - {...b.block} 422 - index={index} 423 - preview={preview} 424 - pageId={pageId} 425 - footnoteIndexMap={footnoteIndexMap} 426 - /> 430 + <h3 className={`h3Block ${className}`} {...headingProps} style={{ ...headingProps.style, fontSize: blockTextSize.h3 }}> 431 + {link(<TextBlock {...textBlockProps} />)} 427 432 </h3> 428 433 ); 429 - // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 430 - // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 431 434 return ( 432 - <h6 className={`h6Block ${className}`} {...blockProps} style={{ ...blockProps.style, fontSize: blockTextSize.h4 }}> 433 - <TextBlock 434 - {...b.block} 435 - index={index} 436 - preview={preview} 437 - pageId={pageId} 438 - footnoteIndexMap={footnoteIndexMap} 439 - /> 435 + <h6 className={`h6Block ${className}`} {...headingProps} style={{ ...headingProps.style, fontSize: blockTextSize.h4 }}> 436 + {link(<TextBlock {...textBlockProps} />)} 440 437 </h6> 441 438 ); 442 439 } ··· 472 469 ))} 473 470 </ul> 474 471 ) : null; 475 - let orderedChildren = 476 - props.item.orderedListChildren?.children?.length ? ( 477 - <ol className="-ml-[7px] sm:ml-[7px]"> 478 - {props.item.orderedListChildren.children.map((child, index) => ( 479 - <OrderedListItem 480 - pages={props.pages} 481 - pollData={props.pollData} 482 - bskyPostData={props.bskyPostData} 483 - index={[...props.index, index]} 484 - item={child} 485 - did={props.did} 486 - key={index} 487 - className={props.className} 488 - pageId={props.pageId} 489 - startIndex={props.item.orderedListChildren?.startIndex} 490 - /> 491 - ))} 492 - </ol> 493 - ) : null; 472 + let orderedChildren = props.item.orderedListChildren?.children?.length ? ( 473 + <ol className="-ml-[7px] sm:ml-[7px]"> 474 + {props.item.orderedListChildren.children.map((child, index) => ( 475 + <OrderedListItem 476 + pages={props.pages} 477 + pollData={props.pollData} 478 + bskyPostData={props.bskyPostData} 479 + index={[...props.index, index]} 480 + item={child} 481 + did={props.did} 482 + key={index} 483 + className={props.className} 484 + pageId={props.pageId} 485 + startIndex={props.item.orderedListChildren?.startIndex} 486 + /> 487 + ))} 488 + </ol> 489 + ) : null; 494 490 return ( 495 491 <li className={`pb-0! flex flex-row gap-2`}> 496 492 <div ··· 525 521 pageId?: string; 526 522 startIndex?: number; 527 523 }) { 528 - const calculatedIndex = (props.startIndex || 1) + props.index[props.index.length - 1]; 524 + const calculatedIndex = 525 + (props.startIndex || 1) + props.index[props.index.length - 1]; 529 526 let children = props.item.children?.length ? ( 530 527 <ol className="-ml-[7px] sm:ml-[7px]"> 531 528 {props.item.children.map((child, index) => ( ··· 544 541 ))} 545 542 </ol> 546 543 ) : null; 547 - let unorderedChildren = 548 - props.item.unorderedListChildren?.children?.length ? ( 549 - <ul className="-ml-[7px] sm:ml-[7px]"> 550 - {props.item.unorderedListChildren.children.map((child, index) => ( 551 - <ListItem 552 - pages={props.pages} 553 - pollData={props.pollData} 554 - bskyPostData={props.bskyPostData} 555 - index={[...props.index, index]} 556 - item={child} 557 - did={props.did} 558 - key={index} 559 - className={props.className} 560 - pageId={props.pageId} 561 - /> 562 - ))} 563 - </ul> 564 - ) : null; 544 + let unorderedChildren = props.item.unorderedListChildren?.children?.length ? ( 545 + <ul className="-ml-[7px] sm:ml-[7px]"> 546 + {props.item.unorderedListChildren.children.map((child, index) => ( 547 + <ListItem 548 + pages={props.pages} 549 + pollData={props.pollData} 550 + bskyPostData={props.bskyPostData} 551 + index={[...props.index, index]} 552 + item={child} 553 + did={props.did} 554 + key={index} 555 + className={props.className} 556 + pageId={props.pageId} 557 + /> 558 + ))} 559 + </ul> 560 + ) : null; 565 561 return ( 566 562 <li className={`pb-0! flex flex-row gap-2`}> 567 - <div className="listMarker shrink-0 mx-2 z-1 mt-[14px]"> 563 + <div className="listMarker shrink-0 mx-2 z-1 mt-[4px]"> 568 564 {calculatedIndex}. 569 565 </div> 570 566 <div className="flex flex-col w-full">
-18
app/lish/[did]/[publication]/dashboard/Actions.tsx
··· 21 21 return ( 22 22 <> 23 23 <NewDraftActionButton publication={props.publication} /> 24 - {canSeePro && !isPro && <MobileUpgrade />} 25 - 26 24 <PublicationShareButton /> 27 25 <PublicationSettingsButton publication={props.publication} /> 28 26 </> ··· 85 83 </Menu> 86 84 ); 87 85 } 88 - 89 - const MobileUpgrade = () => { 90 - return ( 91 - <UpgradeModal 92 - asChild 93 - trigger={ 94 - <ActionButton 95 - label="Upgrade to Leaflet Pro" 96 - icon={<LeafletPro />} 97 - className={`sm:hidden block bg-[var(--accent-light)]!`} 98 - style={{ backgroundColor: "var(--accent-light) important!" }} 99 - /> 100 - } 101 - /> 102 - ); 103 - };
+10 -4
app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx
··· 241 241 if (!isPro) 242 242 return ( 243 243 <div 244 - className={`sm:mx-auto pt-4 s rounded-lg border ${ 244 + className={`sm:mx-auto mt-2 sm:mt-4 rounded-lg border ${ 245 245 props.showPageBackground 246 - ? "border-border-light p-2" 246 + ? "border-border-light p-3" 247 247 : "border-transparent" 248 248 }`} 249 249 style={{ ··· 296 296 </> 297 297 )} 298 298 </div> 299 - <TrafficChart data={filledTraffic} isLoading={analyticsLoading} metric={trafficMetric} /> 299 + <TrafficChart 300 + data={filledTraffic} 301 + isLoading={analyticsLoading} 302 + metric={trafficMetric} 303 + /> 300 304 <div className="flex flex-col sm:flex-row gap-4 mt-2"> 301 305 <TopPages 302 306 pages={analyticsData?.topPages || []} ··· 490 494 <div className="text-tertiary"> 491 495 {formatDayTick(String(label))} 492 496 </div> 493 - <div>{Number(pageviews).toLocaleString()} {metricLabel}</div> 497 + <div> 498 + {Number(pageviews).toLocaleString()} {metricLabel} 499 + </div> 494 500 </div> 495 501 ); 496 502 }}
+1 -1
app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx
··· 48 48 if (subscribers.length === 0) 49 49 return ( 50 50 <div 51 - className={`italic text-tertiary flex flex-col gap-0 text-center justify-center mt-4 border rounded-md ${props.showPageBackground ? "border-border-light p-2" : "border-transparent"}`} 51 + className={`italic text-tertiary flex flex-col gap-0 text-center justify-center py-4 border rounded-md ${props.showPageBackground ? "border-border-light p-2" : "border-transparent"}`} 52 52 style={ 53 53 props.showPageBackground 54 54 ? {
+1 -4
app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx
··· 7 7 import { GoBackSmall } from "components/Icons/GoBackSmall"; 8 8 import { PRODUCT_DEFINITION } from "stripe/products"; 9 9 10 - export const ManageProSubscription = (props: { backToMenu: () => void }) => { 10 + export const ManageProSubscription = (props: {}) => { 11 11 const [loading, setLoading] = useState(false); 12 12 const [error, setError] = useState<string | null>(null); 13 13 const { identity } = useIdentityData(); ··· 34 34 <div> 35 35 <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"> 36 36 Manage Subscription 37 - <button type="button" onClick={props.backToMenu}> 38 - <GoBackSmall className="text-accent-contrast" /> 39 - </button> 40 37 </div> 41 38 <div className="text-secondary text-center flex flex-col justify-center gap-2 py-2"> 42 39 <div>
+5 -5
app/lish/createPub/CreatePubForm.tsx
··· 151 151 <div className=" pt-0.5 flex flex-col text-sm text-tertiary "> 152 152 <p className="font-bold italic">Show In Discover</p> 153 153 <p className="text-sm text-tertiary font-normal"> 154 - Your posts will appear on our{" "} 155 - <a href="/discover" target="_blank"> 156 - Discover 157 - </a>{" "} 158 - page. You can change this at any time! 154 + Your posts will appear in{" "} 155 + <a href="/reader" target="_blank"> 156 + Leaflet Reader 157 + </a> 158 + . You can change this at any time! 159 159 </p> 160 160 </div> 161 161 </Checkbox>
+6 -11
app/lish/createPub/UpdatePubForm.tsx
··· 165 165 onToggle={() => setShowInDiscover(!showInDiscover)} 166 166 > 167 167 <div className=" pt-0.5 flex flex-col text-sm text-tertiary "> 168 - <p className="font-bold"> 169 - Show In{" "} 170 - <a href="/discover" target="_blank"> 171 - Discover 168 + <p className="font-bold italic">Show In Discover</p> 169 + <p className="text-xs text-tertiary font-normal"> 170 + Your posts will appear in{" "} 171 + <a href="/reader" target="_blank"> 172 + Leaflet Reader 172 173 </a> 173 - </p> 174 - <p className="text-xs text-tertiary font-normal"> 175 - Your posts will appear on our{" "} 176 - <a href="/discover" target="_blank"> 177 - Discover 178 - </a>{" "} 179 - page. You can change this at any time! 174 + . You can change this at any time! 180 175 </p> 181 176 </div> 182 177 </Toggle>
+56 -81
components/ActionBar/ProfileButton.tsx
··· 8 8 import { mutate } from "swr"; 9 9 import { SpeedyLink } from "components/SpeedyLink"; 10 10 import { Popover } from "components/Popover"; 11 - import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 12 11 import { Modal } from "components/Modal"; 13 - import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal"; 12 + import { InlineUpgrade } from "app/lish/[did]/[publication]/UpgradeModal"; 14 13 import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription"; 15 14 import { ManageDomains } from "components/Domains/ManageDomains"; 16 15 import { WebSmall } from "components/Icons/WebSmall"; 17 16 import { useIsPro, useCanSeePro } from "src/hooks/useEntitlement"; 18 - import { useState } from "react"; 17 + import { LeafletPro } from "components/Icons/LeafletPro"; 18 + import { ButtonPrimary } from "components/Buttons"; 19 19 20 20 export const ProfileButton = () => { 21 21 let { identity } = useIdentityData(); 22 22 let { data: record } = useRecordFromDid(identity?.atp_did); 23 23 let isMobile = useIsMobile(); 24 - let [state, setState] = useState< 25 - "menu" | "manage-subscription" | "manage-domains" 26 - >("menu"); 27 24 let isPro = useIsPro(); 28 25 let canSeePro = useCanSeePro(); 29 26 ··· 32 29 asChild 33 30 side={isMobile ? "top" : "right"} 34 31 align={isMobile ? "center" : "start"} 35 - onOpenChange={() => setState("menu")} 36 - className="w-xs" 32 + className="w-xs py-1!" 37 33 trigger={ 38 34 <ActionButton 39 35 nav ··· 53 49 /> 54 50 } 55 51 > 56 - {state === "manage-subscription" ? ( 57 - <ManageProSubscription backToMenu={() => setState("menu")} /> 58 - ) : state === "manage-domains" ? ( 59 - <ManageDomains backToMenu={() => setState("menu")} /> 60 - ) : ( 61 - <div className="flex flex-col gap-0.5"> 62 - {record && ( 63 - <> 64 - <SpeedyLink 65 - className="no-underline!" 66 - href={`/p/${record.handle}`} 67 - > 68 - <button 69 - type="button" 70 - className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline! w-full" 71 - > 72 - View Profile 73 - </button> 74 - </SpeedyLink> 52 + <div className="flex flex-col gap-0.5"> 53 + {record && ( 54 + <> 55 + <SpeedyLink 56 + className="no-underline! menuItem -mx-[8px]" 57 + href={`/p/${record.handle}`} 58 + > 59 + <button type="button" className="flex gap-2 "> 60 + <AccountSmall /> 61 + View Profile 62 + </button> 63 + </SpeedyLink> 64 + </> 65 + )} 66 + 67 + <ManageDomains /> 68 + <hr className="border-border-light border-dashed" /> 75 69 76 - <hr className="border-border-light border-dashed" /> 77 - </> 78 - )} 79 - {canSeePro && ( 80 - <> 81 - {!isPro ? ( 82 - <Modal 83 - trigger={ 84 - <div className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline! bg-[var(--accent-light)]! border border-transparent hover:border-accent-contrast"> 85 - Get Leaflet Pro 86 - <ArrowRightTiny /> 87 - </div> 88 - } 89 - > 90 - <UpgradeContent /> 91 - </Modal> 92 - ) : ( 93 - <button 94 - className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!" 95 - type="button" 96 - onClick={() => setState("manage-subscription")} 97 - > 70 + {canSeePro && isPro && ( 71 + <> 72 + <Modal 73 + trigger={ 74 + <div className="menuItem -mx-[8px] "> 75 + <LeafletPro /> 98 76 Manage Pro Subscription 99 - <ArrowRightTiny /> 100 - </button> 101 - )} 102 - 103 - <hr className="border-border-light border-dashed" /> 104 - </> 105 - )} 106 - 107 - <button 108 - className="menuItem -mx-[8px] text-left flex items-center gap-2 hover:no-underline!" 109 - type="button" 110 - onClick={() => setState("manage-domains")} 111 - > 112 - <WebSmall /> 113 - Connected Domains 114 - </button> 115 - 116 - <hr className="border-border-light border-dashed" /> 117 - 118 - <button 119 - type="button" 120 - className="menuItem -mx-[8px] text-left flex items-center gap-2 hover:no-underline!" 121 - onClick={async () => { 122 - await fetch("/api/auth/logout"); 123 - mutate("identity", null); 124 - }} 125 - > 126 - <LogoutSmall /> 127 - Log Out 128 - </button> 129 - </div> 130 - )} 77 + </div> 78 + } 79 + > 80 + <ManageProSubscription /> 81 + </Modal> 82 + <hr className="border-border-light border-dashed" /> 83 + </> 84 + )} 85 + <button 86 + type="button" 87 + className="menuItem -mx-[8px] text-left flex items-center gap-2 hover:no-underline!" 88 + onClick={async () => { 89 + await fetch("/api/auth/logout"); 90 + mutate("identity", null); 91 + }} 92 + > 93 + <LogoutSmall /> 94 + Log Out 95 + </button> 96 + {canSeePro && !isPro && ( 97 + <> 98 + {" "} 99 + <hr className="border-border-light border-dashed" /> 100 + <div className="py-2"> 101 + <InlineUpgrade /> 102 + </div> 103 + </> 104 + )} 105 + </div> 131 106 </Popover> 132 107 ); 133 108 };
+3 -3
components/Buttons.tsx
··· 37 37 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 38 38 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 39 39 bg-accent-1 disabled:bg-border-light 40 - border border-accent-1 rounded-md disabled:border-border-light 40 + border border-accent-1 rounded-md disabled:border-border-light disabled:outline-none! disabled:cursor-not-allowed! 41 41 outline-2 outline-transparent outline-offset-1 focus:outline-accent-1 hover:outline-accent-1 42 42 text-base font-bold text-accent-2 disabled:text-border disabled:hover:text-border 43 43 flex gap-2 items-center justify-center shrink-0 ··· 77 77 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 78 78 bg-bg-page disabled:bg-border-light 79 79 border border-accent-contrast rounded-md 80 - outline-2 outline-transparent focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 80 + outline-2 outline-transparent focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 disabled:outline-none! disabled:cursor-not-allowed! 81 81 text-base font-bold text-accent-contrast disabled:text-border disabled:hover:text-border 82 82 flex gap-2 items-center justify-center shrink-0 83 83 ${props.className} ··· 116 116 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 117 117 bg-transparent hover:bg-[var(--accent-light)] 118 118 border border-transparent rounded-md hover:border-[var(--accent-light)] 119 - outline-2 outline-transparent focus:outline-[var(--accent-light)] hover:outline-[var(--accent-light)] outline-offset-1 119 + outline-2 outline-transparent focus:outline-[var(--accent-light)] hover:outline-[var(--accent-light)] outline-offset-1 disabled:outline-none! disabled:cursor-not-allowed! 120 120 text-base font-bold text-accent-contrast disabled:text-border 121 121 flex gap-2 items-center justify-center shrink-0 122 122 ${props.className}
+6 -5
components/Domains/AddDomainForm.tsx
··· 18 18 19 19 return ( 20 20 <div className="flex flex-col gap-1 max-w-full"> 21 - <div> 22 - <h3 className="text-secondary">Add a New Domain</h3> 23 - <div className="text-xs italic text-secondary"> 24 - Don&apos;t include the protocol or path, just the base domain name 25 - </div> 21 + <h3>Add a Domain</h3> 22 + <div className="text-sm text-secondary"> 23 + <div className="font-bold">Just include the base domain</div> 24 + Don't include the protocol{" "} 25 + <span className="text-tertiary">(like https://) </span> 26 + or path <span className="text-tertiary">(you can add that later)</span> 26 27 </div> 27 28 28 29 <Input
-8
components/Domains/DomainList.tsx
··· 14 14 15 15 export function DomainList(props: { 16 16 onSelectDomain: (domain: string) => void; 17 - onAddDomain: () => void; 18 17 filter?: (domain: CustomDomain) => boolean; 19 18 }) { 20 19 let { identity } = useIdentityData(); ··· 79 78 </div> 80 79 </> 81 80 )} 82 - <button 83 - onMouseDown={() => props.onAddDomain()} 84 - className="text-accent-contrast font-bold text-sm flex gap-2 items-center px-1 py-0.5" 85 - type="button" 86 - > 87 - <AddTiny /> Add a New Domain 88 - </button> 89 81 </div> 90 82 ); 91 83 }
+47 -47
components/Domains/DomainSettingsView.tsx
··· 9 9 useIdentityData, 10 10 mutateIdentityData, 11 11 } from "components/IdentityProvider"; 12 - import { ButtonPrimary } from "components/Buttons"; 12 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 13 13 import { getDomainAssignment } from "./domainAssignment"; 14 + import { RefreshSmall } from "components/Icons/RefreshSmall"; 15 + import { GoToArrow } from "components/Icons/GoToArrow"; 14 16 15 17 export function DomainSettingsView(props: { 16 18 domain: string; ··· 34 36 35 37 return ( 36 38 <div className="flex flex-col gap-[6px] text-sm text-primary max-w-full"> 37 - <h3 className="text-secondary">{props.domain}</h3> 39 + <div className="w-full flex gap-2 items-center"> 40 + <h3 className="w-full grow min-w-0 truncate"> 41 + {needsSetup ? `Verify ${props.domain}` : props.domain} 42 + </h3> 43 + <button 44 + className="text-accent-contrast rotate-180 shrink-0" 45 + onMouseDown={() => props.onBack()} 46 + type="button" 47 + > 48 + <GoToArrow /> 49 + </button> 50 + {needsSetup && <VerifyButton verify={() => mutateDomainStatus()} />} 51 + </div> 38 52 39 53 {needsSetup ? ( 40 54 <> ··· 42 56 To verify this domain, add the following record to your DNS provider 43 57 for <strong>{props.domain}</strong>. 44 58 </div> 45 - <table className="border border-border-light rounded-md"> 59 + <table className="border border-border-light rounded-md text-left "> 46 60 <thead> 47 61 <tr> 48 - <th className="p-1 py-1 text-tertiary">Type</th> 49 - <th className="p-1 py-1 text-tertiary">Name</th> 50 - <th className="p-1 py-1 text-tertiary">Value</th> 62 + <th className="px-2 pt-1 text-tertiary">Type</th> 63 + <th className="px-2 pt-1 text-tertiary">Name</th> 64 + <th className="px-2 pt-1 text-tertiary">Value</th> 51 65 </tr> 52 66 </thead> 53 67 <tbody> 54 68 {data?.verification && ( 55 69 <tr> 56 - <td className="p-1 py-1"> 70 + <td className="px-2 pb-1"> 57 71 <div>{data.verification[0].type}</div> 58 72 </td> 59 - <td className="p-1 py-1"> 73 + <td className="px-2 pb-1"> 60 74 <div style={{ wordBreak: "break-word" }}> 61 75 {data.verification[0].domain} 62 76 </div> 63 77 </td> 64 - <td className="p-1 py-1"> 78 + <td className="px-2 pb-1"> 65 79 <div style={{ wordBreak: "break-word" }}> 66 80 {data.verification[0].value} 67 81 </div> ··· 71 85 {data?.config && 72 86 (isSubdomain ? ( 73 87 <tr> 74 - <td className="p-1 py-1"> 88 + <td className="px-2 pb-1"> 75 89 <div>CNAME</div> 76 90 </td> 77 - <td className="p-1 py-1"> 91 + <td className="px-2 pb-1"> 78 92 <div style={{ wordBreak: "break-word" }}> 79 93 {props.domain.split(".").slice(0, -2).join(".")} 80 94 </div> 81 95 </td> 82 - <td className="p-1 py-1"> 96 + <td className="px-2 pb-1"> 83 97 <div style={{ wordBreak: "break-word" }}> 84 98 { 85 99 data.config.recommendedCNAME.sort( ··· 91 105 </tr> 92 106 ) : ( 93 107 <tr> 94 - <td className="p-1 py-1"> 108 + <td className="px-2 pb-1"> 95 109 <div>A</div> 96 110 </td> 97 - <td className="p-1 py-1"> 111 + <td className="px-2 pb-1"> 98 112 <div style={{ wordBreak: "break-word" }}>@</div> 99 113 </td> 100 - <td className="p-1 py-1"> 114 + <td className="px-2 pb-1"> 101 115 <div style={{ wordBreak: "break-word" }}> 102 116 { 103 117 data.config.recommendedIPv4.sort( ··· 181 195 )} 182 196 183 197 <div className="flex flex-col gap-2 mt-2"> 184 - <div className="flex gap-3 justify-between items-center"> 185 - <button 186 - className="text-accent-contrast" 187 - onMouseDown={() => props.onBack()} 188 - type="button" 189 - > 190 - Back 191 - </button> 192 - <div className="flex gap-2 items-center"> 193 - {needsSetup && ( 194 - <VerifyButton verify={() => mutateDomainStatus()} /> 195 - )} 196 - </div> 197 - </div> 198 - 199 198 <hr className="border-border-light" /> 200 199 201 200 <DeleteDomainButton ··· 220 219 221 220 if (!confirming) { 222 221 return ( 223 - <div className="flex gap-3 justify-between items-center"> 224 - <button 225 - className="text-accent-contrast font-bold text-sm" 226 - type="button" 227 - onMouseDown={() => setConfirming(true)} 228 - > 229 - Delete Domain 230 - </button> 231 - </div> 222 + <ButtonTertiary 223 + fullWidth 224 + compact 225 + type="button" 226 + onMouseDown={() => setConfirming(true)} 227 + > 228 + Delete Domain 229 + </ButtonTertiary> 232 230 ); 233 231 } 234 232 235 233 return ( 236 - <div className="flex flex-col gap-1 text-xs"> 237 - <p className="text-secondary"> 234 + <div className="flex flex-col gap-1 text-sm accent-container rounded-md p-2"> 235 + <p className="text-secondary text-center"> 238 236 Are you sure you want to delete <strong>{props.domain}</strong>? This 239 237 will remove all assignments and cannot be undone. 240 238 </p> 241 - <div className="flex gap-2 justify-end"> 239 + <div className="flex gap-2 justify-center"> 242 240 <button 243 - className="text-accent-contrast" 241 + className="text-accent-contrast font-bold" 244 242 onMouseDown={() => setConfirming(false)} 245 243 type="button" 246 244 > ··· 248 246 </button> 249 247 <ButtonPrimary 250 248 compact 249 + className="text-sm" 251 250 disabled={loading} 252 251 onMouseDown={async () => { 253 252 setLoading(true); ··· 270 269 function VerifyButton(props: { verify: () => Promise<any> }) { 271 270 let [loading, setLoading] = useState(false); 272 271 return ( 273 - <button 274 - className="text-accent-contrast w-fit" 272 + <ButtonPrimary 273 + compact 274 + className="w-[118px]!" 275 275 type="button" 276 276 onClick={async (e) => { 277 277 e.preventDefault(); ··· 280 280 setLoading(false); 281 281 }} 282 282 > 283 - {loading ? <DotLoader /> : "verify"} 284 - </button> 283 + {loading ? <DotLoader /> : "Check Status"} 284 + </ButtonPrimary> 285 285 ); 286 286 }
+32 -38
components/Domains/ManageDomains.tsx
··· 1 1 "use client"; 2 2 import { useState } from "react"; 3 - import { GoBackSmall } from "components/Icons/GoBackSmall"; 4 3 import { DomainList } from "./DomainList"; 5 4 import { AddDomainForm } from "./AddDomainForm"; 6 5 import { DomainSettingsView } from "./DomainSettingsView"; 7 - import { GoToArrow } from "components/Icons/GoToArrow"; 6 + import { Modal } from "components/Modal"; 7 + import { ButtonPrimary } from "components/Buttons"; 8 + import { WebSmall } from "components/Icons/WebSmall"; 8 9 9 10 type State = 10 11 | "list" 11 12 | "add-domain" 12 13 | { type: "domain-settings"; domain: string }; 13 14 14 - export function ManageDomains(props: { backToMenu: () => void }) { 15 + export function ManageDomains() { 15 16 let [state, setState] = useState<State>("list"); 16 - 17 - if (state === "add-domain") { 18 - return ( 19 - <div className="px-3 py-1"> 17 + return ( 18 + <Modal 19 + className="w-md" 20 + trigger={ 21 + <div className="menuItem -mx-[8px] "> 22 + <WebSmall /> 23 + Domain Settings 24 + </div> 25 + } 26 + > 27 + {state === "add-domain" ? ( 20 28 <AddDomainForm 21 29 onDomainAdded={(domain) => 22 30 setState({ type: "domain-settings", domain }) 23 31 } 24 32 onBack={() => setState("list")} 25 33 /> 26 - </div> 27 - ); 28 - } 29 - 30 - if (typeof state === "object" && state.type === "domain-settings") { 31 - return ( 32 - <div className="px-3 py-1"> 34 + ) : typeof state === "object" && state.type === "domain-settings" ? ( 33 35 <DomainSettingsView 34 36 domain={state.domain} 35 37 onBack={() => setState("list")} 36 38 onRemoveAssignment={() => setState("list")} 37 39 onDeleteDomain={() => setState("list")} 38 40 /> 39 - </div> 40 - ); 41 - } 42 - 43 - return ( 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 /> 53 - </button> 54 - </div> 55 - <hr className="border-border-light -mx-3" /> 56 - <DomainList 57 - onSelectDomain={(domain) => 58 - setState({ type: "domain-settings", domain }) 59 - } 60 - onAddDomain={() => setState("add-domain")} 61 - /> 62 - </div> 41 + ) : ( 42 + <div className="flex flex-col gap-2"> 43 + <div className="flex justify-between"> 44 + <h3>Domains</h3> 45 + <ButtonPrimary onClick={() => setState("add-domain")}> 46 + Add 47 + </ButtonPrimary> 48 + </div> 49 + <DomainList 50 + onSelectDomain={(domain) => 51 + setState({ type: "domain-settings", domain }) 52 + } 53 + /> 54 + </div> 55 + )} 56 + </Modal> 63 57 ); 64 58 }
+19
components/Icons/RefreshSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const RefreshSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M8.98383 20.9565C8.45201 20.8077 8.14158 20.2559 8.29033 19.724C8.43933 19.1925 8.99108 18.8818 9.52276 19.0305C11.3292 19.5357 13.9867 19.6991 15.9397 18.6274C17.5194 17.7603 18.5445 16.0553 18.6533 14.2845C18.7528 12.664 18.0857 10.9792 16.2923 9.83579L16.8257 12.498C16.934 13.0394 16.5828 13.5663 16.0414 13.6748C15.5001 13.7829 14.9731 13.4318 14.8647 12.8905L13.8381 7.7627C13.7299 7.22145 14.0812 6.69455 14.6223 6.58595L19.7501 5.55932C20.2915 5.45099 20.8182 5.80243 20.9269 6.3436C21.0353 6.88515 20.6841 7.41197 20.1426 7.52035L17.2772 8.09442C19.7665 9.63761 20.7945 12.0545 20.65 14.4071C20.5014 16.8268 19.1147 19.1667 16.9025 20.381C14.292 21.8136 11.0224 21.5265 8.98383 20.9565ZM3.85412 17.468C3.30272 17.4971 2.83216 17.0736 2.80287 16.5222C2.774 15.9709 3.19731 15.5001 3.74868 15.471L6.36991 15.3333C4.0138 13.7866 3.04124 11.4299 3.18206 9.13602C3.33085 6.71632 4.71736 4.37626 6.92959 3.1621C9.54014 1.72942 12.8097 2.0165 14.8483 2.5866C15.38 2.73536 15.6903 3.28729 15.5418 3.81903C15.3928 4.35058 14.841 4.66124 14.3093 4.51253C12.5029 4.00737 9.84545 3.84396 7.89238 4.91563C6.31265 5.78265 5.28773 7.48782 5.17881 9.25855C5.07299 10.9821 5.83329 12.7794 7.89468 13.9196C7.89737 13.9211 7.89978 13.9238 7.90246 13.9253L7.74891 11.0246C7.71993 10.4733 8.14354 10.0027 8.69473 9.97331C9.24601 9.9444 9.71675 10.3678 9.74598 10.9191L10.0223 16.1418C10.051 16.6929 9.62755 17.1637 9.07645 17.1931L3.85412 17.468Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+7 -7
components/Modal.tsx
··· 23 23 <Dialog.Root open={open} onOpenChange={onOpenChange}> 24 24 <Dialog.Trigger asChild={asChild}>{trigger}</Dialog.Trigger> 25 25 <Dialog.Portal> 26 - <Dialog.Overlay className="fixed z-10 inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-60" /> 26 + <Dialog.Overlay className="fixed z-[100] inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-60" /> 27 27 <Dialog.Content 28 28 className={` 29 - z-20 fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 30 - overflow-y-scroll no-scrollbar w-max max-w-screen h-fit max-h-screen p-3 flex flex-col 29 + z-[100] fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 30 + overflow-y-scroll no-scrollbar w-max max-w-[calc(100vw-32px)] h-fit max-h-[calc(100dvh-32px)] p-3 flex flex-col 31 31 32 32 `} 33 33 > ··· 44 44 <Dialog.Title> 45 45 <h3>{title}</h3> 46 46 </Dialog.Title> 47 - ) : ( 48 - <Dialog.Title /> 49 - )} 50 - <Dialog.Description>{children}</Dialog.Description> 47 + ) : null} 48 + <Dialog.Description asChild> 49 + <div>{children}</div> 50 + </Dialog.Description> 51 51 </div> 52 52 </Dialog.Content> 53 53 </Dialog.Portal>
+1 -1
components/PageLayouts/DashboardLayout.tsx
··· 304 304 <DisplayToggle setState={setState} display={display} /> 305 305 <Separator classname="h-4 min-h-4!" /> 306 306 307 - {props.hasPubs ? ( 307 + {props.hasPubs || props.hasArchived ? ( 308 308 <> 309 309 <FilterOptions 310 310 hasPubs={props.hasPubs}
+37 -5
components/SubscriptionSuccessModal.tsx
··· 4 4 import { useEffect, useState } from "react"; 5 5 import { useIdentityData } from "./IdentityProvider"; 6 6 import { Modal } from "./Modal"; 7 + import { ButtonPrimary } from "./Buttons"; 8 + import { theme } from "tailwind.config"; 7 9 8 10 export function SubscriptionSuccessModal() { 9 11 let searchParams = useSearchParams(); ··· 39 41 open={open} 40 42 onOpenChange={handleOpenChange} 41 43 trigger={<span />} 42 - title="Welcome to Pro" 43 - className="w-80" 44 + className="sm:max-w-sm sm:w-[1000px] w-full" 44 45 > 45 46 {loading ? ( 46 47 <div className="flex flex-col items-center gap-3 py-4"> ··· 50 51 </p> 51 52 </div> 52 53 ) : ( 53 - <div className="flex flex-col gap-2 py-2"> 54 - <p className="text-secondary text-sm"> 55 - Your Pro subscription is active. Thanks for supporting Leaflet! 54 + <div className="flex flex-col py-2 justify-center text-center"> 55 + <div className="mx-auto pb-2"> 56 + <ProIllo /> 57 + </div> 58 + <h2 className="pb-1">Welcome to Pro!</h2> 59 + <p className="text-secondary pb-3"> 60 + <strong>Thank you for supporting Leaflet.</strong> 61 + <br /> 62 + We're looking forward to bringing you even cooler stuff in the near 63 + future! 56 64 </p> 65 + <ButtonPrimary className="mx-auto mb-2"> 66 + Got it, Thanks! 67 + </ButtonPrimary> 57 68 </div> 58 69 )} 59 70 </Modal> 60 71 ); 61 72 } 73 + 74 + const ProIllo = () => { 75 + return ( 76 + <svg 77 + width="69" 78 + height="58" 79 + viewBox="0 0 69 58" 80 + fill="none" 81 + xmlns="http://www.w3.org/2000/svg" 82 + > 83 + <path 84 + d="M32.0028 25.5087C34.1077 19.0924 49.3153 7.03434 59.1925 6.73505C62.9478 6.62126 66.9368 7.93871 68.026 10.7968C69.2287 13.9525 54.4704 19.3379 55.6833 23.6235C57.4984 30.0369 63.8312 24.671 67.058 30.0369C68.3413 32.171 71.6957 37.8804 63.4278 44.6788C49.9594 55.7531 0.301929 63.5712 0.00262139 50.701C-0.296686 37.8307 25.1212 46.4852 32.0028 25.5087Z" 85 + fill={theme.colors["accent-2"]} 86 + /> 87 + <path 88 + d="M48.5194 32.3093C48.8592 29.0701 50.985 29.1006 51.3352 32.3093C51.6854 35.5175 52.7318 37.5464 53.2116 38.0711C53.6914 38.5957 54.5596 39.2064 57.5085 39.8731C60.4575 40.5397 60.3849 42.4955 57.5085 42.7656C54.6319 43.0356 54.0461 43.8922 53.2116 44.8047C52.3773 45.7174 51.7271 47.6088 51.3445 50.7618C50.9618 53.9152 48.8913 53.889 48.5287 50.7618C48.1661 47.6345 46.9105 45.316 46.443 44.8047C45.9756 44.2936 44.4606 43.0173 41.9461 42.7656C39.4329 42.5133 39.4663 40.2925 41.9485 39.8731C44.4301 39.4535 45.5199 38.7092 46.1035 38.0711C46.6875 37.4324 48.1795 35.5473 48.5194 32.3093ZM45.8013 9.06458C47.281 9.02478 47.2573 9.66421 47.2336 10.3109C47.2184 10.7239 47.2034 11.1415 47.58 11.3874C47.9515 11.6299 48.8873 11.5453 49.961 11.4479C51.6815 11.2918 53.7599 11.1026 54.4579 12.1757C55.0709 13.1204 54.2904 14.0612 53.5418 14.9612C52.9082 15.7229 52.2984 16.4565 52.5768 17.1422C52.8046 17.702 53.2717 18.0189 53.7301 18.328C54.48 18.8337 55.2053 19.3223 54.8159 20.8369C54.4042 22.438 51.9287 22.8718 49.6076 23.2783C47.663 23.6189 45.8256 23.9402 45.4037 24.9129C45.1392 25.5232 45.5794 25.8083 46.071 26.1267C46.658 26.5069 47.3206 26.9367 46.9499 28.0287C46.0811 30.5841 43.1787 30.584 40.4557 30.584C38.1188 30.584 35.9124 30.5848 35.2357 32.2C34.8454 33.1326 35.4363 33.2982 36.0844 33.4788C36.8779 33.7 37.7587 33.9461 37.0307 35.6529C35.6356 38.9212 30.4299 38.9511 25.6653 38.9779C23.9183 38.9877 22.2295 38.9975 20.8104 39.1662C20.6487 39.1857 20.5045 39.2783 20.4197 39.4173C19.1139 41.56 18.1896 43.9185 17.2017 46.4324C16.9847 46.9844 16.7643 47.5445 16.5367 48.1111C16.2356 48.8599 15.3837 49.2231 14.6347 48.9226C13.8854 48.6217 13.5223 47.7699 13.8232 47.0206C14.9441 44.2301 16.2366 41.6636 17.6504 39.308C19.9582 35.0584 24.5076 29.8437 29.3181 25.9267C33.5984 22.3631 37.6865 19.6964 42.4437 17.6724C43.039 17.4191 42.8625 16.7216 42.2414 16.9004C38.0221 18.1197 34.2911 20.5387 30.4249 22.9761C26.2202 25.6269 22.6975 29.2123 20.529 31.8187C20.1467 32.2782 19.3088 31.8997 19.4292 31.3141C20.0314 28.3848 20.6472 25.6099 21.3847 23.5876L21.1568 23.6155C19.1554 23.8912 18.628 24.3386 17.9155 25.1175L17.6179 25.4965C16.9451 26.4889 16.4173 28.2406 16.0903 30.9351C15.7399 33.8221 13.9408 33.9804 13.4256 31.4722L13.3419 30.9351C12.9877 27.8816 11.7615 25.6166 11.3051 25.1175C10.905 24.6807 9.72273 23.9639 7.78941 23.6271L6.91515 23.5085C4.613 23.2781 4.49701 21.3576 6.48964 20.7811L6.91515 20.6834C9.03563 20.325 10.1146 19.8083 10.7331 19.2674L10.9726 19.0349C11.5073 18.4498 12.8205 16.3628 13.2559 13.4987L13.3326 12.9151C13.665 9.75445 15.7386 9.78415 16.081 12.9151C16.4228 16.0477 17.4469 18.5219 17.9155 19.0349C18.3841 19.547 19.2315 20.0327 22.1101 20.6834C22.3847 20.7455 22.6307 20.8242 22.8519 20.9067C24.1417 19.5507 24.5953 19.9503 25.1677 20.4533C25.6512 20.878 26.2194 21.3761 27.4417 20.9508C28.2408 20.6723 29.2729 18.8604 30.4249 16.8399C32.1094 13.8855 34.0498 10.4831 35.8891 10.7666C38.1076 11.1085 37.9705 12.3354 37.8655 13.2801C37.7918 13.9427 37.7355 14.4651 38.5165 14.445C39.3116 14.4237 40.0856 13.4902 40.9835 12.4058C42.2245 10.9072 43.7026 9.12113 45.8013 9.06458ZM14.6091 17.4445C13.9823 18.9641 13.2385 20.0856 12.7304 20.6416C12.2231 21.1964 11.5835 21.6482 10.7842 22.0251C11.5797 22.3604 12.3113 22.7903 12.8467 23.2923L13.0629 23.5108L13.2582 23.7434C13.7158 24.3301 14.1835 25.2206 14.5696 26.1778C14.5888 26.2253 14.604 26.2757 14.6231 26.3243C14.9795 25.259 15.4614 24.2722 16.1577 23.5108C16.6072 23.0193 17.1851 22.4298 18.1364 21.9786C17.2881 21.6125 16.6597 21.1905 16.1577 20.6416C15.7874 20.2365 15.512 19.7124 15.3276 19.3232C15.112 18.8679 14.8934 18.3199 14.6905 17.7096C14.6619 17.6235 14.6374 17.5339 14.6091 17.4445ZM19.4199 1.31479C19.6048 -0.446532 20.7618 -0.429975 20.9522 1.31479C21.1426 3.05974 21.712 4.43815 21.9729 4.72349C22.234 5.00854 22.7074 5.27969 24.3097 5.64193C25.9123 6.00463 25.8731 7.06668 24.3097 7.21374C22.7454 7.36058 22.4268 7.61501 21.9729 8.11126C21.5192 8.60757 21.1649 9.63552 20.9568 11.3502C20.7487 13.0652 19.6217 13.051 19.4246 11.3502C19.2275 9.65095 18.5469 8.39067 18.2922 8.11126C18.038 7.83329 17.2135 7.35062 15.8461 7.21374C14.4793 7.07668 14.4968 5.87032 15.8461 5.64193C17.1956 5.41388 17.7886 5.07044 18.1062 4.72349C18.4236 4.37637 19.2351 3.07635 19.4199 1.31479Z" 89 + fill={theme.colors["accent-1"]} 90 + /> 91 + </svg> 92 + ); 93 + };
+13
lexicons/src/facet.ts
··· 64 64 required: [], 65 65 properties: {}, 66 66 }, 67 + footnote: { 68 + type: "object", 69 + description: "Facet feature for a footnote reference", 70 + required: ["footnoteId", "contentPlaintext"], 71 + properties: { 72 + footnoteId: { type: "string" }, 73 + contentPlaintext: { type: "string" }, 74 + contentFacets: { 75 + type: "array", 76 + items: { type: "ref", ref: "#main" }, 77 + }, 78 + }, 79 + }, 67 80 }; 68 81 69 82 export const PubLeafletRichTextFacet = {
+12
lib/tinybird.ts
··· 94 94 date_from: p.string().optional(), 95 95 date_to: p.string().optional(), 96 96 path: p.string().optional(), 97 + referrer_host: p.string().optional(), 97 98 }, 98 99 tokens: [PROD_READ_TOKEN], 99 100 nodes: [ ··· 116 117 {% if defined(path) %} 117 118 AND path = {{String(path)}} 118 119 {% end %} 120 + {% if defined(referrer_host) %} 121 + AND domain(referrer) = {{String(referrer_host)}} 122 + {% end %} 119 123 GROUP BY day 120 124 ORDER BY day ASC 121 125 `, ··· 146 150 date_from: p.string().optional(), 147 151 date_to: p.string().optional(), 148 152 path: p.string().optional(), 153 + referrer_host: p.string().optional(), 149 154 limit: p.int32().optional(10), 150 155 }, 151 156 nodes: [ ··· 169 174 {% if defined(path) %} 170 175 AND path = {{String(path)}} 171 176 {% end %} 177 + {% if defined(referrer_host) %} 178 + AND domain(referrer) = {{String(referrer_host)}} 179 + {% end %} 172 180 GROUP BY referrer_host 173 181 ORDER BY pageviews DESC 174 182 LIMIT {{Int32(limit, 10)}} ··· 199 207 domains: p.string(), 200 208 date_from: p.string().optional(), 201 209 date_to: p.string().optional(), 210 + referrer_host: p.string().optional(), 202 211 limit: p.int32().optional(10), 203 212 }, 204 213 nodes: [ ··· 216 225 {% end %} 217 226 {% if defined(date_to) %} 218 227 AND fromUnixTimestamp64Milli(timestamp) <= parseDateTimeBestEffort({{String(date_to)}}) 228 + {% end %} 229 + {% if defined(referrer_host) %} 230 + AND domain(referrer) = {{String(referrer_host)}} 219 231 {% end %} 220 232 GROUP BY path 221 233 ORDER BY pageviews DESC
+1 -1
next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 - import "./.next/types/routes.d.ts"; 3 + import "./.next/dev/types/routes.d.ts"; 4 4 5 5 // NOTE: This file should not be edited 6 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+26 -21
src/utils/addImage.ts
··· 19 19 let fileID = v7(); 20 20 let url = client.storage.from("minilink-user-assets").getPublicUrl(fileID) 21 21 .data.publicUrl; 22 - let dimensions = await getImageDimensions(file); 22 + // Re-encode through canvas to bake EXIF orientation into pixel data. 23 + // iPhone photos have EXIF rotation metadata that browsers respect, but 24 + // Supabase's image transformation pipeline strips without applying. 25 + let { blob: uploadBlob, width, height } = await normalizeOrientation(file); 26 + 23 27 await cache.put( 24 28 new URL(url + "?local"), 25 - new Response(file, { 29 + new Response(uploadBlob, { 26 30 headers: { 27 - "Content-Type": file.type, 28 - "Content-Length": file.size.toString(), 31 + "Content-Type": uploadBlob.type, 32 + "Content-Length": uploadBlob.size.toString(), 29 33 }, 30 34 }), 31 35 ); ··· 41 45 type: "image", 42 46 local: rep.clientID, 43 47 src: url, 44 - height: dimensions.height, 45 - width: dimensions.width, 48 + height, 49 + width, 46 50 }, 47 51 }); 48 - await client.storage.from("minilink-user-assets").upload(fileID, file, { 52 + await client.storage.from("minilink-user-assets").upload(fileID, uploadBlob, { 49 53 cacheControl: "public, max-age=31560000, immutable", 50 54 }); 51 55 await rep.mutate.assertFact({ ··· 55 59 fallback: thumbhash, 56 60 type: "image", 57 61 src: url, 58 - height: dimensions.height, 59 - width: dimensions.width, 62 + height, 63 + width, 60 64 }, 61 65 }); 62 66 } ··· 95 99 return thumbHash; 96 100 } 97 101 98 - function getImageDimensions( 102 + async function normalizeOrientation( 99 103 file: File, 100 - ): Promise<{ width: number; height: number }> { 101 - let url = URL.createObjectURL(file); 102 - return new Promise((resolve, reject) => { 103 - const img = new Image(); 104 - img.onload = function () { 105 - resolve({ width: img.width, height: img.height }); 106 - URL.revokeObjectURL(url); 107 - }; 108 - img.onerror = reject; 109 - img.src = url; 110 - }); 104 + ): Promise<{ blob: Blob; width: number; height: number }> { 105 + let bitmap = await createImageBitmap(file); 106 + let canvas = document.createElement("canvas"); 107 + canvas.width = bitmap.width; 108 + canvas.height = bitmap.height; 109 + let ctx = canvas.getContext("2d")!; 110 + ctx.drawImage(bitmap, 0, 0); 111 + bitmap.close(); 112 + let blob = await new Promise<Blob>((resolve) => 113 + canvas.toBlob((b) => resolve(b!), "image/webp", 0.92), 114 + ); 115 + return { blob, width: canvas.width, height: canvas.height }; 111 116 }
+9
src/utils/slugify.ts
··· 1 + export function slugify(text: string): string { 2 + return text 3 + .toLowerCase() 4 + .trim() 5 + .replace(/[^\w\s-]/g, "") 6 + .replace(/[\s_]+/g, "-") 7 + .replace(/-+/g, "-") 8 + .replace(/^-|-$/g, ""); 9 + }
+28
supabase/migrations/20260310000000_fix_search_tags_object_error.sql
··· 1 + -- Fix search_tags function to handle documents where data->'tags' is not a JSON array 2 + -- This prevents "cannot extract elements from an object" errors 3 + CREATE OR REPLACE FUNCTION search_tags(search_query text) 4 + RETURNS TABLE (name text, document_count bigint) AS $$ 5 + BEGIN 6 + RETURN QUERY 7 + SELECT 8 + LOWER(tag::text) as name, 9 + COUNT(DISTINCT d.uri) as document_count 10 + FROM ( 11 + SELECT uri, data 12 + FROM "public"."documents" 13 + WHERE jsonb_typeof(data->'tags') = 'array' 14 + ) d, 15 + jsonb_array_elements_text(d.data->'tags') as tag 16 + WHERE 17 + CASE 18 + WHEN search_query = '' THEN true 19 + ELSE LOWER(tag::text) LIKE '%' || search_query || '%' 20 + END 21 + GROUP BY 22 + LOWER(tag::text) 23 + ORDER BY 24 + COUNT(DISTINCT d.uri) DESC, 25 + LOWER(tag::text) ASC 26 + LIMIT 20; 27 + END; 28 + $$ LANGUAGE plpgsql STABLE;