Compare commits

..

28 commits

Author SHA1 Message Date
25af4639bf Update case transformer 2026-01-15 22:45:38 +00:00
eb52756c6d Added theming and fix css.
Added unparsed view
2026-01-15 22:26:08 +00:00
e0a8e503b9 routing fix 2026-01-06 17:00:47 +00:00
6ee055b691 test 2026-01-06 16:52:46 +00:00
4bf58513bc update 2026-01-06 16:51:05 +00:00
12eaeb4351 update 2026-01-06 16:47:18 +00:00
3d344b86ac update 2026-01-06 16:41:39 +00:00
fb6912b30f test 2026-01-06 15:48:41 +00:00
e3eac33abf test 2026-01-06 15:44:53 +00:00
7087f21891 g 2026-01-06 15:38:11 +00:00
bb466fbaab test 2026-01-06 15:37:06 +00:00
a4f260f5a3 test 2026-01-06 15:37:01 +00:00
0e6ce1eec2 test 2026-01-06 15:31:57 +00:00
287c47d53d updates 2026-01-06 15:11:08 +00:00
d8de720f8c update 2026-01-06 15:04:02 +00:00
6378b75bf3 composer 2026-01-06 14:19:33 +00:00
be63cb82e6 update 2026-01-06 14:10:58 +00:00
13de7316ef update 2026-01-06 14:08:13 +00:00
e1ce554705 comp 2026-01-06 13:52:31 +00:00
5196b108c5 compose 2026-01-06 13:51:05 +00:00
1143315258 svgs 2026-01-06 13:49:28 +00:00
82edaf99e8 gitignore 2026-01-06 13:34:23 +00:00
192a6f7b09 update git ignore 2026-01-06 13:33:21 +00:00
e3da8db7c3 docker updates 2026-01-06 13:28:32 +00:00
a8f66644ad update docker 2026-01-06 13:13:59 +00:00
b15f09e604 update dockerfile 2026-01-06 13:10:06 +00:00
7ed4512bbc update yaml 2026-01-06 13:07:16 +00:00
40b74704e7 Added docker support 2026-01-06 12:51:40 +00:00
54 changed files with 849 additions and 1028 deletions

23
.gitignore vendored
View file

@ -1,6 +1,27 @@
node_modules node_modules
frontend/src/assets/fontawesome/svg/* *.svg
frontend/src/assets/fontawesome/svg/0.svg frontend/src/assets/fontawesome/svg/0.svg
*.db *.db
.zed/settings.json .zed/settings.json
**.pyc **.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/*

32
backend/.dockerignore Normal file
View file

@ -0,0 +1,32 @@
__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,10 +1,39 @@
FROM python:3.11-slim # ---- Builder stage ----
FROM python:3.12-slim AS builder
WORKDIR /app WORKDIR /app
# Install dependencies
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY ./app ./app # ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] # 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"]

View file

@ -1,10 +1,23 @@
import os
from dotenv import load_dotenv
from sqlmodel import Session, SQLModel, create_engine # type: ignore from sqlmodel import Session, SQLModel, create_engine # type: ignore
DATABASE_URL = "sqlite:///./notes.db" load_dotenv()
# Get database URL from environment, with proper fallback
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine( # If DATABASE_URL is not set or empty, use default SQLite
DATABASE_URL, echo=True, connect_args={"check_same_thread": False} 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)
def create_db_and_tables(): def create_db_and_tables():

View file

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

View file

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

21
backend/start.sh Normal file
View file

@ -0,0 +1,21 @@
#!/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 "*"

44
compose.yaml Normal file
View file

@ -0,0 +1,44 @@
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:

27
frontend/.dockerignore Normal file
View file

@ -0,0 +1,27 @@
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

31
frontend/Dockerfile Normal file
View file

@ -0,0 +1,31 @@
# ---------- 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;"]

48
frontend/nginx.conf Normal file
View file

@ -0,0 +1,48 @@
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", "openapi-typescript": "^7.10.1",
"type-fest": "^5.3.1", "type-fest": "^5.3.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^5.4.21", "vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0", "vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.15" "vitest": "^4.0.15"
} }

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 718 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 585 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 772 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 711 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 856 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 860 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 573 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 755 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 554 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 979 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 590 B

View file

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

View file

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

View file

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

View file

@ -1,74 +0,0 @@
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,44 +2,28 @@
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@import "@catppuccin/tailwindcss/macchiato.css"; @import "@catppuccin/tailwindcss/macchiato.css";
@theme {
--color-base:
;
}
@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 { :root {
--color-ctp-base: #24273a; --black: 15, 18, 25;
--color-ctp-mantle: #1e2030; --gray: 96, 115, 159;
--color-ctp-crust: #181926; --gray-light: 229, 233, 240;
--color-ctp-text: #cad3f5; --gray-dark: 34, 41, 57;
--color-ctp-mauve: #c6a0f6; --box-shadow:
--color-ctp-blue: #8aadf4; 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-danger: #e26f6f;
--color-success: #6fe29b;
--color-warn: #e2c56f;
} }
/* Override MDXEditor and all its children */ /* Override MDXEditor and all its children */
@ -47,19 +31,19 @@
._mdxeditor-root-content-editable, ._mdxeditor-root-content-editable,
.mdxeditor-root-contenteditable, .mdxeditor-root-contenteditable,
div[contenteditable="true"] { div[contenteditable="true"] {
color: var(--color-ctp-text) !important; color: var(--color-text) !important;
} }
._listItemChecked_1tncs_73::before { ._listItemChecked_1tncs_73::before {
--accentSolid: var(--color-ctp-mauve) !important; --accentSolid: var(--color-accent) !important;
border-color: var(--color-ctp-mauve-900) !important; border-color: var(--color-accent) !important;
border: 2px; border: 2px;
} }
._listItemChecked_1tncs_73::after { ._listItemChecked_1tncs_73::after {
border-color: var(--color-ctp-mauve-900) !important; border-color: var(--color-accent) !important;
} }
.standard-input { .standard-input {
@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; @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;
} }

View file

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

View file

@ -6,32 +6,43 @@ import CheckIcon from "../../../assets/fontawesome/svg/circle-check.svg?react";
import SpinnerIcon from "../../../assets/fontawesome/svg/rotate.svg?react"; import SpinnerIcon from "../../../assets/fontawesome/svg/rotate.svg?react";
// @ts-ignore // @ts-ignore
import WarningIcon from "../../../assets/fontawesome/svg/circle-exclamation.svg?react"; import WarningIcon from "../../../assets/fontawesome/svg/circle-exclamation.svg?react";
import { Login } from "@/pages/Login";
export const StatusIndicator = () => { export const StatusIndicator = () => {
const { encryptionKey } = useAuthStore(); const { encryptionKey } = useAuthStore();
const { updating, setShowModal } = useUIStore(); const { updating, setShowModal, editorView, setEditorView, setModalContent } =
useUIStore();
return ( return (
<div <div
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" 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"
onClick={() => { onClick={() => {
if (!encryptionKey) { if (!encryptionKey) {
setModalContent(Login);
setShowModal(true); setShowModal(true);
} }
}} }}
> >
<div
className="select-none"
onClick={() =>
setEditorView(editorView == "parsed" ? "unparsed" : "parsed")
}
>
{editorView}
</div>
{!encryptionKey ? ( {!encryptionKey ? (
<WarningIcon className="h-4 w-4 my-1 [&_.fa-primary]:fill-ctp-yellow [&_.fa-secondary]:fill-ctp-orange" /> <WarningIcon className="h-4 w-4 my-1 [&_.fa-primary]:fill-warn [&_.fa-secondary]:fill-orange" />
) : updating ? ( ) : updating ? (
<> <>
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" /> <SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-warn [&_.fa-secondary]:fill-sapphire" />
{/*<span className="text-sm text-ctp-subtext0 font-medium"> {/*<span className="text-sm text-subtext font-medium">
Saving... Saving...
</span>*/} </span>*/}
</> </>
) : ( ) : (
<> <>
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" /> <CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-success [&_.fa-secondary]:fill-teal" />
{/*<span className="text-sm text-ctp-subtext0 font-medium">Saved</span>*/} {/*<span className="text-sm text-subtext font-medium">Saved</span>*/}
</> </>
)} )}
</div> </div>

View file

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

View file

@ -65,10 +65,10 @@ export const DroppableFolder = ({
> >
{(folder.notes?.length ?? 0) > 0 && ( {(folder.notes?.length ?? 0) > 0 && (
<CaretRightIcon <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-ctp-mauve`} 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`}
/> />
)} )}
<FolderIcon className="w-4 h-4 min-h-4 min-w-4 fill-ctp-mauve mr-1" /> <FolderIcon className="w-4 h-4 min-h-4 min-w-4 fill-accent mr-1" />
<span className="truncate">{folder.name}</span> <span className="truncate">{folder.name}</span>
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,33 +7,37 @@
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
@apply bg-ctp-mantle rounded-full; @apply bg-surface0 rounded-full;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
@apply bg-ctp-surface2 rounded-full; @apply bg-surface1 rounded-full;
} }
*::-webkit-scrollbar-thumb:hover { *::-webkit-scrollbar-thumb:hover {
@apply bg-ctp-overlay0; @apply bg-overlay0;
} }
/* Firefox scrollbar */ /* Firefox scrollbar */
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--color-ctp-surface2) var(--color-ctp-mantle); scrollbar-color: var(--color-surface1) var(--color-surface0);
} }
.tiptap-editor { .tiptap-editor {
@apply flex flex-col h-full bg-ctp-base; @apply flex flex-col h-full bg-base;
} }
.ProseMirror { .ProseMirror {
@apply text-ctp-text; @apply text-text;
} }
.editor-toolbar { .editor-toolbar {
@apply flex gap-2 px-4 bg-ctp-mantle border-b border-ctp-surface2 flex-wrap items-center; @apply flex gap-2 px-4 bg-surface0 border-b border-surface1 flex-wrap items-center;
}
.editor-content {
@apply h-full;
} }
.toolbar-group { .toolbar-group {
@ -41,19 +45,19 @@
} }
.toolbar-divider { .toolbar-divider {
@apply w-px h-6 bg-ctp-surface2; @apply w-px h-6 bg-surface1;
} }
.editor-toolbar button { .editor-toolbar button {
@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; @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;
} }
.editor-toolbar button:hover:not(:disabled) { .editor-toolbar button:hover:not(:disabled) {
@apply bg-ctp-surface0; @apply bg-surface0;
} }
.editor-toolbar button.active { .editor-toolbar button.active {
@apply bg-ctp-mauve text-ctp-base; @apply bg-accent text-base;
} }
.editor-toolbar button:disabled { .editor-toolbar button:disabled {
@ -66,59 +70,59 @@
.ProseMirror p.is-editor-empty:first-child::before { .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
@apply float-left text-ctp-overlay0 pointer-events-none h-0; @apply float-left text-overlay0 pointer-events-none h-0;
} }
.ProseMirror ul { .ProseMirror ul {
@apply mb-0!; @apply mb-0!;
} }
.ProseMirror h1 { .ProseMirror h1 {
@apply text-3xl font-bold text-ctp-mauve mt-8 mb-4; @apply text-3xl font-bold mt-6 mb-4 text-accent;
} }
.ProseMirror h2 { .ProseMirror h2 {
@apply text-2xl font-semibold text-ctp-blue mt-6 mb-3; @apply text-2xl font-semibold text-accent mt-4 mb-3;
} }
.ProseMirror h3 { .ProseMirror h3 {
@apply text-xl font-semibold text-ctp-mauve mt-5 mb-2; @apply text-xl font-semibold text-accent mt-5 mb-2;
} }
.ProseMirror code { .ProseMirror code {
@apply bg-ctp-surface0 text-ctp-peach px-1.5 py-0.5 rounded text-sm; @apply bg-surface0 text-accent px-1.5 py-0.5 rounded text-sm;
font-family: "JetBrains Mono", "Fira Code", monospace; font-family: "JetBrains Mono", "Fira Code", monospace;
} }
.ProseMirror .code-block { .ProseMirror .code-block {
@apply bg-ctp-surface0 border border-ctp-surface2 rounded-sm p-4 my-4 overflow-x-auto; @apply bg-surface0 border border-surface1 rounded-sm p-4 my-4 overflow-x-auto;
font-family: "JetBrains Mono", "Fira Code", monospace; font-family: "JetBrains Mono", "Fira Code", monospace;
} }
.ProseMirror .code-block code { .ProseMirror .code-block code {
@apply bg-transparent p-0 text-ctp-text; @apply bg-transparent p-0 text-text;
} }
.ProseMirror blockquote { .ProseMirror blockquote {
@apply border-l-4 border-ctp-mauve pl-4 ml-0 text-ctp-subtext0 italic; @apply border-l-4 border-accent pl-4 ml-0 text-subtext italic;
} }
.ProseMirror hr { .ProseMirror hr {
@apply border-none border-t-2 border-ctp-surface2 my-8; @apply border-none border-t-2 border-surface1 my-8;
} }
.ProseMirror a { .ProseMirror a {
@apply text-ctp-blue underline; @apply text-accent underline;
} }
.ProseMirror a:hover { .ProseMirror a:hover {
@apply text-ctp-sapphire; @apply text-accent;
} }
.ProseMirror strong { .ProseMirror strong {
@apply text-ctp-peach font-semibold; @apply text-accent font-semibold;
} }
.ProseMirror em { .ProseMirror em {
@apply text-ctp-yellow; @apply text-accent;
} }
/* Task List (Checkboxes) */ /* Task List (Checkboxes) */
@ -135,7 +139,7 @@
} }
.ProseMirror ul[data-type="taskList"] > li > label input[type="checkbox"] { .ProseMirror ul[data-type="taskList"] > li > label input[type="checkbox"] {
@apply cursor-pointer m-0 accent-ctp-mauve; @apply cursor-pointer m-0 accent-accent;
} }
.ProseMirror ul[data-type="taskList"] > li > div { .ProseMirror ul[data-type="taskList"] > li > div {
@ -147,18 +151,18 @@
} }
.ProseMirror li[data-checked="true"] > div > p { .ProseMirror li[data-checked="true"] > div > p {
@apply line-through text-ctp-overlay0; @apply line-through text-text/40;
text-decoration-style: wavy; text-decoration-style: wavy;
text-decoration-thickness: 1px; text-decoration-thickness: 1px;
} }
.ProseMirror u { .ProseMirror u {
@apply decoration-ctp-mauve; @apply decoration-accent;
/*text-decoration-style: wavy;*/ /*text-decoration-style: wavy;*/
} }
.ProseMirror li::marker { .ProseMirror li::marker {
@apply text-ctp-mauve; @apply text-accent;
} }
/* tiptap.css */ /* tiptap.css */
@ -173,8 +177,18 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.note-link { hr {
@apply text-ctp-pink-500; border: none;
@apply underline; height: 3px;
@apply cursor-pointer; 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;
} }

View file

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

View file

@ -1,6 +1,27 @@
import { Note, NoteRead } from "@/api/notes"; import { Note, NoteRead } from "@/api/notes";
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; 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 { interface UIState {
updating: boolean; updating: boolean;
@ -9,17 +30,26 @@ interface UIState {
showModal: boolean; showModal: boolean;
setShowModal: (show: boolean) => void; setShowModal: (show: boolean) => void;
modalContent: React.ComponentType | null;
setModalContent: (content: React.ComponentType) => void;
sideBarResize: number; sideBarResize: number;
setSideBarResize: (size: number) => void; setSideBarResize: (size: number) => void;
sideBarView: string; sideBarView: string;
setSideBarView: (view: string) => void; setSideBarView: (view: string) => void;
editorView: string;
setEditorView: (view: string) => void;
selectedNote: NoteRead | null; selectedNote: NoteRead | null;
setSelectedNote: (note: NoteRead | null) => void; setSelectedNote: (note: NoteRead | null) => void;
selectedFolder: number | null; selectedFolder: number | null;
setSelectedFolder: (id: number | null) => void; setSelectedFolder: (id: number | null) => void;
colourScheme: ColourState;
setColourScheme: (colors: ColourState) => void;
} }
export const useUIStore = create<UIState>()( export const useUIStore = create<UIState>()(
@ -33,6 +63,10 @@ export const useUIStore = create<UIState>()(
setShowModal: (show) => { setShowModal: (show) => {
set({ showModal: show }); set({ showModal: show });
}, },
modalContent: null,
setModalContent: (content) => {
set({ modalContent: content });
},
sideBarResize: 300, sideBarResize: 300,
setSideBarResize: (size) => { setSideBarResize: (size) => {
set({ sideBarResize: size }); set({ sideBarResize: size });
@ -41,6 +75,10 @@ export const useUIStore = create<UIState>()(
setSideBarView: (view) => { setSideBarView: (view) => {
set({ sideBarView: view }); set({ sideBarView: view });
}, },
editorView: "parsed",
setEditorView: (view) => {
set({ editorView: view });
},
selectedNote: null, selectedNote: null,
setSelectedNote: (id: NoteRead | null) => { setSelectedNote: (id: NoteRead | null) => {
@ -51,6 +89,28 @@ export const useUIStore = create<UIState>()(
setSelectedFolder: (id: number | null) => { setSelectedFolder: (id: number | null) => {
set({ selectedFolder: id }); 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

@ -1,28 +0,0 @@
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;
};