JustToThePoint English Website Version
JustToThePoint en español

Diary Tab UI (Editor). Layout and the Keyboard Problem.

Before you marry a person, you should first make them use a computer with slow internet to see who they really are. — Will Ferrell

In real life, I assure you, there is no such thing as algebra. — Fran Lebowitz

There are only two things I can’t stand in this world: People who are intolerant of other people’s cultures, and the Dutch. — Nigel Powers, “Austin Powers in Goldmember”

Difficult people

Understanding the Diary Tab UI: A Complete Breakdown

What We’re Building

This is a diary/journaling interface for a mobile app. Think of it like a two-panel layout where you can browse your past entries on one side and write new ones on the other. The big challenge here isn’t the writing itself – it’s making sure the keyboard doesn’t ruin everything.

The Layout Structure

The Two-Column Design. Picture your screen split into two distinct areas:

  1. Left Side (The Sidebar):
    This is your “history browser.” It shows a scrollable list of everything you’ve written before. Each entry in this list shows you three things at a glance: the date you wrote it, how many words it contains, and the first few lines as a preview.
    At the bottom of this column, there’s always a “+ New Entry” button waiting for you to start something fresh.
  2. Right Side (The Editor):
    This is where the actual writing happens. It takes up the full height of the screen and contains a big text area where you can type as much as you want.
    Above this text area, there’s a toolbar with Save and Delete buttons, plus a little status indicator that tells you whether your changes have been saved or not.

When you haven’t selected any entry yet, this right side is empty – just waiting for you to either pick something from the left or hit that “+ New Entry” button.

The Keyboard Problem: Why This Is Hard

How Desktop vs. Mobile Works

When you write code for a regular computer (like a Python desktop app), you never have to think about the keyboard. It’s a physical thing sitting on your desk. Your screen stays exactly the same size whether someone is typing or not.

Mobile phones are completely different. The keyboard isn’t physical – it’s software that slides up from the bottom of the screen. When it appears, it literally covers up the bottom portion of whatever was showing. If your text input happens to be near the bottom of the screen, the keyboard will hide it completely. The person using your app can't see what they're typing.

What KeyboardAvoidingView Actually Does

KeyboardAvoidingView is a special container component that watches for the keyboard appearing and disappearing. When it detects the keyboard sliding up, it automatically adjusts its own size or position to make sure the text input stays visible above the keyboard.

Think of it like this: imagine you’re writing on a piece of paper, and someone slides a big board over the bottom half. You’d instinctively push your paper upward so you can keep writing on the visible part. That’s exactly what KeyboardAvoidingView does – it pushes your content up so the part you’re actively using stays in view.

Platform Differences: iOS vs. Android

Why They Behave Differently

Apple and Google made fundamentally different decisions about how their operating systems handle the keyboard, and this is one of the most frustrating things about mobile development. The same KeyboardAvoidingView needs to work differently depending on which phone it’s running on.

The behavior Prop: Two Different Strategies

On iOS (using “padding”):
When the keyboard appears, iOS adds empty space (padding) to the bottom of your view that’s exactly the same height as the keyboard. Your content doesn’t actually get smaller – it gets pushed up because there’s now invisible padding at the bottom. The content scrolls up smoothly and naturally, which feels right on an iPhone.

On Android (using “height”):
Android works differently. Instead of adding padding, it actually shrinks the available height of your view so that whatever remains fits perfectly above the keyboard. The view literally becomes shorter while the keyboard is visible. If you tried to use “padding” on Android, you’d often get a double-offset problem – the content would jump up twice as far as it should, which looks broken and confusing.

The Android Manifest Connection

There’s another layer to this on Android. Every Android app has a configuration file called AndroidManifest.xml that contains a setting called windowSoftInputMode. This setting tells Android what to do with your app’s window when the keyboard appears. Expo (the framework we're using) sets this to adjustResize automatically, which means “please resize my window when the keyboard shows up.” This is why the “height” behavior works correctly – the operating system is already expecting to resize things.

The Platform API: Writing Code That Knows Where It’s Running

What Platform.OS Gives You

React Native provides a Platform object that lets your code check what kind of device it’s running on. Platform.OS returns a string – either

Instead of maintaining two completely separate codebases, you can have one codebase with small conditional branches that say “if iOS, do this; if Android, do that.” The keyboard behavior difference is a perfect example – both platforms need the same component, but they need it configured differently.

Responsive Layout with useWindowDimensions and Keyboard Events

Why Window Dimensions Matter

Mobile phones come in wildly different sizes – from small iPhone SE screens to massive Android tablets. Hard-coding sizes (like “make this 300 pixels wide”) will look wrong on different devices. useWindowDimensions is a React Native hook that gives you the actual width and height of the device’s screen in real-time. Even better, it updates automatically if the screen size changes (like when a tablet rotates from portrait to landscape).

Keyboard Event Listeners

The Keyboard module lets you listen for when the keyboard appears and disappears. It gives you events that tell you the exact height of the keyboard and how long the animation will take. This is crucial for creating smooth, polished experiences – you can animate your layout changes to match the keyboard’s own animation timing, so everything moves together naturally instead of jumping around.

<KeyboardAvoidingView
  behavior={Platform.OS === "ios" ? "padding" : "height"}
>
Platform behavior Effect
iOS "padding" Adds padding to the bottom of the view equal to the keyboard height. Content scrolls up naturally.
Android "height" Shrinks the view’s height so content fits above the keyboard.
Android "padding" Often causes double-offset on Android — avoid.

Step 1 — Replace app/(tabs)/diary.tsx

This is the main screen for the Diary tab in the app. When a user taps the “Diary” tab at the bottom of the screen, this is what they see. It’s a full editing interface where users can write journal entries in Markdown format.

This replaces a temporary test screen with the real, production-ready diary interface. Think of it like swapping out a cardboard mockup for the actual furniture in a room.

The key word here is editor-only. This screen lets users write diary entries, but doesn’t yet let them preview what the formatted Markdown will look like (that comes later on).

The useDiary hook import is unchanged — only the rendering changes.

The Layout: Two Columns Side by Side

Imagine the screen divided into two vertical sections, like a book split down the middle:

┌──────────────┬──────────────────────────────┐
│              │                              │
│  LEFT SIDE   │        RIGHT SIDE            │
│  (narrow)    │        (wide)                │
│              │                              │
│  List of     │        Text editor           │
│  diary       │        where you type        │
│  entries     │        your Markdown         │
│  (scrollable)│        (full height)         │
│              │                              │
│              │                              │
│              │                              │
└──────────────┴──────────────────────────────┘
// app/(tabs)/diary.tsx
//
// DIARY TAB — Full editor UI
//
// Two-column layout:
//   Left  (fixed width): Scrollable list of diary entries
//   Right (flex 1):      Full-height Markdown text editor
//
// KEY PATTERNS:
//   KeyboardAvoidingView — keeps the editor visible above the keyboard
//   TextInput multiline  — full-height Markdown text field
//   useDiary hook        — all state, auto-save, file I/O

// React is the core library. useRef is a tool for holding a reference to something (like a text box)
// without causing the screen to re-render.
import React, { useRef } from "react";
// React Native components
import {
  View, // A basic container, like a DIV in web development. Used to group and position elements.
  Text, // Displays text on screen.
  TextInput, // A box where users can type text.
  ScrollView, // Makes content scrollable when there's too much to fit on screen.
  TouchableOpacity, // Makes elements tappable (they become slightly transparent when pressed).
  StyleSheet, // A way to define styles (colors, sizes, spacing) in one place.
  KeyboardAvoidingView, // Very important! This prevents the keyboard from covering up
  // the text input when the user is typing. When the keyboard slides up,
  // this component shifts the content so the user can still see what they're typing.
  Platform // Lets the code check if it's running on iOS or Android, since they behave differently.
  ActivityIndicator, // A spinning loading animation (shown when something is loading).
  Alert, // Shows popup dialog boxes (like "Are you sure you want to delete this file?")
  useWindowDimensions, // A function that tells you the current screen width and height.
} from "react-native";
// A custom "hook" (reusable piece of logic) that manages all diary-related state and operations.
import { useDiary } from "../../src/hooks/useDiary";
//  A type (a TypeScript thing) that describes the possible states of saving: saving, saved, error, etc.
import type { SaveStatus } from "../../src/hooks/useDiary";
// Another type that describes the basic info about a diary entry.
import type { DiaryEntrySummary } from "../../src/db/types";
// Utility functions for working with dates. isToday checks if a given date is today.
// formatDate formats a date into a readable string like "June 15, 2026"
import { isToday, formatDate } from "../../src/utils/dateUtils";

// ── Colour palette ────────────────────────────────────────────────────────────
// The code defines a set of colors all in one place (an object called C).
// This is a common pattern — instead of typing the same color code over and over,
// you give each one a meaningful name. This makes the code easier to read and change.
const C = {
  bg:      "#1a1f2e",
  panel:   "#1e2338",
  panel2:  "#222840",
  border:  "#2d3550",
  accent:  "#5b8fd4",
  text:    "#dde4f0",
  muted:   "#9ca3af",
  faint:   "#6b7280",
  gold:    "#c9a84c",
  dim:     "#4b5563",
  red:     "#f87171",
  green:   "#86efac",
  warn:    "#f59e0b",
};


// ══════════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ══════════════════════════════════════════════════════════════════════════════
// This is the main function that returns what the user sees.
export default function DiaryScreen() {
  // This line "calls" the useDiary hook.
  // Think of a hook as a way to "plug into" functionality that was built elsewhere.
  // The diary object that comes back contains all the data and functions needed to manage diary entries.
  const diary = useDiary();
  // This gets the current screen width in pixels.
  const { width: screenWidth } = useWindowDimensions();
  // Take 32% of the screen width.
  // Round down to a whole number (Math.floor).
  // But don't let it exceed 180 pixels (Math.min).
  const sidebarWidth = Math.min(180, Math.floor(screenWidth * 0.32));

  return (
    // KeyboardAvoidingView wraps the ENTIRE screen.
    // When the keyboard appears, it shifts the content upward
    // so the editor stays visible above the keyboard.
    // The user can always see what they're typing.
    //
    <KeyboardAvoidingView
      style={s.root}
      // The code uses different behavior based on the platform:
      // iOS: Uses "padding" — adds padding to push content up
      // Android: Uses "height" — adjusts the height of the container
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      // keyboardVerticalOffset: accounts for the tab bar height.
      // Without this, on iOS the view shifts up more than needed.
      // 90 is a reasonable estimate for the tab bar + status bar.
      keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}
    >
      // The KeyboardAvoidingView contains three main children, side by side:
      {/* 1. Left column: entry list */}
      <EntryList
        // It receives two "props" (pieces of data passed in):
        diary={diary} // the diary data and functions
        sidebarWidth={sidebarWidth} // how wide it should be
      />

      {/* 2. Vertical divider. A thin vertical line (a decorative separator). */}
      <View style={s.divider} />

      {/* 3. Right column: editor */}
      // It receives a single prop: diary — the same diary data and functions
      <EditorPanel diary={diary} />

    </KeyboardAvoidingView>
  );
}

// ══════════════════════════════════════════════════════════════════════════════
// LEFT COLUMN — ENTRY LIST
// ══════════════════════════════════════════════════════════════════════════════
// This is the main container for the left sidebar of the application. What it receives (props):
function EntryList({
  diary, // This is a custom hook result that manages the diary state. It contains methods and data like:
  // entries: An array of diary entries. loading: A boolean indicating if data is still being fetched
  // currentDate: The currently selected entry's date. openEntry(): A function to select/open an entry
  // newEntry(): A function to create a new entry
  sidebarWidth, // A number that controls how wide the sidebar should be.
}: {
  diary:        ReturnType<typeof useDiary>;
  sidebarWidth: number;
}) {
  // What it displays:
  return (
    <View style={[s.sidebar, { width: sidebarWidth }]}>

      {/* 1. Header Section.  Shows "Journal" label and the total count of entries */}
      <View style={s.sidebarHeader}>
        <Text style={s.sidebarLabel}>Journal</Text>
        <Text style={s.sidebarCount}>{diary.entries.length}</Text>
      </View>

      {/* 2. Entry List Section: This has three possible states: */}
      {diary.loading ? (
        /* **Loading state**: Shows a spinner (ActivityIndicator) while data loads */
        <View style={s.sidebarLoading}>
          <ActivityIndicator size="small" color={C.gold} />
        </View>
      ) : (
        <ScrollView
          style={s.sidebarScroll}
          showsVerticalScrollIndicator={false}
        >
          /* **Empty state**: Shows helpful text "No entries yet. Tap + New Entry to begin." when there are no entries */
          {diary.entries.length === 0 ? (
            <View style={s.emptyEntries}>
              <Text style={s.emptyEntriesText}>
                No entries yet.{"\n"}
                Tap + New Entry{"\n"}to begin.
              </Text>
            </View>
          ) : (
            /* Populated state: Shows a scrollable list of all diary entries using the EntryListItem component for each entry */
            diary.entries.map((entry) => (
              <EntryListItem // EntryListItem Component. It represents a single entry in the list.
              // entry: An object containing summary information about a diary entry.
                key={entry.date} // The date of the entry
                entry={entry}
                // isSelected is a boolean indicating if this is the currently selected entry
                isSelected={entry.date === diary.currentDate}
                // onPress: A function to call when this entry is tapped
                onPress={() => diary.openEntry(entry.date)}
              />
            ))
          )}
        </ScrollView>
      )}

      {/* 3. **New Entry** button. A button at the bottom labeled "+ New Entry" */}
      <TouchableOpacity
        style={s.newEntryBtn}
        // This button triggers the diary's newEntry() function when tapped
        onPress={diary.newEntry}
        accessibilityLabel="New diary entry"
      >
        <Text style={s.newEntryBtnText}>+ New Entry</Text>
      </TouchableOpacity>

    </View>
  );
}


// ── Entry list item ─────────────────────────────────────────────────────────
// EntryListItem Component. It represents a single entry in the list.
function EntryListItem({
  entry, // An object containing summary information about a diary entry.
  isSelected, // A boolean indicating if this is the currently selected entry.
  onPress, // A function to call when this entry is tapped.
}: {
  entry:      DiaryEntrySummary;
  isSelected: boolean;
  onPress:    () => void;
}) {
  const today = isToday(entry.date);
  // What it displays:
  return (
    <TouchableOpacity
      style={[s.entryItem, isSelected && s.entryItemSelected]}
      onPress={onPress}
      activeOpacity={0.7}
    >
      {/* 1. Visual States. Gold left border on selected */}
      {isSelected && <View style={s.entryItemBorder} />}

      {/* 2. Date Display. */}
      <View style={s.entryDateRow}>
        <Text style={[s.entryDate, isSelected && s.entryDateSelected]}>
          {entry.date} /* Shows the entry's date */
        </Text>
        /* If the entry is from today, shows a small dot indicator next to the date */
        {today && <View style={s.todayDot} />}
      </View>

      {/* 3. Word count */}
      <Text style={s.entryMeta}>
        /* Displays how many words are in the entry.
        Correctly handles pluralization ("1 word" vs "2 words"). */
        {entry.wordCount} word{entry.wordCount !== 1 ? "s" : ""}
      </Text>

      {/* 4. Preview text */}
      {entry.preview ? ( /* Only shows if there's preview text available */
        /* Shows the first two lines of the entry content as a preview */
        <Text style={s.entryPreview} numberOfLines={2}>
          {entry.preview}
        </Text>
      ) : null}
    </TouchableOpacity>
  );
}

// ══════════════════════════════════════════════════════════════════════════════
// RIGHT COLUMN — EDITOR PANEL
// This code creates the right-side panel of a diary application where users can write their prayer entries.
// Think of it like the main writing area in a note-taking app.
// ══════════════════════════════════════════════════════════════════════════════
// This is a function that receives a diary object as input. This diary object contains all the state and
// functions needed to manage diary entries - like the current date, the text content,
// and functions to save or delete entries.
function EditorPanel({ diary }: { diary: ReturnType<typeof useDiary> }) {

  // Ref to the TextInput — used to focus it programmatically when needed
  const inputRef = useRef<TextInput>(null);

  // The Two Main Views
  // ── 1. No entry open ──────────────────────────────────────────────────────────
  // The first thing the component checks is whether there's a current date selected.
  // If not, it shows a welcoming screen with:
  if (!diary.currentDate) {
    return (
      <View style={s.noEntry}>
        <Text style={s.noEntryIcon}>📔</Text> // A notebook emoji 📔
        <Text style={s.noEntryTitle}>Your Prayer Diary</Texcc // The title "Your Prayer Diary"
        // A privacy message telling users their words are private and stored only on their device
        <Text style={s.noEntryText}>
          Write freely — your words are private,{"\n"}
          stored only on this device.
        </Textt>
        // A button to create a new entry for today
        <TouchableOpacity
          style={s.noEntryBtn}
          onPress={diary.newEntry}
        >
          <Text style={s.noEntryBtnText}>+ New Entry for Today</Text>
        </TouchableOpacity>
      </View>
    );
  }

confirmDelete() is a small wrapper that shows a native confirmation dialog before actually deleting the diary entry. It uses React Native’s Alert.alert, which displays a platform‑appropriate modal.

  // ── Delete confirmation ────────────────────────────────────────────────────
  // confirmDelete() does not delete anything itself.
  // It asks the user via a native modal. If the user confirms, it delegates the actual deletion to diary.deleteEntry().
  function confirmDelete() {
    Alert.alert(
      "Delete this entry?", // 🧩 The first argument is the title shown at the top of the dialog.
      // 🧩 The second argument is the message text.
      `This will permanently delete the entry for ${diary.currentDate}. This cannot be undone.`,
      [
        // 🧩 The array of buttons is the third argument.
        // First button — Cancel
        // This button: Closes the alert; Does nothing else; Uses the "cancel" style.
        { text: "Cancel",  style: "cancel" },
        // Second button — Delete
        // This is the dangerous action:
        {
          text:    "Delete",
          style:   "destructive", // "destructive" style highlights it in red (iOS) or emphasizes danger (Android).
          // onPress runs only if the user taps Delete.
          // The ! (non‑null assertion) tells TypeScript: “I know currentDate is defined here.”
          onPress: () => diary.deleteEntry(diary.currentDate!),
        },
      ]
    );
  }

  // ── Entry header: formatted date ───────────────────────────────────────────
  const dateLabel  = formatDate(diary.currentDate, "long");  // "Wednesday, 14 May 2026"
  const todayEntry = isToday(diary.currentDate);

  return (
    // 2. When an Entry is Open
    // If there IS a current date selected, it shows the full editor interface with three main sections stacked vertically.
    // This View fills all remaining horizontal space (flex: 1)
    // and stacks vertically: toolbar → hint → editor
    // The Editor Interface Components
    <View style={s.editor}>

      {/* ── The Toolbar (Top Section) ───────────────────────────────────────────────────────────
      This is the control panel at the top with several elements: */}
      <View style={s.toolbar}>

        {/* Date + Today badge */}
        // It shows the formatted date (like "Wednesday, 14 May 2026") in a long, readable format.
        <View style={s.toolbarLeft}>
          <Text style={s.toolbarDate} numberOfLines={1}>
            {dateLabel}
          </Text>
          // If the entry is for today, it also shows a small "Today" badge.
          {todayEntry && (
            <View style={s.todayBadge}>
              <Text style={s.todayBadgeText}>Today</Text>
            </View>
          )}
        </View>

        {/* Save status indicator.
        A small indicator that shows whether your writing is being saved, has been saved, or if there was an error. */}
        <SaveStatusBadge status={diary.saveStatus} isDirty={diary.isDirty} />

        {/* Save button */}
        <TouchableOpacity
          style={[
            s.toolbarBtn,
            s.toolbarBtnSave,
            // If the entry is not dirty, visually dim the button
            !diary.isDirty && s.toolbarBtnDisabled,
          ]}
          onPress={diary.saveEntry}
          // Disable the button when:
          // 1. The entry is not dirty (nothing to save)
          // 2. A save operation is currently in progress
          disabled={!diary.isDirty || diary.saving}
          accessibilityLabel="Save entry"
        >
          <Text style={[
            s.toolbarBtnText,
            // Dim the text when the entry is not dirty
            !diary.isDirty && s.toolbarBtnTextDisabled,
          ]}>
            {/* While saving, show a spinner-like text; otherwise show "Save".
            The text switches between: "Save"  and "Saving…" (during the async save operation) */}
            {diary.saving ? "Saving…" : "Save"}
          </Text>
        </TouchableOpacity>

        {/* Delete button. Always enabled. */}
        <TouchableOpacity
          style={[s.toolbarBtn, s.toolbarBtnDelete]}
          // Trigger the confirmation dialog before actually deleting the entry.
          onPress={confirmDelete}
          accessibilityLabel="Delete entry"
        >
          <Text style={s.toolbarBtnDeleteText}>Delete</Text>
        </TouchableOpacity>

      </View>

      {/* ── The Hint Bar (Middle Section) ──────────────────────────────────────────────────────
      This shows helpful information that changes based on the situation: */}
      <View style={s.hintBar}>
        <Text style={s.hintText}>
          {diary.isDirty
            // If you have unsaved changes: Shows "● Unsaved changes · auto-saves in 2 seconds"
            ? "● Unsaved changes · auto-saves in 2 seconds"
            : diary.saveStatus === "idle"
              // If everything is saved: Shows tips about Markdown formatting support
              ? "Markdown supported · # headings · **bold** · - lists"
              : ""}
        </Text>
      </View>

      {/* ──The Text Editor (Bottom Section) ────────────────────────────────────────────────────────
      This is the main writing area - a large text box that: */}
      {/*
        TextInput with multiline={true} is the React Native equivalent of
        QTextEdit in PySide6. Allows you to write multiple paragraphs, not just one line.
        This is like a word processor versus a single-line text field.

        React Native TextInput (multiline):
          - Plain text only (use react-native-render-html for preview)
          - Scrolls natively when content exceeds the view
          - Styled via StyleSheet like any other component
          - Must set textAlignVertical: "top" on Android to start at top-left

        WHY ScrollView is not needed around TextInput:
          A multiline TextInput is itself scrollable when its content exceeds
          its height. We set flex: 1 so it fills the remaining space.
      */}
      <TextInput
        // We have previoulsy defined: const inputRef = useRef(null);
        ref={inputRef}
        // Styling. flex: 1 (so it fills the remaining space).
        style={s.textInput}
        // The text comes from diary.content.
        value={diary.content}
        // Every keystroke updates the diary state via setContent.
        onChangeText={diary.setContent}

        // TextInput becomes a multi‑paragraph editor when multiline={true}.
        multiline={true}

        // textAlignVertical: "top" — Android-specific.
        // Without this, Android centres the cursor vertically in the input,
        // so the first line starts in the middle of the screen.
        // On iOS this is not needed (cursor always starts at top).
        textAlignVertical="top"

        // It shows example text when empty
        placeholder={
          "Begin your prayer reflection...\n\n" +
          "Markdown is supported:\n" +
          "# Heading\n**bold** *italic*\n- list item"
        }
        placeholderTextColor={C.dim}

        // Auto-correction Features
        autoCorrect={true} // Auto-correct fixes typos as you type
        autoCapitalize="sentences" // Sentences automatically capitalize their first letter
        spellCheck={true} // Spell checking is enabled

        // scrollEnabled: allows the TextInput to scroll independently
        // It scrolls automatically when content exceeds its height.
        // Combined with flex: 1, it behaves like a full editor pane.
        // As your entry gets longer, the text area scrolls automatically.
        // You don't need to add a separate scroll container - the text input handles this itself.
        scrollEnabled={true}

        // Dark keyboard on iOS to match our app’s theme.
        keyboardAppearance="dark"

        // Enter key creates a new line (not submit)
        returnKeyType="default"
        blurOnSubmit={false}    // Keyboard doesn't dismiss when you press Enter
      />

    </View>
  );
}


// ══════════════════════════════════════════════════════════════════════════════
// SAVE STATUS BADGE
// This is a separate small component that shows the save status visually.
// ══════════════════════════════════════════════════════════════════════════════
// It reacts to two inputs:
// status → the current save state. one of "idle" | "saving" | "saved" | "error"
// isDirty → whether the entry has unsaved changes
function SaveStatusBadge({
  status,
  isDirty,
}: {
  status:  SaveStatus;
  isDirty: boolean;
}) {
  // This is a lookup table that converts each save state into:
  // an emoji; a label; and a background color for visual distinction
  // This is a clean pattern: instead of writing multiple if statements, you define a declarative mapping.
  const config: Record<SaveStatus, { emoji: string; label: string; bg: string }> = {
    idle:   { emoji: "",   label: "",          bg: "transparent" },
    saving: { emoji: "💾", label: "Saving…",   bg: "#1a2438" },
    saved:  { emoji: "✅", label: "Saved",     bg: "#0f2a1a" },
    error:  { emoji: "❌", label: "Save error", bg: "#2a1010" },
  };

  // Extract the config. We pull out the relevant UI pieces for the current state.
  const { emoji, label, bg } = config[status];

  // Four Possible States:
  // Show nothing when status is idle (the save system is idle) AND (the entry is) not dirty.
  // This avoids clutter.
  if (status === "idle" && !isDirty) return null;

  return (
    // We wrap everything in a styled container, applying the background color from the config.
    <View style={[s.statusBadge, { backgroundColor: bg }]}>
      {status === "saving" ? (
        // When saving, it uses a spinner (native, animated).
        <ActivityIndicator size="small" color={C.accent} style={{ marginRight: 4 }} />
      ) : (
        // For other states, it uses emoji icons
        <Text style={s.statusEmoji}>{emoji}</Text>
      )}
      // If the label is empty (idle state), we don’t render anything.
      {label ? <Text style={s.statusLabel}>{label}</Text> : null}
    </View>
  );
}


// ══════════════════════════════════════════════════════════════════════════════
// STYLES
// ══════════════════════════════════════════════════════════════════════════════

const s = StyleSheet.create({

  // Root: full-screen, horizontal split, keyboard-aware
  root: {
    flex:            1,
    flexDirection:   "row",
    backgroundColor: C.bg,
  },

  divider: {
    width:           1,
    backgroundColor: C.border,
  },

  // ── Sidebar ────────────────────────────────────────────────────────────────
  sidebar: {
    backgroundColor: C.panel,
    // No flex: 1 — width is set dynamically via the sidebarWidth prop
  },

  sidebarHeader: {
    flexDirection:     "row",
    justifyContent:    "space-between",
    alignItems:        "center",
    paddingHorizontal: 12,
    paddingVertical:   10,
    borderBottomWidth: 1,
    borderBottomColor: C.border,
  },
  sidebarLabel: {
    fontSize:      10,
    fontWeight:    "700",
    color:         C.faint,
    textTransform: "uppercase",
    letterSpacing: 0.8,
  },
  sidebarCount: {
    fontSize:          10,
    color:             C.accent,
    backgroundColor:   "#1a2a3a",
    paddingHorizontal: 5,
    paddingVertical:   1,
    borderRadius:      8,
    overflow:          "hidden",
  },

  sidebarScroll: {
    flex: 1,
  },
  sidebarLoading: {
    flex:           1,
    alignItems:     "center",
    justifyContent: "center",
    padding:        20,
  },

  emptyEntries: {
    padding:     20,
    alignItems:  "center",
  },
  emptyEntriesText: {
    fontSize:   12,
    color:      C.dim,
    textAlign:  "center",
    lineHeight: 18,
  },

  // Entry list item
  entryItem: {
    paddingHorizontal: 12,
    paddingVertical:   10,
    borderBottomWidth: 1,
    borderBottomColor: "#1a1f2e",
  },
  entryItemSelected: {
    backgroundColor: C.panel2,
  },
  entryItemBorder: {
    position:        "absolute",
    left:            0,
    top:             0,
    bottom:          0,
    width:           2,
    backgroundColor: C.gold,
  },
  entryDateRow: {
    flexDirection: "row",
    alignItems:    "center",
    gap:           5,
    marginBottom:  3,
  },
  entryDate: {
    fontSize:   11,
    color:      C.muted,
    fontFamily: "monospace",
    fontWeight: "600",
  },
  entryDateSelected: {
    color: C.gold,
  },
  todayDot: {
    width:           6,
    height:          6,
    borderRadius:    3,
    backgroundColor: C.gold,
    flexShrink:      0,
  },
  entryMeta: {
    fontSize:     10,
    color:        C.dim,
    marginBottom: 3,
  },
  entryPreview: {
    fontSize:   11,
    color:      C.faint,
    lineHeight: 15,
  },

  // New entry button
  newEntryBtn: {
    paddingVertical:   12,
    paddingHorizontal: 12,
    borderTopWidth:    1,
    borderTopColor:    C.border,
    alignItems:        "center",
  },
  newEntryBtnText: {
    fontSize:   12,
    color:      C.gold,
    fontWeight: "700",
  },

  // ── No entry state ────────────────────────────────────────────────────────
  noEntry: {
    flex:           1,
    alignItems:     "center",
    justifyContent: "center",
    padding:        32,
    gap:            12,
  },
  noEntryIcon:    { fontSize: 52 },
  noEntryTitle:   { fontSize: 20, fontWeight: "bold", color: C.muted },
  noEntryText:    { fontSize: 13, color: C.faint, textAlign: "center",
                    lineHeight: 20 },
  noEntryBtn:     { backgroundColor: C.accent, borderRadius: 10,
                    paddingVertical: 12, paddingHorizontal: 28 },
  noEntryBtnText: { color: "#fff", fontSize: 14, fontWeight: "700" },

  // ── Editor column ─────────────────────────────────────────────────────────
  editor: {
    flex:            1,
    flexDirection:   "column",
    backgroundColor: C.bg,
  },

  // Toolbar
  toolbar: {
    flexDirection:   "row",
    alignItems:      "center",
    paddingVertical: 8,
    paddingHorizontal: 14,
    backgroundColor: C.panel,
    borderBottomWidth: 1,
    borderBottomColor: C.border,
    gap:             8,
    flexWrap:        "wrap",
  },
  toolbarLeft: {
    flex:          1,
    flexDirection: "row",
    alignItems:    "center",
    gap:           8,
  },
  toolbarDate: {
    fontSize:   12,
    color:      C.text,
    fontWeight: "600",
    flex:       1,
  },
  todayBadge: {
    backgroundColor:   "#2a2010",
    borderRadius:      4,
    paddingHorizontal: 6,
    paddingVertical:   2,
    borderWidth:       1,
    borderColor:       C.gold,
    flexShrink:        0,
  },
  todayBadgeText: {
    fontSize:   9,
    color:      C.gold,
    fontWeight: "700",
  },

  // Toolbar buttons
  toolbarBtn: {
    borderRadius:      6,
    paddingHorizontal: 10,
    paddingVertical:   6,
    flexShrink:        0,
  },
  toolbarBtnSave: {
    backgroundColor: C.accent,
  },
  toolbarBtnDisabled: {
    backgroundColor: C.border,
  },
  toolbarBtnText: {
    fontSize:   12,
    color:      "#fff",
    fontWeight: "700",
  },
  toolbarBtnTextDisabled: {
    color: C.faint,
  },
  toolbarBtnDelete: {
    backgroundColor: "#2a1010",
    borderWidth:     1,
    borderColor:     "#4a2020",
  },
  toolbarBtnDeleteText: {
    fontSize: 12,
    color:    C.red,
  },

  // Save status badge
  statusBadge: {
    flexDirection:     "row",
    alignItems:        "center",
    borderRadius:      5,
    paddingHorizontal: 8,
    paddingVertical:   4,
    gap:               4,
    flexShrink:        0,
  },
  statusEmoji: { fontSize: 11 },
  statusLabel: { fontSize: 11, color: C.muted, fontWeight: "600" },

  // Hint bar
  hintBar: {
    paddingHorizontal: 14,
    paddingVertical:   5,
    borderBottomWidth: 1,
    borderBottomColor: C.border,
    backgroundColor:   C.panel,
  },
  hintText: {
    fontSize:  10,
    color:     C.dim,
    fontStyle: "italic",
    lineHeight: 14,
  },

  // Text input (the main editor)
  textInput: {
    flex:             1,                 // fill all remaining vertical space
    padding:          16,
    color:            C.text,
    fontSize:         15,
    lineHeight:       24,
    // Use a slightly more readable font than the system default.
    // "monospace" gives a code-editor feel, appropriate for Markdown.
    // In Chunk 27 (Design System) we will load a proper font (e.g. JetBrains Mono).
    fontFamily:       Platform.OS === "ios" ? "Courier New" : "monospace",
    textAlignVertical: "top",           // Android: cursor at top-left
    // backgroundColor is inherited from the root View
  },
});

Run and test

  1. npx expo start --clear.
  2. Navigate to the 📔 Diary tab. 3, Test checklist
Test Expected
Open Diary tab “Your Prayer Diary” with + New Entry button
Tap + New Entry Right panel shows today’s date in toolbar, template text
Tap the text area Keyboard slides up, editor scrolls up (not hidden!)
Type text Cursor visible above keyboard at all times
● unsaved badge Appears while typing
Stop typing 2s “💾 Saving…” → “✅ Saved”
Left list entry Date + word count + preview visible
Tap another date Opens that entry (saves current first if dirty)
Tap Save button Manual save, works without waiting 2s
Tap Delete Confirmation alert → entry removed from list
Restart app Entry still present in left list
Open entry after restart Full content restored
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.