Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
334a017
feat: add patchbook-lab example app
thephez Apr 30, 2026
d2eff6b
fix(patchbook-lab): disable Save button when no unsaved edits
thephez Apr 30, 2026
7a87c50
fix(patchbook-lab): refresh editor metadata after save without flicker
thephez Apr 30, 2026
387bc6a
refactor(patchbook-lab): simplify editor loading check
thephez Apr 30, 2026
1390cbb
style(patchbook-lab): lock notes panels to viewport-relative shared h…
thephez Apr 30, 2026
1264747
feat(patchbook-lab): block save when body exceeds 5120-byte field limit
thephez Apr 30, 2026
27c4034
feat(patchbook-lab): show loading indicator in editor title
thephez Apr 30, 2026
547acf9
feat(patchbook-lab): rework mobile UI into Apple Notes-style stack
thephez May 1, 2026
4a257fd
test(patchbook-lab): cover mobile workspace flows and empty states
thephez May 1, 2026
c6626fa
style(patchbook-lab): nudge mobile EmptyState upward toward visual ce…
thephez May 1, 2026
039db46
feat(patchbook-lab): add Remember Me read-only browsing mode
thephez May 1, 2026
778f67b
test(patchbook-lab): cover login modal cancel/close/switch paths
thephez May 1, 2026
7553555
feat(patchbook-lab): cache notes in localStorage with stale-while-rev…
thephez May 1, 2026
bc79999
fix(patchbook-lab): handle concurrent save conflicts and stop spuriou…
thephez May 1, 2026
a81e6a9
style(patchbook-lab): merge desktop notes panes and refine editor chrome
thephez May 1, 2026
08ad9b8
fix(patchbook-lab): render only one title input per breakpoint
thephez May 1, 2026
168be02
fix(patchbook-lab): paint cached notes on first render for remembered…
thephez May 2, 2026
f1a3089
perf(patchbook-lab): lazy-load Evo SDK to keep app shell off the crit…
thephez May 2, 2026
5d462f1
perf(patchbook-lab): trust SDK contract cache and only evict on contr…
thephez May 2, 2026
8099df0
refactor(dashnote): rename example app from patchbook-lab to dashnote
thephez May 4, 2026
99c598f
style(dashnote): fuse title and body into single editor surface
thephez May 4, 2026
151db36
fix(dashnote): show joined notes shell from md instead of xl
thephez May 4, 2026
54a68c5
feat(dashnote): replace byte counter with progress bar
thephez May 4, 2026
d796ddd
refactor(dashnote): align src/ layout and document SDK calls
thephez May 4, 2026
ab4393e
docs(dashnote): add CLAUDE.md and align README with sibling apps
thephez May 4, 2026
907677b
feat(dashnote): show DPNS name on logged-in identity
thephez May 4, 2026
e2955a9
feat(dashnote): add single-file read-only lite companion
thephez May 4, 2026
e9397fb
refactor(dashnote): trim duplication in lite companion
thephez May 5, 2026
7fa48e7
docs: update example apps readme
thephez May 5, 2026
5eb21cd
fix(dashnote): apply CodeRabbit review feedback
thephez May 5, 2026
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
1 change: 1 addition & 0 deletions example-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Stand-alone applications built on top of the same `@dashevo/evo-sdk` used by the

- [dashmint-lab/](./dashmint-lab/) — React + TypeScript + Vite SPA for minting, viewing, transferring, and trading NFT-style collectible cards on Dash Platform testnet. Shares the browser-safe SDK core (`setupDashClient-core.mjs`) with the Node tutorials at the repo root.
- [dashproof-lab/](./dashproof-lab/) — React + TypeScript + Vite proof-of-existence tutorial app that hashes files locally in the browser, anchors SHA-256 proofs on Dash Platform testnet, verifies files by hash, and reviews proof history by owner or chain ID. Also uses the shared browser-safe SDK core from the parent repo.
- [dashnote/](./dashnote/) — React + TypeScript + Vite notes app for Dash Platform testnet. Create, edit, and delete notes against a small `note` data contract; supports a "Remember Me" read-only browse mode, optimistic localStorage cache, and ships a single-file zero-build read-only companion at `dashnote-lite.html`. Also uses the shared browser-safe SDK core from the parent repo.
28 changes: 28 additions & 0 deletions example-apps/dashnote/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
coverage
*.local
playwright-report
test-results
e2e/.auth

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
4 changes: 4 additions & 0 deletions example-apps/dashnote/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
node_modules
coverage
public/dashnote-lite.html
1 change: 1 addition & 0 deletions example-apps/dashnote/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
65 changes: 65 additions & 0 deletions example-apps/dashnote/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

This file provides guidance to Claude Code when working in [example-apps/dashnote/](.).

## Project Overview

React + TypeScript + Vite app for personal notes on Dash Platform testnet. Notes have an optional `title`, a required `message`, and Platform-managed `$createdAt` / `$updatedAt` / `$revision`. The UI is a flat recent-notes list plus a single editor/detail pane; auth is required to create/update/delete, but read-only browse works without a mnemonic.

## Commands

- `npm run dev` — start Vite dev server
- `npm run build` — typecheck (`tsc -b`) then bundle
- `npm run lint` — ESLint
- `npm run test` — Vitest suite in [test/](test/)
- `npm run format` / `format:check` — Prettier
- `npm run preview` — serve production build locally

## Architecture

- **[src/dash/](src/dash/)** — one file per Platform SDK operation. Each exports an async function with a leading JSDoc block naming the SDK method it wraps. No hooks, no UI wrappers — the SDK call is the function. Files in this folder always reference `@dashevo/evo-sdk` (or shared core re-exports / SDK-shape types). App-shell utilities that don't touch the SDK live in [src/lib/](src/lib/) instead.
- **Shared SDK core** — [src/dash/client.ts](src/dash/client.ts) and [src/dash/keyManager.ts](src/dash/keyManager.ts) re-export `createClient` and `IdentityKeyManager` from `../../../../setupDashClient-core.mjs` (the canonical browser-safe core at the host repo root). No vendoring. The `@dashevo/evo-sdk` bare specifier is aliased in [vite.config.ts](vite.config.ts) to this app's locally installed browser bundle so the shared core resolves the SDK from here.
- **[src/session/](src/session/)** — `SessionContext.tsx` provides the context (`status`, `error`, `sdk`, `keyManager`, `identityId`, `contractId`, `setContractId`, `log`, `login`, `enterReadOnly`, `logout`) and `useSession.ts` is the consumer hook. Mnemonic lives only in the keyManager closure — never in state, never in localStorage. The SDK + `IdentityKeyManager` are dynamically imported on first auth so the ~8MB WASM bundle doesn't block initial paint.
- **[src/components/](src/components/)** — standard React. Modals/panels call `src/dash/` functions directly. Notable: [NotesWorkspace.tsx](src/components/NotesWorkspace.tsx) (two-pane list + editor with optimistic cache + background revalidation), [NoteEditor.tsx](src/components/NoteEditor.tsx) (fused title/body editor with byte-budget progress bar), [LoginModal.tsx](src/components/LoginModal.tsx) (paste-or-register contract flow).
- **[src/hooks/](src/hooks/)** — app-specific hooks: [useTheme.ts](src/hooks/useTheme.ts) (dark mode toggle), [useMediaQuery.ts](src/hooks/useMediaQuery.ts) (`window.matchMedia` via `useSyncExternalStore`).
- **[src/lib/](src/lib/)** — pure utilities, no SDK references: [logger.ts](src/lib/logger.ts) (`Logger` type + `errorMessage(err)`), [notesCache.ts](src/lib/notesCache.ts) (localStorage-backed note list keyed by identity + contract + network), [rememberedIdentity.ts](src/lib/rememberedIdentity.ts) (last-logged-in identity ID for read-only browse), [fieldLimits.ts](src/lib/fieldLimits.ts) (UTF-8 byte counters for title/message), [format.ts](src/lib/format.ts).
- **[src/dash/types.ts](src/dash/types.ts)** — shared SDK types (`DashSdk`, `DashKeyManager`, query result shapes) used across every dash helper.
- **[public/dashnote-lite.html](public/dashnote-lite.html)** — single-file zero-build companion. Read-only Recent notes (with optional owner filter) + Get-by-ID only, loads `@dashevo/evo-sdk` from `esm.sh`, and ships alongside the React app at `<...>/dashnote/dashnote-lite.html` (Vite copies `public/*` into `dist/`). Intentionally self-contained as a learning reference — don't import app code into it.
- **[test/](test/)** — Vitest + Testing Library. All test files live in this flat directory and are named after the subject under test (e.g. `NotesWorkspace.test.tsx`, `SessionContext.test.tsx`, `notesCache.test.ts`) — they are **not** co-located next to source files, and the directory is **not** mirrored against `src/`. Default Vitest env is `node`; component tests opt into DOM with a `// @vitest-environment jsdom` pragma at the top of the file.

## Note contract

Schema lives in [src/dash/contract.ts](src/dash/contract.ts) as `NOTE_SCHEMAS`. One document type, `note`:

- `title` — optional string, max 120 chars, position 0
- `message` — required string, max 10000 chars, position 1
- `$createdAt`, `$updatedAt` — required (Platform-managed)
- Indices: `byOwnerUpdated` (`$ownerId`, `$updatedAt`) and `byOwnerCreated` (`$ownerId`, `$createdAt`)
- `documentsMutable: true`, `canBeDeleted: true` — notes are editable and deletable

`DEFAULT_CONTRACT_ID` is `8d6heK6CoskLBi6Rs7cChRG9RuckcZqZst28BdviBe8y`. Overrides are stored under `localStorage['dashnote.contractId']`. Settings can also register a fresh contract for the logged-in identity and immediately switch the app to it.

## SDK Patterns

- **Connect**: `await createClient("testnet")` from [client.ts](src/dash/client.ts) — re-exported from the shared core, which internally does `EvoSDK.testnetTrusted()` + `sdk.connect()`. App code never constructs the SDK directly.
- **Key derivation**: `IdentityKeyManager` from the shared core; `keyManager.getAuth()` returns `{ identity, identityKey, signer }`
- **Register contract**: `new DataContract({ ownerId, identityNonce, schemas, fullValidation })` then `sdk.contracts.publish({ dataContract, identityKey, signer })`. Nonce is `(sdk.identities.nonce(id) || 0n) + 1n`.
- **Create note**: `sdk.documents.create({ document, identityKey, signer })` where `document = new Document({ properties: { title?, message }, documentTypeName: "note", dataContractId, ownerId })`
- **Update note**: fetch existing via `sdk.documents.get(...)`, bump `revision = BigInt(existing.revision) + 1n`, then `sdk.documents.replace({ document, identityKey, signer })`
- **Delete note**: `sdk.documents.delete({ document: { id, ownerId, dataContractId, documentTypeName: "note" }, identityKey, signer })`
- **List my notes**: `sdk.documents.query({ dataContractId, documentTypeName: "note", where: [["$ownerId", "==", ownerId]], orderBy: [["$ownerId", "asc"], ["$updatedAt", "asc"]], limit })`
- **Get one note**: `sdk.documents.get(contractId, "note", noteId)`

`normalizeNotes()` and `normalizeSingleNote()` in [queries.ts](src/dash/queries.ts) flatten whatever shape `sdk.documents.query` / `sdk.documents.get` returns (array, Map, or plain object) into `NoteRecord[]` so UI code never branches on it.

## Gotchas

- Update flow **must** fetch the document first to get the current revision; submitting a replace with the wrong `revision` will fail the state transition. The pattern is `BigInt(existing.revision ?? 0) + 1n`.
- `keepsHistory` on the contract is forced to `false`. `keepsHistory: true` triggers [dashpay/platform#3165](https://github.com/dashpay/platform/issues/3165) — `sdk.contracts.fetch()` returns undefined and breaks `sdk.documents.query` with "Data contract not found". v1 of dashnote shows revision metadata only — older note bodies are not reconstructable from the network.
- Read-only mode (`session.status === "readonly"`) sets `keyManager` to `null`. Any write path (`createNote`, `updateNote`, `deleteNote`, `registerContract`) must guard for an authenticated session.
- The notes cache in [src/lib/notesCache.ts](src/lib/notesCache.ts) is keyed by `identityId + contractId + network`. Switching identity, contract, or network invalidates the cache. Schema is versioned (`SCHEMA_VERSION = 1`); bumping it discards prior cached payloads.
- Background revalidation runs every `BACKGROUND_REFRESH_MS` (30s); refocus revalidation is throttled to `FOCUS_REFRESH_MIN_MS` (10s). Both compare via `notesEqualByRevision` so identical results don't trigger re-renders.
- Title/message length is enforced in **bytes**, not chars — emoji and non-ASCII multi-byte sequences eat budget. [src/lib/fieldLimits.ts](src/lib/fieldLimits.ts) is the source of truth; the editor's progress bar and the contract `maxLength` should stay in sync.
- The `Logger` from [src/lib/logger.ts](src/lib/logger.ts) is plumbed through every dash helper. `level: "success"` and `level: "error"` also raise sonner toasts via `SessionContext.log`.
- The Evo SDK WASM bundle is ~8MB; this is expected and not a build error.
- `allowJs: true` in [tsconfig.app.json](tsconfig.app.json) so TypeScript can import the JSDoc-typed `.mjs` core at the host repo root.
122 changes: 122 additions & 0 deletions example-apps/dashnote/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Dashnote — Dash Platform Notes

`Dashnote` is a React + TypeScript + Vite example app for personal notes on Dash Platform testnet.

The app stays close to the tutorial `note` contract shape, but extends it with an optional `title`, a required `message`, and required Platform timestamps. Notes are editable, deletable, and shown in a calm two-pane notebook UI.

## Prerequisites

- Node >= 20
- A funded Dash Platform testnet identity (BIP-39 mnemonic + identity index) for write operations
- Read-only mode works without any identity — visitors can read notes for any identity ID against the bundled contract

## Quick start

```bash
npm install
npm run dev
```

Other scripts:

```bash
npm run build # tsc -b && vite build
npm run test # Vitest suite
npm run lint # ESLint
npm run format # Prettier (write)
npm run format:check # Prettier (check only)
npm run preview # Serve the production build
```

## Current app behavior

- The app auto-connects in read-only mode on load against the bundled default contract.
- Creating, editing, and deleting notes requires login with a testnet identity.
- The primary screen is a flat recent-notes list plus a note editor/detail pane.
- `title` is optional; `message` is required.
- If `title` is blank, the UI uses the first non-empty line of `message`.
- If both are blank, the note renders as `Untitled`.
- Field length is enforced in **bytes**, not characters; the editor's progress bar reflects the UTF-8 budget.

## Contract and Settings flow

- The app ships with a bundled deployed note contract ID (`8d6heK6CoskLBi6Rs7cChRG9RuckcZqZst28BdviBe8y`) so read-only browse and verification flows work immediately on a fresh machine.
- The login modal becomes a Settings modal after authentication.
- Settings can:
- paste and reuse an existing note contract ID
- register a fresh Dashnote note contract on testnet
- switch the app to that newly registered contract immediately
- Overrides persist under `localStorage['dashnote.contractId']`. Clearing storage falls back to the bundled default.

## Contract schema notes

- The schema in [`src/dash/contract.ts`](./src/dash/contract.ts) defines a single document type, `note`, with `title` (optional, max 120 chars), `message` (required, max 10000 chars), and required Platform-managed `$createdAt` / `$updatedAt`.
- Indices: `byOwnerUpdated` (`$ownerId`, `$updatedAt`) and `byOwnerCreated` (`$ownerId`, `$createdAt`) — both are how the recent-notes list paginates and sorts.
- `documentsMutable: true` and `canBeDeleted: true` — notes are editable and deletable.
- `keepsHistory` is forced to `false`. `keepsHistory: true` triggers [dashpay/platform#3165](https://github.com/dashpay/platform/issues/3165), where `sdk.contracts.fetch()` returns undefined and breaks `sdk.documents.query` with "Data contract not found". This is why v1 only shows revision metadata, not previous versions of notes.

## Platform operations at a glance

Every SDK call lives in its own file under [`src/dash/`](./src/dash/). Open the file to see the full implementation with a JSDoc header naming the SDK method it wraps.

| Operation | File | SDK method |
| ---------------------- | ---------------------------------------------------- | --------------------------------------------- |
| Connect to testnet | [`src/dash/client.ts`](./src/dash/client.ts) | `EvoSDK.testnetTrusted()` + `sdk.connect()` |
| Derive identity keys | [`src/dash/keyManager.ts`](./src/dash/keyManager.ts) | wallet/key derivation helpers |
| Register note contract | [`src/dash/contract.ts`](./src/dash/contract.ts) | `sdk.contracts.publish` |
| Create a note | [`src/dash/createNote.ts`](./src/dash/createNote.ts) | `sdk.documents.create` |
| Update a note | [`src/dash/updateNote.ts`](./src/dash/updateNote.ts) | `sdk.documents.get` + `sdk.documents.replace` |
| Delete a note | [`src/dash/deleteNote.ts`](./src/dash/deleteNote.ts) | `sdk.documents.delete` |
| List my notes | [`src/dash/queries.ts`](./src/dash/queries.ts) | `sdk.documents.query` |
| Get one note | [`src/dash/queries.ts`](./src/dash/queries.ts) | `sdk.documents.get` |

Update flow always fetches the document first to read its current revision, then submits a replace with `revision = BigInt(existing.revision ?? 0) + 1n`. Replays without bumping the revision are rejected by the state transition.

Supporting files:

- **[`src/dash/types.ts`](./src/dash/types.ts)** — shared SDK types (`DashSdk`, `DashKeyManager`, query result shapes) wired through every dash helper.
- **[`src/lib/logger.ts`](./src/lib/logger.ts)** — `Logger` function type and `errorMessage(err)` helper. Plumbed through every dash call so progress messages stream to the activity log and `level: "success" | "error"` raise sonner toasts.
- **[`src/lib/notesCache.ts`](./src/lib/notesCache.ts)** — localStorage-backed note list keyed by `identityId + contractId + network`. Powers optimistic paint on reload before background revalidation completes.
- **[`src/lib/rememberedIdentity.ts`](./src/lib/rememberedIdentity.ts)** — last-logged-in identity ID for read-only browse. Never stores the mnemonic.

## Reading the codebase

Recommended order for understanding how the app works:

1. **[`src/dash/`](./src/dash/)** — start here. One file per Platform operation, each with a JSDoc block naming the SDK method. Read [`createNote.ts`](./src/dash/createNote.ts) first (simplest write flow), then [`updateNote.ts`](./src/dash/updateNote.ts) (the fetch → bump revision → replace pattern).

2. **[`src/dash/contract.ts`](./src/dash/contract.ts)** — the `note` schema, indices, and the `registerContract` / `ensureContract` helpers used by Settings.

3. **[`src/session/SessionContext.tsx`](./src/session/SessionContext.tsx)** — manages the SDK connection, identity, contract ID, and activity log. The mnemonic never enters React state; it lives only inside the `keyManager` closure and is garbage-collected on logout. The consumer hook lives in [`useSession.ts`](./src/session/useSession.ts).

4. **[`src/components/`](./src/components/)** — standard React UI. [`NotesWorkspace.tsx`](./src/components/NotesWorkspace.tsx) is the two-pane list + editor with optimistic cache and background revalidation. [`NoteEditor.tsx`](./src/components/NoteEditor.tsx) is the fused title/body editor with a UTF-8 byte-budget progress bar. [`LoginModal.tsx`](./src/components/LoginModal.tsx) wires the paste-or-register contract flow.

5. **[`src/hooks/`](./src/hooks/)** — [`useTheme`](./src/hooks/useTheme.ts) for dark mode, [`useMediaQuery`](./src/hooks/useMediaQuery.ts) for the mobile-vs-desktop layout switch via `window.matchMedia`.

6. **[`src/lib/`](./src/lib/)** — pure utilities, no SDK references: [`fieldLimits.ts`](./src/lib/fieldLimits.ts) (UTF-8 byte counters), [`format.ts`](./src/lib/format.ts), plus `logger.ts` / `notesCache.ts` / `rememberedIdentity.ts` described above.

For deeper architecture and gotchas, see [`CLAUDE.md`](./CLAUDE.md).

## Tests

[`test/`](./test/) uses Vitest + Testing Library, flat-not-mirrored, named after the subject under test. The default Vitest environment is Node; component tests opt into jsdom per-file with `// @vitest-environment jsdom`. Run with `npm run test`.

The suite covers:

- contract schema and registration config
- note query normalization and sorting
- create / update / delete mutation helpers
- note-title fallback formatting
- notes cache load/save/clear and revision-equality
- remembered identity persistence
- notebook UI flows for auth gating, create, update, and delete

## Tech stack

- React 19
- TypeScript
- Vite 8
- Tailwind CSS v4
- Vitest 4 + Testing Library
- `@dashevo/evo-sdk`
- sonner (toasts)
23 changes: 23 additions & 0 deletions example-apps/dashnote/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";

export default defineConfig([
globalIgnores(["coverage", "dist", "playwright-report", "test-results"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
]);
13 changes: 13 additions & 0 deletions example-apps/dashnote/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashnote — Dash Platform Notes</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading