JustToThePoint English Website Version
JustToThePoint en español

Prayers Tab UI

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

Fourier Series

The Two-Column Layout

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)         │     │
│   └──────────────┘     └────────────────────────────────────┘     │
│                                                                    │
└───────────────────────────────────────────────────────────────────┘

Concept Mapping: Qt → React Native

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:

Other mappings

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.

The most important rule: 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 Wisely

The 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:

Step 1 — Create reusable components

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.tsx

This 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?

// 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 },
});

Step 2 — Replace app/(tabs)/prayers.tsx

This 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",
  },
});

Step 3 — Run and test

  1. npx expo start --clear
  2. Navigate to the ✝ Prayers tab.
  3. Left sidebar. Shows “Prayers” header with count (2).
  4. List items. “Ignatian Morning Prayer” and “Evening Examen” visible.
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.