Compare commits

..

1 commit

Author SHA1 Message Date
b3787eeb3c Temp build for backlinks 2026-01-06 12:09:37 +00:00
54 changed files with 1027 additions and 848 deletions

23
.gitignore vendored
View file

@ -1,27 +1,6 @@
node_modules
*.svg
frontend/src/assets/fontawesome/svg/*
frontend/src/assets/fontawesome/svg/0.svg
*.db
.zed/settings.json
**.pyc
!xmark.svg
!plus.svg
!circle-check.svg
!rotate.svg
!circle-exclamation.svg
!folder.svg
!tags.svg
!caret-right.svg
!folder-plus.svg
!file-circle-plus.svg
!bold.svg
!italic.svg
!strikethrough.svg
!code.svg
!list-ul.svg
!list-ol.svg
!square-check.svg
!code-simple.svg
!quote-left.svg
!gear.svg
frontend/dist/*

View file

@ -1,32 +0,0 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
env/
*.db
*.sqlite
*.sqlite3
.pytest_cache/
.coverage
htmlcov/
.tox/
.mypy_cache/
.dmypy.json
dmypy.json
.vscode/
.idea/
*.log
.DS_Store
.env
.env.local
*.md
!README.md
.git/
.gitignore
Dockerfile
docker-compose*.yml
compose*.yaml

View file

@ -1,39 +1,10 @@
# ---- Builder stage ----
FROM python:3.12-slim AS builder
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
COPY ./app ./app
# Copy installed packages from builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Copy application code
COPY app ./app
# Copy startup script
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh
# Create data directory for SQLite and other persistent data
RUN mkdir -p /app/data
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Add healthcheck with longer start period for initialization
HEALTHCHECK --interval=10s --timeout=5s --start-period=40s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" || exit 1
EXPOSE 8000
# Run with startup script
CMD ["/app/start.sh"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View file

@ -1,23 +1,10 @@
import os
from dotenv import load_dotenv
from sqlmodel import Session, SQLModel, create_engine # type: ignore
load_dotenv()
# Get database URL from environment, with proper fallback
DATABASE_URL = os.getenv("DATABASE_URL")
DATABASE_URL = "sqlite:///./notes.db"
# If DATABASE_URL is not set or empty, use default SQLite
if not DATABASE_URL or DATABASE_URL.strip() == "":
DATABASE_URL = "sqlite:////app/data/notes.db"
print(f"WARNING: DATABASE_URL not set, using default: {DATABASE_URL}")
else:
print(f"Using DATABASE_URL: {DATABASE_URL}")
# Only use check_same_thread for SQLite
connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args)
engine = create_engine(
DATABASE_URL, echo=True, connect_args={"check_same_thread": False}
)
def create_db_and_tables():

View file

@ -1,5 +1,3 @@
import os
from fastapi import FastAPI # type: ignore
from fastapi.middleware.cors import CORSMiddleware # type:ignore
@ -8,10 +6,10 @@ from app.routes import auth, folders, notes, tags
app = FastAPI(title="Notes API")
cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",")
# CORS - adjust origins for production
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_origins=["http://localhost:5173"], # Vite dev server
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -32,9 +30,3 @@ app.include_router(tags.router, prefix="/api")
@app.get("/")
def root():
return {"message": "Notes API"}
@app.get("/health")
def health():
"""Health check endpoint for Docker and Coolify"""
return {"status": "healthy"}

View file

@ -1,12 +1,13 @@
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
from sqlmodel import Session, SQLModel, select
from app.auth import create_session, hash_password, require_auth, verify_password
from app.database import get_session
from app.models import Session as SessionModel
from app.models import User
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
from sqlmodel import Session, SQLModel, select
router = APIRouter(prefix="/auth", tags=["auth"])
@ -29,8 +30,8 @@ class UserResponse(SQLModel):
id: int
username: str
email: str
salt: str
wrapped_master_key: str
salt: str # Client needs this for key derivation
wrapped_master_key: str # Client needs this to unwrap the master key
@router.post("/register")
@ -71,7 +72,7 @@ def register(
key="session_id",
value=session_id,
httponly=True,
secure=True,
secure=True, # HTTPS only in production
samesite="lax",
max_age=30 * 24 * 60 * 60, # 30 days
)
@ -146,15 +147,15 @@ def list_sessions(
return {"sessions": sessions}
@router.delete("/sessions/{session_token}")
@router.delete("/sessions/{session_token}") # Renamed from session_id
def revoke_session(
session_token: str,
session_token: str, # Renamed to avoid conflict with Cookie parameter
current_user: User = Depends(require_auth),
db: Session = Depends(get_session),
):
session = db.exec(
select(SessionModel)
.where(SessionModel.session_id == session_token)
.where(SessionModel.session_id == session_token) # Use renamed variable
.where(SessionModel.user_id == current_user.id)
).first()

View file

@ -1,21 +0,0 @@
#!/bin/sh
set -e
echo "========================================="
echo "Starting FastNotes API..."
echo "========================================="
echo "DATABASE_URL: ${DATABASE_URL:-'not set'}"
echo "CORS_ORIGINS: ${CORS_ORIGINS:-'not set'}"
echo "SECRET_KEY: ${SECRET_KEY:+'***set***'}"
echo "Working directory: $(pwd)"
echo "Contents of /app:"
ls -la /app
echo "========================================="
# Create data directory if it doesn't exist
mkdir -p /app/data
echo "Created/verified /app/data directory"
# Start uvicorn
echo "Starting uvicorn..."
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips "*"

View file

@ -1,44 +0,0 @@
services:
api:
build:
context: ./backend
container_name: fastnotes-api
environment:
- DATABASE_URL=${DATABASE_URL:-sqlite:////app/data/notes.db}
- SECRET_KEY=${SECRET_KEY:-change-this-in-production}
- CORS_ORIGINS=${CORS_ORIGINS:-*}
# Internal only - accessed via nginx proxy
expose:
- "8000"
volumes:
- api_data:/app/data
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()",
]
interval: 10s
timeout: 5s
start_period: 40s
retries: 3
ui:
build:
context: ./frontend
args:
# Frontend will use /api path (proxied by nginx)
VITE_API_URL: ${VITE_API_URL:-/api}
container_name: fastnotes-ui
# Coolify manages ports via its proxy - expose instead of publish
expose:
- "80"
depends_on:
- api
restart: unless-stopped
volumes:
api_data:

View file

@ -1,27 +0,0 @@
node_modules/
dist/
build/
.git/
.gitignore
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.vscode/
.idea/
coverage/
.nyc_output/
*.md
!README.md
Dockerfile
docker-compose*.yml
compose*.yaml
.eslintcache
.cache
*.tsbuildinfo

View file

@ -1,31 +0,0 @@
# ---------- Builder ----------
FROM node:20-alpine AS builder
WORKDIR /app
# Accept build argument
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci --prefer-offline --no-audit
# Copy source and build
COPY . .
RUN npm run build
# ---------- Runtime ----------
FROM nginx:stable-alpine AS runtime
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View file

@ -1,48 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Static files with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy - routes /api requests to backend service
location /api {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,7 @@
"openapi-typescript": "^7.10.1",
"type-fest": "^5.3.1",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite": "^5.4.21",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.15"
}

View file

@ -3,48 +3,52 @@ import createClient from "openapi-fetch";
import { camelizeKeys, decamelizeKeys } from "humps";
import type { paths } from "@/types/api";
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/";
// Create the base client with full type safety
export const client = createClient<paths>({
baseUrl: API_URL,
credentials: "include",
});
// Add middleware to automatically transform requests and responses
client.use({
async onRequest({ request }) {
const cloned = request.clone();
// Transform request body from camelCase to snake_case
if (request.body) {
try {
const bodyText = await request.text();
if (bodyText) {
const bodyJson = JSON.parse(bodyText);
const transformedBody = decamelizeKeys(bodyJson);
try {
const bodyText = await cloned.text();
if (bodyText) {
const bodyJson = JSON.parse(bodyText);
const transformedBody = decamelizeKeys(bodyJson);
// Preserve headers and ensure Content-Type is set
const headers = new Headers(request.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const headers = new Headers(request.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
return new Request(request.url, {
method: request.method,
headers: headers,
body: JSON.stringify(transformedBody),
credentials: request.credentials,
mode: request.mode,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer,
integrity: request.integrity,
});
}
return new Request(request.url, {
method: request.method,
headers: headers,
body: JSON.stringify(transformedBody),
credentials: request.credentials,
mode: request.mode,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer,
integrity: request.integrity,
});
} catch (e) {
// If not JSON, pass through unchanged
}
} catch (e) {
// If not JSON, pass through unchanged
}
return request;
},
async onResponse({ response }) {
// Transform response body from snake_case to camelCase
if (response.body) {
try {
const clonedResponse = response.clone();
@ -57,6 +61,7 @@ client.use({
headers: response.headers,
});
} catch (e) {
// If not JSON, return original response
return response;
}
}

View file

@ -24,7 +24,7 @@ const getFolderTree = async () => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const { data, error } = await client.GET("/folders/tree", {});
const { data, error } = await client.GET("/api/folders/tree", {});
const newData = data as unknown as FolderTreeResponse;
@ -36,7 +36,7 @@ const getFolderTree = async () => {
const updateFolder = async (id: number, folder: FolderUpdate) => {
console.log(`Updating folder ${id} with:`, folder);
try {
const response = await client.PATCH("/folders/{folder_id}", {
const response = await client.PATCH("/api/folders/{folder_id}", {
params: { path: { folder_id: id } },
body: folder,
});
@ -50,10 +50,11 @@ const updateFolder = async (id: number, folder: FolderUpdate) => {
export const folderApi = {
tree: () => getFolderTree(),
list: () => client.GET("/folders/", {}),
create: (folder: FolderCreate) => client.POST("/folders/", { body: folder }),
list: () => client.GET("/api/folders/", {}),
create: (folder: FolderCreate) =>
client.POST("/api/folders/", { body: folder }),
delete: (id: number) =>
client.DELETE("/folders/{folder_id}", {
client.DELETE("/api/folders/{folder_id}", {
params: { path: { folder_id: id } },
}),
update: (id: number, updateData: FolderUpdate) =>

View file

@ -26,13 +26,13 @@ const createNote = async (note: NoteCreate) => {
};
console.log(encryptedNote);
return client.POST(`/notes/`, { body: encryptedNote });
return client.POST(`/api/notes/`, { body: encryptedNote });
};
const fetchNotes = async () => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const { data, error } = await client.GET(`/notes/`);
const { data, error } = await client.GET(`/api/notes/`);
if (error) {
throw new Error(error);
@ -74,7 +74,7 @@ const updateNote = async (id: number, note: Partial<NoteRead>) => {
encryptedNote.folderId = note.folderId;
}
const { data, error } = await client.PATCH(`/notes/{note_id}`, {
const { data, error } = await client.PATCH(`/api/notes/{note_id}`, {
body: encryptedNote,
params: {
path: {
@ -94,7 +94,7 @@ const updateNote = async (id: number, note: Partial<NoteRead>) => {
export const notesApi = {
list: () => fetchNotes(),
get: (id: number) =>
client.GET(`/notes/{note_id}`, {
client.GET(`/api/notes/{note_id}`, {
params: {
path: {
note_id: id,
@ -104,7 +104,7 @@ export const notesApi = {
create: (note: NoteCreate) => createNote(note),
update: (id: number, note: Partial<NoteRead>) => updateNote(id, note),
delete: (id: number) =>
client.DELETE(`/notes/{note_id}`, {
client.DELETE(`/api/notes/{note_id}`, {
params: {
path: {
note_id: id,

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M0 56C0 42.7 10.7 32 24 32l48 0 16 0 124 0c68.5 0 124 55.5 124 124c0 34.7-14.3 66.2-37.3 88.7C339.7 264.9 368 307.1 368 356c0 68.5-55.5 124-124 124L88 480l-16 0-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l24 0 0-176L48 80 24 80C10.7 80 0 69.3 0 56zM212 232c42 0 76-34 76-76s-34-76-76-76L96 80l0 152 116 0zM96 280l0 152 148 0c42 0 76-34 76-76s-34-76-76-76l-32 0L96 280z"/></svg>

Before

Width:  |  Height:  |  Size: 718 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M112 166.6l0 178.7L201.4 256 112 166.6z"/><path class="fa-primary" d="M201.4 256L112 166.6l0 178.7L201.4 256zm45.3-22.6c12.5 12.5 12.5 32.8 0 45.3l-128 128c-9.2 9.2-22.9 11.9-34.9 6.9s-19.8-16.6-19.8-29.6l0-256c0-12.9 7.8-24.6 19.8-29.6s25.7-2.2 34.9 6.9l128 128z"/></svg>

Before

Width:  |  Height:  |  Size: 585 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 256a208 208 0 1 0 416 0A208 208 0 1 0 48 256zm240 96a32 32 0 1 1 -64 0 32 32 0 1 1 64 0zM232 152c0-13.3 10.7-24 24-24s24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112z"/><path class="fa-primary" d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>

Before

Width:  |  Height:  |  Size: 772 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M216.6 105.4c9.6-9.2 9.9-24.3 .8-33.9s-24.3-9.9-33.9-.8l-176 168C2.7 243.2 0 249.4 0 256s2.7 12.8 7.4 17.4l176 168c9.6 9.2 24.8 8.8 33.9-.8s8.8-24.8-.8-33.9L58.8 256 216.6 105.4zm142.9 0L517.2 256 359.4 406.6c-9.6 9.2-9.9 24.3-.8 33.9s24.3 9.9 33.9 .8l176-168c4.7-4.5 7.4-10.8 7.4-17.4s-2.7-12.8-7.4-17.4l-176-168c-9.6-9.2-24.8-8.8-33.9 .8s-8.8 24.8 .8 33.9z"/></svg>

Before

Width:  |  Height:  |  Size: 711 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M399.1 1.1c-12.7-3.9-26.1 3.1-30 15.8l-144 464c-3.9 12.7 3.1 26.1 15.8 30s26.1-3.1 30-15.8l144-464c3.9-12.7-3.1-26.1-15.8-30zm71.4 118.5c-9.1 9.7-8.6 24.9 1.1 33.9L580.9 256 471.6 358.5c-9.7 9.1-10.2 24.3-1.1 33.9s24.3 10.2 33.9 1.1l128-120c4.8-4.5 7.6-10.9 7.6-17.5s-2.7-13-7.6-17.5l-128-120c-9.7-9.1-24.9-8.6-33.9 1.1zm-301 0c-9.1-9.7-24.3-10.2-33.9-1.1l-128 120C2.7 243 0 249.4 0 256s2.7 13 7.6 17.5l128 120c9.7 9.1 24.9 8.6 33.9-1.1s8.6-24.9-1.1-33.9L59.1 256 168.4 153.5c9.7-9.1 10.2-24.3 1.1-33.9z"/></svg>

Before

Width:  |  Height:  |  Size: 856 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 64c0-8.8 7.2-16 16-16l160 0 0 80c0 17.7 14.3 32 32 32l80 0 0 60.5c-48.2 31.4-80 85.8-80 147.5c0 35.4 10.5 68.4 28.5 96L64 464c-8.8 0-16-7.2-16-16L48 64z"/><path class="fa-primary" d="M64 464l220.5 0c12 18.4 27.4 34.5 45.3 47.3c-3.2 .5-6.4 .7-9.7 .7L64 512c-35.3 0-64-28.7-64-64L0 64C0 28.7 28.7 0 64 0L229.5 0c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3l0 44.1c-17.2 4.9-33.4 12.3-48 21.8l0-60.5-80 0c-17.7 0-32-14.3-32-32l0-80L64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16zM432 224a144 144 0 1 1 0 288 144 144 0 1 1 0-288zm16 80c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 48-48 0c-8.8 0-16 7.2-16 16s7.2 16 16 16l48 0 0 48c0 8.8 7.2 16 16 16s16-7.2 16-16l0-48 48 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-48 0 0-48z"/></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 96c0-8.8 7.2-16 16-16l132.1 0c6.4 0 12.5 2.5 17 7l45.3 45.3c7.5 7.5 17.7 11.7 28.3 11.7L448 144c8.8 0 16 7.2 16 16l0 256c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16L48 96zm96 192c0 13.3 10.7 24 24 24l64 0 0 64c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24z"/><path class="fa-primary" d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64L289.9 96 247 53.1C233.5 39.6 215.2 32 196.1 32L64 32zM48 96c0-8.8 7.2-16 16-16l132.1 0c6.4 0 12.5 2.5 17 7l45.3 45.3c7.5 7.5 17.7 11.7 28.3 11.7L448 144c8.8 0 16 7.2 16 16l0 256c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16L48 96zM232 376c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l64 0 0 64z"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 96l0 320c0 8.8 7.2 16 16 16l384 0c8.8 0 16-7.2 16-16l0-256c0-8.8-7.2-16-16-16l-161.4 0c-10.6 0-20.8-4.2-28.3-11.7L213.1 87c-4.5-4.5-10.6-7-17-7L64 80c-8.8 0-16 7.2-16 16z"/><path class="fa-primary" d="M0 96C0 60.7 28.7 32 64 32l132.1 0c19.1 0 37.4 7.6 50.9 21.1L289.9 96 448 96c35.3 0 64 28.7 64 64l0 256c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l384 0c8.8 0 16-7.2 16-16l0-256c0-8.8-7.2-16-16-16l-161.4 0c-10.6 0-20.8-4.2-28.3-11.7L213.1 87c-4.5-4.5-10.6-7-17-7L64 80z"/></svg>

Before

Width:  |  Height:  |  Size: 860 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M59.9 186.6l26.2 24.9c24 22.8 24 66.2 0 89L59.9 325.4c8.5 24 21.5 46.4 38 65.7l34.6-10.2c31.8-9.4 69.4 12.3 77.2 44.6l8.5 35.1c24.6 4.5 51.3 4.5 75.9 0l8.5-35.1c7.8-32.3 45.3-53.9 77.2-44.6l34.6 10.2c16.5-19.3 29.5-41.7 38-65.7l-26.2-24.9c-24-22.8-24-66.2 0-89l26.2-24.9c-8.5-24-21.5-46.4-38-65.7l-34.6 10.2c-31.8 9.4-69.4-12.3-77.2-44.6l-8.5-35.1c-24.6-4.5-51.3-4.5-75.9 0l-8.5 35.1c-7.8 32.3-45.3 53.9-77.2 44.6L97.9 120.9c-16.5 19.3-29.5 41.7-38 65.7zM352 256a96 96 0 1 1 -192 0 96 96 0 1 1 192 0z"/><path class="fa-primary" d="M256 0c17 0 33.6 1.7 49.8 4.8c7.9 1.5 21.8 6.1 29.4 20.1c2 3.7 3.6 7.6 4.6 11.8l9.3 38.5C350.5 81 360.3 86.7 366 85l38-11.2c4-1.2 8.1-1.8 12.2-1.9c16.1-.5 27 9.4 32.3 15.4c22.1 25.1 39.1 54.6 49.9 86.3c2.6 7.6 5.6 21.8-2.7 35.4c-2.2 3.6-4.9 7-8 10L459 246.3c-4.2 4-4.2 15.5 0 19.5l28.7 27.3c3.1 3 5.8 6.4 8 10c8.2 13.6 5.2 27.8 2.7 35.4c-10.8 31.7-27.8 61.1-49.9 86.3c-5.3 6-16.3 15.9-32.3 15.4c-4.1-.1-8.2-.8-12.2-1.9L366 427c-5.7-1.7-15.5 4-16.9 9.8l-9.3 38.5c-1 4.2-2.6 8.2-4.6 11.8c-7.7 14-21.6 18.5-29.4 20.1C289.6 510.3 273 512 256 512s-33.6-1.7-49.8-4.8c-7.9-1.5-21.8-6.1-29.4-20.1c-2-3.7-3.6-7.6-4.6-11.8l-9.3-38.5c-1.4-5.8-11.2-11.5-16.9-9.8l-38 11.2c-4 1.2-8.1 1.8-12.2 1.9c-16.1 .5-27-9.4-32.3-15.4c-22-25.1-39.1-54.6-49.9-86.3c-2.6-7.6-5.6-21.8 2.7-35.4c2.2-3.6 4.9-7 8-10L53 265.7c4.2-4 4.2-15.5 0-19.5L24.2 218.9c-3.1-3-5.8-6.4-8-10C8 195.3 11 181.1 13.6 173.6c10.8-31.7 27.8-61.1 49.9-86.3c5.3-6 16.3-15.9 32.3-15.4c4.1 .1 8.2 .8 12.2 1.9L146 85c5.7 1.7 15.5-4 16.9-9.8l9.3-38.5c1-4.2 2.6-8.2 4.6-11.8c7.7-14 21.6-18.5 29.4-20.1C222.4 1.7 239 0 256 0zM218.1 51.4l-8.5 35.1c-7.8 32.3-45.3 53.9-77.2 44.6L97.9 120.9c-16.5 19.3-29.5 41.7-38 65.7l26.2 24.9c24 22.8 24 66.2 0 89L59.9 325.4c8.5 24 21.5 46.4 38 65.7l34.6-10.2c31.8-9.4 69.4 12.3 77.2 44.6l8.5 35.1c24.6 4.5 51.3 4.5 75.9 0l8.5-35.1c7.8-32.3 45.3-53.9 77.2-44.6l34.6 10.2c16.5-19.3 29.5-41.7 38-65.7l-26.2-24.9c-24-22.8-24-66.2 0-89l26.2-24.9c-8.5-24-21.5-46.4-38-65.7l-34.6 10.2c-31.8 9.4-69.4-12.3-77.2-44.6l-8.5-35.1c-24.6-4.5-51.3-4.5-75.9 0zM208 256a48 48 0 1 0 96 0 48 48 0 1 0 -96 0zm48 96a96 96 0 1 1 0-192 96 96 0 1 1 0 192z"/></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M128 56c0-13.3 10.7-24 24-24l208 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-68.7 0L144.7 432l87.3 0c13.3 0 24 10.7 24 24s-10.7 24-24 24L24 480c-13.3 0-24-10.7-24-24s10.7-24 24-24l68.7 0L239.3 80 152 80c-13.3 0-24-10.7-24-24z"/></svg>

Before

Width:  |  Height:  |  Size: 573 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M24 56c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24l0 120 16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l16 0 0-96-8 0C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432l33.2 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-88 0c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM216 72l272 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-272 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 160l272 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-272 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 160l272 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-272 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M64 64a32 32 0 1 0 0 64 32 32 0 1 0 0-64zm120 8c-13.3 0-24 10.7-24 24s10.7 24 24 24l304 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L184 72zm0 160c-13.3 0-24 10.7-24 24s10.7 24 24 24l304 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-304 0zm0 160c-13.3 0-24 10.7-24 24s10.7 24 24 24l304 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-304 0zM96 256a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zM64 384a32 32 0 1 0 0 64 32 32 0 1 0 0-64z"/></svg>

Before

Width:  |  Height:  |  Size: 755 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M248 72c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 160L40 232c-13.3 0-24 10.7-24 24s10.7 24 24 24l160 0 0 160c0 13.3 10.7 24 24 24s24-10.7 24-24l0-160 160 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-160 0 0-160z"/></svg>

Before

Width:  |  Height:  |  Size: 554 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 288c0-8.8 7.2-16 16-16l64 0c8.8 0 16 7.2 16 16l0 64c0 8.8-7.2 16-16 16l-64 0c-8.8 0-16-7.2-16-16l0-32 0-32zm256 0c0-8.8 7.2-16 16-16l64 0c8.8 0 16 7.2 16 16l0 64c0 8.8-7.2 16-16 16l-64 0c-8.8 0-16-7.2-16-16l0-32 0-32z"/><path class="fa-primary" d="M0 216C0 149.7 53.7 96 120 96l16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0c-39.8 0-72 32.2-72 72l0 10c5.1-1.3 10.5-2 16-2l64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32 0-32 0-72zm48 72l0 32 0 32c0 8.8 7.2 16 16 16l64 0c8.8 0 16-7.2 16-16l0-64c0-8.8-7.2-16-16-16l-64 0c-8.8 0-16 7.2-16 16zm336-16l-64 0c-8.8 0-16 7.2-16 16l0 32 0 32c0 8.8 7.2 16 16 16l64 0c8.8 0 16-7.2 16-16l0-64c0-8.8-7.2-16-16-16zM256 320l0-32 0-72c0-66.3 53.7-120 120-120l16 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-16 0c-39.8 0-72 32.2-72 72l0 10c5.1-1.3 10.5-2 16-2l64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32z"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 96l0 320c0 8.8 7.2 16 16 16l320 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16L64 80c-8.8 0-16 7.2-16 16zm63 143c9.4-9.4 24.6-9.4 33.9 0l47 47L303 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9L209 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9z"/><path class="fa-primary" d="M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l320 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16L64 80zM0 96C0 60.7 28.7 32 64 32l320 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM337 209L209 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L303 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>

Before

Width:  |  Height:  |  Size: 979 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M145.5 138c4-21.5 17.9-37.4 41.7-47.4c24.7-10.4 59.4-13.7 99.9-7.5c12.8 2 52.4 9.5 64.9 12.8c12.8 3.3 25.9-4.3 29.3-17.2s-4.3-25.9-17.2-29.3c-14.7-3.8-56.1-11.7-69.7-13.8c-46.2-7.1-90.4-4.1-125.7 10.7c-36.1 15.1-63.3 43.1-70.5 83.9c-.1 .4-.1 .9-.2 1.3c-2.8 23.4 .5 44.2 9.8 62.2c9.2 17.8 23.2 31.2 38.8 41.5c2.4 1.6 5 3.2 7.5 4.7L24 240c-13.3 0-24 10.7-24 24s10.7 24 24 24l464 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-192.2 0c-9.9-3.1-19.7-6-29.2-8.8l-.3-.1c-37.7-11.1-70.5-20.7-93.3-35.8c-10.9-7.2-18.2-14.9-22.6-23.5c-4.2-8.2-6.6-18.9-4.9-33.8zM364 337.1c3.7 8.6 5.5 20.1 2.6 36.3c-3.8 21.8-17.8 37.9-41.8 48c-24.7 10.4-59.4 13.7-99.8 7.5c-20.1-3.2-54.3-14.6-81.2-23.6c0 0 0 0 0 0s0 0 0 0c-5.9-2-11.4-3.8-16.3-5.4c-12.6-4.1-26.1 2.8-30.3 15.4s2.8 26.2 15.4 30.3c4 1.3 8.8 2.9 14 4.7c26.6 8.9 66.4 22.2 90.9 26.2l.1 0c46.2 7.1 90.4 4.1 125.7-10.7c36.1-15.1 63.3-43.1 70.5-83.9c4-22.9 2.4-43.5-5-61.7l-57.2 0c5.7 5.3 9.7 11 12.3 17.1z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d="M48 80l149.5 0c4.2 0 8.3 1.7 11.3 4.7l168 168c6.2 6.2 6.2 16.4 0 22.6L243.3 408.8c-6.2 6.2-16.4 6.2-22.6 0l-168-168c-3-3-4.7-7.1-4.7-11.3L48 80zm32 64a32 32 0 1 0 64 0 32 32 0 1 0 -64 0z"/><path class="fa-primary" d="M345 39.1c-9.3-9.4-24.5-9.5-33.9-.2s-9.5 24.5-.2 33.9L438.6 202.1c33.9 34.3 33.9 89.4 0 123.7L326.7 439.1c-9.3 9.4-9.2 24.6 .2 33.9s24.6 9.2 33.9-.2L472.8 359.6c52.4-53 52.4-138.2 0-191.2L345 39.1zM242.7 50.7c-12-12-28.3-18.7-45.3-18.7L48 32C21.5 32 0 53.5 0 80L0 229.5c0 17 6.7 33.3 18.7 45.3l168 168c25 25 65.5 25 90.5 0L410.7 309.3c25-25 25-65.5 0-90.5l-168-168zM48 80l149.5 0c4.2 0 8.3 1.7 11.3 4.7l168 168c6.2 6.2 6.2 16.4 0 22.6L243.3 408.8c-6.2 6.2-16.4 6.2-22.6 0l-168-168c-3-3-4.7-7.1-4.7-11.3L48 80zm96 64a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><defs><style>.fa-secondary{opacity:.4}</style></defs><path class="fa-secondary" d=""/><path class="fa-primary" d="M345 137c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-119 119L73 103c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l119 119L39 375c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l119-119L311 409c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-119-119L345 137z"/></svg>

Before

Width:  |  Height:  |  Size: 590 B

View file

@ -57,7 +57,7 @@ export const FolderContextMenu: React.FC<FolderContextMenuProps> = ({
try {
await createFolderMutation.mutateAsync({
name: "New Folder",
parentId: folder.id,
parent_id: folder.id,
});
onClose();
} catch (error) {
@ -73,7 +73,7 @@ export const FolderContextMenu: React.FC<FolderContextMenuProps> = ({
top: y,
left: x,
}}
className="bg-overlay0 border border-surface1 rounded-md shadow-lg p-2 min-w-[200px] z-50"
className="bg-ctp-surface0 border border-ctp-surface2 rounded-md shadow-lg p-2 min-w-[200px] z-50"
onClick={(e) => e.stopPropagation()}
>
<input
@ -89,7 +89,7 @@ export const FolderContextMenu: React.FC<FolderContextMenuProps> = ({
}}
onBlur={handleRename}
autoFocus
className="w-full px-2 py-1 bg-surface1 border border-surface1 rounded text-sm text-text focus:outline-none focus:border-accent"
className="w-full px-2 py-1 bg-ctp-surface1 border border-ctp-surface2 rounded text-sm text-ctp-text focus:outline-none focus:border-ctp-mauve"
/>
</div>
);
@ -102,25 +102,25 @@ export const FolderContextMenu: React.FC<FolderContextMenuProps> = ({
top: y,
left: x,
}}
className="bg-overlay0 border border-surface1 rounded-md shadow-lg py-1 min-w-[160px] z-50"
className="bg-ctp-surface0 border border-ctp-surface2 rounded-md shadow-lg py-1 min-w-[160px] z-50"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setIsRenaming(true)}
className="w-full text-left px-3 py-1.5 hover:bg-surface1 text-sm text-text transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-surface1 text-sm text-ctp-text transition-colors"
>
Rename
</button>
<button
onClick={handleCreateSubfolder}
className="w-full text-left px-3 py-1.5 hover:bg-surface1 text-sm text-text transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-surface1 text-sm text-ctp-text transition-colors"
>
New Subfolder
</button>
<div className="border-t border-surface1 my-1" />
<div className="border-t border-ctp-surface2 my-1" />
<button
onClick={handleDelete}
className="w-full text-left px-3 py-1.5 hover:bg-danger hover:text-base text-sm text-danger transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-red hover:text-ctp-base text-sm text-ctp-red transition-colors"
>
Delete
</button>

View file

@ -36,7 +36,7 @@ export const NoteContextMenu: React.FC<NoteContextMenuProps> = ({
await createNoteMutation.mutateAsync({
title: `${note.title} (Copy)`,
content: note.content,
folderId: note.folderId || null,
folder_id: note.folder_id || null,
});
onClose();
} catch (error) {
@ -57,25 +57,25 @@ export const NoteContextMenu: React.FC<NoteContextMenuProps> = ({
top: y,
left: x,
}}
className="bg-overlay0 border border-surface1 rounded-md shadow-lg py-1 min-w-[160px] z-50"
className="bg-ctp-surface0 border border-ctp-surface2 rounded-md shadow-lg py-1 min-w-[160px] z-50"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={handleRename}
className="w-full text-left px-3 py-1.5 hover:bg-surface1 text-sm text-text transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-surface1 text-sm text-ctp-text transition-colors"
>
Rename
</button>
<button
onClick={handleDuplicate}
className="w-full text-left px-3 py-1.5 hover:bg-surface1 text-sm text-text transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-surface1 text-sm text-ctp-text transition-colors"
>
Duplicate
</button>
<div className="border-t border-surface1 my-1" />
<div className="border-t border-ctp-surface2 my-1" />
<button
onClick={handleDelete}
className="w-full text-left px-3 py-1.5 hover:bg-danger hover:text-base text-sm text-danger transition-colors"
className="w-full text-left px-3 py-1.5 hover:bg-ctp-red hover:text-ctp-base text-sm text-ctp-red transition-colors"
>
Delete
</button>

View file

@ -105,7 +105,7 @@ export const ContextMenuProvider = ({
e.preventDefault();
closeContextMenu();
}}
className=" h-screen w-screen bg-surface1/25 z-40 fixed top-0 left-0"
className=" h-screen w-screen bg-ctp-crust/25 z-40 fixed top-0 left-0"
></div>
)}
{children}

View file

@ -0,0 +1,74 @@
import { Mark, markInputRule } from "@tiptap/core";
export const NoteLink = Mark.create({
name: "noteLink",
inclusive: false,
addAttributes() {
return {
noteId: {
default: null,
parseHTML: (element) => {
return element.getAttribute("data-note-id");
},
renderHTML: (attributes) => {
return {
"data-note-id": attributes.noteId,
};
},
},
title: {
default: "",
parseHTML: (element) => {
return element.getAttribute("data-title");
},
renderHTML: (attributes) => {
return {
"data-title": attributes.title,
};
},
},
};
},
parseHTML() {
return [
{
tag: "span[data-note-id]",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["span", { ...HTMLAttributes, class: "note-link" }, 0];
},
addInputRules() {
return [
markInputRule({
find: /\[\[([^\]]+)\]\]$/,
type: this.type,
getAttributes: (match) => {
const title = match[1];
// TODO: Look up noteId from title
return {
title,
noteId: null, // For now
};
},
}),
];
},
addStorage() {
return {
markdown: {
serialize: (state, mark, parent, index) => {
// Get the text content
const textContent = parent.child(index).text || mark.attrs.title;
// Return the complete link, no wrapping
return `[[${mark.attrs.title}:${mark.attrs.noteId}]]`;
},
},
};
},
});

View file

@ -2,28 +2,44 @@
@plugin "@tailwindcss/typography";
@import "@catppuccin/tailwindcss/macchiato.css";
:root {
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--box-shadow:
0 2px 6px rgba(30, 32, 48, 0.4), 0 8px 24px rgba(30, 32, 48, 0.5),
0 16px 32px rgba(30, 32, 48, 0.6);
}
@theme {
--color-base: #24273a;
--color-surface0: #1e2030;
--color-surface1: #181926;
--color-overlay0: #363a4f;
--color-overlay1: #494d64;
--color-text: #cad3f5;
--color-subtext: #b8c0e0;
--color-accent: #e2a16f;
--color-base:
;
}
--color-danger: #e26f6f;
--color-success: #6fe29b;
--color-warn: #e2c56f;
@theme {
/* Map Tailwind classes to CSS variables */
--color-ctp-base: var(--color-ctp-base);
--color-ctp-mantle: var(--color-ctp-mantle);
--color-ctp-crust: var(--color-ctp-crust);
--color-ctp-text: var(--color-ctp-text);
--color-ctp-subtext0: #a5adcb;
--color-ctp-overlay0: #6e738d;
--color-ctp-mauve: var(--color-ctp-mauve);
--color-ctp-blue: var(--color-ctp-blue);
--color-ctp-green: #a6da95;
--color-ctp-red: #ed8796;
--color-ctp-yellow: #eed49f;
--color-ctp-teal: #8bd5ca;
--color-ctp-sapphire: #7dc4e4;
--color-ctp-peach: #f5a97f;
/* Surface colors */
--color-ctp-surface0: #363a4f;
--color-ctp-surface1: #494d64;
--color-ctp-surface2: #5b6078;
}
/* Default values (Macchiato) - injected by JS, but good as fallback */
:root {
--color-ctp-base: #24273a;
--color-ctp-mantle: #1e2030;
--color-ctp-crust: #181926;
--color-ctp-text: #cad3f5;
--color-ctp-mauve: #c6a0f6;
--color-ctp-blue: #8aadf4;
}
/* Override MDXEditor and all its children */
@ -31,19 +47,19 @@
._mdxeditor-root-content-editable,
.mdxeditor-root-contenteditable,
div[contenteditable="true"] {
color: var(--color-text) !important;
color: var(--color-ctp-text) !important;
}
._listItemChecked_1tncs_73::before {
--accentSolid: var(--color-accent) !important;
border-color: var(--color-accent) !important;
--accentSolid: var(--color-ctp-mauve) !important;
border-color: var(--color-ctp-mauve-900) !important;
border: 2px;
}
._listItemChecked_1tncs_73::after {
border-color: var(--color-accent) !important;
border-color: var(--color-ctp-mauve-900) !important;
}
.standard-input {
@apply border border-overlay0 rounded-sm px-2 py-1 w-full focus:outline-none focus:ring-2 focus:ring-accent bg-base text-text placeholder:text-overlay1;
@apply border border-ctp-mauve rounded-sm px-2 py-1 w-full focus:outline-none focus:ring-2 focus:ring-ctp-mauve bg-ctp-base text-ctp-text placeholder:text-ctp-overlay0;
}

View file

@ -1,10 +1,4 @@
import {
ChangeEvent,
ChangeEventHandler,
useEffect,
useRef,
useState,
} from "react";
import { useEffect, useRef, useState } from "react";
import "../../main.css";
import { AnimatePresence, motion } from "framer-motion";
import { useAuthStore } from "@/stores/authStore";
@ -32,7 +26,7 @@ function Home() {
} | null>(null);
const { encryptionKey } = useAuthStore();
const { showModal, setUpdating, selectedNote, editorView } = useUIStore();
const { showModal, setUpdating, selectedNote } = useUIStore();
const newFolderRef = useRef<HTMLInputElement>(null);
const updateNoteMutation = useUpdateNote();
@ -121,60 +115,33 @@ function Home() {
}
};
const setUnparsedContent = (event: ChangeEvent<HTMLTextAreaElement>) => {
if (editingNote) {
setEditingNote({ ...editingNote, content: event.target.value });
}
};
return (
<div className="flex bg-base h-screen text-text overflow-hidden">
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
{/* Sidebar */}
<AnimatePresence>{showModal && <Modal />}</AnimatePresence>
<Sidebar />
{/* Main editor area */}
<div className="flex flex-col w-full h-screen overflow-hidden">
{" "}
{editingNote ? (
<>
<input
type="text"
id="noteTitle"
placeholder="Untitled note..."
value={editingNote.title || ""}
onChange={(e) => setTitle(e.target.value)}
className="w-full self-center p-4 pb-2 pt-2 text-3xl font-semibold focus:outline-none border-transparent focus:border-accent transition-colors placeholder:text-overlay0 text-text bg-surface1"
/>
<div className="h-full lg:w-3xl w-full mx-auto overflow-y-hidden">
{" "}
{editorView == "parsed" ? (
<TiptapEditor
key={editingNote.id}
content={editingNote.content || ""}
onChange={setContent}
/>
) : (
<textarea
value={editingNote.content || ""}
className="w-full font-mono p-4 bg-transparent focus:outline-none resize-none text-text"
style={{
minHeight: "calc(100vh - 55px)",
}}
onChange={setUnparsedContent}
/>
)}
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-overlay0">
<div className="text-center">
<PlusIcon className="w-16 h-16 mx-auto mb-4 fill-current opacity-50" />
<p className="text-lg">Select a note or create a new one</p>
</div>
</div>
)}
<div className="flex flex-col w-full h-screen overflow-y-auto items-center justify-center">
{/*<Editor />*/}
<div className="h-full lg:w-3xl w-full">
<input
type="text"
id="noteTitle"
name=""
placeholder="Untitled note..."
value={editingNote?.title || ""}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-4 pb-0 text-3xl font-semibold bg-transparent focus:outline-none border-transparent focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/>
<TiptapEditor
key={editingNote?.id}
content={editingNote?.content || ""}
onChange={setContent}
/>
</div>
</div>
<StatusIndicator />
@ -185,16 +152,14 @@ function Home() {
export default Home;
const Modal = () => {
const { setShowModal, modalContent, showModal } = useUIStore();
const ModalContent = modalContent;
if (!showModal || !ModalContent) return null;
const { setShowModal } = useUIStore();
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowModal(false)}
className="fixed inset-0 h-screen w-screen flex items-center justify-center bg-crust/70 backdrop-blur-sm z-50"
className="fixed inset-0 h-screen w-screen flex items-center justify-center bg-ctp-crust/70 backdrop-blur-sm z-50"
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
@ -202,17 +167,16 @@ const Modal = () => {
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", duration: 0.3 }}
onClick={(e) => e.stopPropagation()}
className="relative w-full max-w-md mx-4 bg-base rounded-xl border-surface1 border p-8 shadow-2xl"
className="relative w-full max-w-md mx-4 bg-ctp-base rounded-xl border-ctp-surface2 border p-8 shadow-2xl"
>
<button
onClick={() => setShowModal(false)}
className="absolute top-4 right-4 p-2 hover:bg-surface0 rounded-sm transition-colors group"
className="absolute top-4 right-4 p-2 hover:bg-ctp-surface0 rounded-sm transition-colors group"
aria-label="Close modal"
>
<XmarkIcon className="w-5 h-5 fill-overlay0 group-hover:fill-text transition-colors" />
<XmarkIcon className="w-5 h-5 fill-ctp-overlay0 group-hover:fill-ctp-text transition-colors" />
</button>
<ModalContent />
{/*<Login />*/}
<Login />
</motion.div>
</motion.div>
);

View file

@ -6,43 +6,32 @@ import CheckIcon from "../../../assets/fontawesome/svg/circle-check.svg?react";
import SpinnerIcon from "../../../assets/fontawesome/svg/rotate.svg?react";
// @ts-ignore
import WarningIcon from "../../../assets/fontawesome/svg/circle-exclamation.svg?react";
import { Login } from "@/pages/Login";
export const StatusIndicator = () => {
const { encryptionKey } = useAuthStore();
const { updating, setShowModal, editorView, setEditorView, setModalContent } =
useUIStore();
const { updating, setShowModal } = useUIStore();
return (
<div
className="fixed bottom-2 right-3 bg-surface0 border border-surface1 rounded-sm px-2 py-0.5 flex items-center gap-2.5 shadow-lg backdrop-blur-sm"
className="fixed bottom-2 right-3 bg-ctp-surface0 border border-ctp-surface2 rounded-sm px-2 py-0.5 flex items-center gap-2.5 shadow-lg backdrop-blur-sm"
onClick={() => {
if (!encryptionKey) {
setModalContent(Login);
setShowModal(true);
}
}}
>
<div
className="select-none"
onClick={() =>
setEditorView(editorView == "parsed" ? "unparsed" : "parsed")
}
>
{editorView}
</div>
{!encryptionKey ? (
<WarningIcon className="h-4 w-4 my-1 [&_.fa-primary]:fill-warn [&_.fa-secondary]:fill-orange" />
<WarningIcon className="h-4 w-4 my-1 [&_.fa-primary]:fill-ctp-yellow [&_.fa-secondary]:fill-ctp-orange" />
) : updating ? (
<>
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-warn [&_.fa-secondary]:fill-sapphire" />
{/*<span className="text-sm text-subtext font-medium">
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" />
{/*<span className="text-sm text-ctp-subtext0 font-medium">
Saving...
</span>*/}
</>
) : (
<>
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-success [&_.fa-secondary]:fill-teal" />
{/*<span className="text-sm text-subtext font-medium">Saved</span>*/}
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" />
{/*<span className="text-sm text-ctp-subtext0 font-medium">Saved</span>*/}
</>
)}
</div>

View file

@ -41,7 +41,7 @@ export const Sidebar = () => {
const { encryptionKey } = useAuthStore();
const { setSideBarResize, sideBarResize, setColourScheme } = useUIStore();
const { setSideBarResize, sideBarResize } = useUIStore();
useEffect(() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
@ -163,7 +163,7 @@ export const Sidebar = () => {
>
<div className="flex-row-reverse flex h-screen">
<div
className="h-full bg-surface1 w-0.5 hover:cursor-ew-resize hover:bg-accent/50 transition-colors"
className="h-full bg-ctp-surface0 w-0.5 hover:cursor-ew-resize hover:bg-ctp-mauve transition-colors"
onMouseDown={handleMouseDown}
></div>
<div
@ -171,7 +171,7 @@ export const Sidebar = () => {
style={{ width: `${sideBarResize}px` }}
>
<SidebarHeader setNewFolder={setNewFolder} />
<div className="flex-1 overflow-y-auto bg-surface1 border-r border-surface1">
<div className="flex-1 overflow-y-auto bg-ctp-mantle border-r border-ctp-surface2">
<>
<div
className="w-full p-4 sm:block hidden"
@ -203,14 +203,14 @@ export const Sidebar = () => {
{/* Loading state */}
{isLoading && (
<div className="flex items-center justify-center py-8 text-subtext0">
<div className="flex items-center justify-center py-8 text-ctp-subtext0">
<div className="text-sm">Loading folders...</div>
</div>
)}
{/* Error state */}
{error && (
<div className="flex items-center justify-center py-8 text-danger">
<div className="flex items-center justify-center py-8 text-ctp-red">
<div className="text-sm">Failed to load folders</div>
</div>
)}
@ -236,19 +236,16 @@ export const Sidebar = () => {
</>
)}
</div>
{/*<div className="fixed bottom-1 left-2">
<button onClick={setColour}>purple</button>
</div>*/}
<DragOverlay>
{activeItem?.type === "note" && (
<div className="bg-surface0 rounded-md px-2 py-1 shadow-lg border border-accent">
<div className="bg-ctp-surface0 rounded-md px-2 py-1 shadow-lg border border-ctp-mauve">
{activeItem.data.title}
</div>
)}
{activeItem?.type === "folder" && (
<div className="bg-surface0 rounded-md px-1 py-0.5 shadow-lg flex items-center gap-1 text-sm">
<FolderIcon className="w-3 h-3 fill-accent mr-1" />
<div className="bg-ctp-surface0 rounded-md px-1 py-0.5 shadow-lg flex items-center gap-1 text-sm">
<FolderIcon className="w-3 h-3 fill-ctp-mauve mr-1" />
{activeItem.data.name}
</div>
)}

View file

@ -39,8 +39,8 @@ export const DraggableNote = ({ note }: { note: NoteRead }) => {
}}
className={` rounded-sm px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
selectedNote?.id === note.id
? "bg-accent text-base"
: "hover:bg-surface1"
? "bg-ctp-mauve text-ctp-base"
: "hover:bg-ctp-surface1"
}`}
>
<span className="truncate">

View file

@ -65,10 +65,10 @@ export const DroppableFolder = ({
>
{(folder.notes?.length ?? 0) > 0 && (
<CaretRightIcon
className={`w-4 h-4 min-h-4 min-w-4 mr-1 transition-all duration-200 ease-in-out ${collapse ? "rotate-90" : ""} fill-accent`}
className={`w-4 h-4 min-h-4 min-w-4 mr-1 transition-all duration-200 ease-in-out ${collapse ? "rotate-90" : ""} fill-ctp-mauve`}
/>
)}
<FolderIcon className="w-4 h-4 min-h-4 min-w-4 fill-accent mr-1" />
<FolderIcon className="w-4 h-4 min-h-4 min-w-4 fill-ctp-mauve mr-1" />
<span className="truncate">{folder.name}</span>
</div>
</div>

View file

@ -29,7 +29,7 @@ export const FolderTree = ({ folder, depth = 0 }: FolderTreeProps) => {
className="overflow-hidden flex flex-col"
>
{/* The line container */}
<div className="ml-2 pl-3 border-l border-surface1">
<div className="ml-2 pl-3 border-l border-ctp-surface2">
{/* Notes */}
<div className="flex flex-col gap-0.5">
{folder.notes.map((note) => (

View file

@ -5,46 +5,16 @@ import FolderPlusIcon from "@assets/fontawesome/svg/folder-plus.svg?react";
import TagsIcon from "@assets/fontawesome/svg/tags.svg?react";
// @ts-ignore
import FileCirclePlusIcon from "@assets/fontawesome/svg/file-circle-plus.svg?react";
// @ts-ignore
import GearIcon from "@assets/fontawesome/svg/gear.svg?react";
import { useUIStore } from "@/stores/uiStore";
import { useCreateNote } from "@/hooks/useFolders";
import { NoteCreate } from "@/api/notes";
import { Login } from "@/pages/Login";
import { ColourState } from "@/stores/uiStore";
const Test = () => {
const { colourScheme, setColourScheme } = useUIStore();
const handleColor = (key: string, value: string) => {
setColourScheme({
...colourScheme,
[key]: value,
});
};
return (
<>
{Object.entries(colourScheme).map(([key, value]) => (
<div key={key}>
<label>{key}</label>
<input
type="color"
value={value}
onChange={(e) => handleColor(key, e.target.value)}
/>
</div>
))}
</>
);
};
export const SidebarHeader = ({
setNewFolder,
}: {
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
}) => {
const { selectedFolder, setShowModal, setModalContent } = useUIStore();
const { selectedFolder } = useUIStore();
const createNote = useCreateNote();
const handleCreate = async () => {
createNote.mutate({
@ -53,34 +23,22 @@ export const SidebarHeader = ({
folder_id: selectedFolder,
} as NoteCreate);
};
const handleSettings = () => {
setModalContent(Test);
setShowModal(true);
};
return (
<div className="w-full p-2 border-b border-surface1 bg-surface1">
<div className="flex items-center justify-around bg-surface0 rounded-lg p-1 gap-1">
<div className="w-full p-2 border-b border-ctp-surface2 bg-ctp-mantle">
<div className="flex items-center justify-around bg-ctp-surface0 rounded-lg p-1 gap-1">
<button
onClick={() => setNewFolder(true)}
className="hover:bg-accent active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
className="hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
title="New folder"
>
<FolderPlusIcon className="w-5 h-5 group-hover:fill-base transition-all duration-200 fill-accent" />
<FolderPlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-all duration-200 fill-ctp-mauve" />
</button>
<button
onClick={handleCreate}
className="hover:bg-accent active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
className="hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
title="New note"
>
<FileCirclePlusIcon className="w-5 h-5 group-hover:fill-base transition-all duration-200 fill-accent" />
</button>
<button
onClick={handleSettings}
className="hover:bg-accent active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
title="New note"
>
<GearIcon className="w-5 h-5 group-hover:fill-base transition-all duration-200 fill-accent" />
<FileCirclePlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-all duration-200 fill-ctp-mauve" />
</button>
</div>
</div>

View file

@ -30,10 +30,14 @@ export const Login = () => {
onSubmit={handleSubmit}
className="gap-4 flex flex-col max-w-md mx-auto"
>
<h2 className="text-2xl font-semibold text-text mb-2">Welcome Back</h2>
<h2 className="text-2xl font-semibold text-ctp-text mb-2">
Welcome Back
</h2>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-subtext">Username</label>
<label className="text-sm font-medium text-ctp-subtext0">
Username
</label>
<input
type="text"
placeholder="Enter your username"
@ -44,7 +48,9 @@ export const Login = () => {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-subtext">Password</label>
<label className="text-sm font-medium text-ctp-subtext0">
Password
</label>
<input
type="password"
className="standard-input"
@ -55,7 +61,7 @@ export const Login = () => {
</div>
{error && (
<div className="bg-danger/10 border border-danger text-danger px-3 py-2 rounded-sm text-sm">
<div className="bg-ctp-red/10 border border-ctp-red text-ctp-red px-3 py-2 rounded-sm text-sm">
{error}
</div>
)}
@ -66,11 +72,11 @@ export const Login = () => {
id="remember"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="accent-accent cursor-pointer"
className="accent-ctp-mauve cursor-pointer"
/>
<label
htmlFor="remember"
className="text-sm text-subtext cursor-pointer"
className="text-sm text-ctp-subtext0 cursor-pointer"
>
Remember me
</label>
@ -78,7 +84,7 @@ export const Login = () => {
<button
type="submit"
className="bg-accent hover:bg-accent/90 text-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-base"
className="bg-ctp-mauve hover:bg-ctp-mauve/90 text-ctp-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-2 focus:ring-offset-ctp-base"
>
Login
</button>

View file

@ -27,10 +27,14 @@ export const Register = () => {
onSubmit={handleSubmit}
className="gap-4 flex flex-col max-w-md mx-auto"
>
<h2 className="text-2xl font-semibold text-text mb-2">Create Account</h2>
<h2 className="text-2xl font-semibold text-ctp-text mb-2">
Create Account
</h2>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-subtext">Username</label>
<label className="text-sm font-medium text-ctp-subtext0">
Username
</label>
<input
type="text"
placeholder="Choose a username"
@ -41,7 +45,7 @@ export const Register = () => {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-subtext">Email</label>
<label className="text-sm font-medium text-ctp-subtext0">Email</label>
<input
type="email"
placeholder="Enter your email"
@ -52,7 +56,9 @@ export const Register = () => {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-subtext">Password</label>
<label className="text-sm font-medium text-ctp-subtext0">
Password
</label>
<input
type="password"
className="standard-input"
@ -63,14 +69,14 @@ export const Register = () => {
</div>
{error && (
<div className="bg-danger/10 border border-danger text-danger px-3 py-2 rounded-sm text-sm">
<div className="bg-ctp-red/10 border border-ctp-red text-ctp-red px-3 py-2 rounded-sm text-sm">
{error}
</div>
)}
<button
type="submit"
className="bg-accent hover:bg-accent/90 text-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-base"
className="bg-ctp-mauve hover:bg-ctp-mauve/90 text-ctp-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-2 focus:ring-offset-ctp-base"
>
Register
</button>

View file

@ -1,6 +1,6 @@
export const Test = () => {
return (
<div className="h-screen w-screen flex items-center justify-center bg-base p-4">
<div className="h-screen w-screen flex items-center justify-center bg-ctp-base p-4">
<input
type="text"
placeholder="Folder name..."

View file

@ -22,6 +22,7 @@ import SquareCheckIcon from "../assets/fontawesome/svg/square-check.svg?react";
import CodeBracketIcon from "../assets/fontawesome/svg/code-simple.svg?react";
// @ts-ignore
import QuoteLeftIcon from "../assets/fontawesome/svg/quote-left.svg?react";
import { NoteLink } from "@/extensions/NoteLink";
interface TiptapEditorProps {
content: string;
@ -37,6 +38,7 @@ export const TiptapEditor = ({
const editor = useEditor({
extensions: [
ListKit,
NoteLink,
StarterKit.configure({
heading: {
levels: [1, 2, 3, 4, 5, 6],
@ -79,12 +81,7 @@ export const TiptapEditor = ({
}
return (
<div
className="tiptap-editor pt-0! overflow-y-scroll"
style={{
minHeight: "calc(100vh - 55px)",
}}
>
<div className="tiptap-editor pt-0!">
{/* Toolbar */}
{/*<div className="editor-toolbar">
<div className="toolbar-group">
@ -93,28 +90,28 @@ export const TiptapEditor = ({
className={editor.isActive("bold") ? "active" : ""}
title="Bold (Ctrl+B)"
>
<BoldIcon className="w-4 h-4 fill-text" />
<BoldIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive("italic") ? "active" : ""}
title="Italic (Ctrl+I)"
>
<ItalicIcon className="w-4 h-4 fill-text" />
<ItalicIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive("strike") ? "active" : ""}
title="Strikethrough"
>
<StrikethroughIcon className="w-4 h-4 fill-text" />
<StrikethroughIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={editor.isActive("code") ? "active" : ""}
title="Inline code"
>
<CodeIcon className="w-4 h-4 fill-text" />
<CodeIcon className="w-4 h-4 fill-ctp-text" />
</button>
</div>
@ -158,35 +155,35 @@ export const TiptapEditor = ({
className={editor.isActive("bulletList") ? "active" : ""}
title="Bullet list"
>
<ListUlIcon className="w-4 h-4 fill-text" />
<ListUlIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive("orderedList") ? "active" : ""}
title="Numbered list"
>
<ListOlIcon className="w-4 h-4 fill-text" />
<ListOlIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleTaskList().run()}
className={editor.isActive("taskList") ? "active" : ""}
title="Task list"
>
<SquareCheckIcon className="w-4 h-4 fill-text" />
<SquareCheckIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive("codeBlock") ? "active" : ""}
title="Code block"
>
<CodeBracketIcon className="w-4 h-4 fill-text" />
<CodeBracketIcon className="w-4 h-4 fill-ctp-text" />
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive("blockquote") ? "active" : ""}
title="Quote"
>
<QuoteLeftIcon className="w-4 h-4 fill-text" />
<QuoteLeftIcon className="w-4 h-4 fill-ctp-text" />
</button>
</div>
@ -194,6 +191,19 @@ export const TiptapEditor = ({
</div>*/}
{/* Editor content */}
<button
onClick={() => {
editor
?.chain()
.focus()
.setMark("noteLink", { noteId: 123, title: "Test Note" })
.insertContent("Test Note")
.run();
}}
className="bg-ctp-blue text-ctp-base px-4 py-2 rounded"
>
Insert Test Link
</button>
<EditorContent
editor={editor}
className="editor-content h-min-screen p-4! pt-0!"

View file

@ -7,37 +7,33 @@
}
*::-webkit-scrollbar-track {
@apply bg-surface0 rounded-full;
@apply bg-ctp-mantle rounded-full;
}
*::-webkit-scrollbar-thumb {
@apply bg-surface1 rounded-full;
@apply bg-ctp-surface2 rounded-full;
}
*::-webkit-scrollbar-thumb:hover {
@apply bg-overlay0;
@apply bg-ctp-overlay0;
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-surface1) var(--color-surface0);
scrollbar-color: var(--color-ctp-surface2) var(--color-ctp-mantle);
}
.tiptap-editor {
@apply flex flex-col h-full bg-base;
@apply flex flex-col h-full bg-ctp-base;
}
.ProseMirror {
@apply text-text;
@apply text-ctp-text;
}
.editor-toolbar {
@apply flex gap-2 px-4 bg-surface0 border-b border-surface1 flex-wrap items-center;
}
.editor-content {
@apply h-full;
@apply flex gap-2 px-4 bg-ctp-mantle border-b border-ctp-surface2 flex-wrap items-center;
}
.toolbar-group {
@ -45,19 +41,19 @@
}
.toolbar-divider {
@apply w-px h-6 bg-surface1;
@apply w-px h-6 bg-ctp-surface2;
}
.editor-toolbar button {
@apply p-2 bg-transparent border-none rounded-sm text-text cursor-pointer transition-all duration-150 text-sm font-semibold min-w-8 flex items-center justify-center;
@apply p-2 bg-transparent border-none rounded-sm text-ctp-text cursor-pointer transition-all duration-150 text-sm font-semibold min-w-8 flex items-center justify-center;
}
.editor-toolbar button:hover:not(:disabled) {
@apply bg-surface0;
@apply bg-ctp-surface0;
}
.editor-toolbar button.active {
@apply bg-accent text-base;
@apply bg-ctp-mauve text-ctp-base;
}
.editor-toolbar button:disabled {
@ -70,59 +66,59 @@
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
@apply float-left text-overlay0 pointer-events-none h-0;
@apply float-left text-ctp-overlay0 pointer-events-none h-0;
}
.ProseMirror ul {
@apply mb-0!;
}
.ProseMirror h1 {
@apply text-3xl font-bold mt-6 mb-4 text-accent;
@apply text-3xl font-bold text-ctp-mauve mt-8 mb-4;
}
.ProseMirror h2 {
@apply text-2xl font-semibold text-accent mt-4 mb-3;
@apply text-2xl font-semibold text-ctp-blue mt-6 mb-3;
}
.ProseMirror h3 {
@apply text-xl font-semibold text-accent mt-5 mb-2;
@apply text-xl font-semibold text-ctp-mauve mt-5 mb-2;
}
.ProseMirror code {
@apply bg-surface0 text-accent px-1.5 py-0.5 rounded text-sm;
@apply bg-ctp-surface0 text-ctp-peach px-1.5 py-0.5 rounded text-sm;
font-family: "JetBrains Mono", "Fira Code", monospace;
}
.ProseMirror .code-block {
@apply bg-surface0 border border-surface1 rounded-sm p-4 my-4 overflow-x-auto;
@apply bg-ctp-surface0 border border-ctp-surface2 rounded-sm p-4 my-4 overflow-x-auto;
font-family: "JetBrains Mono", "Fira Code", monospace;
}
.ProseMirror .code-block code {
@apply bg-transparent p-0 text-text;
@apply bg-transparent p-0 text-ctp-text;
}
.ProseMirror blockquote {
@apply border-l-4 border-accent pl-4 ml-0 text-subtext italic;
@apply border-l-4 border-ctp-mauve pl-4 ml-0 text-ctp-subtext0 italic;
}
.ProseMirror hr {
@apply border-none border-t-2 border-surface1 my-8;
@apply border-none border-t-2 border-ctp-surface2 my-8;
}
.ProseMirror a {
@apply text-accent underline;
@apply text-ctp-blue underline;
}
.ProseMirror a:hover {
@apply text-accent;
@apply text-ctp-sapphire;
}
.ProseMirror strong {
@apply text-accent font-semibold;
@apply text-ctp-peach font-semibold;
}
.ProseMirror em {
@apply text-accent;
@apply text-ctp-yellow;
}
/* Task List (Checkboxes) */
@ -139,7 +135,7 @@
}
.ProseMirror ul[data-type="taskList"] > li > label input[type="checkbox"] {
@apply cursor-pointer m-0 accent-accent;
@apply cursor-pointer m-0 accent-ctp-mauve;
}
.ProseMirror ul[data-type="taskList"] > li > div {
@ -151,18 +147,18 @@
}
.ProseMirror li[data-checked="true"] > div > p {
@apply line-through text-text/40;
@apply line-through text-ctp-overlay0;
text-decoration-style: wavy;
text-decoration-thickness: 1px;
}
.ProseMirror u {
@apply decoration-accent;
@apply decoration-ctp-mauve;
/*text-decoration-style: wavy;*/
}
.ProseMirror li::marker {
@apply text-accent;
@apply text-ctp-mauve;
}
/* tiptap.css */
@ -177,18 +173,8 @@
margin-bottom: 0 !important;
}
hr {
border: none;
height: 3px;
background: repeating-linear-gradient(
90deg,
var(--color-accent) 0px,
var(--color-accent) 8px,
var(--color-accent) 16px
);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 6'%3E%3Cpath d='M0 3 Q 3 0, 6 3 T 12 3 T 18 3 T 24 3' stroke='black' stroke-width='2' fill='none'/%3E%3C/svg%3E")
repeat-x;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 6'%3E%3Cpath d='M0 3 Q 3 0, 6 3 T 12 3 T 18 3 T 24 3' stroke='black' stroke-width='2' fill='none'/%3E%3C/svg%3E")
repeat-x;
margin: 2em 0;
.note-link {
@apply text-ctp-pink-500;
@apply underline;
@apply cursor-pointer;
}

View file

@ -33,9 +33,7 @@ interface AuthState {
clearAll: () => void;
}
const API_URL = import.meta.env.PROD
? "/api" // ← Same domain, different path
: "http://localhost:8000/api";
const API_URL = "http://localhost:8000/api";
export const useAuthStore = create<AuthState>()(
persist(

View file

@ -1,27 +1,6 @@
import { Note, NoteRead } from "@/api/notes";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { Login } from "@/pages/Login";
interface HSL {
H: Number;
S: Number;
L: Number;
}
export interface ColourState {
base: string;
surface0: string;
surface1: string;
overlay0: string;
overlay1: string;
text: string;
subtext: string;
accent: string;
warn: string;
success: string;
danger: string;
}
interface UIState {
updating: boolean;
@ -30,26 +9,17 @@ interface UIState {
showModal: boolean;
setShowModal: (show: boolean) => void;
modalContent: React.ComponentType | null;
setModalContent: (content: React.ComponentType) => void;
sideBarResize: number;
setSideBarResize: (size: number) => void;
sideBarView: string;
setSideBarView: (view: string) => void;
editorView: string;
setEditorView: (view: string) => void;
selectedNote: NoteRead | null;
setSelectedNote: (note: NoteRead | null) => void;
selectedFolder: number | null;
setSelectedFolder: (id: number | null) => void;
colourScheme: ColourState;
setColourScheme: (colors: ColourState) => void;
}
export const useUIStore = create<UIState>()(
@ -63,10 +33,6 @@ export const useUIStore = create<UIState>()(
setShowModal: (show) => {
set({ showModal: show });
},
modalContent: null,
setModalContent: (content) => {
set({ modalContent: content });
},
sideBarResize: 300,
setSideBarResize: (size) => {
set({ sideBarResize: size });
@ -75,10 +41,6 @@ export const useUIStore = create<UIState>()(
setSideBarView: (view) => {
set({ sideBarView: view });
},
editorView: "parsed",
setEditorView: (view) => {
set({ editorView: view });
},
selectedNote: null,
setSelectedNote: (id: NoteRead | null) => {
@ -89,28 +51,6 @@ export const useUIStore = create<UIState>()(
setSelectedFolder: (id: number | null) => {
set({ selectedFolder: id });
},
colourScheme: {
base: "#24273a",
surface0: "#1e2030",
surface1: "#181926",
overlay0: "#363a4f",
overlay1: "#494d64",
text: "#cad3f5",
subtext: "#b8c0e0",
accent: "#e2a16f",
danger: "#e26f6f",
success: "#6fe29b",
warn: "#e2c56f",
},
setColourScheme: (colors: ColourState) => {
set({ colourScheme: colors });
Object.entries(colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--color-${key}`, value);
});
},
}),
{

View file

@ -0,0 +1,28 @@
import { Folder, FolderTreeNode, FolderTreeResponse } from "@/api/folders";
import { NoteRead } from "@/api/notes";
export const createNoteMap = (
folderTree: FolderTreeResponse,
): Map<number, NoteRead> => {
const flatenedNotes = flattenNotes(folderTree);
const noteMap = new Map();
for (const note of flatenedNotes) {
noteMap.set(note.id, note);
}
return noteMap;
};
export const flattenNotes = (response: FolderTreeResponse): NoteRead[] => {
const allNotes = [...response.orphanedNotes];
const processFolder = (folder: FolderTreeNode) => {
allNotes.push(...folder.notes);
folder.children.forEach(processFolder);
};
response.folders.forEach(processFolder);
return allNotes;
};