/** * GardenJournal — the cozy gardener's notebook shell for PixieSprout. * * APPROACH (Bekky, 2026-08-11): the journal is a self-contained notebook PANEL * (not a full-screen bleed) that sits in the app's content area between the * "Care" header (top) and the bottom nav bar. The journal image is contain-fit * and centered; the OUTER layer is TRANSPARENT so the app's botanical overlay * (already rendered behind every tab) shows through the cream margin around the * journal. The journal's own cream margin provides a natural fade so the * botanicals don't overlap the paper. * * The paper is EMPTY (no ruled lines, no section borders baked in) — those are * COMPUTER-GENERATED React Native components laid on top so they resize with the * content and align with the text. This component only provides the journal * background + the content container mapped onto the writing area. * * Pure React Native — no native modules, OTA-safe. */ import React, { useMemo } from 'react'; import { Dimensions, Image, ScrollView, StyleSheet, Text, View, ViewStyle, StyleProp, } from 'react-native'; import { journalFullPage } from '../constants/images'; type Props = { children: React.ReactNode; style?: StyleProp; /** Handwritten title written in the journal's blank header (e.g. "Care"). */ title?: string; /** Compact mode for the Home page journal (smaller). */ compact?: boolean; /** Enlarge to fill the visible screen (Care tab anchor). */ fullPage?: boolean; }; // journal art is 911x1726 (aspect 0.5278 — tall, matches the content area) const IMG_W = 911; const IMG_H = 1726; const IMG_ASPECT = IMG_W / IMG_H; // The top botanical sprig pokes up above the paper. Its tip is at image row // ~32 (0.0185 of height). To let it show into the Care header without moving // the content, we shift the journal image up by this overlap (scaled) and // remove the outer clip so the sprig renders above the content area, behind // the transparent header. const SPRIG_OVERLAP_FRAC = 0.02; // fraction of image height to let poke up // Measured geometry of the journal art (fractions of the IMAGE): // notebook panel spans y ~0.019..0.928, x ~0.041..0.986 // blank header zone (title) ~y 0.05..0.20 // spiral rings occupy left ~x 0.05..0.13; page stack + bookmarks ~x 0.84..0.99 // content writing area ~x 0.15..0.82, starts ~y 0.22 const TITLE_Y_FRAC = 0.13; // center of the handwritten title within the header // The "Care" title now lives in the app header ABOVE the journal (uniform with // every other tab), so the journal's blank header zone is free. Content starts // high on the page (just below the top sprig) so tasks fit more on one page. const CONTENT_TOP_FRAC = 0.12; // where the "Due Today" heading sits — padded down // from the top, but only about half the previous gap (Bekky, 2026-08-12: "cut // it in half"). const CONTENT_LEFT_FRAC = 0.15; // clear the spiral const CONTENT_RIGHT_FRAC = 0.82; // clear the page stack + bookmarks // The content box must NOT extend into the bottom floral / page stack (which // starts ~0.85 of the image height). Cap the content bottom so the text stays // on the paper and never overlaps the decorative page stack // (Bekky, 2026-08-12 — "going into the page stack ruins immersion"). // The notebook PAPER in the art ends at y ~0.90 of the image height (MEASURED // from the asset, 2026-08-12 — the cream paper spans 0.094..0.90); below that // is the decorative bottom-floral margin. To make the paper reach just above // the nav bar (Bekky, 2026-08-12), we scale the image so the paper's bottom // edge (0.90) lands at the content-area bottom, keeping the top anchored. const NOTEBOOK_PAPER_BOTTOM_FRAC = 0.90; // The cream paper STARTS at y ~0.094 of the image (below the top sprig). const NOTEBOOK_PAPER_TOP_FRAC = 0.094; // The content box must NOT extend into the bottom floral / page stack (which // starts ~0.85 of the image height). Cap the content bottom at 0.80 so the // text stays on the paper and never overlaps the decorative page stack // (Bekky, 2026-08-12 — "going into the page stack ruins immersion"). // NOTE (2026-08-13): the scroll field's bottom gap (paper bottom → scroll // bottom) was ~4x the top gap (paper top → scroll top), so the journal // "stopped short too high". Make the two gaps EQUAL so the scroll field is // vertically centered on the paper: // top gap = CONTENT_TOP_FRAC - PAPER_TOP_FRAC // bottom gap = PAPER_BOTTOM_FRAC - CONTENT_BOTTOM_FRAC // equal => CONTENT_BOTTOM_FRAC = PAPER_BOTTOM_FRAC - (CONTENT_TOP_FRAC - PAPER_TOP_FRAC) const CONTENT_BOTTOM_FRAC = NOTEBOOK_PAPER_BOTTOM_FRAC - (CONTENT_TOP_FRAC - NOTEBOOK_PAPER_TOP_FRAC); const win = Dimensions.get('window'); export function GardenJournal({ children, style, title, compact = false, fullPage = false, }: Props) { // For fullPage, the journal fills the content area BELOW the "Care" header // (84px, in normal flow above it) down to the bottom nav bar. This is the // approved v14 geometry — the journal sits below the header, not behind it. // The SafeAreaView reserves the status-bar inset at the top. const HEADER_H = 84; // App.tsx styles.header height const NAV_BAR_H = Math.round(win.width * 0.95 * 0.236 + 16); // ~nav height + system inset const CONTENT_BOTTOM_PAD = 8; const cw = fullPage ? win.width : win.width * 0.9; const ch = fullPage ? win.height - HEADER_H - NAV_BAR_H - CONTENT_BOTTOM_PAD : win.width * 0.9 / IMG_ASPECT * 0.5; const { pageLeft, pageTop, pageW, pageH } = useMemo(() => { // FILL the HEIGHT (bigger, like the approved v14) so the journal spans the // full content area between header and nav bar. The width overflows and // crops slightly (left/right), and the FULL height is visible — so the top // sprig is shown, not cropped. const scale = Math.max(cw / IMG_W, ch / IMG_H); const pw = IMG_W * scale; const ph = IMG_H * scale; const pl = (cw - pw) / 2; const pt = (ch - ph) / 2; return { pageLeft: pl, pageTop: pt, pageW: pw, pageH: ph }; }, [fullPage]); // How far the sprig pokes up above the content area (scaled). The journal // image is shifted up by this so the sprig tip reaches into the header, // while the content stays anchored at pageTop + pageH*CONTENT_TOP_FRAC. const sprigOverlap = fullPage ? pageH * SPRIG_OVERLAP_FRAC : 0; // fullPage: keep the WIDTH exactly as it is now (pageW) and only extend the // LENGTH (height) so the notebook PAPER (which ends at 0.928 of the image // height) reaches just above the nav bar (the content-area bottom, ch). // This is a non-uniform vertical stretch — width unchanged, paper taller. // paperBottom = top + 0.928 * height = ch => height = (ch - top) / 0.928 const fullTop = pageTop - sprigOverlap; const fullHeight = fullPage ? (ch - fullTop) / NOTEBOOK_PAPER_BOTTOM_FRAC : pageH + sprigOverlap; // Convert a fraction-of-image to an absolute offset within the container. const pxFromImageFrac = (fx: number, fy: number) => ({ x: pageLeft + pageW * fx, y: pageTop + pageH * fy, }); const titlePos = pxFromImageFrac(0.5, TITLE_Y_FRAC); return ( {/* the journal as a contain-fit panel — outer layer transparent so the botanical overlay shows through the cream margin around it */} {/* handwritten title in the blank header zone */} {title ? ( {title} ) : null} {/* content mapped onto the journal's writing area. The content SCROLLS within the paper (Bekky, 2026-08-12) — the box is clipped to the paper's writing area and scrolls vertically so all tasks are reachable while staying visually on the page. */} {children} ); } // helpers (compact sizing keeps it simple: smaller page centered) function cwForCompact(pageW: number, compact?: boolean) { return compact ? pageW * 0.9 : pageW; } function chForCompact(pageH: number, compact?: boolean) { return compact ? pageH * 0.9 : pageH; } const styles = StyleSheet.create({ outer: { position: 'relative', flex: 1, // NOTE: no overflow:'hidden' here — the top sprig is shifted up to poke // into the Care header, so it must be allowed to render above the content // area. The header (rendered before this, transparent) sits on top via // zIndex so the sprig shows behind the title/gear/bell. backgroundColor: 'transparent', // let the botanical overlay show through }, outerFull: { flex: 1, }, outerCompact: { borderRadius: 10, overflow: 'hidden', }, sheet: { position: 'absolute', }, titleRow: { position: 'absolute', justifyContent: 'center', alignItems: 'center', height: 60, zIndex: 5, }, titleRowCompact: { height: 40 }, titleText: { color: '#2E6B3F', fontSize: 34, fontFamily: 'Caveat', textShadowColor: 'rgba(46,107,63,0.15)', textShadowRadius: 3, textShadowOffset: { width: 1, height: 1 }, }, titleTextCompact: { fontSize: 24 }, content: { position: 'absolute', overflow: 'hidden', }, contentInner: { paddingBottom: 24, }, contentFull: { overflow: 'hidden', }, contentCompact: { paddingHorizontal: 18, }, });