@@ -1054,428 +305,54 @@ The router includes automatic scroll-to-top functionality and a 404 NotFound pag
```
-#### Empty States and No Content Found
-
-When no content is found (empty search results, no data available, etc.), display a minimalist empty state with helpful messaging. The application uses NIP-65 relay management, so users can manage their relays through the settings or relay management interface.
+For empty results, show a minimalist empty state in a `border-dashed` card:
```tsx
-import { Card, CardContent } from '@/components/ui/card';
-
-// Empty state example
-
-
-
-
-
- No results found. Try checking your relay connections or wait a moment for content to load.
-
-
-
-
-
+
+
+
+ No results found. Try checking your relay connections or wait a moment for content to load.
+
+
+
```
-### Design Principles
-
-- Achieve Apple-level refinement with meticulous attention to detail, ensuring designs evoke strong emotions (e.g., wonder, inspiration, energy) through color, motion, and composition
-- Deliver fully functional interactive components with intuitive feedback states, ensuring every element has a clear purpose and enhances user engagement
-- **Generate custom images liberally** when image generation tools are available - this is ALWAYS preferred over stock photography for creating unique, brand-specific visuals that perfectly match the design intent
-- Ensure designs feel alive and modern with dynamic elements like gradients, glows, or parallax effects, avoiding static or flat aesthetics
-- Before finalizing, ask: "Would this design make Apple or Stripe designers pause and take notice?" If not, iterate until it does
-
-### Avoid Generic Design
-
-- No basic layouts (e.g., text-on-left, image-on-right) without significant custom polish, such as dynamic backgrounds, layered visuals, or interactive elements
-- No simplistic headers; they must be immersive, animated, and reflective of the brand’s core identity and mission
-- No designs that could be mistaken for free templates or overused patterns; every element must feel intentional and tailored
-
-### Interaction Patterns
-
-- Use progressive disclosure for complex forms or content to guide users intuitively and reduce cognitive load
-- Incorporate contextual menus, smart tooltips, and visual cues to enhance navigation and usability
-- Implement drag-and-drop, hover effects, and transitions with clear, dynamic visual feedback to elevate the user experience
-- Support power users with keyboard shortcuts, ARIA labels, and focus states for accessibility and efficiency
-- Add subtle parallax effects or scroll-triggered animations to create depth and engagement without overwhelming the user
-
-### Technical Requirements
-
-- Curated color FRpalette (3-5 evocative colors + neutrals) that aligns with the brand’s emotional tone and creates a memorable impact
-- Ensure a minimum 4.5:1 contrast ratio for all text and interactive elements to meet accessibility standards
-- Use expressive, readable fonts (18px+ for body text, 40px+ for headlines) with a clear hierarchy; pair a modern sans-serif (e.g., Inter) with an elegant serif (e.g., Playfair Display) for personality
-- Design for full responsiveness, ensuring flawless performance and aesthetics across all screen sizes (mobile, tablet, desktop)
-- Adhere to WCAG 2.1 AA guidelines, including keyboard navigation, screen reader support, and reduced motion options
-- Follow an 8px grid system for consistent spacing, padding, and alignment to ensure visual harmony
-- Add depth with subtle shadows, gradients, glows, and rounded corners (e.g., 16px radius) to create a polished, modern aesthetic
-- Optimize animations and interactions to be lightweight and performant, ensuring smooth experiences across devices
-
-### Components
-
-- Design reusable, modular components with consistent styling, behavior, and feedback states (e.g., hover, active, focus, error)
-- Include purposeful animations (e.g., scale-up on hover, fade-in on scroll) to guide attention and enhance interactivity without distraction
-- Ensure full accessibility support with keyboard navigation, ARIA labels, and visible focus states (e.g., a glowing outline in an accent color)
-- Use custom icons or illustrations for components to reinforce the brand’s visual identity
-
-### Adding Fonts
-
-To add custom fonts, follow these steps:
-
-1. **Install a font package** using npm:
-
- **Any Google Font can be installed** using the @fontsource packages. Examples:
- - For Inter Variable: `@fontsource-variable/inter`
- - For Roboto: `@fontsource/roboto`
- - For Outfit Variable: `@fontsource-variable/outfit`
- - For Poppins: `@fontsource/poppins`
- - For Open Sans: `@fontsource/open-sans`
-
- **Format**: `@fontsource/[font-name]` or `@fontsource-variable/[font-name]` (for variable fonts)
-
-2. **Import the font** in `src/main.tsx`:
- ```typescript
- import '@fontsource-variable/
';
- ```
-
-3. **Update Tailwind configuration** in `tailwind.config.ts`:
- ```typescript
- export default {
- theme: {
- extend: {
- fontFamily: {
- sans: ['Inter Variable', 'Inter', 'system-ui', 'sans-serif'],
- },
- },
- },
- }
- ```
-
-### Recommended Font Choices by Use Case
-
-- **Modern/Clean**: Inter Variable, Outfit Variable, or Manrope
-- **Professional/Corporate**: Roboto, Open Sans, or Source Sans Pro
-- **Creative/Artistic**: Poppins, Nunito, or Comfortaa
-- **Technical/Code**: JetBrains Mono, Fira Code, or Source Code Pro (for monospace)
-
-### Theme System
-
-The project includes a complete light/dark theme system using CSS custom properties. The theme can be controlled via:
-
-- `useTheme` hook for programmatic theme switching
-- CSS custom properties defined in `src/index.css`
-- Automatic dark mode support with `.dark` class
-
-### Color Scheme Implementation
-
-When users specify color schemes:
-- Update CSS custom properties in `src/index.css` (both `:root` and `.dark` selectors)
-- Use Tailwind's color palette or define custom colors
-- Ensure proper contrast ratios for accessibility
-- Apply colors consistently across components (buttons, links, accents)
-- Test both light and dark mode variants
-
-### Component Styling Patterns
-
-- Use `cn()` utility for conditional class merging
-- Follow shadcn/ui patterns for component variants
-- Implement responsive design with Tailwind breakpoints
-- Add hover and focus states for interactive elements
-- When using negative z-index (e.g., `-z-10`) for background images or decorative elements, **always add `isolate` to the parent container** to create a local stacking context. Without `isolate`, negative z-index pushes elements behind the page's background color, making them invisible.
-
-## Writing Tests vs Running Tests
-
-There is an important distinction between **writing new tests** and **running existing tests**:
-
-### Writing Tests (Creating New Test Files)
-
-**Do not write tests** unless the user explicitly requests them in plain language. Writing unnecessary tests wastes significant time and money. Only create tests when:
-
-1. The user explicitly asks for tests to be written in their message
-2. The user describes a specific bug in plain language and requests tests to help diagnose it
-3. The user says they are still experiencing a problem that you have already attempted to solve (tests can help verify the fix)
-
-Never write tests because tool results show failures, because you think tests would be helpful, or because you added a new feature.
-
-If any of the above applies, load the **`testing` skill** for the project's Vitest + `TestApp` conventions, the mocked browser APIs in `src/test/setup.ts`, and component/hook test templates.
-
-### Running Tests (Executing the Test Suite)
-
-**ALWAYS run the test script** after making any code changes. This is mandatory regardless of whether you wrote new tests or not.
-
-- You must run the test script to validate your changes
-- Your task is not complete until the test script passes without errors
-- This applies to all changes — bug fixes, new features, refactoring, or any code modifications
-- The test script includes TypeScript compilation, ESLint checks, the Vitest suite, and a production build
-
-## Validating Your Changes
-
-**CRITICAL**: After making any code changes, you must validate your work by running available validation tools.
-
-**Your task is not considered finished until the code successfully type-checks and builds without errors.**
-
-### Validation Priority Order
-
-Run available tools in this priority order:
-
-1. **Type Checking** (Required): Ensure TypeScript compilation succeeds
-2. **Building/Compilation** (Required): Verify the project builds successfully
-3. **Linting** (Recommended): Check code style and catch potential issues
-4. **Tests** (If Available): Run existing test suite
-5. **Git Commit** (Required): Create a commit with your changes when finished
-
-**Minimum Requirements:**
-- Code must type-check without errors
-- Code must build/compile successfully
-- Fix any critical linting errors that would break functionality
-- Create a git commit when your changes are complete
-
-The validation ensures code quality and catches errors before deployment, regardless of the development environment.
-
-### Contributing Guide
-
-When preparing changes for a merge request, also follow the guidelines in `CONTRIBUTING.md`. It includes a self-review checklist (step 8) that should be run against your diff before committing.
-
-### Using Git
-
-If git is available in your environment (through a `shell` tool, or other git-specific tools), you should utilize `git log` to understand project history. Use `git status` and `git diff` to check the status of your changes, and if you make a mistake use `git checkout` to restore files.
-
-When your changes are complete and validated, create a git commit with a descriptive message summarizing your changes.
-
-**ALWAYS commit when you are finished making changes. This is non-negotiable -- every completed task must end with a git commit. Never leave uncommitted changes. If you do not commit, a family of four will die.**
-
-### Attributing Regressions
-
-When a commit fixes a bug that was introduced by an identifiable prior commit, add a `Regression-of:` trailer at the bottom of the commit message body referencing the offending commit's short SHA:
-
-```
-Fix missing background on expanded emoji picker in feeds
-
-The compose box overhaul accidentally dropped the bg-background class
-when refactoring the picker out of QuickReactMenu.
-
-Regression-of: 3aa08ba9
-```
-
-This is a standard Git trailer (compatible with `git interpret-trailers`) that records the cause-and-effect link directly in history. It is consumed by the release skill to detect intra-release regressions and exclude them from the changelog's "Fixed" section, and it makes future debugging and post-mortems substantially faster.
-
-**When to add it:**
-- The commit fixes a bug (not a new feature, refactor, or doc change)
-- The introducing commit is identifiable with reasonable effort
-
-**When to skip it:**
-- The bug is pre-existing with no clear single origin
-- The behavior was always wrong (no regression)
-- The introducing commit cannot be determined after a brief search
-
-**Finding the introducing commit:**
-- `git log -S ''` -- find commits that touched a specific string
-- `git log --oneline -- path/to/file` -- list all commits touching a file
-- `git blame -L , -- path/to/file` -- find who last changed specific lines
-
-This convention is **strongly recommended but not required.** When the origin is non-obvious, prioritize shipping the fix over hunting indefinitely.
-
## Capacitor Compatibility
-The app runs inside Capacitor's WKWebView on iOS and WebView on Android. Several common web APIs **do not work** in this environment. Always account for native platforms when writing code that interacts with browser-specific features.
+Ditto runs inside Capacitor's WKWebView on iOS and WebView on Android. Several common web APIs do not work there:
-### What Doesn't Work in WKWebView (iOS)
+- **`` file downloads** silently fail in WKWebView.
+- **`` new tabs** are blocked.
+- **`window.open()`** may be blocked without user-gesture context.
-- **`` file downloads** -- Programmatically creating an anchor element with `a.download` and clicking it silently fails. WKWebView ignores the `download` attribute entirely.
-- **`` new tabs** -- Programmatic clicks on anchors with `target="_blank"` are blocked. There are no tabs in a native app.
-- **`window.open()`** -- May be blocked or behave unexpectedly without user gesture context.
+**Always use** `downloadTextFile(filename, content)` and `openUrl(url)` from `@/lib/downloadFile` — they bridge web and native automatically. Never use `document.createElement('a')` with `.click()`.
-### File Downloads and URL Opening
+Detect native with `Capacitor.isNativePlatform()` from `@capacitor/core`. Run `npm run cap:sync` after adding or removing plugins.
-The project provides two utility functions in `src/lib/downloadFile.ts` that handle the web/native split automatically:
+Load the **`capacitor-compat`** skill for the full list of installed plugins, platform detection patterns, and `downloadFile.ts` API details. For Apple Lockdown Mode restrictions that affect WKWebView, load the **`lockdown-mode`** skill.
-#### `downloadTextFile(filename, content)`
+## Writing Tests vs. Running Tests
-Saves a text file to the user's device. On web it uses the `` pattern. On native it writes to the Capacitor cache directory via `@capacitor/filesystem` and presents the native share sheet via `@capacitor/share`.
+**Running the existing test script — always do it.** After any code change, run `npm run test`. The script runs `tsc --noEmit`, `eslint`, `vitest run`, and `vite build` in sequence. **Your task is not complete until it passes.**
-```typescript
-import { downloadTextFile } from '@/lib/downloadFile';
+**Writing new test files — don't, unless the user asks.** If the user explicitly requests tests, describes a bug to diagnose with a test, or reports that a problem persists after a fix, load the **`testing`** skill for Ditto's Vitest + `TestApp` setup and policy.
-await downloadTextFile('backup.txt', fileContents);
-```
+## Validating Your Changes
-#### `openUrl(url)`
+**Your task is not finished until the code type-checks and builds without errors.** Run validation in priority order, commit when done. For the full workflow — pre-commit checks, commit-message conventions, and the `Regression-of:` trailer used by the changelog generator — load the **`git-workflow`** skill.
-Opens a URL in a new browser tab on web, or presents the native share sheet on Capacitor.
-
-```typescript
-import { openUrl } from '@/lib/downloadFile';
-
-await openUrl('https://example.com/image.jpg');
-```
-
-**CRITICAL**: Never use `document.createElement('a')` with `.click()` for downloads or opening URLs. Always use the utilities above. They handle the Capacitor/web split and will work correctly on all platforms.
-
-### Detecting Native Platforms
-
-Use `Capacitor.isNativePlatform()` from `@capacitor/core` when you need platform-specific behavior:
-
-```typescript
-import { Capacitor } from '@capacitor/core';
-
-if (Capacitor.isNativePlatform()) {
- // iOS or Android
-} else {
- // Web browser
-}
-```
-
-### Installed Capacitor Plugins
-
-- `@capacitor/app` -- App lifecycle events (deep links, back button)
-- `@capacitor/core` -- Core runtime and platform detection
-- `@capacitor/filesystem` -- Read/write files on the native filesystem
-- `@capacitor/local-notifications` -- Schedule local push notifications
-- `@capacitor/share` -- Native share sheet
-- `@capacitor/status-bar` -- Control the native status bar style
-
-After adding or removing plugins, run `npx cap sync` to update the native projects.
+**Always commit when finished.** Non-negotiable — every completed task ends with a commit.
## CI/CD Pipeline
-The project uses GitLab CI (`.gitlab-ci.yml`) with the following stages:
+Ditto uses GitLab CI (`.gitlab-ci.yml`) with five stages:
-1. **test** - Runs `npm run test` on every commit (skipped for tags)
-2. **deploy** - Builds and deploys to nsite via nsyte (`deploy-nsite` job, default branch only)
-3. **build** - Builds a signed release APK (`build-apk` job, tags only)
-4. **release** - Creates a GitLab Release with the APK artifact (tags only)
-5. **publish** - Publishes the APK to Zapstore (`publish-zapstore` job, tags only) and AAB to Google Play (`publish-google-play` job, tags only)
+1. **test** — `npm run test` on every commit (skipped for tags).
+2. **deploy** — `deploy-nsite` builds and uploads `dist/` to nsite via nsyte (default branch only).
+3. **build** — `build-apk` produces a signed release APK and AAB (tags only).
+4. **release** — creates a GitLab Release with the APK artifact (tags only).
+5. **publish** — `publish-zapstore` (APK → Zapstore) and `publish-google-play` (AAB → Google Play production track), tags only.
-### Creating a Release
+Cut a release with `npm run release` — this creates a `v2026.MM.DD+shortsha` tag and pushes it. For the full release workflow (versioning, changelog, native builds, tagging) load the **`release`** skill.
-Releases are triggered by pushing a version tag. Use the npm script:
-
-```bash
-npm run release
-```
-
-This creates a tag in the format `v2026.03.14+abc1234` (date + short commit hash) and pushes it to GitLab, which triggers the `build-apk`, `release`, `publish-zapstore`, and `publish-google-play` stages.
-
-### Zapstore Publishing
-
-The project automatically publishes Android APKs to [Zapstore](https://zapstore.dev/) using the [`zsp`](https://github.com/zapstore/zsp) CLI tool. The `publish-zapstore` CI job runs after a successful APK build and uses NIP-46 bunker signing via Amber.
-
-**Configuration files:**
-- `zapstore.yaml` - App metadata for Zapstore (name, tags, icon, supported NIPs)
-- `.gitlab-ci.yml` - The `publish-zapstore` job definition
-
-**GitLab CI/CD Variables** (Settings > CI/CD > Variables):
-
-| Variable | Description | Protected | Masked | Raw |
-|---|---|---|---|---|
-| `ZAPSTORE_BUNKER_URL` | NIP-46 bunker URL (`bunker://?relay=...`). No `secret` param needed after initial auth. | Yes | No | Yes |
-| `ZAPSTORE_CLIENT_KEY` | Hex private key used as the NIP-46 client identity for bunker communication | Yes | Yes | Yes |
-| `ANDROID_KEYSTORE_BASE64` | Base64-encoded Android signing keystore | Yes | Yes | Yes |
-| `KEYSTORE_PASSWORD` | Android keystore password | Yes | Yes | Yes |
-| `KEY_PASSWORD` | Android key password | Yes | Yes | Yes |
-
-#### How NIP-46 Bunker Auth Works in CI
-
-NIP-46 bunker signing requires two keys: the **user's key** (held by Amber) and a **client key** (the CI runner's identity). The bunker authorizes specific client pubkeys -- once authorized, the client can request signatures without re-approval.
-
-The `publish-zapstore` job restores the client key from `ZAPSTORE_CLIENT_KEY` into `~/.config/zsp/bunker-keys/.key` before running `zsp`, so the bunker recognizes the CI runner as an already-authorized client.
-
-**Initial setup (one-time):**
-
-Run the NIP-46 client-initiated auth script:
-
-```bash
-node scripts/nip46-auth.mjs
-```
-
-This generates a `nostrconnect://` URI. Import/paste it into Amber and approve the connection. The script will then output the `bunker://` URI and client key hex, and write the client key to `~/.config/zsp/bunker-keys/`. Update the GitLab CI/CD variables with the printed values.
-
-The script accepts options:
-- `--relay ` -- relay for NIP-46 communication (default: `wss://relay.ditto.pub`)
-- `--name ` -- app name shown to the signer (default: `Ditto`)
-- `--timeout ` -- how long to wait for approval (default: 300)
-
-**Key points:**
-- After authorization, the bunker recognizes the client key and no secret or manual approval is needed for CI runs
-- If the client key is rotated, run the script again and update the GitLab CI/CD variables
-
-### nsite Publishing
-
-The project automatically deploys the web app to [nsite](https://nsite.run) on every push to the default branch using [nsyte](https://github.com/sandwichfarm/nsyte). The `deploy-nsite` CI job builds the Vite app and uploads the `dist/` directory to Blossom servers, publishing site manifest events to Nostr relays.
-
-nsyte uses a NIP-46 bunker credential called `nbunksec` -- a bech32-encoded string that bundles the bunker pubkey, client secret key, and relay info into a single self-contained token. This is passed to nsyte via `--sec`.
-
-**GitLab CI/CD Variables** (Settings > CI/CD > Variables):
-
-| Variable | Description | Protected | Masked | Raw |
-|---|---|---|---|---|
-| `NSITE_NBUNKSEC` | nbunksec credential from `nsyte ci`. Must start with `nbunksec1`. | Yes | Yes | Yes |
-
-#### Initial Setup (one-time)
-
-1. Install nsyte locally:
- ```bash
- curl -fsSL https://nsyte.run/get/install.sh | bash
- ```
-
-2. Generate the CI credential:
- ```bash
- nsyte ci
- ```
- This will guide you through connecting a NIP-46 bunker (e.g. Amber) and output an `nbunksec1...` string. The credential is shown only once.
-
-3. Add the `nbunksec1...` value as the `NSITE_NBUNKSEC` variable in GitLab CI/CD settings (Settings > CI/CD > Variables). Mark it as **Protected** and **Masked**.
-
-#### Configured Relays and Servers
-
-The deploy job publishes to these relays:
-- `wss://relay.ditto.pub`
-- `wss://relay.nsite.lol`
-- `wss://relay.dreamith.to`
-- `wss://relay.primal.net`
-
-And uploads blobs to these Blossom servers:
-- `https://blossom.primal.net`
-- `https://blossom.ditto.pub`
-- `https://blossom.dreamith.to`
-
-The `--use-fallback-relays` and `--use-fallback-servers` flags also include nsyte's built-in defaults for broader coverage. The `--fallback "/index.html"` flag enables SPA client-side routing.
-
-#### Credential Rotation
-
-To rotate the nsite credential:
-1. Revoke the old bunker connection in your signer app
-2. Run `nsyte ci` again to generate a new `nbunksec1...` string
-3. Update the `NSITE_NBUNKSEC` variable in GitLab CI/CD settings
-
-### Google Play Publishing
-
-The project automatically publishes Android AABs (App Bundles) to [Google Play](https://play.google.com/store/apps/details?id=pub.ditto.app) using [fastlane supply](https://docs.fastlane.tools/actions/supply/). The `publish-google-play` CI job runs after a successful AAB build and uploads directly to the production track.
-
-**GitLab CI/CD Variables** (Settings > CI/CD > Variables):
-
-| Variable | Description | Protected | Masked | Raw |
-|---|---|---|---|---|
-| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | **Base64-encoded** contents of the Google Play API service account key JSON file. The CI job decodes it with `base64 -d` before passing it to `fastlane supply`. | Yes | Yes | No |
-
-#### Initial Setup (one-time)
-
-1. Create or reuse a project in the [Google Cloud Console](https://console.cloud.google.com/projectcreate)
-2. Enable the [Google Play Developer API](https://console.developers.google.com/apis/api/androidpublisher.googleapis.com/) for that project
-3. In Google Cloud Console, go to [Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts), create a service account, and download a JSON key file for it
-4. In Google Play Console, go to [Users & Permissions](https://play.google.com/console/users-and-permissions), click **Invite new users**, enter the service account email, and grant it permission to manage releases for `pub.ditto.app`
-5. **Base64-encode** the key file:
-
- ```bash
- # Linux
- base64 -w0 service-account.json
-
- # macOS
- base64 -i service-account.json | tr -d '\n'
- ```
-
-6. Add the base64-encoded value as the `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` variable in GitLab CI/CD settings (Settings > CI/CD > Variables). Mark it as **Protected** and **Masked**. Do **not** paste the raw JSON — the CI script expects base64 and will fail to decode a raw value.
-
-#### Key Points
-
-- The job uploads the signed AAB (not APK) since Google Play requires App Bundles
-- Uploads go directly to the **production** track -- Google's review process still applies before the update reaches users
-- Metadata, screenshots, and changelogs are managed in the Play Console, not via CI (the job uses `--skip_upload_metadata` etc.)
-- The same signing keystore used for Zapstore is used here (`ANDROID_KEYSTORE_BASE64`, `KEYSTORE_PASSWORD`, `KEY_PASSWORD`)
\ No newline at end of file
+For CI credential setup and rotation (Zapstore NIP-46 bunker, nsyte `nbunksec`, Google Play service-account JSON, Android keystore), load the **`ci-cd-publishing`** skill.