Skip to content
Open
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Remote storage is visible by default for the GitHub beta path.
# Set this to false only for builds that should be strictly local-only.
NEXT_PUBLIC_ENABLE_REMOTE_STORAGE=true

# GitHub beta uses a personal access token entered by the user and encrypted in browser storage.
# Do not commit tokens.
72 changes: 61 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
# Next.js template
# OpenNotes

This is a Next.js template with shadcn/ui.
OpenNotes is a beta, local-first markdown notebook for calm personal writing. It is built with Next.js and stores notes in your browser today, with remote storage work underway.

## Adding components
## What works now

To add components to your app, run the following command:
- Create, edit, rename, and delete markdown notes.
- Local persistence through IndexedDB in the current browser profile.
- A focused editor with markdown formatting, wikilinks, slash commands, command palette, mobile sidebar, and light/dark themes.
- No OpenNotes account or OpenNotes server is required for the local workflow.

## Beta caveats

OpenNotes is not production-ready remote-sync software yet. In this branch:

- GitHub sync is a beta path for people comfortable granting a fine-grained token to one repo.
- Dropbox sync is planned after the GitHub provider is proven end to end.
- Browser-local storage can be cleared by private browsing modes, browser resets, storage pressure, or manual site-data deletion.
- Export/backup and full PWA/offline installability are still roadmap items unless implemented in a later branch.

If your notes are important, keep an independent copy outside OpenNotes.

## Where data lives

Notes are saved as records in the browser's IndexedDB database for this app. The current implementation does not upload note content to an OpenNotes backend. Clearing site data for the app will remove the local vault from that browser profile.

## Connecting GitHub

The current GitHub beta is intentionally no-backend: OpenNotes asks for a fine-grained GitHub token and stores it encrypted in this browser.

Recommended token setup:

1. Create the GitHub repo first, for example `opennotes-vault`.
2. Create a fine-grained token at <https://github.com/settings/personal-access-tokens/new>.
3. Set **Repository access** to only that repo.
4. Set **Contents** permission to **Read and write**.
5. Paste the token into OpenNotes Settings → Sync to GitHub.

OpenNotes does not auto-create repos in the fine-grained token flow. If the repo is missing or the token was not granted access, connection fails with a clear error.

OAuth "Sign in with GitHub" is the better production path, but it requires a tiny token-broker service because this app is currently a static export. That belongs after v2 is clean.

## Roadmap

1. Harden the local-first note workflow and backup/export story.
2. Wire GitHub sync end to end, including authentication, provider selection, sync status, and conflict handling.
3. Add Dropbox only after the provider contract is reliable with GitHub.
4. Revisit PWA/installability once real icon assets and offline behavior are in place.

## Local development

```bash
npx shadcn@latest add button
corepack enable
pnpm install
pnpm dev
```

This will place the ui components in the `components` directory.
Then open http://localhost:3000.

## Using components
## Validation commands

To use the components in your app, import them as follows:

```tsx
import { Button } from "@/components/ui/button";
```bash
pnpm exec eslint .
pnpm exec tsc --noEmit
pnpm exec vitest run
pnpm exec next build
```

## License

Apache-2.0
40 changes: 21 additions & 19 deletions app/landing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Zap,
Lock,
} from "lucide-react"
import Link from "next/link"

export default function Landing() {
return (
Expand All @@ -23,17 +24,17 @@ export default function Landing() {
<br className="sm:hidden" /> Your storage.
</h1>
<p className="mx-auto mb-8 max-w-xl text-lg leading-relaxed text-muted-foreground">
A markdown editor that stores your files in GitHub, Dropbox, or just
your browser. No subscription. No lock-in. No server.
A local-first markdown editor for calm, portable notes. Your vault
starts in this browser. GitHub sync is under active development.
</p>
<div className="flex flex-col justify-center gap-3 sm:flex-row">
<a
<Link
href="/"
className="inline-flex items-center justify-center rounded-lg bg-foreground px-6 py-3 text-sm font-medium text-background transition-opacity hover:opacity-90"
>
Try it now
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Link>
<a
href="https://github.com/opennotes/opennotes"
target="_blank"
Expand Down Expand Up @@ -61,11 +62,11 @@ export default function Landing() {
<GitBranch className="h-5 w-5 text-foreground" />
</div>
<h3 className="mb-2 text-base font-semibold text-foreground">
GitHub
GitHub sync next
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
Your notes in a private repo. Free version history. Every change
is a commit.
Remote sync to a private repo is the next major milestone. Do
not rely on it as a working backup in this beta.
</p>
</div>

Expand All @@ -74,11 +75,11 @@ export default function Landing() {
<Cloud className="h-5 w-5 text-foreground" />
</div>
<h3 className="mb-2 text-base font-semibold text-foreground">
Dropbox
Dropbox later
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
Syncs everywhere Dropbox does. No new accounts needed. Works
offline.
Dropbox support is planned after the GitHub provider and sync
conflict flow are proven end to end.
</p>
</div>

Expand All @@ -90,8 +91,8 @@ export default function Landing() {
Just this browser
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
No account needed. Files saved in your browser with IndexedDB.
Connect storage later.
No account needed. Notes are saved locally in this browser with
IndexedDB while remote sync is still in progress.
</p>
</div>
</div>
Expand All @@ -111,8 +112,8 @@ export default function Landing() {
You own your data
</h4>
<p className="text-sm leading-relaxed text-muted-foreground">
Files live in your storage, not ours. We never see what you
write.
In the current beta, notes live in your browser storage. Keep
your own backup for anything important.
</p>
</div>
</div>
Expand All @@ -126,8 +127,8 @@ export default function Landing() {
Plain markdown
</h4>
<p className="text-sm leading-relaxed text-muted-foreground">
Every file is a .md file. No proprietary format. Export
anytime.
The editor works with markdown content and is designed around
portable notes rather than a proprietary document model.
</p>
</div>
</div>
Expand All @@ -141,7 +142,8 @@ export default function Landing() {
Works offline
</h4>
<p className="text-sm leading-relaxed text-muted-foreground">
Edit without internet. Changes sync when you are back online.
The local browser vault can be edited without an OpenNotes
account or server. Remote sync is not production-ready yet.
</p>
</div>
</div>
Expand All @@ -156,9 +158,9 @@ export default function Landing() {
OpenNotes — open source, Apache 2.0
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<a href="/" className="transition-colors hover:text-foreground">
<Link href="/" className="transition-colors hover:text-foreground">
App
</a>
</Link>
<a
href="https://github.com/opennotes/opennotes"
className="transition-colors hover:text-foreground"
Expand Down
1 change: 0 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default function RootLayout({
<head>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#16a34a" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="OpenNotes" />
</head>
<body className="font-sans antialiased">
Expand Down
14 changes: 9 additions & 5 deletions components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ export function Editor({
}: EditorProps) {
const parentRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
const initialContentRef = useRef(content)
const initialFilePathsRef = useRef(filePaths)
const onChangeRef = useRef(onChange)
const onSaveRef = useRef(onSave)
const onNavigateRef = useRef(onNavigate)

// Keep refs current so the editor's captured callbacks always call latest
onChangeRef.current = onChange
onSaveRef.current = onSave
onNavigateRef.current = onNavigate
useEffect(() => {
onChangeRef.current = onChange
onSaveRef.current = onSave
onNavigateRef.current = onNavigate
}, [onChange, onSave, onNavigate])

useEffect(() => {
if (!parentRef.current || viewRef.current) return
const view = createEditor({
parent: parentRef.current,
initialContent: content,
filePaths,
initialContent: initialContentRef.current,
filePaths: initialFilePathsRef.current,
onChange: (c) => onChangeRef.current(c),
onSave: () => onSaveRef.current(),
onNavigate: (p) => onNavigateRef.current?.(p),
Expand Down
4 changes: 1 addition & 3 deletions components/editor/TiptapEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ export function TiptapEditor({

useEffect(() => {
lastEmittedMarkdownRef.current = content
setSlashMenuProps(null)
setSelectedSlashIndex(0)
}, [docKey])
}, [content, docKey])

const editor = useEditor({
immediatelyRender: false,
Expand Down
9 changes: 6 additions & 3 deletions components/editor/extensions/SlashCommand.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Extension } from "@tiptap/core"
import { Editor, Extension } from "@tiptap/core"
import Suggestion from "@tiptap/suggestion"

export interface SlashCommandItem {
title: string
description: string
icon: string
command: (props: { editor: any; range: { from: number; to: number } }) => void
command: (props: {
editor: Editor
range: { from: number; to: number }
}) => void
}

export const slashCommandItems: SlashCommandItem[] = [
Expand Down Expand Up @@ -103,7 +106,7 @@ export const SlashCommand = Extension.create({
range,
props,
}: {
editor: any
editor: Editor
range: { from: number; to: number }
props: SlashCommandItem
}) => {
Expand Down
Loading