Skip to content

Commit fb19a9c

Browse files
thephezclaude
andauthored
feat: add dashnote example app (#74)
* feat: add patchbook-lab example app Vite + React + TypeScript notebook UI demonstrating mutable document CRUD on Dash Platform: register a note contract, then create, edit, and delete notes against it via the shared evo-sdk core. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(patchbook-lab): disable Save button when no unsaved edits Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(patchbook-lab): refresh editor metadata after save without flicker Same-id updates skipped the detail refetch because setSelectedId with an unchanged value didn't re-run the load effect, leaving stale revision / updatedAt and a stuck dirty state. Extract loadNoteDetail and call it directly from handleSave. Suppress the loading placeholder when the already-loaded data still matches the current selection so the editor form and notes list don't unmount during background refreshes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(patchbook-lab): simplify editor loading check Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(patchbook-lab): lock notes panels to viewport-relative shared height Both panels previously sized to their content, so heights mismatched and jumped between notes. At xl breakpoint the grid wrapper now claims a fixed viewport-relative height; both sections fill it via flex-column, and the editor textarea expands into remaining space. Mobile layout is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(patchbook-lab): block save when body exceeds 5120-byte field limit Surface the platform's per-field byte limit in the editor with a live counter above the body and a disabled Save button when over. Also truncate the header title preview to a single line and drop the "Apple Notes-like" callout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(patchbook-lab): show loading indicator in editor title Replace the previous note's stale title with a pulsing "Loading…" state while a newly selected note is fetched, and unwrap the now-single-child header flex row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(patchbook-lab): rework mobile UI into Apple Notes-style stack On mobile, the workspace now switches to a list/editor stack with a back button, fullscreen editor, search bar, compose FAB, and a centered empty-state CTA when signed out. The list and editor panes paint edge-to-edge on a shared surface so previously distinct cards no longer read as disjoint blocks. Desktop two-pane layout is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(patchbook-lab): cover mobile workspace flows and empty states Adds a parameterizable matchMedia stub and exercises the mobile-only back/compose/delete flows, the desktop-vs-mobile auto-select gate, the NoteList search filter, the no-contract EmptyState branch, and the Bridge identity link on the auth-gating EmptyState. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(patchbook-lab): nudge mobile EmptyState upward toward visual center The tutorial header above the workspace pushed the EmptyState's geometric center below the screen's visual center on mobile. Translate the centered cluster up by 64px so the sign-in CTA sits closer to where the eye expects it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(patchbook-lab): add Remember Me read-only browsing mode Persists only the most recently logged-in identity ID so returning visitors automatically resume in a read-only browsing view of their notes. Adds a "browsing" session status, account-style login UX with a read-only identity field plus switch/forget links, and mirrors the same controls in the authenticated settings view. Mnemonic and keys remain in-memory only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(patchbook-lab): cover login modal cancel/close/switch paths Adds tests for the previously uncovered LoginModal flows: Cancel button, switch-then-submit (rememberMe stays on), state reset on modal reopen, and the settings-view Use-different-identity, Forget, Close, and Logout-side-effect paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(patchbook-lab): cache notes in localStorage with stale-while-revalidate Persists per-identity note lists (titles, bodies, revisions) so reloads paint instantly from cache, then revalidate against Platform in the background. Authenticated saves are gated on first revalidation completing so cached state can't clobber a newer chain revision; conflicts during edits surface a warning rather than silently overwriting. Refreshing indicator in the list header during revalidation; visibilitychange + 30s interval drive mid-session freshness, with throttling and write-in-flight guards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(patchbook-lab): handle concurrent save conflicts and stop spurious warnings After a failed update, refresh from chain and surface the conflict warning if the revision moved past what was loaded — so a retry isn't a silent overwrite when another window saved first (typically exposed via an identity nonce error). The warning supersedes the raw error since it's the actionable signal that a retry will overwrite. Also advance the baselines synchronously after a successful save so the post-save reload's read-time conflict detector doesn't false-positive against its own write. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(patchbook-lab): merge desktop notes panes and refine editor chrome Wraps the My notes list and Note editor in one card with an internal divider on desktop, moves the title into the editor header as an editable heading with hover affordance, and consolidates byte count into the metadata footer. Both header rows share a fixed height so the bottom borders align across the divider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(patchbook-lab): render only one title input per breakpoint JSDOM ignores CSS, so md:hidden left both title inputs in the accessibility tree and getByLabelText returned multiple matches. Gate the desktop header input and mobile body input on the existing isDesktop signal so exactly one is mounted at a time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(patchbook-lab): paint cached notes on first render for remembered identity Boot in browsing mode when an identity is remembered so the signed-in gate, "No notes yet," and "No note selected" no longer flash through before the cache hydrates. NotesWorkspace seeds notes (and the desktop editor pane) synchronously from localStorage, and reloadNotes waits for the SDK instead of wiping cached state during rehydrate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(patchbook-lab): lazy-load Evo SDK to keep app shell off the critical path Defer the @dashevo/evo-sdk import (and its ~8MB WASM bundle) until the user actually connects or logs in, and strip the SDK chunk from Vite's auto-injected modulepreload hints so the browser doesn't race to fetch it during initial paint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(patchbook-lab): trust SDK contract cache and only evict on contract change Every note query and fetch was calling refreshContractCache, which evicts the SDK's cached contract schema and forces a refetch on every documents operation. The contract is immutable per session, so this was costing a round-trip per note read with no benefit. Drop the per-query eviction and instead evict from setContractId only when the contract ID actually changes (settings update or fresh register). Add SessionContext tests covering both branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashnote): rename example app from patchbook-lab to dashnote Renames the directory, package name/displayName, HTML title, README, log strings, and the four localStorage keys (patchbook-lab.* → dashnote.*). Existing browser sessions lose remembered contract, identity, cached notes, and theme — acceptable for a testnet example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(dashnote): fuse title and body into single editor surface Move the title input out of the header bar into the content area as the first line of a borderless editor surface, and drop the search row's divider so panes don't compete with the shell border. Wraps the global input/textarea font reset in @layer base so utility font sizes can win. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dashnote): show joined notes shell from md instead of xl The two-column shell only applied at xl, so 768-1279px viewports rendered the list and editor stacked vertically as separate cards. Move the shell breakpoint to md and use a 260px list column at md, growing to 340px at lg+, so tablets get the same Apple Notes layout as larger screens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashnote): replace byte counter with progress bar Bytes vs characters has no fixed conversion (emoji are 4 bytes each), so 'X / 5120 bytes' didn't tell users how much room they had left. Show a thin progress bar that fills against the 5120-byte limit, turning amber at 90% and red when oversize. Bar appears at 75%+ and stays hidden otherwise; precise byte count surfaces via the title tooltip and ARIA valuetext. Also reorder the desktop footer to lead with Revision and drop the redundant relative-time parenthetical from Updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashnote): align src/ layout and document SDK calls Move non-SDK utilities (logger, notesCache, rememberedIdentity) out of src/dash/ into src/lib/, move useMediaQuery into src/hooks/, and add "SDK method:" JSDoc headers to each src/dash/ operation file so the folder reads as a tour of Platform calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dashnote): add CLAUDE.md and align README with sibling apps Mirror the dashmint-lab and dashproof-lab README structure (prerequisites, ops table mapping action → file → SDK method, reading-order tour, tech stack) and add a CLAUDE.md dev guide covering architecture, SDK patterns, and contract gotchas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashnote): show DPNS name on logged-in identity Resolve `sdk.dpns.username(identityId)` after login and persist the (id, name) pair to localStorage so a returning visitor sees their name in the sidebar, settings, and remembered-identity panels without re-querying. DPNS bindings are immutable, so cached pairs are never revalidated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashnote): add single-file read-only lite companion Mirrors dashmint-lite / dashproof-lite: zero-build static HTML loading the Evo SDK from esm.sh. Recent notes (with optional owner filter), get note by ID, click-to-copy on truncated identifiers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashnote): trim duplication in lite companion Merge the two list functions and number coercers, drop redundant epoch fields in favor of ISO-only timestamps (lex-sortable), and inline the shortId helper into copyIdSpan. 427 → 394 lines, behaviour unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: update example apps readme * fix(dashnote): apply CodeRabbit review feedback Address actionable findings from CodeRabbit review on PR #74. Net effect: nine bug fixes / a11y improvements in dashnote, plus tighter test coverage for the cache, session, and reload-stale-response invariants. - a11y: drawer toggle aria-label flips with state; OperationResultNotice uses assertive aria-live for errors so role="alert" semantics hold - LoginModal: clear stale error and mnemonic when modal reopens - NotesWorkspace: monotonic reload token + session-snapshot guard so a late listMyNotes response from a previous identity/contract can't clobber state or write the wrong workspace into cache - notesCache: key by identityId + contractId + network (per CLAUDE.md); clearCachedNotes sweeps every contract+network slot for an identity - useTheme: optional-chain .matches so missing matchMedia falls back to "dark" instead of throwing - SessionContext: isolate DPNS lookup failures so a name-service hiccup doesn't fail an otherwise-valid session; clear any prior remembered identity when login uses rememberMe: false (prevents logout falling back to the wrong identity) - App.test.tsx: import ReactNode instead of using bare React.ReactNode (jsx: react-jsx doesn't put React in scope as a namespace) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 476b61d commit fb19a9c

59 files changed

Lines changed: 12345 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

example-apps/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Stand-alone applications built on top of the same `@dashevo/evo-sdk` used by the
66

77
- [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.
88
- [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.
9+
- [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.

example-apps/dashnote/.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
coverage
14+
*.local
15+
playwright-report
16+
test-results
17+
e2e/.auth
18+
19+
# Editor directories and files
20+
.vscode/*
21+
!.vscode/extensions.json
22+
.idea
23+
.DS_Store
24+
*.suo
25+
*.ntvs*
26+
*.njsproj
27+
*.sln
28+
*.sw?
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist
2+
node_modules
3+
coverage
4+
public/dashnote-lite.html
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

example-apps/dashnote/CLAUDE.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code when working in [example-apps/dashnote/](.).
4+
5+
## Project Overview
6+
7+
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.
8+
9+
## Commands
10+
11+
- `npm run dev` — start Vite dev server
12+
- `npm run build` — typecheck (`tsc -b`) then bundle
13+
- `npm run lint` — ESLint
14+
- `npm run test` — Vitest suite in [test/](test/)
15+
- `npm run format` / `format:check` — Prettier
16+
- `npm run preview` — serve production build locally
17+
18+
## Architecture
19+
20+
- **[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.
21+
- **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.
22+
- **[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.
23+
- **[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).
24+
- **[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`).
25+
- **[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).
26+
- **[src/dash/types.ts](src/dash/types.ts)** — shared SDK types (`DashSdk`, `DashKeyManager`, query result shapes) used across every dash helper.
27+
- **[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.
28+
- **[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.
29+
30+
## Note contract
31+
32+
Schema lives in [src/dash/contract.ts](src/dash/contract.ts) as `NOTE_SCHEMAS`. One document type, `note`:
33+
34+
- `title` — optional string, max 120 chars, position 0
35+
- `message` — required string, max 10000 chars, position 1
36+
- `$createdAt`, `$updatedAt` — required (Platform-managed)
37+
- Indices: `byOwnerUpdated` (`$ownerId`, `$updatedAt`) and `byOwnerCreated` (`$ownerId`, `$createdAt`)
38+
- `documentsMutable: true`, `canBeDeleted: true` — notes are editable and deletable
39+
40+
`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.
41+
42+
## SDK Patterns
43+
44+
- **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.
45+
- **Key derivation**: `IdentityKeyManager` from the shared core; `keyManager.getAuth()` returns `{ identity, identityKey, signer }`
46+
- **Register contract**: `new DataContract({ ownerId, identityNonce, schemas, fullValidation })` then `sdk.contracts.publish({ dataContract, identityKey, signer })`. Nonce is `(sdk.identities.nonce(id) || 0n) + 1n`.
47+
- **Create note**: `sdk.documents.create({ document, identityKey, signer })` where `document = new Document({ properties: { title?, message }, documentTypeName: "note", dataContractId, ownerId })`
48+
- **Update note**: fetch existing via `sdk.documents.get(...)`, bump `revision = BigInt(existing.revision) + 1n`, then `sdk.documents.replace({ document, identityKey, signer })`
49+
- **Delete note**: `sdk.documents.delete({ document: { id, ownerId, dataContractId, documentTypeName: "note" }, identityKey, signer })`
50+
- **List my notes**: `sdk.documents.query({ dataContractId, documentTypeName: "note", where: [["$ownerId", "==", ownerId]], orderBy: [["$ownerId", "asc"], ["$updatedAt", "asc"]], limit })`
51+
- **Get one note**: `sdk.documents.get(contractId, "note", noteId)`
52+
53+
`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.
54+
55+
## Gotchas
56+
57+
- 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`.
58+
- `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.
59+
- Read-only mode (`session.status === "readonly"`) sets `keyManager` to `null`. Any write path (`createNote`, `updateNote`, `deleteNote`, `registerContract`) must guard for an authenticated session.
60+
- 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.
61+
- 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.
62+
- 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.
63+
- 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`.
64+
- The Evo SDK WASM bundle is ~8MB; this is expected and not a build error.
65+
- `allowJs: true` in [tsconfig.app.json](tsconfig.app.json) so TypeScript can import the JSDoc-typed `.mjs` core at the host repo root.

example-apps/dashnote/README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Dashnote — Dash Platform Notes
2+
3+
`Dashnote` is a React + TypeScript + Vite example app for personal notes on Dash Platform testnet.
4+
5+
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.
6+
7+
## Prerequisites
8+
9+
- Node >= 20
10+
- A funded Dash Platform testnet identity (BIP-39 mnemonic + identity index) for write operations
11+
- Read-only mode works without any identity — visitors can read notes for any identity ID against the bundled contract
12+
13+
## Quick start
14+
15+
```bash
16+
npm install
17+
npm run dev
18+
```
19+
20+
Other scripts:
21+
22+
```bash
23+
npm run build # tsc -b && vite build
24+
npm run test # Vitest suite
25+
npm run lint # ESLint
26+
npm run format # Prettier (write)
27+
npm run format:check # Prettier (check only)
28+
npm run preview # Serve the production build
29+
```
30+
31+
## Current app behavior
32+
33+
- The app auto-connects in read-only mode on load against the bundled default contract.
34+
- Creating, editing, and deleting notes requires login with a testnet identity.
35+
- The primary screen is a flat recent-notes list plus a note editor/detail pane.
36+
- `title` is optional; `message` is required.
37+
- If `title` is blank, the UI uses the first non-empty line of `message`.
38+
- If both are blank, the note renders as `Untitled`.
39+
- Field length is enforced in **bytes**, not characters; the editor's progress bar reflects the UTF-8 budget.
40+
41+
## Contract and Settings flow
42+
43+
- The app ships with a bundled deployed note contract ID (`8d6heK6CoskLBi6Rs7cChRG9RuckcZqZst28BdviBe8y`) so read-only browse and verification flows work immediately on a fresh machine.
44+
- The login modal becomes a Settings modal after authentication.
45+
- Settings can:
46+
- paste and reuse an existing note contract ID
47+
- register a fresh Dashnote note contract on testnet
48+
- switch the app to that newly registered contract immediately
49+
- Overrides persist under `localStorage['dashnote.contractId']`. Clearing storage falls back to the bundled default.
50+
51+
## Contract schema notes
52+
53+
- 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`.
54+
- Indices: `byOwnerUpdated` (`$ownerId`, `$updatedAt`) and `byOwnerCreated` (`$ownerId`, `$createdAt`) — both are how the recent-notes list paginates and sorts.
55+
- `documentsMutable: true` and `canBeDeleted: true` — notes are editable and deletable.
56+
- `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.
57+
58+
## Platform operations at a glance
59+
60+
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.
61+
62+
| Operation | File | SDK method |
63+
| ---------------------- | ---------------------------------------------------- | --------------------------------------------- |
64+
| Connect to testnet | [`src/dash/client.ts`](./src/dash/client.ts) | `EvoSDK.testnetTrusted()` + `sdk.connect()` |
65+
| Derive identity keys | [`src/dash/keyManager.ts`](./src/dash/keyManager.ts) | wallet/key derivation helpers |
66+
| Register note contract | [`src/dash/contract.ts`](./src/dash/contract.ts) | `sdk.contracts.publish` |
67+
| Create a note | [`src/dash/createNote.ts`](./src/dash/createNote.ts) | `sdk.documents.create` |
68+
| Update a note | [`src/dash/updateNote.ts`](./src/dash/updateNote.ts) | `sdk.documents.get` + `sdk.documents.replace` |
69+
| Delete a note | [`src/dash/deleteNote.ts`](./src/dash/deleteNote.ts) | `sdk.documents.delete` |
70+
| List my notes | [`src/dash/queries.ts`](./src/dash/queries.ts) | `sdk.documents.query` |
71+
| Get one note | [`src/dash/queries.ts`](./src/dash/queries.ts) | `sdk.documents.get` |
72+
73+
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.
74+
75+
Supporting files:
76+
77+
- **[`src/dash/types.ts`](./src/dash/types.ts)** — shared SDK types (`DashSdk`, `DashKeyManager`, query result shapes) wired through every dash helper.
78+
- **[`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.
79+
- **[`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.
80+
- **[`src/lib/rememberedIdentity.ts`](./src/lib/rememberedIdentity.ts)** — last-logged-in identity ID for read-only browse. Never stores the mnemonic.
81+
82+
## Reading the codebase
83+
84+
Recommended order for understanding how the app works:
85+
86+
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).
87+
88+
2. **[`src/dash/contract.ts`](./src/dash/contract.ts)** — the `note` schema, indices, and the `registerContract` / `ensureContract` helpers used by Settings.
89+
90+
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).
91+
92+
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.
93+
94+
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`.
95+
96+
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.
97+
98+
For deeper architecture and gotchas, see [`CLAUDE.md`](./CLAUDE.md).
99+
100+
## Tests
101+
102+
[`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`.
103+
104+
The suite covers:
105+
106+
- contract schema and registration config
107+
- note query normalization and sorting
108+
- create / update / delete mutation helpers
109+
- note-title fallback formatting
110+
- notes cache load/save/clear and revision-equality
111+
- remembered identity persistence
112+
- notebook UI flows for auth gating, create, update, and delete
113+
114+
## Tech stack
115+
116+
- React 19
117+
- TypeScript
118+
- Vite 8
119+
- Tailwind CSS v4
120+
- Vitest 4 + Testing Library
121+
- `@dashevo/evo-sdk`
122+
- sonner (toasts)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import reactHooks from "eslint-plugin-react-hooks";
4+
import reactRefresh from "eslint-plugin-react-refresh";
5+
import tseslint from "typescript-eslint";
6+
import { defineConfig, globalIgnores } from "eslint/config";
7+
8+
export default defineConfig([
9+
globalIgnores(["coverage", "dist", "playwright-report", "test-results"]),
10+
{
11+
files: ["**/*.{ts,tsx}"],
12+
extends: [
13+
js.configs.recommended,
14+
tseslint.configs.recommended,
15+
reactHooks.configs.flat.recommended,
16+
reactRefresh.configs.vite,
17+
],
18+
languageOptions: {
19+
ecmaVersion: 2020,
20+
globals: globals.browser,
21+
},
22+
},
23+
]);

example-apps/dashnote/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Dashnote — Dash Platform Notes</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)