···4444import { List, parseBlocksToList } from "src/utils/parseBlocksToList";
4545import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks";
4646import { Lock } from "src/utils/lock";
4747+import type { PubLeafletPublication } from "lexicons/api";
47484849export async function publishToPublication({
4950 root_entity,
···108109109110 let existingRecord =
110111 (draft?.documents?.data as PubLeafletDocument.Record | undefined) || {};
112112+113113+ // Extract theme for standalone documents (not for publications)
114114+ let theme: PubLeafletPublication.Theme | undefined;
115115+ if (!publication_uri) {
116116+ theme = await extractThemeFromFacts(facts, root_entity, agent);
117117+ }
118118+111119 let record: PubLeafletDocument.Record = {
112120 $type: "pub.leaflet.document",
113121 author: credentialSession.did!,
114122 ...(publication_uri && { publication: publication_uri }),
123123+ ...(theme && { theme }),
115124 publishedAt: new Date().toISOString(),
116125 ...existingRecord,
117126 title: title || "Untitled",
···606615 ? never
607616 : T /* maybe literal, not the whole `string` */
608617 : T; /* not a string */
618618+619619+async function extractThemeFromFacts(
620620+ facts: Fact<any>[],
621621+ root_entity: string,
622622+ agent: AtpBaseClient,
623623+): Promise<PubLeafletPublication.Theme | undefined> {
624624+ let scan = scanIndexLocal(facts);
625625+626626+ let pageBackground = scan.eav(root_entity, "theme/page-background")?.[0]?.data
627627+ .value;
628628+ let cardBackground = scan.eav(root_entity, "theme/card-background")?.[0]?.data
629629+ .value;
630630+ let primary = scan.eav(root_entity, "theme/primary")?.[0]?.data.value;
631631+ let accentBackground = scan.eav(root_entity, "theme/accent-background")?.[0]
632632+ ?.data.value;
633633+ let accentText = scan.eav(root_entity, "theme/accent-text")?.[0]?.data.value;
634634+ let showPageBackground = !scan.eav(
635635+ root_entity,
636636+ "theme/card-border-hidden",
637637+ )?.[0]?.data.value;
638638+ let backgroundImage = scan.eav(root_entity, "theme/background-image")?.[0];
639639+ let backgroundImageRepeat = scan.eav(
640640+ root_entity,
641641+ "theme/background-image-repeat",
642642+ )?.[0];
643643+644644+ // Helper to convert hex/hsba color string to RGB/RGBA object
645645+ const parseColorToRGB = (
646646+ colorStr: string,
647647+ ):
648648+ | { $type: "pub.leaflet.theme.color#rgb"; r: number; g: number; b: number }
649649+ | {
650650+ $type: "pub.leaflet.theme.color#rgba";
651651+ r: number;
652652+ g: number;
653653+ b: number;
654654+ a: number;
655655+ }
656656+ | undefined => {
657657+ // Try hex format first: #RRGGBB
658658+ const hexMatch = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(colorStr);
659659+ if (hexMatch) {
660660+ return {
661661+ $type: "pub.leaflet.theme.color#rgb" as const,
662662+ r: parseInt(hexMatch[1], 16),
663663+ g: parseInt(hexMatch[2], 16),
664664+ b: parseInt(hexMatch[3], 16),
665665+ };
666666+ }
667667+668668+ // Try hsba format: hsba(h, s%, b%, a)
669669+ const hsbaMatch =
670670+ /^hsba\((\d+),\s*(\d+)%,\s*(\d+)%,\s*(\d+(?:\.\d+)?)\)$/i.exec(colorStr);
671671+ if (hsbaMatch) {
672672+ const h = parseInt(hsbaMatch[1]);
673673+ const s = parseInt(hsbaMatch[2]) / 100;
674674+ const b = parseInt(hsbaMatch[3]) / 100;
675675+ const a = Math.round(parseFloat(hsbaMatch[4]) * 100);
676676+677677+ // Convert HSB to RGB
678678+ const c = b * s;
679679+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
680680+ const m = b - c;
681681+682682+ let r = 0,
683683+ g = 0,
684684+ bl = 0;
685685+ if (h >= 0 && h < 60) {
686686+ r = c;
687687+ g = x;
688688+ bl = 0;
689689+ } else if (h >= 60 && h < 120) {
690690+ r = x;
691691+ g = c;
692692+ bl = 0;
693693+ } else if (h >= 120 && h < 180) {
694694+ r = 0;
695695+ g = c;
696696+ bl = x;
697697+ } else if (h >= 180 && h < 240) {
698698+ r = 0;
699699+ g = x;
700700+ bl = c;
701701+ } else if (h >= 240 && h < 300) {
702702+ r = x;
703703+ g = 0;
704704+ bl = c;
705705+ } else {
706706+ r = c;
707707+ g = 0;
708708+ bl = x;
709709+ }
710710+711711+ return {
712712+ $type: "pub.leaflet.theme.color#rgba" as const,
713713+ r: Math.round((r + m) * 255),
714714+ g: Math.round((g + m) * 255),
715715+ b: Math.round((bl + m) * 255),
716716+ a,
717717+ };
718718+ }
719719+720720+ return undefined;
721721+ };
722722+723723+ let theme: PubLeafletPublication.Theme = {
724724+ showPageBackground: showPageBackground ?? true,
725725+ };
726726+727727+ if (pageBackground) theme.backgroundColor = parseColorToRGB(pageBackground);
728728+ if (cardBackground) theme.pageBackground = parseColorToRGB(cardBackground);
729729+ if (primary) theme.primary = parseColorToRGB(primary);
730730+ if (accentBackground)
731731+ theme.accentBackground = parseColorToRGB(accentBackground);
732732+ if (accentText) theme.accentText = parseColorToRGB(accentText);
733733+734734+ // Upload background image if present
735735+ if (backgroundImage?.data) {
736736+ let imageData = await fetch(backgroundImage.data.src);
737737+ if (imageData.status === 200) {
738738+ let binary = await imageData.blob();
739739+ let blob = await agent.com.atproto.repo.uploadBlob(binary, {
740740+ headers: { "Content-Type": binary.type },
741741+ });
742742+743743+ theme.backgroundImage = {
744744+ $type: "pub.leaflet.theme.backgroundImage",
745745+ image: blob.data.blob,
746746+ repeat: backgroundImageRepeat?.data.value ? true : false,
747747+ ...(backgroundImageRepeat?.data.value && {
748748+ width: backgroundImageRepeat.data.value,
749749+ }),
750750+ };
751751+ }
752752+ }
753753+754754+ // Only return theme if at least one property is set
755755+ if (Object.keys(theme).length > 1 || theme.showPageBackground !== true) {
756756+ return theme;
757757+ }
758758+759759+ return undefined;
760760+}
+1-1
app/(home-pages)/discover/PubListing.tsx
···1616 },
1717) => {
1818 let record = props.record as PubLeafletPublication.Record;
1919- let theme = usePubTheme(record);
1919+ let theme = usePubTheme(record.theme);
2020 let backgroundImage = record?.theme?.backgroundImage?.image?.ref
2121 ? blobRefToSrc(
2222 record?.theme?.backgroundImage?.image?.ref,
+1-1
app/(home-pages)/reader/ReaderContent.tsx
···102102 let postRecord = props.documents.data as PubLeafletDocument.Record;
103103 let postUri = new AtUri(props.documents.uri);
104104105105- let theme = usePubTheme(pubRecord);
105105+ let theme = usePubTheme(pubRecord?.theme);
106106 let backgroundImage = pubRecord?.theme?.backgroundImage?.image?.ref
107107 ? blobRefToSrc(
108108 pubRecord?.theme?.backgroundImage?.image?.ref,
···66import { validate as _validate } from '../../../lexicons'
77import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef'
99+import type * as PubLeafletPublication from './publication'
910import type * as PubLeafletPagesLinearDocument from './pages/linearDocument'
1011import type * as PubLeafletPagesCanvas from './pages/canvas'
1112···2122 publishedAt?: string
2223 publication?: string
2324 author: string
2525+ theme?: PubLeafletPublication.Theme
2426 pages: (
2527 | $Typed<PubLeafletPagesLinearDocument.Main>
2628 | $Typed<PubLeafletPagesCanvas.Main>