Compare commits

...
27 Commits
Author SHA1 Message Date
github-actions[bot] d8da61c060 chore: bump version to 0.4.3 2026-03-07 19:30:50 +00:00
github-actions[bot] 09571eda59 chore: pin docker-compose to v0.4.2 [skip ci] 2026-03-07 19:12:29 +00:00
github-actions[bot] 97d8f67a8b chore: bump version to 0.4.2 2026-03-07 19:08:57 +00:00
github-actions[bot] a9f7b71a62 chore: pin docker-compose to v0.4.1 [skip ci] 2026-03-07 19:06:10 +00:00
github-actions[bot] a3d3ffa4e8 chore: bump version to 0.4.1 2026-03-07 19:02:42 +00:00
github-actions[bot] 80705cebfd chore: pin docker-compose to v0.4.0 [skip ci] 2026-03-07 18:52:47 +00:00
github-actions[bot] efdbd535f1 chore: bump version to 0.4.0 2026-03-07 18:48:50 +00:00
Frederic CabassutandClaude Opus 4.6 064b2c5209 chore: update favicon assets from RealFaviconGenerator
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:48:13 +01:00
Frederic CabassutandClaude Opus 4.6 1a15cbc401 feat: add marketing landing page for crumbs.bretzel.app
Static landing page with parchment/retro theme matching the app aesthetic.
Includes GitHub Pages deploy workflow, responsive layout, feature grid,
deploy quick-start, and 404 page. Screenshot placeholders ready for
real captures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:48:13 +01:00
Frederic CabassutandClaude Opus 4.6 1cfffa1a6e fix: patch transitive dependency vulnerabilities via pnpm overrides
Override serialize-javascript (>=7.0.3), esbuild (<0.25.0 → 0.25.12),
and cookie (>=0.7.0) to resolve Dependabot alerts #1, #2, #3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:18:52 +01:00
Frederic CabassutandClaude Opus 4.6 36f6f68feb docs: add open source community files and README badges
Add LICENSE, CONTRIBUTING, CHANGELOG, SECURITY, CODE_OF_CONDUCT,
GitHub issue/PR templates, CODEOWNERS. Update package.json with
license/repo/author metadata. Add badges and logo to README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:07:56 +01:00
github-actions[bot] 2465d1b93a chore: pin docker-compose to v0.3.3 [skip ci] 2026-03-07 07:09:56 +00:00
github-actions[bot] b8a87d1626 chore: bump version to 0.3.3 2026-03-07 07:04:44 +00:00
Frederic CabassutandClaude Opus 4.6 b7b9bca29a fix: update docker-compose only after Docker image is pushed
Move docker-compose.yml update from release.sh (pre-build) to a
post-release job in both bump.yml and release.yml. This ensures the
image tag in docker-compose only appears on main after the image
exists on ghcr.io, preventing Dokploy from trying to pull a
non-existent image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:04:18 +01:00
github-actions[bot] bd5819da61 chore: bump version to 0.3.2 2026-03-07 06:56:57 +00:00
Frederic CabassutandClaude Opus 4.6 96e0212d55 fix: include tags in sync pull response and update API spec
- GET /api/sync now attaches tags to notes (matching GET /api/notes
  and GET /api/search behavior)
- Add trashedAt field to Note schema in OpenAPI spec
- Add optional id field to NoteInput schema
- Add 400 error response for POST /api/sync
- Add response schema for GET attachments list
- Regenerate API.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:52:36 +01:00
Frederic CabassutandClaude Opus 4.6 5cccaa911a docs: update FEATURES.md, DEPLOYMENT.md, and CLAUDE.md
- FEATURES.md: add rich text editor section (Tiptap, formatting toolbar,
  raw markdown mode) and expand text formatting list
- DEPLOYMENT.md: fix docker-compose example to use image from ghcr.io
  instead of local build
- CLAUDE.md: add documentation maintenance instructions, update CI/CD
  section to reflect consolidated bump.yml workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:50:10 +01:00
github-actions[bot] 7d2c545ac5 chore: bump version to 0.3.1 2026-03-07 06:45:06 +00:00
Frederic CabassutandClaude Opus 4.6 ba4f37d413 fix: consolidate release pipeline into bump workflow
- Update release.sh to also bump docker-compose.yml image tag in the
  same commit, keeping the tagged commit in sync
- Add CI and Docker release jobs to bump.yml so it's self-contained
  (GITHUB_TOKEN pushes don't trigger other workflows)
- Remove "Pin docker-compose" post-step from release.yml since it's
  now handled by release.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:44:15 +01:00
github-actions[bot] 6f7560abd1 chore: bump version to 0.3.0 2026-03-07 06:08:01 +00:00
Frederic CabassutandClaude Opus 4.6 5e46545dc6 feat: replace pretzel emoji with favicon across the app
Use the new favicon SVG in the header, login, setup, empty state,
and error pages. Keep the pretzel emoji in the footer tagline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
Frederic CabassutandClaude Opus 4.6 f459f39ae6 feat: add proper favicon set from RealFaviconGenerator
Replace inline emoji favicon with full cross-platform favicon support
including SVG, PNG, ICO, Apple touch icon, and PWA manifest icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
Frederic CabassutandClaude Opus 4.6 b71bdf63e6 rename "Horizontal rule" to "Divider" in formatting toolbar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
github-actions[bot] 14f7c1deaa chore: bump version to 0.2.3 2026-03-07 05:32:22 +00:00
Frederic CabassutandClaude Opus 4.6 56b0510789 fix: update docker-compose image tag after CI builds the image
Move docker-compose version pinning from release script to the release
workflow so it runs after the image is pushed to GHCR. Dokploy will
always find the referenced image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:48:28 +01:00
Frederic Cabassut bd9762600e chore: bump version to 0.2.2 2026-03-06 23:46:32 +01:00
Frederic CabassutandClaude Opus 4.6 f1ee1744e7 chore: pin docker-compose image to version tag on release
Release script now updates docker-compose.yml with the exact version
tag so Dokploy always pulls the correct image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:45:47 +01:00
43 changed files with 1894 additions and 582 deletions
+2
View File
@@ -0,0 +1,2 @@
# Default owner for everything in the repo
* @fredicious
+40
View File
@@ -0,0 +1,40 @@
---
name: Bug Report
about: Something isn't working as expected
title: "bug: "
labels: bug
assignees: ""
---
## Description
A clear summary of the bug.
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
What should happen.
## Actual Behavior
What happens instead.
## Environment
- **Crumbs version**: (e.g. 0.3.2)
- **Browser**: (e.g. Chrome 120, Firefox 121)
- **OS**: (e.g. macOS 15, Ubuntu 24.04)
- **Deployment**: (Docker / Node.js / dev server)
## Screenshots
If applicable, add screenshots to help explain the problem.
## Additional Context
Any other relevant information (logs, related issues, etc.).
+27
View File
@@ -0,0 +1,27 @@
---
name: Feature Request
about: Suggest an idea or improvement
title: "feat: "
labels: enhancement
assignees: ""
---
## Description
A clear description of the feature you'd like to see.
## Motivation
Why is this feature important? What problem does it solve?
## Proposed Solution
How do you think this should work?
## Alternatives Considered
Have you considered other approaches? If so, why were they ruled out?
## Additional Context
Any mockups, links, or references that help illustrate the request.
+18
View File
@@ -0,0 +1,18 @@
## Summary
What does this PR do and why?
## Test Plan
- [ ] Unit tests added/updated
- [ ] E2E tests added/updated (if user-facing)
- [ ] `make check` passes
- [ ] `make test` passes
## Related Issues
Closes #
## Screenshots
If this changes the UI, include before/after screenshots.
+86 -1
View File
@@ -18,6 +18,8 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.release-tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
@@ -34,4 +36,87 @@ jobs:
node-version: 22
- name: Run release script
run: bash scripts/release.sh ${{ github.event.inputs.bump }}
env:
BUMP_TYPE: ${{ github.event.inputs.bump }}
run: bash scripts/release.sh "$BUMP_TYPE"
- name: Output new tag
id: release-tag
run: echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
ci:
needs: bump
uses: ./.github/workflows/ci.yml
release:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: [bump, ci]
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.bump.outputs.tag }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},value=${{ needs.bump.outputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.bump.outputs.tag }}
type=raw,value=latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump.outputs.tag }}
generate_release_notes: true
pin-docker-compose:
name: Pin docker-compose to released version
runs-on: ubuntu-latest
needs: [bump, release]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update docker-compose.yml
env:
TAG: ${{ needs.bump.outputs.tag }}
run: |
VERSION="${TAG#v}"
sed -i "s|image: ghcr.io/bretzel-app/crumbs:.*|image: ghcr.io/bretzel-app/crumbs:${VERSION}|" docker-compose.yml
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docker-compose.yml
git diff --cached --quiet && exit 0
git commit -m "chore: pin docker-compose to ${TAG} [skip ci]"
git push origin main
+25
View File
@@ -54,3 +54,28 @@ jobs:
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
pin-docker-compose:
name: Pin docker-compose to released version
runs-on: ubuntu-latest
needs: release
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update docker-compose.yml
env:
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
sed -i "s|image: ghcr.io/bretzel-app/crumbs:.*|image: ghcr.io/bretzel-app/crumbs:${VERSION}|" docker-compose.yml
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docker-compose.yml
git diff --cached --quiet && exit 0
git commit -m "chore: pin docker-compose to ${TAG} [skip ci]"
git push origin main
+36
View File
@@ -0,0 +1,36 @@
name: Deploy Website
on:
push:
branches: [main]
paths: ['website/**']
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: website
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+42
View File
@@ -0,0 +1,42 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.2] - 2026-03-07
### Fixed
- Update docker-compose only after Docker image is pushed
## [0.3.1] - 2026-03-07
### Fixed
- Include tags in sync pull response and update API spec
### Changed
- Update FEATURES.md, DEPLOYMENT.md, and CLAUDE.md documentation
## [0.3.0] - 2026-03-06
### Added
- Tag management system with auto-extraction from note content
- Full-text search across titles, content, and tags
- Dark mode with system preference detection
- Note pinning, archive, and trash workflows
- Image attachments with drag-and-drop upload
- 12 color themes for note cards
- PWA support with offline-first IndexedDB sync
- LWW CRDT-based conflict resolution
- Docker deployment with multi-stage build
- CI/CD pipeline with GitHub Actions
- Comprehensive E2E test suite with Playwright
- OpenAPI spec and auto-generated API docs
### Changed
- Initial public release
[0.3.2]: https://github.com/bretzel-app/crumbs/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/bretzel-app/crumbs/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/bretzel-app/crumbs/releases/tag/v0.3.0
+17 -2
View File
@@ -135,10 +135,14 @@ GitHub Actions with two workflows:
4. **E2E Tests** — Playwright with Chromium (depends on step 3, uploads report on failure)
5. **Docker Build** — validates the Docker image builds (depends on step 3)
**Release** (`.github/workflows/release.yml`) — runs on `v*` tags:
- Runs full CI pipeline, then builds + pushes Docker image to `ghcr.io` (GitHub Container Registry)
**Bump & Release** (`.github/workflows/bump.yml`) — workflow_dispatch (primary release method):
- Runs `scripts/release.sh` to bump version in `package.json` + `docker-compose.yml`, tag, and push
- Runs full CI pipeline, then builds + pushes Docker image to `ghcr.io`
- Creates a GitHub Release with auto-generated notes
**Release** (`.github/workflows/release.yml`) — runs on `v*` tags (fallback for manual/local tag pushes):
- Same as above: CI pipeline, Docker build/push, GitHub Release
**Docker**: Multi-stage Dockerfile (node:22-slim), exposes port 3000, persists data to `/data` volume.
## Testing philosophy: BDD/TDD
@@ -188,3 +192,14 @@ E2E tests follow Cucumber BDD conventions via comments. Follow the [Better Gherk
```
**Resilience test** — ask: *"Will this comment need to change if the UI implementation changes?"* If yes, rewrite it to remove implementation details. The Playwright code underneath handles the *how*; the comments describe the *what*.
## Documentation
Keep docs in `docs/` up to date when making changes. If a feature, API endpoint, deployment config, or architectural decision changes, update the relevant doc file in the same commit or PR:
| Doc | Covers |
|-----|--------|
| `docs/FEATURES.md` | User-facing feature list |
| `docs/ARCHITECTURE.md` | System design, data flow, schema, tech stack |
| `docs/DEPLOYMENT.md` | Docker, env vars, reverse proxy, backups |
| `docs/openapi.yaml` | API spec (run `pnpm docs:api` to regenerate `docs/API.md`) |
+31
View File
@@ -0,0 +1,31 @@
# Code of Conduct
## Our Commitment
Crumbs is a community project, and we want everyone to feel welcome regardless of background, identity, or experience level. We are committed to providing a harassment-free experience for all participants.
## Expected Behavior
- Be respectful and considerate in communication
- Give and accept constructive feedback gracefully
- Focus on what is best for the project and community
- Show empathy towards others
## Unacceptable Behavior
- Harassment, intimidation, or discrimination of any kind
- Personal attacks, insults, or derogatory language
- Publishing others' private information without consent
- Any conduct that would be considered inappropriate in a professional setting
## Scope
This code of conduct applies to all project spaces — issues, pull requests, discussions, and any other communication channel associated with Crumbs.
## Enforcement
Instances of unacceptable behavior may be reported to the maintainers at **security@bretzel.app**. All reports will be reviewed and handled confidentially. Maintainers reserve the right to remove, edit, or reject contributions that violate this code of conduct, and to temporarily or permanently ban contributors for repeated or severe violations.
## Attribution
This code of conduct is inspired by the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1.
+88
View File
@@ -0,0 +1,88 @@
# Contributing to Crumbs
Thanks for your interest in contributing! This guide will help you get started.
## Development Setup
```bash
# Clone the repo
git clone https://github.com/bretzel-app/crumbs.git
cd crumbs
# Install dependencies
pnpm install
# Start the dev server
pnpm dev
```
Open http://localhost:5173 and set your password on first visit.
Run `make help` to see all available commands.
## Project Structure
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full system design. Key directories:
- `src/routes/` — SvelteKit pages and API endpoints
- `src/lib/components/` — Svelte 5 components (runes, not stores)
- `src/lib/server/db/` — Drizzle ORM schema and database connection
- `src/lib/sync/` — Offline sync and CRDT logic
- `tests/unit/` — Vitest unit tests
- `tests/e2e/` — Playwright end-to-end tests
## Making Changes
1. **Fork and branch** — Create a feature branch from `main`
2. **Follow existing patterns** — Match the code style you see in the codebase
3. **Write tests** — Add unit tests for logic, E2E tests for user-facing features
4. **Run checks before pushing:**
```bash
make check # Type checking
make lint # Linting
make test-unit # Unit tests
make test-e2e # E2E tests
```
5. **Open a PR** — Target `main`, describe what and why
## Commit Messages
We use conventional-style commit messages:
```
feat: add tag autocomplete
fix: prevent duplicate notes on sync
docs: update deployment guide
chore: bump dependencies
```
Keep the subject line under 70 characters. Use the body for context when needed.
## Code Style
- **Svelte 5 runes** — Use `$state`, `$derived`, `$props`, `$effect` (not Svelte 4 stores)
- **TypeScript** — Strict mode, no `any` unless absolutely necessary
- **Tailwind CSS v4** — Use CSS variables from `src/app.css`, never hardcoded Tailwind colors
- **Design system** — Follow the Retro Parchment aesthetic described in `CLAUDE.md`
## Testing
- **Unit tests**: Pure logic (parsing, auth, sync). Run with `make test-unit`
- **E2E tests**: User workflows in the browser. Run with `make test-e2e`
- E2E tests use Gherkin-style Given/When/Then comments (see `CLAUDE.md` for conventions)
## Reporting Bugs
Use [GitHub Issues](https://github.com/bretzel-app/crumbs/issues) with the bug report template. Include:
- Steps to reproduce
- Expected vs actual behavior
- Browser/OS and Crumbs version
## Security Issues
**Do not open public issues for security vulnerabilities.** See [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
## License
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-present Bretzel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14 -2
View File
@@ -1,4 +1,13 @@
# 🥨 Crumbs by Bretzel
[![CI](https://github.com/bretzel-app/crumbs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bretzel-app/crumbs/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/github/v/release/bretzel-app/crumbs)](https://github.com/bretzel-app/crumbs/releases)
[![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://github.com/bretzel-app/crumbs/pkgs/container/crumbs)
<p align="center">
<img src="static/favicon-96x96.png" alt="Crumbs logo" width="96" height="96">
</p>
# Crumbs by Bretzel
A self-hostable, offline-first note-taking app inspired by Google Keep. Part of the [Bretzel](https://bretzel.app) app universe.
@@ -62,6 +71,9 @@ See [docs/FEATURES.md](docs/FEATURES.md) for detailed feature documentation.
| [Architecture](docs/ARCHITECTURE.md) | Local-first sync design, tech rationale, DB schema |
| [Deployment](docs/DEPLOYMENT.md) | Docker, Node.js, reverse proxy, backups, env vars |
| [API](docs/API.md) | REST API reference (auto-generated) |
| [Contributing](CONTRIBUTING.md) | How to contribute |
| [Security](SECURITY.md) | Vulnerability reporting policy |
| [Changelog](CHANGELOG.md) | Release history |
## CI/CD
@@ -72,4 +84,4 @@ Configure registry via GitHub Secrets: `REGISTRY_URL`, `REGISTRY_USER`, `REGISTR
## License
MIT
[MIT](LICENSE)
+44
View File
@@ -0,0 +1,44 @@
# Security Policy
## Supported Versions
| Version | Supported |
|---------|--------------------|
| latest | Yes |
| < latest | No |
We recommend always running the latest version of Crumbs.
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them via [GitHub's private vulnerability reporting](https://github.com/bretzel-app/crumbs/security/advisories/new).
Include as much of the following as possible:
- Description of the vulnerability
- Steps to reproduce
- Impact assessment
- Suggested fix (if any)
You should receive an initial response within 48 hours. We will work with you to understand the issue and coordinate a fix before any public disclosure.
## Scope
Since Crumbs is a self-hosted application, the following are in scope:
- Authentication bypass or session hijacking
- SQL injection or data leaks
- Cross-site scripting (XSS)
- Server-side request forgery (SSRF)
- Path traversal or file access vulnerabilities
- Denial of service via crafted input
## Disclosure Policy
We follow coordinated disclosure. Once a fix is available, we will:
1. Release a patched version
2. Publish a security advisory on GitHub
3. Credit the reporter (unless anonymity is requested)
+1 -1
View File
@@ -1,6 +1,6 @@
services:
crumbs:
image: ghcr.io/bretzel-app/crumbs:latest
image: ghcr.io/bretzel-app/crumbs:0.4.2
container_name: crumbs
ports:
- "3000:3000"
+24
View File
@@ -103,6 +103,7 @@ List notes
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -125,6 +126,7 @@ Create a note
```json
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -147,6 +149,7 @@ Create a note
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -181,6 +184,7 @@ Get a note
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -212,6 +216,7 @@ Partial update — only include changed fields.
```json
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -234,6 +239,7 @@ Partial update — only include changed fields.
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -288,6 +294,19 @@ Without `attachmentId`, lists all attachments. With `attachmentId`, returns the
**Response:** `200`
```json
[
{
"id": "id",
"noteId": "noteId",
"filename": "filename",
"mimeType": "mimeType",
"size": 1,
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
```
---
### `POST /api/notes/{id}/attachments`
@@ -366,6 +385,7 @@ Searches across note titles, content, and tags. Excludes trashed notes.
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -423,6 +443,7 @@ Pull changes since timestamp
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -451,6 +472,7 @@ Push local changes
"operation": "create",
"timestamp": 1,
"data": {
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -473,4 +495,6 @@ Push local changes
}
```
**Errors:** `400` Invalid changes payload
---
+1 -1
View File
@@ -16,7 +16,7 @@ Open http://localhost:3000 and set your password.
# docker-compose.yml
services:
crumbs:
build: .
image: ghcr.io/bretzel-app/crumbs:latest
ports:
- "8080:3000" # Change external port
volumes:
+15 -4
View File
@@ -8,16 +8,27 @@
- Content supports full Markdown syntax
- Auto-save on editor close
### Markdown Support
- **Bold** (`**text**`)
- *Italic* (`*text*`)
### Rich Text Editor
- WYSIWYG editing powered by Tiptap
- Formatting toolbar with visual controls
- Raw Markdown mode toggle for direct editing
- Undo/Redo with keyboard shortcuts
### Text Formatting
- **Bold** (`**text**` / Ctrl+B)
- *Italic* (`*text*` / Ctrl+I)
- Underline (Ctrl+U)
- ~~Strikethrough~~ (`~~text~~` / Ctrl+Shift+X)
- Headings (`# H1` through `###### H6`)
- Blockquotes (`> quote`)
- Unordered lists (`- item`)
- Ordered lists (`1. item`)
- Code blocks (triple backtick)
- Inline code (single backtick)
- Tables
- Links (auto-linkified URLs)
- Links — insert, edit, and remove with URL input
- Dividers (horizontal rules)
- Text alignment (left, center, right, justify)
- Line breaks
### Checklists / Task Lists
+17 -1
View File
@@ -187,7 +187,13 @@ paths:
type: string
responses:
'200':
description: Attachment list or file content
description: Attachment list (without attachmentId) or file content (with attachmentId)
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Attachment'
post:
summary: Upload an image attachment
tags: [Attachments]
@@ -306,6 +312,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Success'
'400':
description: Invalid changes payload
components:
securitySchemes:
@@ -340,6 +348,10 @@ components:
type: boolean
trashed:
type: boolean
trashedAt:
type: string
format: date-time
nullable: true
checklistMode:
type: boolean
sortOrder:
@@ -360,6 +372,10 @@ components:
NoteInput:
type: object
properties:
id:
type: string
format: uuid
description: Optional — generated if omitted
title:
type: string
content:
+41 -29
View File
@@ -1,8 +1,15 @@
{
"name": "crumbs",
"private": true,
"version": "0.2.1",
"version": "0.4.3",
"type": "module",
"license": "MIT",
"author": "Bretzel (https://bretzel.app)",
"homepage": "https://crumbs.bretzel.app",
"repository": {
"type": "git",
"url": "https://github.com/bretzel-app/crumbs.git"
},
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -22,43 +29,43 @@
"docs:api": "tsx scripts/generate-api-docs.ts"
},
"dependencies": {
"@sveltejs/adapter-node": "^5.0.0",
"@tiptap/core": "^3.20.0",
"@tiptap/extension-link": "^3.20.0",
"@tiptap/extension-placeholder": "^3.20.0",
"@tiptap/extension-task-item": "^3.20.0",
"@tiptap/extension-task-list": "^3.20.0",
"@tiptap/extension-text-align": "^3.20.0",
"@tiptap/extension-underline": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"argon2": "^0.43.0",
"better-sqlite3": "^11.0.0",
"drizzle-orm": "^0.44.0",
"idb": "^8.0.0",
"@sveltejs/adapter-node": "^5.5.4",
"@tiptap/core": "^3.20.1",
"@tiptap/extension-link": "^3.20.1",
"@tiptap/extension-placeholder": "^3.20.1",
"@tiptap/extension-task-item": "^3.20.1",
"@tiptap/extension-task-list": "^3.20.1",
"@tiptap/extension-text-align": "^3.20.1",
"@tiptap/extension-underline": "^3.20.1",
"@tiptap/pm": "^3.20.1",
"@tiptap/starter-kit": "^3.20.1",
"argon2": "^0.43.1",
"better-sqlite3": "^11.10.0",
"drizzle-orm": "^0.44.7",
"idb": "^8.0.3",
"lucide-svelte": "^0.577.0",
"markdown-it": "^14.0.0",
"markdown-it": "^14.1.1",
"tiptap-markdown": "^0.9.0",
"uuid": "^11.0.0"
"uuid": "^11.1.0"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@sveltejs/kit": "^2.50.2",
"@playwright/test": "^1.58.2",
"@sveltejs/kit": "^2.53.4",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.0.0",
"@types/better-sqlite3": "^7.6.0",
"@types/markdown-it": "^14.0.0",
"@tailwindcss/vite": "^4.2.1",
"@types/better-sqlite3": "^7.6.13",
"@types/markdown-it": "^14.1.2",
"@types/uuid": "^10.0.0",
"@vite-pwa/sveltekit": "^1.0.0",
"drizzle-kit": "^0.31.0",
"svelte": "^5.51.0",
"svelte-check": "^4.4.2",
"tailwindcss": "^4.0.0",
"@vite-pwa/sveltekit": "^1.1.0",
"drizzle-kit": "^0.31.9",
"svelte": "^5.53.7",
"svelte-check": "^4.4.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^3.0.0",
"vitest": "^3.2.4",
"yaml": "^2.8.2"
},
"pnpm": {
@@ -66,6 +73,11 @@
"argon2",
"better-sqlite3",
"esbuild"
]
],
"overrides": {
"serialize-javascript": ">=7.0.3",
"esbuild@<0.25.0": "0.25.12",
"cookie": ">=0.7.0"
}
}
}
+300 -531
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -6,10 +6,12 @@
<meta name="theme-color" content="#f0e6d3" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥨</text></svg>"
/>
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Crumbs" />
<link rel="manifest" href="/site.webmanifest" />
<title>Crumbs</title>
%sveltekit.head%
</head>
+1 -1
View File
@@ -344,7 +344,7 @@
data-testid="format-hr"
>
<Minus size={iconSize} />
<span>Horizontal rule</span>
<span>Divider</span>
</button>
<div class="my-1 h-px bg-[var(--border-subtle)]"></div>
<button
+1
View File
@@ -28,6 +28,7 @@
</button>
<div class="flex items-center gap-2">
<img src="/favicon.svg" alt="" class="h-8 w-8" />
<h1 class="font-['Press_Start_2P'] text-lg text-[var(--primary)]">Crumbs</h1>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@
{#if $notesLoaded && $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
<div class="flex flex-col items-center justify-center py-20 text-[var(--text-muted)]">
<span class="mb-4 text-7xl">🥨</span>
<img src="/favicon.svg" alt="" class="mb-4 h-18 w-18" />
<p class="font-['Press_Start_2P'] text-xs">
{#if $currentFilter === 'trashed'}
No crumbs in trash
+1 -1
View File
@@ -35,7 +35,7 @@
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Enter your password to continue</p>
</div>
+1 -1
View File
@@ -47,7 +47,7 @@
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Welcome to Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Set up your password to get started</p>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="flex min-h-screen items-center justify-center bg-[var(--bg-base)]">
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="mt-4 font-['Press_Start_2P'] text-xl text-[var(--primary)]">
{page.status}
</h1>
+9 -1
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { processSyncPush, getChangesSince } from '$lib/sync/server.js';
import { fetchTagsForNotes } from '$lib/server/tags.js';
export const POST: RequestHandler = async ({ request }) => {
const { changes } = await request.json();
@@ -16,5 +17,12 @@ export const POST: RequestHandler = async ({ request }) => {
export const GET: RequestHandler = async ({ url }) => {
const since = parseInt(url.searchParams.get('since') || '0', 10);
const changes = await getChangesSince(since);
return json(changes);
const tagMap = fetchTagsForNotes(changes.map((n) => n.id));
const changesWithTags = changes.map((note) => ({
...note,
tags: tagMap.get(note.id) ?? []
}));
return json(changesWithTags);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "Crumbs by Bretzel",
"short_name": "Crumbs",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#f9f4ea",
"background_color": "#f9f4ea",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page not found | Crumbs</title>
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon-96x96.png">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<link rel="stylesheet" href="styles.css">
<style>
.error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
text-align: center;
padding: 2rem;
}
.error-code {
font-size: clamp(3rem, 10vw, 6rem);
color: var(--primary);
margin-bottom: 1rem;
}
.error-message {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 2rem;
max-width: 24rem;
}
</style>
</head>
<body>
<div class="error-page">
<h1 class="error-code font-pixel">404</h1>
<p class="error-message">This note seems to have been deleted. Or maybe it never existed.</p>
<a href="/" class="btn btn-primary">Back to home</a>
</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
crumbs.bretzel.app
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crumbs - Your notes. Your server. Your rules.</title>
<meta name="description" content="Crumbs is a self-hostable, offline-first note-taking app. A privacy-respecting Google Keep alternative you can run on your own server with a single Docker command.">
<meta property="og:title" content="Crumbs by Bretzel">
<meta property="og:description" content="Self-hostable, offline-first note-taking. A Google Keep alternative that respects your privacy.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://crumbs.bretzel.app">
<meta property="og:image" content="https://crumbs.bretzel.app/assets/og-image.png">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon-96x96.png">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- ── Header ── -->
<header class="site-header">
<div class="container">
<a href="/" class="logo">
<img src="assets/favicon-96x96.png" alt="Crumbs logo" width="32" height="32">
<span>Crumbs</span>
</a>
<button class="nav-toggle" aria-label="Toggle navigation" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<nav>
<ul class="nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#screenshots">Screenshots</a></li>
<li><a href="#deploy">Deploy</a></li>
<li>
<a href="https://github.com/bretzel-app/crumbs" class="github-link" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
GitHub
</a>
</li>
</ul>
</nav>
</div>
</header>
<!-- ── Hero ── -->
<div class="hero-wrapper">
<!-- Decorative floating note cards -->
<div class="deco-card deco-card-1">Buy groceries<br>- Milk<br>- Eggs</div>
<div class="deco-card deco-card-2">Meeting notes<br>Q3 planning...</div>
<div class="deco-card deco-card-3">Project ideas<br>#startup #saas</div>
<div class="deco-card deco-card-4">Remember to<br>call the vet</div>
<section class="hero">
<div class="container">
<h1 class="animate-in">
Your notes. Your server.<br><span>Your rules.</span>
</h1>
<p class="hero-subtitle animate-in delay-1">
A self-hostable, offline-first note-taking app. The Google Keep alternative that respects your privacy.
</p>
<div class="hero-buttons animate-in delay-2">
<a href="#deploy" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
Get Started
</a>
<a href="https://github.com/bretzel-app/crumbs" class="btn btn-secondary" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
View on GitHub
</a>
</div>
<div class="hero-screenshot animate-in delay-3">
<div class="screenshot-placeholder"></div>
</div>
</div>
</section>
</div>
<!-- ── Features ── -->
<section class="features" id="features">
<div class="container">
<h2 class="animate-in">Everything you need,<br>nothing you don't</h2>
<div class="features-grid">
<!-- Rich Markdown Editor -->
<div class="feature-card animate-in delay-1">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</div>
<h3>Rich Editor</h3>
<p>Tiptap-powered WYSIWYG with full Markdown support. Bold, lists, code blocks, and more.</p>
</div>
<!-- Offline-first PWA -->
<div class="feature-card animate-in delay-2">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
</div>
<h3>Offline-First</h3>
<p>Works without internet. PWA with CRDT-based sync means your notes are always available.</p>
</div>
<!-- Self-hostable -->
<div class="feature-card animate-in delay-3">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
</div>
<h3>Self-Hostable</h3>
<p>One Docker command. Your data stays on your server. No cloud, no tracking, no nonsense.</p>
</div>
<!-- Tags, colors, pins -->
<div class="feature-card animate-in delay-4">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
</div>
<h3>Tags & Colors</h3>
<p>Organize with tags, 12 note colors, and pinned notes. Your notes, your way.</p>
</div>
<!-- Checklists -->
<div class="feature-card animate-in delay-5">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
</div>
<h3>Checklists</h3>
<p>Built-in task lists with checkbox support. Track to-dos right inside your notes.</p>
</div>
<!-- Full-text search -->
<div class="feature-card animate-in delay-6">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<h3>Full-Text Search</h3>
<p>Find anything instantly. SQLite-powered search across all your notes and tags.</p>
</div>
</div>
</div>
</section>
<!-- ── Screenshots ── -->
<section class="screenshots" id="screenshots">
<div class="container">
<h2 class="animate-in">See it in action</h2>
<div class="screenshots-grid">
<div class="screenshot-card animate-in delay-1">
<div class="screenshot-placeholder"></div>
<div class="screenshot-label">Notes grid view</div>
</div>
<div class="screenshot-card animate-in delay-2">
<div class="screenshot-placeholder"></div>
<div class="screenshot-label">Rich text editor</div>
</div>
<div class="screenshot-card animate-in delay-3">
<div class="screenshot-placeholder"></div>
<div class="screenshot-label">Mobile view</div>
</div>
</div>
</div>
</section>
<!-- ── Deploy ── -->
<section class="deploy" id="deploy">
<div class="container">
<h2 class="animate-in">Deploy in seconds</h2>
<p class="deploy-subtitle animate-in delay-1">One file. One command. Done.</p>
<div class="code-block animate-in delay-2">
<div class="code-header">
<span>docker-compose.yml</span>
<button class="copy-btn" onclick="copyCode()">copy</button>
</div>
<pre><code><span class="code-key">services</span>:
<span class="code-key">crumbs</span>:
<span class="code-key">image</span>: <span class="code-string">ghcr.io/bretzel-app/crumbs:latest</span>
<span class="code-key">ports</span>:
- <span class="code-string">"3000:3000"</span>
<span class="code-key">volumes</span>:
- <span class="code-string">crumbs_data:/data</span>
<span class="code-key">volumes</span>:
<span class="code-key">crumbs_data</span>:</code></pre>
</div>
<div class="deploy-thats-it animate-in delay-3">
<p class="font-pixel">That's it.</p>
<p>Open <code>localhost:3000</code>, set a password, and start writing.</p>
</div>
<div class="deploy-cta animate-in delay-4">
<a href="https://github.com/bretzel-app/crumbs/blob/main/docs/DEPLOYMENT.md" class="btn btn-secondary" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
Full deployment docs
</a>
</div>
</div>
</section>
<!-- ── Footer ── -->
<footer class="site-footer">
<div class="container">
<p class="footer-brand font-pixel">Part of the Bretzel app universe</p>
<ul class="footer-links">
<li><a href="https://github.com/bretzel-app/crumbs" target="_blank" rel="noopener">GitHub</a></li>
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/docs/DEPLOYMENT.md" target="_blank" rel="noopener">Docs</a></li>
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/LICENSE" target="_blank" rel="noopener">License</a></li>
</ul>
<span class="footer-version">v0.3.3</span>
</div>
</footer>
<script>
// Mobile nav toggle
const toggle = document.querySelector('.nav-toggle');
const navLinks = document.querySelector('.nav-links');
toggle.addEventListener('click', () => {
const open = navLinks.classList.toggle('open');
toggle.setAttribute('aria-expanded', open);
});
// Close nav on link click (mobile)
navLinks.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
navLinks.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
});
});
// Copy docker-compose snippet
function copyCode() {
const code = `services:
crumbs:
image: ghcr.io/bretzel-app/crumbs:latest
ports:
- "3000:3000"
volumes:
- crumbs_data:/data
volumes:
crumbs_data:`;
navigator.clipboard.writeText(code).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'copied!';
setTimeout(() => { btn.textContent = 'copy'; }, 2000);
});
}
// Intersection Observer for scroll animations
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
// Only observe elements that aren't in the hero (hero animates on load)
document.querySelectorAll('.features .animate-in, .screenshots .animate-in, .deploy .animate-in').forEach(el => {
el.style.opacity = '0';
el.classList.remove('animate-in');
observer.observe(el);
});
</script>
</body>
</html>
+654
View File
@@ -0,0 +1,654 @@
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap');
/* ── Design tokens (ported from app) ── */
:root {
--bg-base: #f0e6d3;
--bg-surface: #faf5eb;
--primary: #C8860A;
--primary-hover: #E8A020;
--text: #1a1a2e;
--text-muted: #6b6272;
--border: #1a1a2e;
--border-subtle: #d4cabb;
--destructive: #a63d2f;
--card-shadow: 2px 2px 0px var(--border-subtle);
--card-shadow-hover: 3px 3px 0px var(--primary);
/* Note colors for decorative elements */
--coral: #faafa8;
--peach: #f39f76;
--sand: #fff8b8;
--mint: #e2f6d3;
--sage: #b4ddd3;
--fog: #d4e4ed;
--storm: #aeccdc;
--dusk: #d3bfdb;
--blossom: #f6e2dd;
--clay: #e9e3d4;
--chalk: #efeff1;
}
/* ── Reset ── */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ── Base ── */
html {
scroll-behavior: smooth;
scroll-padding-top: 5rem;
}
body {
font-family: 'JetBrains Mono', monospace;
color: var(--text);
background-color: var(--bg-base);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Pixel grid overlay */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.03;
background-image: repeating-conic-gradient(#1a1a2e 0% 25%, transparent 0% 50%);
background-size: 4px 4px;
z-index: 0;
}
body > * {
position: relative;
z-index: 1;
}
a {
color: var(--primary);
text-decoration: none;
transition: color 150ms ease-out;
}
a:hover {
color: var(--primary-hover);
}
img {
max-width: 100%;
height: auto;
display: block;
}
/* ── Typography ── */
.font-pixel {
font-family: 'Press Start 2P', cursive;
}
h1, h2, h3 {
font-family: 'Press Start 2P', cursive;
line-height: 1.4;
}
/* ── Layout ── */
.container {
max-width: 72rem;
margin: 0 auto;
padding: 0 1.5rem;
}
section {
padding: 5rem 0;
}
/* ── Header ── */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-base);
border-bottom: 2px solid var(--border-subtle);
padding: 1rem 0;
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text);
font-family: 'Press Start 2P', cursive;
font-size: 1rem;
}
.logo img {
width: 32px;
height: 32px;
image-rendering: pixelated;
}
.nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
list-style: none;
font-size: 0.8rem;
}
.nav-links a {
color: var(--text-muted);
font-weight: 600;
}
.nav-links a:hover {
color: var(--primary);
}
.github-link {
display: flex;
align-items: center;
gap: 0.4rem;
}
.github-link svg {
width: 18px;
height: 18px;
}
/* Mobile nav toggle */
.nav-toggle {
display: none;
background: none;
border: 2px solid var(--border-subtle);
border-radius: 2px;
padding: 0.4rem;
cursor: pointer;
color: var(--text);
}
.nav-toggle svg {
width: 20px;
height: 20px;
display: block;
}
/* ── Hero ── */
.hero {
padding: 6rem 0 4rem;
text-align: center;
}
.hero h1 {
font-size: clamp(1.25rem, 4vw, 2rem);
margin-bottom: 1.5rem;
color: var(--text);
}
.hero h1 span {
color: var(--primary);
}
.hero-subtitle {
font-size: clamp(0.85rem, 2vw, 1rem);
color: var(--text-muted);
max-width: 36rem;
margin: 0 auto 2.5rem;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 3rem;
}
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 2px;
border: 2px solid var(--border);
cursor: pointer;
transition: all 150ms ease-out;
text-decoration: none;
}
.btn svg {
width: 16px;
height: 16px;
}
.btn-primary {
background: var(--primary);
color: var(--bg-surface);
border-color: var(--primary);
box-shadow: var(--card-shadow);
}
.btn-primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
color: var(--bg-surface);
box-shadow: var(--card-shadow-hover);
transform: translate(-1px, -1px);
}
.btn-secondary {
background: var(--bg-surface);
color: var(--text);
box-shadow: var(--card-shadow);
}
.btn-secondary:hover {
border-color: var(--primary);
box-shadow: var(--card-shadow-hover);
transform: translate(-1px, -1px);
}
/* ── Hero screenshot ── */
.hero-screenshot {
max-width: 56rem;
margin: 0 auto;
border: 2px solid var(--border-subtle);
border-radius: 2px;
box-shadow: 4px 4px 0px var(--border-subtle);
overflow: hidden;
background: var(--bg-surface);
}
.screenshot-placeholder {
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--bg-surface), var(--clay));
position: relative;
}
.screenshot-placeholder::after {
content: 'screenshot coming soon';
font-family: 'Press Start 2P', cursive;
font-size: 0.65rem;
color: var(--text-muted);
opacity: 0.5;
}
/* ── Decorative note cards (floating behind hero) ── */
.hero-wrapper {
position: relative;
overflow: hidden;
}
.deco-card {
position: absolute;
border: 2px solid var(--border-subtle);
border-radius: 2px;
box-shadow: var(--card-shadow);
padding: 1rem;
font-size: 0.7rem;
color: var(--text-muted);
opacity: 0.35;
pointer-events: none;
transform: rotate(var(--rotate, 0deg));
width: 10rem;
}
.deco-card-1 {
background: var(--coral);
top: 8%;
left: 3%;
--rotate: -6deg;
}
.deco-card-2 {
background: var(--mint);
top: 20%;
right: 4%;
--rotate: 4deg;
}
.deco-card-3 {
background: var(--sand);
bottom: 15%;
left: 5%;
--rotate: 3deg;
}
.deco-card-4 {
background: var(--fog);
bottom: 10%;
right: 6%;
--rotate: -3deg;
}
/* ── Features grid ── */
.features {
background: var(--bg-surface);
border-top: 2px solid var(--border-subtle);
border-bottom: 2px solid var(--border-subtle);
}
.features h2 {
text-align: center;
font-size: clamp(0.9rem, 2.5vw, 1.25rem);
margin-bottom: 3rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
gap: 1.5rem;
}
.feature-card {
background: var(--bg-base);
border: 2px solid var(--border-subtle);
border-radius: 2px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
transition: all 150ms ease-out;
}
.feature-card:hover {
border-color: var(--primary);
box-shadow: var(--card-shadow-hover);
transform: translate(-1px, -1px);
}
.feature-icon {
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary);
color: var(--bg-surface);
border-radius: 2px;
margin-bottom: 1rem;
box-shadow: 2px 2px 0px var(--border-subtle);
}
.feature-icon svg {
width: 20px;
height: 20px;
}
.feature-card h3 {
font-size: 0.7rem;
margin-bottom: 0.5rem;
}
.feature-card p {
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.5;
}
/* ── Screenshots ── */
.screenshots h2 {
text-align: center;
font-size: clamp(0.9rem, 2.5vw, 1.25rem);
margin-bottom: 3rem;
}
.screenshots-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 1.5rem;
}
.screenshot-card {
border: 2px solid var(--border-subtle);
border-radius: 2px;
box-shadow: var(--card-shadow);
overflow: hidden;
background: var(--bg-surface);
}
.screenshot-card .screenshot-placeholder {
aspect-ratio: 4 / 3;
}
.screenshot-label {
padding: 0.75rem 1rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
border-top: 2px solid var(--border-subtle);
text-align: center;
}
/* ── Deploy / Quick Start ── */
.deploy {
background: var(--bg-surface);
border-top: 2px solid var(--border-subtle);
border-bottom: 2px solid var(--border-subtle);
}
.deploy h2 {
text-align: center;
font-size: clamp(0.9rem, 2.5vw, 1.25rem);
margin-bottom: 1rem;
}
.deploy-subtitle {
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 2.5rem;
}
.code-block {
max-width: 40rem;
margin: 0 auto 2rem;
background: var(--text);
color: var(--bg-surface);
border: 2px solid var(--border);
border-radius: 2px;
box-shadow: 4px 4px 0px var(--border-subtle);
overflow: hidden;
}
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.copy-btn {
background: none;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 2px;
padding: 0.25rem 0.5rem;
color: rgba(255, 255, 255, 0.6);
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
cursor: pointer;
transition: all 150ms ease-out;
}
.copy-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.code-block pre {
padding: 1.25rem;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.6;
}
.code-block code {
font-family: 'JetBrains Mono', monospace;
}
.code-comment {
color: rgba(255, 255, 255, 0.35);
}
.code-key {
color: var(--primary-hover);
}
.code-string {
color: #b4ddd3;
}
.deploy-thats-it {
text-align: center;
margin-bottom: 2rem;
}
.deploy-thats-it .font-pixel {
font-size: 0.8rem;
color: var(--primary);
margin-bottom: 0.5rem;
}
.deploy-thats-it p {
color: var(--text-muted);
font-size: 0.85rem;
}
.deploy-cta {
text-align: center;
}
/* ── Footer ── */
.site-footer {
padding: 3rem 0;
border-top: 2px solid var(--border-subtle);
text-align: center;
}
.footer-brand {
font-size: 0.6rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
list-style: none;
margin-bottom: 1.5rem;
font-size: 0.8rem;
}
.footer-version {
display: inline-block;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
color: var(--text-muted);
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 2px;
padding: 0.2rem 0.6rem;
}
/* ── Animations ── */
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fade-up 400ms ease-out both;
}
.delay-1 { animation-delay: 100ms; }
.delay-2 { animation-delay: 200ms; }
.delay-3 { animation-delay: 300ms; }
.delay-4 { animation-delay: 400ms; }
.delay-5 { animation-delay: 500ms; }
.delay-6 { animation-delay: 600ms; }
/* ── Responsive ── */
@media (max-width: 768px) {
.nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-base);
border-bottom: 2px solid var(--border-subtle);
flex-direction: column;
padding: 1rem 1.5rem;
gap: 1rem;
}
.nav-links.open {
display: flex;
}
.nav-toggle {
display: block;
}
.hero {
padding: 4rem 0 3rem;
}
.deco-card {
display: none;
}
section {
padding: 3.5rem 0;
}
.features-grid {
grid-template-columns: 1fr;
}
.screenshots-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.hero-buttons {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
justify-content: center;
}
.code-block pre {
font-size: 0.7rem;
padding: 1rem;
}
}