Do not worry about your difficulties in mathematics. I can assure you mine are still greater, Albert Einstein

The final goal is a clean, desktop‑class layout inside a mobile app:
┌───────────────────────────────────────────────────────────────────┐
│ │
│ Left Column Right Column │
│ (Fixed 200px) (Takes all remaining space) │
│ │
│ ┌──────────────┐ ┌────────────────────────────────────┐ │
│ │ Prayer list │ │ Title │ │
│ │ (scrollable) │ │ Description │ │
│ │ │ │ Date │ │
│ │ │ │ Cover image (placeholder) │ │
│ └──────────────┘ └────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘
If you come from a Qt (C++ / QML) background, the way React Native handles layout can feel foreign at first.
In Qt, you drop a QHBoxLayout and then add widgets. They arrange horizontally. In React Native, the entire layout system is Flexbox, borrowed from CSS. So instead of separate horizontal / vertical layout classes, you have a single View and you set flexDirection:
<View style={{ flex: 1, flexDirection: "row" }}>
{/* left column */}
{/* right column */}
</View>
In Qt you have size policies:
width: 200 – exactly 200 logical pixels.flexDirection: "row" container, a child with flex: 1 will take all the space that is left after the fixed children are laid out.| Qt | React Native | Deeper explanation |
|---|---|---|
setSpacing(12) |
gap: 12 |
Adds space between children, not around the container. |
setContentsMargins |
padding |
Space inside the container, between its border and its children. |
QScrollArea |
ScrollView |
Scrollable container that gives you a scroll bar when content overflows. |
QListWidget |
FlatList |
A high‑performance, virtualized list that only renders items currently in view. |
addStretch() |
justifyContent: "flex-end" |
Pushes children to the end of the main axis. Other values like "center" or "space-between" also exist. |
flex: 1┌────────────────────────────────────────────────────────────┐
│ container: { flex: 1, flexDirection: "row" } │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ width: 200 │ │ flex: 1 │ │
│ │ Fixed │ │ Takes ALL remaining width │ │
│ │ 200px │ │ (screen width minus 200px) │ │
│ └──────────────┘ └──────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Why does flex: 1 work this way?
In React Native (and CSS Flexbox), the flex property is a weight that determines how remaining space is distributed among siblings. When only one sibling has a non‑zero flex, it gets all the leftover space. If two siblings both have flex: 1, they split the remaining space equally. If one has flex: 2 and another flex: 1, they split in a 2:1 ratio.
Inside a row container, the flex values control width. Inside a column container, they control height. So the same pattern could be used vertically – a fixed header and a scrollable body that fills the rest.
FlatList vs ScrollView – Choose WiselyThe choice between these two is one of the most important performance decisions in React Native.
FlatList |
ScrollView |
|
|---|---|---|
| Renders | Only items currently visible on screen | All children are rendered at mount time, even if off‑screen |
| Use for | Long, homogeneous lists (prayers, diary entries) | Short, mixed content (a form, a page of text with images) |
| Key requirement | keyExtractor to give each item a stable identity (like item.id) |
None |
| Performance | Efficient for 100+ items | Slow, scrolling becomes sluggish with 100+ complex items |
latList creates just enough native list rows to fill the visible area plus a small buffer. As you scroll, it recycles those rows, attaching new data to them. The keyExtractor acts like a unique identifier so the system can track which row is which.
In our layout:
Before we fill the tab screen with layout and data, we extract a small, reusable piece: EmptyState.
Why extract EmptyState?
src/components/EmptyState.tsx// src/components/EmptyState.tsx
//
// A reusable empty-state display shown when a list has no items.
// Used in the Prayers tab when no prayers exist yet.
/* How is it used?
In the future prayers tab screen, if prayerList.length === 0, we can render:
<EmptyState
icon="📖"
title="No prayers yet"
subtitle="Tap the + button to add your first prayer"
actionLabel="Add Prayer"
onAction={() => navigation.navigate("add-prayer")}
/>
This one, relatively small, component replaces a whole block of JSX
that would otherwise be written over and over again.
*/
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
interface EmptyStateProps {
/** A large emoji or Unicode character (like "📖"), immediate visual cue, shown at the top */
icon: string;
/** The main message, usually “No prayers yet” or similar. */
title: string;
/** Optional secondary text explaining what to do. */
subtitle?: string;
/** Optional button label */
actionLabel?: string;
/** Called when the action button is pressed. This lets the user, for example, navigate to a “Create” screen. */
onAction?: () => void;
}
export function EmptyState({
icon, title, subtitle, actionLabel, onAction,
}: EmptyStateProps) {
return (
<View style={s.container}>
<Text style={s.icon}>{icon}</Text>
<Text style={s.title}>{title}</Text>
{subtitle ? <Text style={s.subtitle}>{subtitle}</Text> : null}
{actionLabel && onAction ? (
<TouchableOpacity style={s.btn} onPress={onAction}>
<Text style={s.btnText}>{actionLabel}</Text>
</TouchableOpacity>
) : null}
</View>
);
}
/* The styling (s.container) uses flex: 1 to fill its parent, centers its content,
and uses a gap for spacing. The button uses the same accent colour as the rest of the app. */
const s = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 32,
gap: 10,
},
icon: { fontSize: 48, marginBottom: 4 },
title: { fontSize: 16, fontWeight: "600", color: "#9ca3af",
textAlign: "center" },
subtitle: { fontSize: 13, color: "#6b7280", textAlign: "center",
lineHeight: 20 },
btn: { marginTop: 8, backgroundColor: "#5b8fd4", borderRadius: 8,
paddingVertical: 10, paddingHorizontal: 24 },
btnText: { color: "#fff", fontSize: 14, fontWeight: "600" },
});
src/components/LoadingSpinner.tsxThis is a reusable, full‑screen loading indicator used whenever the app performs an asynchronous operation (SQLite queries, API calls, heavy computation). Instead of writing the same <ActivityIndicator> + <Text> block in every screen, we encapsulate it.
Why extract it?
<LoadingSpinner message="Loading prayers…" > is cleaner than repeating five lines of JSX.// src/components/LoadingSpinner.tsx
//
// Reusable full-screen loading spinner.
// Props: message; type: string; Default: "Loading…" Description: Text shown below the spinner.
// message is optional because in many contexts the generic “Loading…” is enough,
// but when we want to be more specific (“Loading prayers…”, “Saving entry…”) we can pass a custom string.
/* Use: if (prayers.loading) {
return <LoadingSpinner message="Loading prayers…" />;
} */
import React from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
interface LoadingSpinnerProps {
message?: string;
}
export function LoadingSpinner({ message = "Loading…" }: LoadingSpinnerProps) {
return (
<View style={s.container}>
<ActivityIndicator size="large" color="#c9a84c" />
<Text style={s.text}>{message}</Text>
</View>
);
}
The component renders a centred container that:
const s = StyleSheet.create({
container: {
flex: 1, // it makes the component fill the available space
backgroundColor: "#1a1f2e", // app background
alignItems: "center",
justifyContent: "center",
gap: 14, // space between spinner and text
},
text: { color: "#9ca3af", fontSize: 14 },
});
app/(tabs)/prayers.tsxThis is the new foundation of the Prayers tab. It reuses two components we just extracted: LoadingSpinner and EmptyState.
It separates data (from the usePrayers hook) and presentation (this component) completely. Handles loading and error states right at the top, before the main layout – a pattern known as early return.
// app/(tabs)/prayers.tsx
//
// PRAYERS TAB — Core Layout
//
// Two-column layout:
// Left (200px fixed): FlatList of all prayers
// Right (flex 1): Prayer header — title, description, date, image slot
//
// DATA FLOW:
// usePrayers() hook provides all state and actions. This file only renders — zero database calls here.
// This file only renders — zero database calls here.
That is the golden rule of this screen. The usePrayers hook encapsulates all the logic — fetching today’s prayer, the prayer list, pagination, refreshing, etc. The screen component just reacts to the values the hook returns. This makes the screen testable, simple, and reusable.
//Imports and Type Dependencies
import React, { useCallback } from "react";
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { usePrayers } from "../../src/hooks/usePrayers";
import { LoadingSpinner } from "../../src/components/LoadingSpinner";
import { EmptyState } from "../../src/components/EmptyState";
import type { PrayerSummary } from "../../src/db/types";
import { isToday } from "../../src/utils/dateUtils";
// State handling with early returns
// This component uses the same pattern we saw previously, but now with reusable components
/* Why early returns? They keep the “happy path” (the main layout) out of nested conditional blocks. */
// ══════════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ══════════════════════════════════════════════════════════════════════════════
export default function PrayersScreen() {
const prayers = usePrayers();
// ── Loading ────────────────────────────────────────────────────────────────
/* Loading state – replaced by our <LoadingSpinner&t;. */
if (prayers.loading) {
return <LoadingSpinner message="Loading prayers…" />;
}
// ── Error ──────────────────────────────────────────────────────────────────
/* Error state – now uses <EmptyState> with a warning icon,
the error message as subtitle, and a “Retry” button that calls prayers.refresh. */
if (prayers.error) {
return (
<EmptyState
icon="⚠️"
title="Something went wrong"
subtitle={prayers.error}
actionLabel="Retry"
onAction={prayers.refresh}
/>
);
}
// ── Main layout ────────────────────────────────────────────────────────────
return (
// After handling loading and error states with early returns, the screen returns its primary layout.
// The Two‑Column Container
// The outermost View (s.root) uses flexDirection: "row" to place the two columns side by side.
// Left column – PrayerList has a fixed width (200px via s.sidebar)
// Vertical divider – a thin (1px) View with a background colour; purely visual, no interactive role.
// flex: 1 makes it fill the whole screen.
<View style={s.root}>
{/* The screen is broken into three function component:
── LEFT COLUMN: Prayer list ─────────────────────────────────────────
Props: prayers (the entire hook return) */}
<PrayerList prayers={prayers} />
{/* ── VERTICAL DIVIDER ──────────────────────────────────────────────── */}
{/* A 1px wide View acts as a visual separator between the columns. */}
<View style={s.divider} />
{/* ── RIGHT COLUMN: Prayer detail ───────────────────────────────────── */}
<PrayerDetail prayers={prayers} />
</View>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// LEFT COLUMN — PRAYER LIST
// ══════════════════════════════════════════════════════════════════════════════
/**
* The left sidebar showing all prayers.
* Uses FlatList for efficient rendering.
*
* WHY extract this into its own function component?
* React re-renders a component when its props change.
* By separating PrayerList from PrayerDetail, a change in the
* selected prayer does not cause the entire (left) list to re-render — they are
* independent components.
* FlatList optimisation && Readability
*
* PrayerList is a pure UI component that receives the entire usePrayers() hook as a single prop.
* So inside this component, prayers contains: prayerList — array of all prayers;
* prayer — the currently selected prayer; prayerCount; etc.
* This means the list UI is completely driven by the hook’s state.
*/
function PrayerList({ prayers }: { prayers: ReturnType<typeof usePrayers> }) {
// useCallback returns the same function reference as long as its dependencies haven’t changed.
// This prevents FlatList from re‑rendering every visible item on each parent update.
// The only dependencies are the selected prayer ID and the loadById function.
// The dependency [prayers.prayer?.id_myprayer] means: only recreate
// renderItem when the selected prayer changes (which affects highlighting).
// ListRenderItemInfo<PrayerSummary> – ensures TypeScript knows the shape of item,
// giving autocompletion and type safety inside renderItem.
const renderItem = useCallback(
({ item }: ListRenderItemInfo<PrayerSummary>) => (
<PrayerListItem
item={item}
/* renderItem only needs to change when the selected prayer changes.
item.id_myprayer is the ID of the prayer represented by this row in the list.
prayers.prayer?.id_myprayer is the ID of the currently selected prayer (from the hook).
The ?. means: If prayers.prayer is null or undefined, then the whole expression
becomes undefined instead of crashing. */
isSelected={item.id_myprayer === prayers.prayer?.id_myprayer}
/* onPress loads the prayer into the main view */
onPress={() => prayers.loadById(item.id_myprayer)}
/>
),
[prayers.prayer?.id_myprayer, prayers.loadById]
);
return (
<View style={s.sidebar}>
{/* Structure of the sidebar. Header row – a horizontal View with a “Prayers” label
and a count badge (prayers.prayerCount). This immediately tells the user how many prayers exist. */}
<View style={s.sidebarHeader}>
<Text style={s.sidebarLabel}>Prayers</Text>
<Text style={s.sidebarCount}>{prayers.prayerCount}</Text>
</View>
{/* The list itself */}
{prayers.prayerList.length === 0 ? (
// If prayers.prayerList.length === 0, an empty state is shown:
// a simple text prompting the user to add a prayer via the + button
<View style={s.sidebarEmpty}>
<Text style={s.sidebarEmptyText}>
No prayers yet.{"\n"}
Tap + to add one.
</Text>
</View>
) : (
// Otherwise, FlatList renders only visible (actual) items.
// FlatList props. (i) data={prayers.prayerList} – the full list of prayers from the database
// (ii) keyExtractor: React needs a stable, unique string key for each list item.
// String(item.id_myprayer) converts the number to a string.
// (iii) renderItem={renderItem} – the memoised render function.
// (iv) contentContainerStyle={s.sidebarList} – styles applied to the inner scrollable container (e.g., padding).
// (v) showsVerticalScrollIndicator={false} – hides the scrollbar for a cleaner look.
<FlatList
data={prayers.prayerList}
keyExtractor={(item) => String(item.id_myprayer)}
renderItem={renderItem}
contentContainerStyle={s.sidebarList}
showsVerticalScrollIndicator={false}
/>
)}
{/* Refresh button pinned to the bottom of the sidebar, always visible.
Tapping it calls prayers.refresh, which reloads today’s prayer.
The button shows “↺ Refresh” and uses activeOpacity={0.7} for a subtle press effect.*/}
<TouchableOpacity
style={s.refreshBtn}
onPress={prayers.refresh}
activeOpacity={0.7}
>
<Text style={s.refreshBtnText}>↺ Refresh</Text>
</TouchableOpacity>
</View>
);
}
// ── Prayer list item ──────────────────────────────────────────────────────────
/**
* One row in the prayer list.
* Extracted so React can memoize it — unselected items do not re-render
* when a different item is selected.
*
* Props: (i) item — the prayer summary for this row;
* (ii) isSelected — whether this row is the currently selected prayer;
* (iii) onPress — callback to load this prayer when tapped;
*
* Shows:
* - Today badge (gold dot) if the prayer's date is today
* - Prayer title
* - Date (formatted as "14 May" for compactness)
* - Active indicator (left gold border) when selected
*/
function PrayerListItem({
item,
isSelected,
onPress,
}: {
item: PrayerSummary;
isSelected: boolean;
onPress: () => void;
}) {
// This computes whether the prayer’s date matches today’s date.
const today = isToday(item.date);
return (
<TouchableOpacity
style={[
s.listItem, // Base style
isSelected && s.listItemSelected, // If selected: add s.listItemSelected
]}
onPress={onPress} // onPress loads the prayer
/* In reality, it only knows that when tapped, "call the function that the parent gave me".
That function is passed in as a prop: function PrayerListItem({ item, isSelected, onPress }) { ... }
Look at where PrayerListItem is created:
const renderItem = useCallback(
({ item }) => (
<PrayerListItem
item={item}
isSelected={item.id_myprayer === prayers.prayer?.id_myprayer}
onPress={() => prayers.loadById(item.id_myprayer)}
When the row is tapped. Call prayers.loadById(...). Which loads the prayer from the database.
And updates the selected prayer in the UI
/>
),
[prayers.prayer?.id_myprayer, prayers.loadById]
); */
activeOpacity={0.7}
>
{/* Left gold border (Selection Indicator) — only visible when selected */}
{isSelected && <View style={s.listItemBorder} />}
/* Content Container. This holds the title + date. */
<View style={s.listItemContent}>
{/* Title row: title + today dot. The title is shown in up to 2 lines.
If selected → apply a bold or colored style. If the prayer is for today → show a small gold dot */}
<View style={s.listItemTitleRow}>
<Text
style={[s.listItemTitle, isSelected && s.listItemTitleSelected]}
numberOfLines={2}
>
{item.title}
</Text>
{today && <View style={s.todayDot} />}
</View>
{/* Date — It shows the prayer’s date in a compact format: "14 May 2026" */}
<Text style={s.listItemDate}>
{item.date
? item.date
: "no date"}
</Text>
</View>
</TouchableOpacity>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// RIGHT COLUMN — PRAYER DETAIL HEADER
// ══════════════════════════════════════════════════════════════════════════════
/**
* The PrayerDetail component is the main content area of the Prayers tab.
* It receives the entire prayers state from the usePrayers hook and renders one of two view.
* It gives the component direct access to everything it might need (the selected prayer, the total prayer count,
* and actions like refresh or goToPage if required).
*
* (1) An empty‑state prompt when no prayer is selected (e.g., first launch, or database empty).
* (2) The prayer header when a prayer is loaded, showing title, date, description,
* cover image placeholder, page‑count summary, and a temporary development badge.
*
* This component is purely presentational — it contains no state, no effects, and no direct database access.
*/
function PrayerDetail({ prayers }: { prayers: ReturnType<typeof usePrayers> }) {
// State 1: No Prayer Selected
// Early return – This keeps the “happy path” (prayer loaded) free of nested conditionals.
if (!prayers.prayer) {
return (
/* Layout – s.main has flex: 1, so the empty state fills the entire right panel and centres its content. */
<View style={s.main}>
/* The same reusable component EmptyState used elsewhere for errors and empty lists. */
<EmptyState
icon="✝"
title="No prayer selected"
/* Context‑sensitive subtitle – Uses prayers.prayerCount to decide whether the library is empty. */
subtitle={
prayers.prayerCount > 0
? "Tap a prayer in the list to open it."
: "Tap + to add your first prayer."
}
/>
</View>
);
}
const { prayer } = prayers;
/* State 2: Prayer Loaded. When prayers.prayer is not null,
the component renders a detailed header and temporary page‑count area.*/
return (
<View style={s.main}>
{/* ── Prayer header ────────────────────────────────────────────────────
This View groups all the prayer’s metadata: optional cover image, title, date, today badge,
description, and a divider. Each piece is conditionally rendered if its data exists.*/}
<View style={s.detailHeader}>
{/* Cover image Placeholder */}
{prayer.image ? (
/* Only rendered if prayer.image is truthy (presumably a file path or URL). In the current post,
we don’t actually load images, so we show a placeholder rectangle with a camera emoji. */
<View style={s.coverImagePlaceholder}>
/* The style defines height: 120 and width: "100%".
This reserves vertical space, preventing layout jumps when the real image is added */
<Text style={s.coverImageText}>📷</Text>
</View>
) : null}
{/* Title. fontSize: 22, fontWeight: "bold", color: "#c9a84c" (gold).
The large, bold gold text creates a strong visual hierarchy: the title is the most important element. */}
<Text style={s.detailTitle}>{prayer.title}</Text>
{/* Date + today badge */}
<View style={s.detailDateRow}>
<Text style={s.detailDate}>{prayer.date || "No date"}</Text>
/* Today badge – Only rendered when isToday(prayer.date) returns true. */
{isToday(prayer.date) && (
<View style={s.todayBadge}>
<Text style={s.todayBadgeText}>Today</Text>
</View>
)}
</View>
{/* Description. Many prayers may lack a description,
so the whole element is omitted when prayer.description is falsy. */}
{prayer.description ? (
<Text style={s.detailDescription}>{prayer.description}</Text>
) : null}
{/* Divider. A thin horizontal line that visually separates the prayer’s metadata
(title, date, description) from the page‑content area below. */}
<View style={s.detailDivider} />
</View>
{/* ── Page count summary (Temporary Placeholder) ─────────────────────────────────────────────── */}
<View style={s.pageCountArea}>
<Text style={s.pageCountLabel}>
// Displays the number of pages with a dynamic “page” vs “pages”
{prayer.pages.length > 0
? `${prayer.pages.length} page${prayer.pages.length !== 1 ? "s" : ""} · Coming ...`
: "No pages yet"}
</Text>
{/* Preview first page title. */}
{prayer.pages.length > 0 && ( // Only rendered if there is at least one page.
<View style={s.firstPagePreview}>
<Text style={s.firstPageLabel}>First page:</Text>
<Text style={s.firstPageContent} numberOfLines={3}>
// Uses two regex substitutions to strip HTML and make the content readable
// The result is trimmed and limited to 3 lines (numberOfLines={3}).
// The preview is wrapped in a card‑like container (s.firstPagePreview)
// with a dark background, border, and padding.
{prayer.pages[0].content
.replace(/<h2>(.*?)<\/h2>/g, "$1: ")
.replace(/<[^>]+>/g, "")
.trim()}
</Text>
</View>
)}
</View>
{/* ── A development‑only label ────────────────────────────────────────────────────── */}
<View style={s.chunkBadge}>
<Text style={s.chunkBadgeText}>
Core layout complete · Page viewer will come next.
</Text>
</View>
</View>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// STYLES
// ══════════════════════════════════════════════════════════════════════════════
//
// Colour palette (consistent with Chunk 02 definitions):
// #1a1f2e BG dark navy
// #222840 PANEL slightly lighter
// #2d3550 BORDER subtle borders
// #5b8fd4 ACCENT blue
// #dde4f0 TEXT light
// #9ca3af MUTED dim text
// #6b7280 FAINT very dim
// #c9a84c GOLD highlights
// #4b5563 DIM near-invisible
const s = StyleSheet.create({
// ── Root layout ─────────────────────────────────────────────────────────────
root: {
flex: 1,
flexDirection: "row", // ← QHBoxLayout
backgroundColor: "#1a1f2e",
},
// ── Vertical divider ─────────────────────────────────────────────────────────
divider: {
width: 1,
backgroundColor: "#2d3550",
},
// ══ SIDEBAR ══════════════════════════════════════════════════════════════════
sidebar: {
width: 200, // fixed width — equivalent to Qt setFixedWidth(200)
backgroundColor: "#1e2338",
// No flex: 1 here — this column has a fixed width
},
sidebarHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 14,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: "#2d3550",
},
sidebarLabel: {
fontSize: 11,
fontWeight: "700",
color: "#6b7280",
textTransform: "uppercase",
letterSpacing: 0.8,
},
sidebarCount: {
fontSize: 11,
color: "#5b8fd4",
backgroundColor: "#1a2a3a",
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 10,
overflow: "hidden",
},
sidebarList: {
paddingVertical: 4,
},
sidebarEmpty: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
sidebarEmptyText: {
fontSize: 12,
color: "#4b5563",
textAlign: "center",
lineHeight: 18,
},
// ── Sidebar list item ─────────────────────────────────────────────────────────
listItem: {
flexDirection: "row",
paddingVertical: 10,
paddingLeft: 14,
paddingRight: 10,
borderBottomWidth: 1,
borderBottomColor: "#1a1f2e",
// min height so short titles still have a tap target
minHeight: 56,
},
listItemSelected: {
backgroundColor: "#222840",
},
listItemBorder: {
// The gold left border — visible only on selected item
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: 2,
backgroundColor: "#c9a84c",
},
listItemContent: {
flex: 1,
},
listItemTitleRow: {
flexDirection: "row",
alignItems: "flex-start",
gap: 6,
marginBottom: 3,
},
listItemTitle: {
flex: 1,
fontSize: 13,
color: "#9ca3af",
lineHeight: 18,
},
listItemTitleSelected: {
color: "#dde4f0",
fontWeight: "600",
},
listItemDate: {
fontSize: 10,
color: "#4b5563",
fontFamily: "monospace",
},
// ── Today dot ─────────────────────────────────────────────────────────────────
todayDot: {
width: 7,
height: 7,
borderRadius: 4,
backgroundColor: "#c9a84c",
marginTop: 4, // align with text baseline
flexShrink: 0, // don't shrink when title is long
},
// ── Refresh button ────────────────────────────────────────────────────────────
refreshBtn: {
paddingVertical: 12,
paddingHorizontal: 14,
borderTopWidth: 1,
borderTopColor: "#2d3550",
alignItems: "center",
},
refreshBtnText: {
fontSize: 12,
color: "#6b7280",
},
// ══ MAIN PANEL ════════════════════════════════════════════════════════════════
main: {
flex: 1, // ← takes all remaining width (equivalent to Expanding)
padding: 20,
},
// ── Detail header ─────────────────────────────────────────────────────────────
detailHeader: {
marginBottom: 16,
},
coverImagePlaceholder: {
width: "100%",
height: 120,
backgroundColor: "#222840",
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
borderWidth: 1,
borderColor: "#2d3550",
},
coverImageText: {
fontSize: 32,
color: "#4b5563",
},
detailTitle: {
fontSize: 22,
fontWeight: "bold",
color: "#c9a84c",
marginBottom: 6,
lineHeight: 28,
},
detailDateRow: {
flexDirection: "row",
alignItems: "center",
gap: 8,
marginBottom: 10,
},
detailDate: {
fontSize: 12,
color: "#6b7280",
fontFamily: "monospace",
},
todayBadge: {
backgroundColor: "#2a2010",
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
borderWidth: 1,
borderColor: "#c9a84c",
},
todayBadgeText: {
fontSize: 10,
color: "#c9a84c",
fontWeight: "600",
},
detailDescription: {
fontSize: 14,
color: "#9ca3af",
lineHeight: 21,
marginBottom: 16,
},
detailDivider: {
height: 1,
backgroundColor: "#2d3550",
},
// ── Page count area (placeholder for Chunk 12) ────────────────────────────────
pageCountArea: {
marginTop: 16,
gap: 10,
},
pageCountLabel: {
fontSize: 12,
color: "#5b8fd4",
fontWeight: "600",
},
firstPagePreview: {
backgroundColor: "#222840",
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: "#2d3550",
gap: 6,
},
firstPageLabel: {
fontSize: 11,
color: "#6b7280",
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
},
firstPageContent: {
fontSize: 13,
color: "#9ca3af",
lineHeight: 20,
},
// ── Chunk badge ───────────────────────────────────────────────────────────────
chunkBadge: {
position: "absolute",
bottom: 12,
left: 20,
right: 20,
backgroundColor: "#1a1f2e",
borderRadius: 6,
padding: 8,
borderWidth: 1,
borderColor: "#2d3550",
},
chunkBadgeText: {
fontSize: 10,
color: "#4b5563",
textAlign: "center",
fontFamily: "monospace",
},
});
npx expo start --clear