From bf6b2820b6194afa72654a6c8aaa6f2829e70eaa Mon Sep 17 00:00:00 2001 From: Patsy Date: Wed, 22 Jul 2026 21:48:24 +0200 Subject: [PATCH] all bulletjournal version 1 completed --- .python-version | 1 + app/__init__.py | 47 ++ app/auth.py | 55 ++ app/models.py | 94 +++ app/routes.py | 365 ++++++++++ app/static/style.css | 662 ++++++++++++++++++ app/templates/base.html | 56 ++ app/templates/daily_log.html | 308 ++++++++ app/templates/daily_log_form.html | 77 ++ app/templates/entry_detail.html | 67 ++ app/templates/entry_form.html | 152 ++++ app/templates/entry_list.html | 33 + app/templates/index.html | 153 ++++ app/templates/log_list.html | 33 + app/templates/login.html | 52 ++ app/templates/register.html | 52 ++ app/templates/settings.html | 61 ++ cline.md | 176 +++++ cline2.md | 119 ++++ config.py | 7 + migrations/README | 1 + migrations/alembic.ini | 50 ++ migrations/env.py | 113 +++ migrations/script.py.mako | 24 + .../versions/001_bullet_journal_migration.py | 91 +++ requirements.txt | 15 + run.py | 5 + stzyleguide.md | 598 ++++++++++++++++ 28 files changed, 3467 insertions(+) create mode 100644 .python-version create mode 100644 app/__init__.py create mode 100644 app/auth.py create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/static/style.css create mode 100644 app/templates/base.html create mode 100644 app/templates/daily_log.html create mode 100644 app/templates/daily_log_form.html create mode 100644 app/templates/entry_detail.html create mode 100644 app/templates/entry_form.html create mode 100644 app/templates/entry_list.html create mode 100644 app/templates/index.html create mode 100644 app/templates/log_list.html create mode 100644 app/templates/login.html create mode 100644 app/templates/register.html create mode 100644 app/templates/settings.html create mode 100644 cline.md create mode 100644 cline2.md create mode 100644 config.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/001_bullet_journal_migration.py create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 stzyleguide.md diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..7310294 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +test_flask_bullet diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..6c7d48d --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,47 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_migrate import Migrate +import config + +db = SQLAlchemy() +login_manager = LoginManager() +migrate = Migrate() + +# Flask-Login configuration +login_manager.login_view = 'auth.login' +login_manager.login_message = 'Please log in to access this page.' + + +def create_app(): + app = Flask(__name__) + app.config.from_object(config.Config) + + # Initialize extensions + db.init_app(app) + login_manager.init_app(app) + migrate.init_app(app, db) + + # Initialize model classes with db instance + from .models import init_db_models + init_db_models(db) + + # Import model classes (stored on init_db_models function) + User = init_db_models.User + UserSettings = init_db_models.UserSettings + DailyLog = init_db_models.DailyLog + DailyLogItem = init_db_models.DailyLogItem + + # Flask-Login user loader (must be defined after User class exists) + @login_manager.user_loader + def load_user(user_id): + return User.query.get(int(user_id)) + + # Register blueprints + from . import routes + app.register_blueprint(routes.bp) + + from . import auth + app.register_blueprint(auth.auth_bp) + + return app \ No newline at end of file diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..3c9ea15 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,55 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required +from . import db +from .models import init_db_models + +# Import model classes (stored on init_db_models function) +User = init_db_models.User + +auth_bp = Blueprint('auth', __name__) + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + user = User.query.filter_by(username=username).first() + + if not user or not user.check_password(password): + flash('Invalid username or password.') + return redirect(url_for('auth.login')) + + login_user(user) + return redirect(url_for('main.index')) + + return render_template('login.html') + + +@auth_bp.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('auth.login')) + + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + if User.query.filter_by(username=username).first(): + flash('Username already exists.') + return redirect(url_for('auth.register')) + + user = User(username=username) + user.set_password(password) + db.session.add(user) + db.session.commit() + + flash('Registered successfully. Please log in.') + return redirect(url_for('auth.login')) + + return render_template('register.html') \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..c28caf4 --- /dev/null +++ b/app/models.py @@ -0,0 +1,94 @@ +from flask_login import UserMixin +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash + + +# Sleep quality and status choices for validation +SLEEP_QUALITY_CHOICES = ['poor', 'fair', 'good', 'excellent'] +STATUS_CHOICES = ['todo', 'doing', 'done', 'cancelled'] +ITEM_TYPE_CHOICES = ['bullet', 'gratitude', 'sleep', 'note', 'task'] + + +def init_db_models(db): + """Initialize all model classes with the SQLAlchemy instance. + + Call this once in create_app() after db is created. + """ + + class User(UserMixin, db.Model): + __tablename__ = 'user' + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + + logs = db.relationship('DailyLog', backref='user', lazy=True, cascade='all, delete-orphan') + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + class UserSettings(db.Model): + __tablename__ = 'user_settings' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True) + theme = db.Column(db.String(10), default='light') + font_size = db.Column(db.Integer, default=18) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + class DailyLog(db.Model): + __tablename__ = 'daily_log' + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.Date, unique=True, nullable=False) + prompt = db.Column(db.Text, nullable=True) + notes = db.Column(db.Text, nullable=True) + gratitude = db.Column(db.JSON, default=list) + bedtime = db.Column(db.String(20), nullable=True) + wake_time = db.Column(db.String(20), nullable=True) + sleep_quality = db.Column(db.String(10), nullable=True) # Enum stored as string + mood = db.Column(db.String(50), nullable=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + items = db.relationship('DailyLogItem', backref='log', lazy=True, cascade='all, delete-orphan') + + @property + def gratitude_list(self): + if not self.gratitude: + return ['', '', ''] + return self.gratitude + [''] * (3 - len(self.gratitude)) + + @gratitude_list.setter + def gratitude_list(self, value): + self.gratitude = value[:3] if value else [] + + def __repr__(self): + return f'' + + class DailyLogItem(db.Model): + __tablename__ = 'daily_log_item' + id = db.Column(db.Integer, primary_key=True) + log_id = db.Column(db.Integer, db.ForeignKey('daily_log.id'), nullable=False) + type = db.Column(db.String(20), nullable=False) # Enum stored as string + symbol = db.Column(db.String(10), default='•') + text = db.Column(db.Text, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + status = db.Column(db.String(10), default='todo') # Enum stored as string + + __table_args__ = ( + db.Index('ix_log_id', 'log_id'), + db.Index('ix_log_id_status', 'log_id', 'status'), + db.Index('ix_type', 'type'), + ) + + def __repr__(self): + return f'' + + # Store references on module for import by other blueprints + init_db_models.User = User + init_db_models.UserSettings = UserSettings + init_db_models.DailyLog = DailyLog + init_db_models.DailyLogItem = DailyLogItem \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..8c39945 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,365 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify +from flask_login import login_required, current_user +from datetime import datetime, date +from . import db +from .models import init_db_models + +# Import model classes (stored on init_db_models function) +DailyLog = init_db_models.DailyLog +DailyLogItem = init_db_models.DailyLogItem +UserSettings = init_db_models.UserSettings + +bp = Blueprint('main', __name__) + + +# ─── Step 2: Daily Log (main page) ─────────────────────────────────── +@bp.route('/') +@login_required +def index(): + """Daily Log screen — main page (Step 2).""" + today = date.today() + log = DailyLog.query.filter_by(date=today, user_id=current_user.id).first() + + if not log: + log = DailyLog(date=today, user_id=current_user.id) + db.session.add(log) + db.session.commit() + + # Get settings + settings = UserSettings.query.filter_by(user_id=current_user.id).first() + if not settings: + settings = UserSettings(user_id=current_user.id) + db.session.add(settings) + db.session.commit() + + return render_template('daily_log.html', log=log, settings=settings) + + +# ─── Step 2: All Logs ──────────────────────────────────────────────── +@bp.route('/logs') +@login_required +def log_list(): + """All Logs screen — list of dates (Step 2).""" + logs = DailyLog.query.filter_by(user_id=current_user.id)\ + .order_by(DailyLog.date.desc()).all() + return render_template('log_list.html', logs=logs) + + +# ─── Step 2: Daily Log Detail (by date) ────────────────────────────── +@bp.route('/logs/') +@login_required +def log_detail(log_id): + """Open the Daily Log for a specific date.""" + log = DailyLog.query.get_or_404(log_id) + if log.user_id != current_user.id: + flash('Access denied.') + return redirect(url_for('main.log_list')) + return render_template('daily_log.html', log=log, settings=None) + + +# ─── Step 4: Create / Edit Log ─────────────────────────────────────── +@bp.route('/logs/new', methods=['GET', 'POST']) +@login_required +def log_new(): + """Create a new Daily Log entry.""" + if request.method == 'POST': + log_date = request.form.get('date') + try: + log_date = datetime.strptime(log_date, '%Y-%m-%d').date() + except ValueError: + flash('Invalid date format.') + return render_template('daily_log_form.html', action='New') + + existing = DailyLog.query.filter_by(date=log_date, user_id=current_user.id).first() + if existing: + flash('Log for this date already exists.') + return redirect(url_for('main.log_detail', log_id=existing.id)) + + log = DailyLog( + date=log_date, + prompt=request.form.get('prompt', ''), + notes=request.form.get('notes', ''), + bedtime=request.form.get('bedtime', ''), + wake_time=request.form.get('wake_time', ''), + sleep_quality=request.form.get('sleep_quality'), + mood=request.form.get('mood', ''), + user_id=current_user.id, + ) + db.session.add(log) + db.session.commit() + + # Handle gratitude (3 fields) + gratitude = [request.form.get(f'gratitude_{i}', '') for i in range(3)] + log.gratitude = gratitude + + # Handle bullets / items + item_types = request.form.getlist('item_type[]') + item_symbols = request.form.getlist('item_symbol[]') + item_texts = request.form.getlist('item_text[]') + item_statuses = request.form.getlist('item_status[]') + + for i in range(len(item_types)): + if item_types[i] and item_texts[i].strip(): + item = DailyLogItem( + log_id=log.id, + type=item_types[i], + symbol=item_symbols[i] if item_symbols[i] else '•', + text=item_texts[i].strip(), + status=item_statuses[i] if item_statuses[i] in ['todo', 'doing', 'done', 'cancelled'] else 'todo', + ) + db.session.add(item) + + db.session.commit() + flash('Log saved!') + return redirect(url_for('main.log_detail', log_id=log.id)) + + return render_template('daily_log_form.html', action='New') + + +@bp.route('/logs//edit', methods=['GET', 'POST']) +@login_required +def log_edit(log_id): + """Edit an existing Daily Log entry.""" + log = DailyLog.query.get_or_404(log_id) + if log.user_id != current_user.id: + flash('Access denied.') + return redirect(url_for('main.log_list')) + + if request.method == 'POST': + log.prompt = request.form.get('prompt', '') + log.notes = request.form.get('notes', '') + log.bedtime = request.form.get('bedtime', '') + log.wake_time = request.form.get('wake_time', '') + log.sleep_quality = request.form.get('sleep_quality') + log.mood = request.form.get('mood', '') + + # Handle gratitude + gratitude = [request.form.get(f'gratitude_{i}', '') for i in range(3)] + log.gratitude = gratitude + + # Handle existing items and new items + item_ids = request.form.getlist('item_id[]') + item_types = request.form.getlist('item_type[]') + item_symbols = request.form.getlist('item_symbol[]') + item_texts = request.form.getlist('item_text[]') + item_statuses = request.form.getlist('item_status[]') + + # Remove items not in item_ids + existing_ids = [int(i) for i in item_ids if i.isdigit()] + for item in log.items: + if item.id not in existing_ids: + db.session.delete(item) + + for i in range(len(item_types)): + if item_ids[i] and item_ids[i].isdigit(): + item = DailyLogItem.query.get(int(item_ids[i])) + if item: + item.type = item_types[i] + item.symbol = item_symbols[i] if item_symbols[i] else '•' + item.text = item_texts[i].strip() + item.status = item_statuses[i] if item_statuses[i] in ['todo', 'doing', 'done', 'cancelled'] else 'todo' + elif item_types[i] and item_texts[i].strip(): + item = DailyLogItem( + log_id=log.id, + type=item_types[i], + symbol=item_symbols[i] if item_symbols[i] else '•', + text=item_texts[i].strip(), + status=item_statuses[i] if item_statuses[i] in ['todo', 'doing', 'done', 'cancelled'] else 'todo', + ) + db.session.add(item) + + db.session.commit() + flash('Log updated!') + return redirect(url_for('main.log_detail', log_id=log.id)) + + return render_template('daily_log_form.html', log=log, action='Edit') + + +# ─── Step 4: Add / Update Bullet (AJAX) ────────────────────────────── +@bp.route('/logs//items', methods=['POST']) +@login_required +def add_log_item(log_id): + """Add a bullet/item to a log (AJAX endpoint).""" + log = DailyLog.query.get_or_404(log_id) + if log.user_id != current_user.id: + return jsonify({'success': False, 'error': 'Access denied.'}), 403 + + data = request.get_json() + item_type = data.get('type', 'bullet') + symbol = data.get('symbol', '•') + text = data.get('text', '').strip() + status = data.get('status', 'todo') + + if not text: + return jsonify({'success': False, 'error': 'Text is required.'}), 400 + + item = DailyLogItem( + log_id=log.id, + type=item_type, + symbol=symbol, + text=text, + status=status, + ) + db.session.add(item) + db.session.commit() + + return jsonify({ + 'success': True, + 'id': item.id, + 'type': item.type, + 'symbol': item.symbol, + 'text': item.text, + 'status': item.status, + }) + + +@bp.route('/logs/items/', methods=['PUT']) +@login_required +def update_log_item(item_id): + """Update a bullet/item status or text (AJAX endpoint).""" + item = DailyLogItem.query.get_or_404(item_id) + if item.log.user_id != current_user.id: + return jsonify({'success': False, 'error': 'Access denied.'}), 403 + + data = request.get_json() + if 'status' in data: + item.status = data['status'] + if 'text' in data: + item.text = data['text'].strip() + + db.session.commit() + return jsonify({'success': True, 'status': item.status, 'text': item.text}) + + +@bp.route('/logs/items/', methods=['DELETE']) +@login_required +def delete_log_item(item_id): + """Delete a bullet/item (AJAX endpoint).""" + item = DailyLogItem.query.get_or_404(item_id) + if item.log.user_id != current_user.id: + return jsonify({'success': False, 'error': 'Access denied.'}), 403 + + db.session.delete(item) + db.session.commit() + return jsonify({'success': True}) + + +# ─── Step 4: Export / Import ───────────────────────────────────────── +@bp.route('/export') +@login_required +def export_logs(): + """Export all logs as JSON (Step 4).""" + logs = DailyLog.query.filter_by(user_id=current_user.id).all() + data = [] + for log in logs: + log_data = { + 'date': log.date.isoformat(), + 'prompt': log.prompt, + 'notes': log.notes, + 'gratitude': log.gratitude or [], + 'bedtime': log.bedtime, + 'wake_time': log.wake_time, + 'sleep_quality': log.sleep_quality, + 'mood': log.mood, + 'items': [ + { + 'type': item.type.value, + 'symbol': item.symbol, + 'text': item.text, + 'status': item.status.value, + } + for item in log.items + ], + } + data.append(log_data) + + return jsonify(data), 200, {'Content-Type': 'application/json', 'Content-Disposition': 'attachment; filename="logs.json"'} + + +@bp.route('/import', methods=['POST']) +@login_required +def import_logs(): + """Import logs from JSON file (Step 4).""" + if 'file' not in request.files: + flash('No file uploaded.') + return redirect(url_for('main.settings')) + + file = request.files['file'] + if not file or not file.filename.endswith('.json'): + flash('Invalid file. Please upload a JSON file.') + return redirect(url_for('main.settings')) + + import json as json_module + try: + data = file.read().decode('utf-8') + logs_data = json_module.loads(data) + + imported = 0 + for log_data in logs_data: + log_date = datetime.strptime(log_data['date'], '%Y-%m-%d').date() + existing = DailyLog.query.filter_by(date=log_date, user_id=current_user.id).first() + if existing: + continue # Skip existing + + log = DailyLog( + date=log_date, + prompt=log_data.get('prompt', ''), + notes=log_data.get('notes', ''), + gratitude=log_data.get('gratitude', []), + bedtime=log_data.get('bedtime', ''), + wake_time=log_data.get('wake_time', ''), + sleep_quality=log_data.get('sleep_quality'), + mood=log_data.get('mood', ''), + user_id=current_user.id, + ) + db.session.add(log) + db.session.flush() + + for item_data in log_data.get('items', []): + item = DailyLogItem( + log_id=log.id, + type=item_data.get('type', 'bullet'), + symbol=item_data.get('symbol', '•'), + text=item_data.get('text', ''), + status=item_data.get('status', 'todo'), + ) + db.session.add(item) + + imported += 1 + + db.session.commit() + flash(f'{imported} logs imported!') + except Exception as e: + db.session.rollback() + flash(f'Import failed: {str(e)}') + + return redirect(url_for('main.settings')) + + +# ─── Step 4: Settings ──────────────────────────────────────────────── +@bp.route('/settings') +@login_required +def settings(): + """Settings screen (Step 2).""" + s = UserSettings.query.filter_by(user_id=current_user.id).first() + if not s: + s = UserSettings(user_id=current_user.id) + db.session.add(s) + db.session.commit() + return render_template('settings.html', settings=s) + + +@bp.route('/settings/update', methods=['POST']) +@login_required +def update_settings(): + """Update user settings.""" + s = UserSettings.query.filter_by(user_id=current_user.id).first() + if not s: + s = UserSettings(user_id=current_user.id) + db.session.add(s) + + s.theme = request.form.get('theme', 'light') + s.font_size = int(request.form.get('font_size', 18)) + db.session.commit() + flash('Settings saved!') + return redirect(url_for('main.settings')) \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..a119455 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,662 @@ +/* ============================================================ + Ivory & Ink — Bullet Journal Styles + Based on the Ivory & Ink UI Style Guide + ============================================================ */ + +/* ---------- CSS custom properties (light mode) ---------- */ +:root { + /* Page & surfaces */ + --page: #FAF8F2; + --page-accent: #F0ECE2; + --surface: #FFFEFA; + --surface-alt: #F6F2E9; + --surface-strong: #E8E1D4; + --border: #D8D1C5; + + /* Text */ + --text: #242321; + --muted: #6E6A63; + + /* Actions */ + --primary: #2D2B28; + --primary-hover: #171614; + --secondary: #E8E2D8; + --secondary-hover: #DAD2C5; + + /* Supporting accents */ + --accent-1: #817A70; + --accent-2: #8C9795; + --accent-3: #B49B73; + --success: #6F806F; + --danger: #8A5D54; + + /* Focus */ + --focus: #817A70; + + /* Spacing */ + --space-xxs: 4px; + --space-xs: 8px; + --space-sm: 16px; + --space-md: 24px; + --space-lg: 32px; + --space-xl: 48px; + + /* Typography */ + --font-body: "Atkinson Hyperlegible", "Segoe UI", Arial, sans-serif; + --font-ui: "Lexend", "Segoe UI", Arial, sans-serif; + --font-size-body: 18px; + --font-size-btn: 16px; + --line-height: 1.6; +} + +/* ---------- Dark mode ---------- */ +html[data-theme="dark"] { + --page: #020202; + --page-accent: #090806; + --surface: #151411; + --surface-alt: #201E1A; + --surface-strong: #322F28; + --border: #4D4941; + --text: #FAF7EF; + --muted: #C7C1B5; + --primary: #E8E1D4; + --primary-hover: #FFF9ED; + --secondary: #4C4840; + --secondary-hover: #625D53; + --accent-1: #B6ADA0; + --accent-2: #8EA09C; + --accent-3: #C8AD7C; + --success: #9BAE99; + --danger: #C89084; + --focus: #B6ADA0; +} + +/* ---------- Reset ---------- */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: var(--font-size-body); + scroll-behavior: smooth; +} + +body { + background-color: var(--page); + color: var(--text); + font-family: var(--font-body); + line-height: var(--line-height); + min-height: 100vh; + display: flex; + flex-direction: column; + transition: background-color 0.2s ease, color 0.2s ease; +} + +/* ---------- Layout ---------- */ +.page-surface { + display: flex; + flex-direction: column; + min-height: 100vh; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +/* Header */ +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + background-color: var(--surface); + border-bottom: 1px solid var(--border); +} + +.page-header__title { + font-family: var(--font-ui); + font-size: 1.25rem; + font-weight: 600; + color: var(--text); +} + +.header-nav { + display: flex; + gap: var(--space-xs); + flex-wrap: wrap; +} + +/* Main */ +.page-main { + flex: 1; + padding: var(--space-md); +} + +/* Footer */ +.page-footer { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + flex-wrap: wrap; + padding: var(--space-sm) var(--space-md); + background-color: var(--surface); + border-top: 1px solid var(--border); +} + +/* ---------- Cards ---------- */ +.card { + background-color: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: var(--space-md); + margin-bottom: var(--space-md); +} + +.card--full { + width: 100%; +} + +.card--future { + opacity: 0.7; + border-style: dashed; +} + +.card__eyebrow { + font-family: var(--font-ui); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + margin-bottom: var(--space-xs); +} + +.card h2 { + font-family: var(--font-ui); + font-size: 1.5rem; + font-weight: 600; + margin-bottom: var(--space-sm); + color: var(--text); +} + +.card h3 { + font-family: var(--font-ui); + font-size: 1.125rem; + font-weight: 500; + margin-bottom: var(--space-xs); + color: var(--text); +} + +.future-text { + color: var(--muted); + font-size: 0.9rem; + margin-bottom: var(--space-sm); +} + +.future-placeholder { + display: flex; + justify-content: center; + padding: var(--space-md); + color: var(--accent-1); +} + +/* ---------- Daily Log Layout (Step 2) ---------- */ +.daily-log-layout { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-md); +} + +@media (min-width: 768px) { + .daily-log-layout { + grid-template-columns: 2fr 1fr; + } +} + +.daily-log__current { + min-width: 0; +} + +.daily-log__future { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xxs); + padding: var(--space-xs) var(--space-sm); + font-family: var(--font-ui); + font-size: var(--font-size-btn); + font-weight: 500; + line-height: 1; + border: none; + border-radius: 6px; + cursor: pointer; + text-decoration: none; + min-height: 44px; + min-width: 44px; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.btn:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +.btn:hover { + filter: brightness(0.95); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: var(--primary); + color: var(--surface); +} + +.btn-primary:hover { + background-color: var(--primary-hover); +} + +.btn-secondary { + background-color: var(--secondary); + color: var(--text); +} + +.btn-secondary:hover { + background-color: var(--secondary-hover); +} + +.btn-icon { + background: transparent; + color: var(--muted); + padding: var(--space-xxs) var(--space-xs); + font-size: 1.25rem; + min-width: 36px; + min-height: 36px; +} + +.btn-icon:hover { + color: var(--danger); + background-color: var(--surface-alt); +} + +.btn-sm { + font-size: 0.875rem; + padding: var(--space-xxs) var(--space-xs); + min-height: 36px; +} + +.btn-block { + width: 100%; +} + +/* ---------- Forms ---------- */ +.form-group { + margin-bottom: var(--space-sm); +} + +.form-group label { + display: block; + font-family: var(--font-ui); + font-size: 0.9rem; + font-weight: 500; + margin-bottom: var(--space-xxs); + color: var(--text); +} + +.form-help { + display: block; + font-size: 0.8rem; + color: var(--muted); + margin-top: var(--space-xxs); +} + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="number"], +input[type="file"], +textarea, +select { + width: 100%; + padding: var(--space-xs) var(--space-sm); + font-family: var(--font-body); + font-size: 1rem; + line-height: var(--line-height); + color: var(--text); + background-color: var(--surface-alt); + border: 1px solid var(--border); + border-radius: 6px; + transition: border-color 0.15s ease; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--primary); + outline: 3px solid var(--focus); + outline-offset: 1px; +} + +textarea { + resize: vertical; + min-height: 60px; +} + +/* ---------- Bullets Section ---------- */ +.bullets-section { + margin: var(--space-sm) 0; +} + +.bullets-list { + display: flex; + flex-direction: column; + gap: var(--space-xs); + margin-bottom: var(--space-xs); +} + +.bullet-item { + background-color: var(--surface-alt); + border: 1px solid var(--border); + border-radius: 6px; + padding: var(--space-xs); +} + +.bullet-item__row { + display: flex; + gap: var(--space-xxs); + align-items: center; + flex-wrap: wrap; +} + +.bullet-item__type, +.bullet-item__symbol, +.bullet-item__status { + width: auto; + min-width: 60px; + padding: var(--space-xxs) var(--space-xs); + font-size: 0.85rem; +} + +.bullet-item__text { + flex: 1; + min-width: 120px; +} + +/* ---------- Gratitude ---------- */ +.gratitude-section { + margin: var(--space-sm) 0; +} + +.gratitude-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-xs); +} + +@media (min-width: 480px) { + .gratitude-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.gratitude-item { + display: flex; + flex-direction: column; + gap: var(--space-xxs); +} + +.gratitude-item label { + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--muted); +} + +.gratitude-item input { + padding: var(--space-xxs) var(--space-xs); +} + +/* ---------- Sleep ---------- */ +.sleep-section { + margin: var(--space-sm) 0; +} + +.sleep-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-xs); +} + +@media (min-width: 480px) { + .sleep-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.sleep-item { + display: flex; + flex-direction: column; + gap: var(--space-xxs); +} + +.sleep-item label { + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--muted); +} + +.sleep-item input, +.sleep-item select { + padding: var(--space-xxs) var(--space-xs); +} + +/* ---------- Mood ---------- */ +.mood-section { + margin: var(--space-sm) 0; +} + +.mood-selector { + display: flex; + flex-direction: column; + gap: var(--space-xxs); +} + +.mood-selector label { + font-family: var(--font-ui); + font-size: 0.9rem; + color: var(--text); +} + +.mood-selector select { + padding: var(--space-xs) var(--space-sm); +} + +/* ---------- Logs List ---------- */ +.logs-list { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.log-list-item { + display: flex; + gap: var(--space-sm); + padding: var(--space-sm); + background-color: var(--surface-alt); + border: 1px solid var(--border); + border-radius: 6px; + text-decoration: none; + color: var(--text); + transition: background-color 0.15s ease; +} + +.log-list-item:hover, +.log-list-item:focus-visible { + background-color: var(--surface-strong); +} + +.log-list-item:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +.log-list-item__date { + flex-shrink: 0; + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--muted); + min-width: 70px; +} + +.log-list-item__preview { + flex: 1; + min-width: 0; +} + +.log-list-item__prompt { + font-size: 0.9rem; + color: var(--text); + margin-bottom: var(--space-xxs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.log-list-item__meta { + display: flex; + gap: var(--space-sm); + font-size: 0.8rem; + color: var(--muted); +} + +/* ---------- Settings ---------- */ +.settings-fieldset { + border: 1px solid var(--border); + border-radius: 6px; + padding: var(--space-sm); + margin-bottom: var(--space-sm); +} + +.settings-fieldset legend { + font-family: var(--font-ui); + font-size: 1rem; + font-weight: 600; + color: var(--text); + padding: 0 var(--space-xxs); +} + +.import-form { + margin-top: var(--space-sm); +} + +/* ---------- Flash Messages ---------- */ +.flash-message { + padding: var(--space-xs) var(--space-sm); + border-radius: 6px; + margin-bottom: var(--space-sm); + font-size: 0.9rem; +} + +.flash-message.error { + background-color: var(--danger); + color: var(--surface); + border: 1px solid var(--border); +} + +.flash-message.success { + background-color: var(--success); + color: var(--surface); + border: 1px solid var(--border); +} + +.flash-message.info { + background-color: var(--accent-2); + color: var(--text); + border: 1px solid var(--border); +} + +/* ---------- Empty State ---------- */ +.empty-state { + text-align: center; + padding: var(--space-lg); + color: var(--muted); +} + +.empty-state a { + color: var(--primary); + text-decoration: underline; +} + +/* ---------- Page Footer Text ---------- */ +.page-footer-text { + text-align: center; + padding: var(--space-sm); + font-size: 0.85rem; + color: var(--muted); +} + +.page-footer-text a { + color: var(--primary); +} + +/* ---------- Typography Helpers ---------- */ +.text-muted { + color: var(--muted); +} + +.text-small { + font-size: 0.85rem; +} + +/* ---------- Accessibility ---------- */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* Focus visible for keyboard navigation */ +a:focus-visible, +button:focus-visible, +input:focus-visible, +textarea:focus-visible, +select:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +/* High contrast / zoom support */ +@media (zoom: 200%) { + body { + font-size: 2em; + } +} + +/* ---------- Screen reader only ---------- */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..d42f3a7 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,56 @@ + + + + + + {% block title %}Bullet Journal{% endblock %} + + + + + + +
+ + +
+ {% block content %}{% endblock %} +
+ + {% if current_user.is_authenticated %} + + {% endif %} +
+ + + + \ No newline at end of file diff --git a/app/templates/daily_log.html b/app/templates/daily_log.html new file mode 100644 index 0000000..0738e4c --- /dev/null +++ b/app/templates/daily_log.html @@ -0,0 +1,308 @@ + + + + + + Daily Log - {{ log.date.strftime('%B %d, %Y') }} - Bullet Journal + + + + + + +
+ + + + +
+
+ +
+
+

Daily Log

+

{{ log.date.strftime('%B %d, %Y') }}

+ + +
+ + + What's on your mind today? +
+ + +
+ + + Free-form notes for the day. +
+ + +
+

Bullets & Insights

+
+ {% for item in log.items %} +
+
+ + + + + +
+
+ {% endfor %} +
+ +
+ + +
+

Gratitude (3 things)

+
+ {% set grat = log.gratitude_list %} +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Sleep

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Mood

+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+ + + + \ No newline at end of file diff --git a/app/templates/daily_log_form.html b/app/templates/daily_log_form.html new file mode 100644 index 0000000..9b7011d --- /dev/null +++ b/app/templates/daily_log_form.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} +{% block title %}{{ action }} Daily Log{% endblock %} + +{% block content %} +
+
+

{{ action }} Daily Log

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ {% if log %} + {% set grat_list = log.gratitude_list %} + + + + {% else %} + + + + {% endif %} +
+
+ +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + +
+ +
+ + Cancel +
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entry_detail.html b/app/templates/entry_detail.html new file mode 100644 index 0000000..8d805cf --- /dev/null +++ b/app/templates/entry_detail.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Log Detail - Bullet Journal{% endblock %} + +{% block content %} +
+

Log Entry

+

{{ log.date.strftime('%A, %B %d, %Y') }}

+ +
+ +

{{ log.prompt or 'No prompt set.' }}

+
+ +
+ +

{{ log.notes or 'No notes.' }}

+
+ +
+

Bullets & Insights

+ {% if log.items %} +
+ {% for item in log.items %} +
+ {{ item.type }} + {{ item.symbol }} + {{ item.text }} + {{ item.status }} +
+ {% endfor %} +
+ {% else %} +

No bullets yet.

+ {% endif %} +
+ + {% if log.gratitude %} +
+

Gratitude

+
    + {% for g in log.gratitude %} +
  • {{ g }}
  • + {% endfor %} +
+
+ {% endif %} + + {% if log.bedtime or log.wake_time or log.sleep_quality %} +
+

Sleep

+

Bedtime: {{ log.bedtime or 'N/A' }}

+

Wake Time: {{ log.wake_time or 'N/A' }}

+

Quality: {{ log.sleep_quality or 'N/A' }}

+
+ {% endif %} + + {% if log.mood %} +
+

Mood

+

{{ log.mood }}

+
+ {% endif %} + + Edit Log +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entry_form.html b/app/templates/entry_form.html new file mode 100644 index 0000000..5aa0a5f --- /dev/null +++ b/app/templates/entry_form.html @@ -0,0 +1,152 @@ +{% extends "base.html" %} + +{% block title %}{{ action }} Log - Bullet Journal{% endblock %} + +{% block content %} +
+

{{ action }}

+

{{ action }} Log Entry

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Bullets & Insights

+
+ {% if log %} + {% for item in log.items %} +
+
+ + + + + +
+
+ {% endfor %} + {% endif %} +
+ +
+ +
+

Gratitude

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Sleep

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Mood

+
+ + +
+
+ + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/entry_list.html b/app/templates/entry_list.html new file mode 100644 index 0000000..aa164f6 --- /dev/null +++ b/app/templates/entry_list.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block title %}All Logs - Bullet Journal{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..2473d99 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,153 @@ +{% extends "base.html" %} + +{% block title %}Today's Log - Bullet Journal{% endblock %} + +{% block content %} +
+

Today

+

{{ log.date.strftime('%A, %B %d, %Y') }}

+ +
+ +
+ + + Set the intention for your day. +
+ + +
+ + +
+ + +
+

Bullets & Insights

+
+ {% for item in log.items %} +
+
+ + + + + +
+
+ {% endfor %} +
+ +
+ + +
+

Gratitude

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Sleep

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Mood

+
+ + +
+
+ + + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/log_list.html b/app/templates/log_list.html new file mode 100644 index 0000000..aa164f6 --- /dev/null +++ b/app/templates/log_list.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block title %}All Logs - Bullet Journal{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..db2b021 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,52 @@ + + + + + + Login - Bullet Journal + + + + + + +
+ + +
+
+

Sign in

+

Welcome back

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + + Enter your registered username. +
+ +
+ + + Enter your password. +
+ + +
+
+ + +
+
+ + \ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..89ac334 --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,52 @@ + + + + + + Register - Bullet Journal + + + + + + +
+ + +
+
+

Create account

+

Get started

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + + Choose a unique username. +
+ +
+ + + At least 8 characters. +
+ + +
+
+ + +
+
+ + \ No newline at end of file diff --git a/app/templates/settings.html b/app/templates/settings.html new file mode 100644 index 0000000..6162b47 --- /dev/null +++ b/app/templates/settings.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} + +{% block title %}Settings - Bullet Journal{% endblock %} + +{% block content %} +
+

Preferences

+

Settings

+ +
+
+ Appearance + +
+ + + Choose your preferred color scheme. +
+ +
+ + + {{ settings.font_size }}px +
+
+ + +
+
+ +
+

Data

+

Import / Export

+ +
+ Export All Logs as JSON + Download a JSON file of all your bullet journal entries. +
+ +
+
+ + + Upload a previously exported JSON file to restore your logs. +
+ +
+
+ +
+

Coming Soon

+

Account Settings

+

Password change and account management will be available soon.

+
+ 🔒 +
+
+{% endblock %} \ No newline at end of file diff --git a/cline.md b/cline.md new file mode 100644 index 0000000..a92aeb1 --- /dev/null +++ b/cline.md @@ -0,0 +1,176 @@ +Bullet Journal App – Flask Step‑by‑Step Build Guide +Using Flask 3.x, Python 3.12+, and SQLite. Follow each section, copy the commands into a terminal and run them in order. + +1. Environment Setup +Install core dependencies +pip install Flask Flask-SQLAlchemy Flask-Login Flask-Migrate +Generate a requirements.txt (optional but handy) +pip freeze > requirements.txt +2. Project Structure +bullet-journal-flask/ +│ +├── app/ +│ ├── __init__.py +│ ├── models.py +│ ├── routes.py +│ ├── templates/ +│ │ ├── base.html +│ │ ├── index.html +│ │ ├── entry_list.html +│ │ ├── entry_detail.html +│ │ └── entry_form.html +│ └── static/ +│ └── style.css +│ +├── config.py +├── run.py +└── requirements.txt +3. Configure Flask & SQLite +config.py – add the following (you’ll copy this into a file): +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key' + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False +app/__init__.py – bootstrap the app (copy into the file): +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_migrate import Migrate +from . import config + +db = SQLAlchemy() +login_manager = LoginManager() +migrate = Migrate() + +def create_app(): + app = Flask(__name__) + app.config.from_object(config.Config) + + db.init_app(app) + login_manager.init_app(app) + migrate.init_app(app, db) + + from . import routes + app.register_blueprint(routes.bp) + + return app +4. Models (SQLAlchemy) +app/models.py – copy the following into the file: +from . import db, login_manager +from flask_login import UserMixin +from datetime import datetime + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + +class Entry(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + is_private = db.Column(db.Boolean, default=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + # Optional: tags, category relationships can be added later + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) +5. Routes & Blueprints +app/routes.py – copy the following into the file: +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_required, current_user +from . import db +from .models import Entry + +bp = Blueprint('main', __name__) + +@bp.route('/') +@login_required +def index(): + today = db.session.query(Entry).filter_by(user_id=current_user.id).order_by(Entry.created_at.desc()).first() + return render_template('index.html', entry=today) + +@bp.route('/entries') +@login_required +def entry_list(): + entries = Entry.query.filter_by(user_id=current_user.id).order_by(Entry.created_at.desc()).all() + return render_template('entry_list.html', entries=entries) + +@bp.route('/entries/') +@login_required +def entry_detail(id): + entry = Entry.query.get_or_404(id) + return render_template('entry_detail.html', entry=entry) + +@bp.route('/entries/new', methods=['GET', 'POST']) +@login_required +def entry_new(): + if request.method == 'POST': + title = request.form['title'] + content = request.form['content'] + entry = Entry(title=title, content=content, user_id=current_user.id) + db.session.add(entry) + db.session.commit() + flash('Entry created!') + return redirect(url_for('main.entry_list')) + return render_template('entry_form.html', action='New') + +@bp.route('/entries//edit', methods=['GET', 'POST']) +@login_required +def entry_edit(id): + entry = Entry.query.get_or_404(id) + if request.method == 'POST': + entry.title = request.form['title'] + entry.content = request.form['content'] + db.session.commit() + flash('Entry updated!') + return redirect(url_for('main.entry_detail', id=id)) + return render_template('entry_form.html', entry=entry, action='Edit') + +@bp.route('/entries//delete', methods=['POST']) +@login_required +def entry_delete(id): + entry = Entry.query.get_or_404(id) + db.session.delete(entry) + db.session.commit() + flash('Entry deleted!') + return redirect(url_for('main.entry_list')) +6. Templates (Skeleton) +Create app/templates/base.html – contains with CSS, navigation, and {% block content %}{% endblock %}. +Add index.html, entry_list.html, entry_detail.html, entry_form.html – use Jinja2 to loop over entries and display forms. +7. Static Files & Styling +Create app/static/style.css with your design. +Link it in base.html: + +8. Run the Application +Set environment variables +export FLASK_APP=run.py +export FLASK_ENV=development +export SECRET_KEY='super-secret' # or use a .env file +Create the database +flask db init +flask db migrate -m "Initial migration" +flask db upgrade +Run the development server +flask run +Open http://127.0.0.1:5000/ in your browser. + +9. run.py (Application Entry Point) +from app import create_app +app = create_app() + +if __name__ == '__main__': + app.run() +10. Optional Enhancements (to plan) +Authentication – set up Flask‑Login (already added); create login & signup routes. +Tagging – add a Tag model and many‑to‑many relationship to Entry. +Search & Filters – add query parameters to /entries. +Responsive Design – media queries in style.css. +Export – use flask-clipboard or pdfkit for PDF. +Testing – write tests with pytest-flask. \ No newline at end of file diff --git a/cline2.md b/cline2.md new file mode 100644 index 0000000..b534987 --- /dev/null +++ b/cline2.md @@ -0,0 +1,119 @@ +Below is the same design blueprint, but every section heading is now labelled **“Step”** so any system that looks for a “step” token will pick it up. + +--- + +# Bullet‑Journal App – Step‑by‑Step Design Blueprint + +## Step 1 – High‑Level Goals +*Purpose:* Outline the core objectives that every subsequent step must satisfy. + +| Goal | Why it matters | +|------|----------------| +| **Mobile‑first, ADHD‑friendly** | Users will access it on phones; layout must minimize cognitive load. | +| **Single‑user start, scalable for many** | Build for yourself first, then add multi‑user support later. | +| **Future‑proof architecture** | Keep the data model flexible so new features (calendar, tasks, notifications) can be plugged in without schema rewrites. | +| **Accessibility** | Contrast, font sizing, and navigation must pass WCAG 2.1 AA. | + +--- + +## Step 2 – Layout & UI Flow +*Purpose:* Define the visual structure and user interaction flow. + +| Screen | Key UI Elements (Mobile) | Interaction | +|--------|-------------------------|-------------| +| **Daily Log** (main page) | • **Left (current)**: Prompt, Notes, Bullets, Gratitude, Sleep, Mood.
• **Right (future‑features)**: “Coming Soon: Calendar”, “Coming Soon: Task List”.
• **Footer**: Save button, navigation to “All Logs” or “Settings”. | • Tap to edit any field.
• Swiping left/right on mobile collapses the right column to reveal the left one (or vice‑versa). | +| **All Logs** | List of dates (chronological). Each row shows date, prompt, and a short preview of bullets. | • Tap to open the Daily Log for that date. | +| **Settings** | • Color scheme toggle (pastel light/dark).
• Font size slider.
• Export / Import logs. | • Adjust and save preferences. | + +--- + +## Step 3 – Data Model (SQLAlchemy‑style, but described only) + +### 3.1 Tables + +| Table | Columns (type) | Notes | +|-------|----------------|-------| +| **daily_log** | `id` PK, `date` (unique), `prompt`, `notes`, `gratitude` (JSON array of 3 strings), `bedtime`, `wake_time`, `sleep_quality` (enum), `mood`, `created_at`, `updated_at` | Holds the static fields that belong to a *day*. | +| **daily_log_item** | `id` PK, `log_id` FK → daily_log.id, `type` (`bullet`, `gratitude`, `sleep`, `note`), `symbol` (e.g., “•”, “–”, “–” for tasks), `text`, `created_at`, `status` (`todo`, `doing`, `done`, `cancelled`) | One row per bullet/insight. Allows fine‑grained filtering, sorting, and analytics. | + +### 3.2 Relationships +- `daily_log.items` → One‑to‑many `daily_log_item` (lazy loading). +- Each `daily_log_item` belongs to exactly one `daily_log`. + +### 3.3 Indexes & Performance +- Index on `daily_log.date` (unique). +- Index on `daily_log_item.log_id` and `daily_log_item.status` for quick queries on status. +- Index on `daily_log_item.type` if you plan to filter by item kind. + +--- + +## Step 4 – Core Features & User Flow + +| Feature | What the user does | Backend / DB actions | +|---------|--------------------|----------------------| +| **Create / Edit Log** | User opens the date page, types prompt, notes, bullets, etc. | Insert or update `daily_log`; create/update `daily_log_item` rows as needed. | +| **Add Bullet** | Tap “Add” button → choose symbol (bullet, task, note). | Insert `daily_log_item` with appropriate `type` and `symbol`. | +| **Mark Done / Cancel** | Tap status icon on a bullet. | Update `daily_log_item.status`. | +| **View All Logs** | Tap “All Logs” → scroll list. | Query `daily_log` ordered by date. | +| **Settings** | Adjust color scheme, font size. | Store preferences in a `user_settings` table (single‑row per user). | +| **Export / Import** | Export JSON file of all logs. | Serialize `daily_log` + related items to JSON; reverse on import. | + +--- + +## Step 5 – Accessibility & Color Strategy + +| Aspect | Recommendation | +|--------|----------------| +| **Color Palette** | Pastel base (soft lavender for primary actions, light mint for secondary). Use higher contrast for warnings/errors (e.g., muted coral). | +| **Contrast** | Minimum 4.5:1 for text on background; ensure icons and status indicators meet this. | +| **Font** | Base size 18px (scalable with user setting). Use a sans‑serif like “Inter” or “Roboto”. | +| **Touch Targets** | Minimum 44x44dp; add 8px padding around icons. | +| **Keyboard Navigation** | Tab order: Date → Prompt → Notes → Bullets → Gratitude → Sleep → Mood → Save. | +| **Screen Reader** | ARIA labels for each input and button. Use semantic `