Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"expo@expo-plugins": true
}
}
313 changes: 313 additions & 0 deletions .claude/skills/10x-rule-review/SKILL.md

Large diffs are not rendered by default.

172 changes: 90 additions & 82 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,133 @@
# AI Agents Instructions (simple-notepad)

## Mission
This repo is a small Expo + React Native app (using Expo Router and NativeWind/Tailwind) for creating, listing, and editing “notes” and “checklist lists”, persisted in a local SQLite database.

This repo is a small Expo + React Native app (using Expo Router and NativeWind/Tailwind) for creating, listing, and editing "notes" and "checklist lists", persisted in a local SQLite database.

When you (or another AI agent) are asked to implement a change, prefer working through the existing route/components structure and the centralized SQLite data layer in `lib/dataStorage.ts`.

## Tech Stack (what to assume)
- **Runtime**: React Native + Expo
- **Routing**: **Expo Router** (file-based routes under `app/`)
- **Styling**: NativeWind + Tailwind (`global.css`, `tailwind.config.js`, Tailwind classes in JSX)
- **Storage**: `expo-sqlite` with migrations handled in `app/_layout.tsx` via `migrateDbIfNeeded`
- **TypeScript**: `strict: true`
## Quick Safety Checklist (do not break invariants)

## Repo Layout (where things live)
- `app/`: screens/routes (Expo Router)
- Example routes:
- `app/index.tsx`: notes list screen
- `app/add-note.tsx`: create note
- `app/edit-note/[id].tsx`: edit note by numeric id
- `app/note/[id].tsx`: view note by numeric id
- `app/add-list.tsx`: create list
- `app/edit-list/[id].tsx`: edit list by numeric id
- `app/list/[id].tsx`: view list by numeric id
- `components/`: reusable UI pieces (buttons, inputs, forms, etc.)
- `components/NoteForm.tsx`: shared note create/edit form
- `components/AddContentDropdown.tsx`: “add note or list” UI
- `components/state/*`: loading/not-found UI
- `lib/`: non-UI logic
- `lib/dataStorage.ts`: SQLite schema, migrations, and CRUD helpers
- `lib/theme.ts`: navigation theme colors
- `lib/utils.ts`: `cn()` utility for className merging
- `hooks/`: small hooks used by screens
- `useParsedNumericRouteParam`: parses numeric `[id]` params safely
- `useHardwareBackHandler`: handles Android back navigation
- Do not remove/skip the `migrateDbIfNeeded` hook from `SQLiteProvider` in `app/_layout.tsx`.
- Keep `NOTE_TYPE`/`LIST_TYPE` semantics consistent with `getListItemsById` and list update queries.
- Avoid direct SQL edits outside `lib/dataStorage.ts`.
- Do not change `journal_mode` away from `DELETE` — the Android widget requires it (see `NATIVE_CHANGES.md`).
- Do not run `expo prebuild --clean` — it will wipe the widget's native files.

## Running the app (for humans/agents)
Common scripts from `package.json`:
- Dev server (all platforms): `npm run dev` (runs `expo start -c`)
- Platform-specific:
- `npm run ios` (simulator)
- `npm run android`
- `npm run web`
- Clean: `npm run clean` (removes `.expo` and `node_modules`)
## Native Code

## Routing / Screen patterns (Expo Router)
- Routes are defined by filenames under `app/`.
- Dynamic numeric route params use:
- `useParsedNumericRouteParam('id')`
- screens then guard with `isValidId` (invalid id -> not found / redirect behavior)
- For typed routing (`expo` config has `experiments.typedRoutes: true`):
- when passing dynamic routes to `router.push`, existing code uses casts like `as never`.
The `android/` folder contains manual modifications on top of `expo prebuild` output — primarily an Android home-screen widget.

**See [`NATIVE_CHANGES.md`](./NATIVE_CHANGES.md) for the full list of native files, their purpose, AndroidManifest entries, schema dependencies, and upgrade notes.**

Key points for agents:

- Do **not** run `expo prebuild --clean` — use `expo prebuild` (no `--clean`) to preserve widget files.
- After any write operation in `lib/dataStorage.ts`, call `syncAndroidNoteListWidgetFromApp()` (already defined there) so the widget refreshes. All existing CRUD helpers already do this.

## When implementing a feature (agent playbook)

1. **Locate the route** to change/add under `app/`.
2. If the feature needs persistence, identify/extend the correct helper(s) in `lib/dataStorage.ts`.
3. For DB changes:
- update the schema migration in `migrateDbIfNeeded`
- bump `DATABASE_VERSION`
- keep existing migrations compatible (older installs should migrate forward)
4. For UI:
- reuse existing components in `components/` (especially `NoteForm`, `ListForm`, and `components/ui/*`)
- use Tailwind/NW class names (via `className`)
5. Match existing loading/not-found patterns for numeric params and record-type checks.
6. If adding a new write operation in `lib/dataStorage.ts`, call `syncAndroidNoteListWidgetFromApp()` after the DB write (see existing helpers for the pattern).

## SQLite Database Model (most important invariants)

### Where the DB is initialized

`app/_layout.tsx` wraps the router with:

- `<SQLiteProvider databaseName="notes.db" onInit={migrateDbIfNeeded}>`

So the migration function in `lib/dataStorage.ts` is responsible for keeping schema compatible across app updates.

### Schema and versioning
In `lib/dataStorage.ts`:
- `DATABASE_VERSION = 2`
- `content` table columns:
- `id INTEGER PRIMARY KEY NOT NULL`
- `title TEXT NOT NULL`
- `note TEXT NOT NULL`
- `type INTEGER NOT NULL` (added in migration; semantic types below)
- `type` constants:
- `NOTE_TYPE = 0`
- `LIST_TYPE = 1`
- Migration approach:
- Reads `PRAGMA user_version`
- If `user_version >= DATABASE_VERSION`: do nothing
- If missing/old:
- (Re)creates the base table if `user_version === 0`
- Adds the `type` column if it’s missing; assigns default `NOTE_TYPE` and fixes nulls
- Sets `PRAGMA user_version = DATABASE_VERSION`

@lib/dataStorage.ts

### How list content is stored

Lists store their items inside the `note` column as a JSON string:

- `stringifyListItems(items)` stores `{ checked, text }[]`
- `parseListItems(rawContent)` validates/filters parsed items

CRUD helpers to use:

- Notes:
- `addNote`, `getNoteById`, `updateNote`, `deleteNote`
- Lists:
- `addList`, `getListItemsById`, `updateList`, `updateListItems`

Important rule: updating list items uses `UPDATE content SET note = ? WHERE id = ? AND type = ?` (ensures you don’t overwrite a note’s data by accident).
Important rule: updating list items uses `UPDATE content SET note = ? WHERE id = ? AND type = ?` (ensures you don't overwrite a note's data by accident).

## Repo Layout (where things live)

- `app/`: screens/routes (Expo Router)
- `app/index.tsx`: notes list screen
- `app/add-note.tsx`: create note
- `app/edit-note/[id].tsx`: edit note by numeric id
- `app/note/[id].tsx`: view note by numeric id
- `app/add-list.tsx`: create list
- `app/edit-list/[id].tsx`: edit list by numeric id
- `app/list/[id].tsx`: view list by numeric id
- `components/`: reusable UI pieces
- `components/NoteForm.tsx`: shared note create/edit form
- `components/ListForm.tsx`: shared list create/edit form
- `components/AddContentDropdown.tsx`: "add note or list" UI
- `components/navigation/HeaderBackButton.tsx`: back arrow button for screen headers
- `components/state/`: `ScreenLoadingState` and `ScreenNotFoundState`
- `components/ui/`: primitive UI components — `button`, `card`, `icon`, `input`, `textarea`, `text`
- `lib/`: non-UI logic
- `lib/dataStorage.ts`: SQLite schema, migrations, and CRUD helpers
- `lib/theme.ts`: navigation theme colors
- `lib/utils.ts`: `cn()` utility for className merging
- `hooks/`: small hooks used by screens
- `useParsedNumericRouteParam`: parses numeric `[id]` params safely
- `useHardwareBackHandler`: handles Android back navigation
- `useKeyboardOffset`: tracks keyboard visibility on Android to compute bottom padding

## Tech Stack (what to assume)

Expo ~54, Expo Router, NativeWind v4, expo-sqlite, TypeScript strict — see @package.json for exact versions.

## Routing / Screen patterns (Expo Router)

- Dynamic numeric route params use:
- `useParsedNumericRouteParam('id')`
- screens then guard with `isValidId` (invalid id -> not found / redirect behavior)
- For typed routing (`expo` config has `experiments.typedRoutes: true`):
- when passing dynamic routes to `router.push`, existing code uses casts like `as never`.

## UI + UX conventions

- Note create/edit uses `components/NoteForm.tsx`, which calls `onSave(trimmedTitle, noteContent.trim())`.
- List create/edit uses `components/ListForm.tsx`.
- Screens typically return:
- `ScreenLoadingState` while fetching
- `ScreenNotFoundState` when `id` is invalid or the record type doesnt match the expected screen
- `ScreenNotFoundState` when `id` is invalid or the record type doesn't match the expected screen
- Hardware back navigation:
- screens use `useHardwareBackHandler(() => router.replace('/'))` or redirect to a “backTarget”.
- screens use `useHardwareBackHandler(() => router.replace('/'))` or redirect to a "backTarget".
- Use `useKeyboardOffset` for bottom padding on screens with inputs (handles Android accessory bar).
- Use `HeaderBackButton` from `components/navigation/HeaderBackButton.tsx` for screen header back arrows.

## Code Style / Quality Bar
- TypeScript `strict` mode is on, so be careful with `null`, `'loading'`, and route params.

- Use type narrowing (not !) for nullable fields and 'loading' states. Always unwrap numeric route params via useParsedNumericRouteParam — never cast useLocalSearchParams() output directly.
- Follow Prettier config expectations:
- single quotes, `printWidth: 100`, Tailwind plugin support.
- Prefer adding logic to `lib/dataStorage.ts` instead of scattering SQL strings in screens.
- All SQL lives in lib/dataStorage.ts. No exceptions.

## When implementing a feature (agent playbook)
1. **Locate the route** to change/add under `app/`.
2. If the feature needs persistence, identify/extend the correct helper(s) in `lib/dataStorage.ts`.
3. For DB changes:
- update the schema migration in `migrateDbIfNeeded`
- bump `DATABASE_VERSION`
- keep existing migrations compatible (older installs should migrate forward)
4. For UI:
- reuse existing components in `components/` (especially `NoteForm` and `components/ui/*`)
- use Tailwind/NW class names (via `className`)
5. Match existing loading/not-found patterns for numeric params and record-type checks.

## Quick Safety Checklist (do not break invariants)
- Do not remove/skip the `migrateDbIfNeeded` hook from `SQLiteProvider` in `app/_layout.tsx`.
- Keep `NOTE_TYPE`/`LIST_TYPE` semantics consistent with `getListItemsById` and list update queries.
- Avoid direct SQL edits outside `lib/dataStorage.tsx`.
## Running the app (for humans/agents)

Last scanned: 2026-04-29
@README.md

Last scanned: 2026-05-18
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
See [AGENTS.md](./AGENTS.md) for project instructions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.pgarr.simplenotepad.widget

import android.content.Context
import android.content.Intent
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StrikethroughSpan
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import com.pgarr.simplenotepad.R
Expand Down Expand Up @@ -39,18 +42,24 @@ class NoteListRemoteViewsFactory(
val item = items[position]
val rv = RemoteViews(context.packageName, R.layout.widget_list_item)

// Set text, with strikethrough if checked
rv.setTextViewText(R.id.item_text, item.text)
// Set text with strikethrough for checked items (matches app line-through style)
if (item.checked) {
val spannable = SpannableString(item.text)
spannable.setSpan(StrikethroughSpan(), 0, item.text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
rv.setTextViewText(R.id.item_text, spannable)
} else {
rv.setTextViewText(R.id.item_text, item.text)
}

// Swap checkbox icon based on checked state
// Swap checkbox icon based on checked state (custom lucide-style drawables)
val checkboxDrawable = if (item.checked)
android.R.drawable.checkbox_on_background
R.drawable.widget_checkbox_on
else
android.R.drawable.checkbox_off_background
R.drawable.widget_checkbox_off
rv.setImageViewResource(R.id.item_checkbox, checkboxDrawable)

// Dim completed items
rv.setFloat(R.id.item_text, "setAlpha", if (item.checked) 0.4f else 1.0f)
// Dim checked item text to match app muted-foreground (text-muted-foreground)
rv.setFloat(R.id.item_text, "setAlpha", if (item.checked) 0.5f else 1.0f)

// Fill-in intent carries position and listId to the broadcast receiver
val fillIntent = Intent().apply {
Expand Down
4 changes: 0 additions & 4 deletions android/app/src/main/res/drawable/widget_background.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
android:shape="rectangle">

<solid android:color="@color/widget_surface" />
<stroke
android:width="1dp"
android:color="@color/widget_border" />
<!-- ~0.625rem card radius from the app -->
<corners android:radius="10dp" />

</shape>
15 changes: 15 additions & 0 deletions android/app/src/main/res/drawable/widget_checkbox_off.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Matches lucide-react-native Square icon (stroke only, no fill) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,3 L19,3 A2,2 0 0,1 21,5 L21,19 A2,2 0 0,1 19,21 L5,21 A2,2 0 0,1 3,19 L3,5 A2,2 0 0,1 5,3 Z"
android:strokeWidth="2"
android:strokeColor="#0A0A0A"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
20 changes: 20 additions & 0 deletions android/app/src/main/res/drawable/widget_checkbox_on.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Matches lucide-react-native CheckSquare2 icon (filled square + white checkmark) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Filled rounded square -->
<path
android:pathData="M5,3 L19,3 A2,2 0 0,1 21,5 L21,19 A2,2 0 0,1 19,21 L5,21 A2,2 0 0,1 3,19 L3,5 A2,2 0 0,1 5,3 Z"
android:fillColor="#0A0A0A" />
<!-- White checkmark: matches lucide check-square-2 path m9 12 2 2 4-4 -->
<path
android:pathData="M9,12 L11,14 L15,10"
android:strokeWidth="2"
android:strokeColor="#FFFFFF"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
5 changes: 5 additions & 0 deletions android/app/src/main/res/drawable/widget_item_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
</shape>
23 changes: 14 additions & 9 deletions android/app/src/main/res/layout/widget_list_item.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Matches app list item: flex-row items-center gap-3 rounded-md border border-border px-3 py-2 -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="4dp"
android:paddingEnd="4dp">
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_marginBottom="6dp"
android:background="@drawable/widget_item_background">

<!-- Checkbox is faked with an ImageView; RemoteViews can't use CheckBox interactively -->
<!-- Checkbox faked with ImageView; RemoteViews can't use CheckBox interactively -->
<ImageView
android:id="@+id/item_checkbox"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/checkbox_off_background"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/widget_checkbox_off"
android:contentDescription="Toggle item" />

<TextView
Expand All @@ -23,8 +28,8 @@
android:layout_weight="1"
android:textSize="14sp"
android:textColor="@color/widget_text_primary"
android:paddingStart="10dp"
android:paddingStart="12dp"
android:maxLines="2"
android:ellipsize="end" />

</LinearLayout>
</LinearLayout>
Loading
Loading