feat: integrate user manual into help section
This commit is contained in:
@@ -16,6 +16,9 @@ Backend:
|
||||
- HTTP Basic Auth is implemented with `Flask-HTTPAuth`.
|
||||
- Users are currently configured in `BETA_USERS` with Werkzeug password hashes.
|
||||
- `/` renders the active calculator template.
|
||||
- `/help` renders the authenticated Help landing page.
|
||||
- `/help/user-manual` renders `docs/de/user_manual.md` server-side inside the RollCalc Help layout.
|
||||
- `/help/user-manual/screenshots/<path:filename>` serves German manual screenshots from `docs/de/screenshots/`.
|
||||
- `/static/<path:filename>` is intended to serve static files behind Basic Auth.
|
||||
- `/api/health` returns health/version information.
|
||||
- `/api/user` returns the authenticated user.
|
||||
@@ -50,6 +53,30 @@ If `build_info.json` is missing, malformed, or does not contain a usable value,
|
||||
|
||||
The data is loaded centrally in `app.py` and made available to every template as `build_info`.
|
||||
|
||||
## Help and User Manual
|
||||
|
||||
The in-app Help area keeps `docs/de/user_manual.md` as the leading German documentation source. RollCalc reads the Markdown file on each `/help/user-manual` request and renders it server-side with `markdown-it-py`.
|
||||
|
||||
The documentation structure is prepared for language-specific manuals:
|
||||
|
||||
```text
|
||||
docs/
|
||||
de/
|
||||
user_manual.md
|
||||
screenshots/
|
||||
```
|
||||
|
||||
Additional languages can follow the same structure later. The current public route still opens the German manual and no language switcher is implemented.
|
||||
|
||||
Rendering rules:
|
||||
|
||||
- Raw HTML from the Markdown source is disabled.
|
||||
- Heading ids and the table of contents are generated from the Markdown content.
|
||||
- Relative image references such as `screenshots/disclaimer.png` are rewritten to the authenticated manual screenshot route.
|
||||
- Screenshots belong in the active language's `screenshots/` directory, currently `docs/de/screenshots/`; the screenshot route must remain limited to that directory.
|
||||
|
||||
The Help pages use the same HTTP Basic Auth protection as the calculator.
|
||||
|
||||
## Authentication
|
||||
|
||||
RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape:
|
||||
|
||||
@@ -9,6 +9,7 @@ This README is intended for developers maintaining the project, not for end user
|
||||
- Python 3
|
||||
- Flask 2.3.3
|
||||
- Flask-HTTPAuth 4.8.0
|
||||
- markdown-it-py 3.0.0 for server-side Markdown rendering
|
||||
- Vanilla HTML, CSS, and JavaScript
|
||||
- JSON files for product data and forklift/load rules
|
||||
|
||||
@@ -58,7 +59,9 @@ The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.
|
||||
├── fix_article_data.py
|
||||
├── service-worker.js
|
||||
├── templates/
|
||||
│ └── roll_calculator.html
|
||||
│ ├── help_index.html
|
||||
│ ├── roll_calculator.html
|
||||
│ └── user_manual.html
|
||||
├── static/
|
||||
│ ├── article-data.json
|
||||
│ ├── config.json
|
||||
@@ -71,6 +74,9 @@ The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.
|
||||
│ ├── rollcalc_stddev_ranges.js
|
||||
│ └── rollcalc_stddev_integration.js
|
||||
└── docs/
|
||||
├── de/
|
||||
│ ├── screenshots/
|
||||
│ └── user_manual.md
|
||||
├── DEPLOYMENT_GUIDE.md
|
||||
├── QOL_UPDATE_SUMMARY.md
|
||||
├── STDDEV_IMPLEMENTATION_CHECKLIST.md
|
||||
@@ -86,6 +92,9 @@ Implemented routes:
|
||||
| Route | Methods | Auth | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `/` | `GET` | Basic Auth | Renders `templates/roll_calculator.html`. |
|
||||
| `/help` | `GET` | Basic Auth | Renders the Help landing page. |
|
||||
| `/help/user-manual` | `GET` | Basic Auth | Reads `docs/de/user_manual.md`, renders it as HTML, and displays it in the RollCalc Help layout. |
|
||||
| `/help/user-manual/screenshots/<path:filename>` | `GET` | Basic Auth | Serves German user-manual screenshots from `docs/de/screenshots/` only. |
|
||||
| `/static/<path:filename>` | `GET` | Basic Auth | Intended protected static-file serving from `static/`. |
|
||||
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
||||
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
||||
@@ -109,6 +118,41 @@ Do not commit real passwords or print them in logs.
|
||||
|
||||
Access logging is handled by `log_access()`, which reads `access_log.json`, appends a record, and writes the whole file back.
|
||||
|
||||
## Help and User Manual
|
||||
|
||||
RollCalc exposes an authenticated Help area at:
|
||||
|
||||
```text
|
||||
/help
|
||||
```
|
||||
|
||||
The user manual is available at:
|
||||
|
||||
```text
|
||||
/help/user-manual
|
||||
```
|
||||
|
||||
`docs/de/user_manual.md` is the leading source for the German user manual. The app reads this Markdown file on request and renders it server-side with `markdown-it-py`; no generated static HTML copy is maintained. Raw HTML in the Markdown source is disabled during rendering.
|
||||
|
||||
The documentation directory is prepared for additional languages:
|
||||
|
||||
```text
|
||||
docs/
|
||||
de/
|
||||
user_manual.md
|
||||
screenshots/
|
||||
```
|
||||
|
||||
The public route `/help/user-manual` currently opens the German manual. No language switcher is implemented yet.
|
||||
|
||||
Screenshots referenced from the manual as `screenshots/filename.png` should be placed in:
|
||||
|
||||
```text
|
||||
docs/de/screenshots/
|
||||
```
|
||||
|
||||
The application rewrites those relative Markdown image paths to the authenticated manual screenshot route, which is limited to the active language's screenshot directory.
|
||||
|
||||
## Frontend Entry Point
|
||||
|
||||
The active UI is `templates/roll_calculator.html`.
|
||||
|
||||
@@ -3,13 +3,26 @@ Naue Roll Calculator - Beta
|
||||
Flask app with HTTP Basic Authentication
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, request, send_file, send_from_directory, jsonify
|
||||
from flask import (
|
||||
Flask,
|
||||
abort,
|
||||
jsonify,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
send_from_directory,
|
||||
url_for,
|
||||
)
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from markdown_it import MarkdownIt
|
||||
from markupsafe import Markup
|
||||
from werkzeug.security import check_password_hash
|
||||
from functools import wraps
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
app = Flask(__name__)
|
||||
auth = HTTPBasicAuth()
|
||||
@@ -43,6 +56,9 @@ BETA_USERS = {
|
||||
# Logging für Auditing
|
||||
LOG_FILE = "access_log.json"
|
||||
BUILD_INFO_FILE = "build_info.json"
|
||||
DOCS_DIR = os.path.join(app.root_path, "docs")
|
||||
DEFAULT_DOC_LANGUAGE = "de"
|
||||
USER_MANUAL_FILENAME = "user_manual.md"
|
||||
UNKNOWN_BUILD_INFO = {
|
||||
"version": "unknown",
|
||||
"branch": "unknown",
|
||||
@@ -120,6 +136,83 @@ def log_access(username, endpoint, method, status=200):
|
||||
# ROUTES
|
||||
# ============================================================================
|
||||
|
||||
def slugify_heading(text):
|
||||
"""Create stable, URL-friendly heading ids for rendered Markdown."""
|
||||
normalized = unicodedata.normalize("NFKD", text)
|
||||
ascii_text = normalized.encode("ascii", "ignore").decode("ascii")
|
||||
slug = re.sub(r"[^a-zA-Z0-9]+", "-", ascii_text).strip("-").lower()
|
||||
return slug or "section"
|
||||
|
||||
|
||||
def build_manual_renderer():
|
||||
"""Build a Markdown renderer with raw HTML disabled."""
|
||||
renderer = MarkdownIt("commonmark", {"html": False})
|
||||
renderer.enable("table")
|
||||
return renderer
|
||||
|
||||
|
||||
def get_manual_path(language=DEFAULT_DOC_LANGUAGE):
|
||||
"""Return the Markdown path for a language-specific manual."""
|
||||
return os.path.join(DOCS_DIR, language, USER_MANUAL_FILENAME)
|
||||
|
||||
|
||||
def get_manual_screenshot_dir(language=DEFAULT_DOC_LANGUAGE):
|
||||
"""Return the screenshot directory for a language-specific manual."""
|
||||
return os.path.join(DOCS_DIR, language, "screenshots")
|
||||
|
||||
|
||||
def rewrite_manual_asset_paths(markdown_text, language=DEFAULT_DOC_LANGUAGE):
|
||||
"""Route relative manual screenshot links through the protected docs route."""
|
||||
screenshot_url = url_for("manual_screenshot", filename="")
|
||||
return markdown_text.replace("](screenshots/", f"]({screenshot_url}")
|
||||
|
||||
|
||||
def add_heading_ids_and_toc(tokens):
|
||||
"""Attach ids to headings and create a compact table of contents."""
|
||||
toc = []
|
||||
used_slugs = {}
|
||||
|
||||
for index, token in enumerate(tokens):
|
||||
if token.type != "heading_open":
|
||||
continue
|
||||
|
||||
level = int(token.tag[1])
|
||||
inline_token = tokens[index + 1] if index + 1 < len(tokens) else None
|
||||
title = (
|
||||
inline_token.content
|
||||
if inline_token and inline_token.type == "inline"
|
||||
else ""
|
||||
)
|
||||
base_slug = slugify_heading(title)
|
||||
count = used_slugs.get(base_slug, 0)
|
||||
used_slugs[base_slug] = count + 1
|
||||
slug = base_slug if count == 0 else f"{base_slug}-{count + 1}"
|
||||
token.attrSet("id", slug)
|
||||
|
||||
if level <= 2:
|
||||
toc.append({
|
||||
"level": level,
|
||||
"title": title,
|
||||
"id": slug
|
||||
})
|
||||
|
||||
return toc
|
||||
|
||||
|
||||
def render_user_manual(language=DEFAULT_DOC_LANGUAGE):
|
||||
"""Read and render the Markdown user manual into HTML and a generated TOC."""
|
||||
renderer = build_manual_renderer()
|
||||
manual_path = get_manual_path(language)
|
||||
|
||||
with open(manual_path, "r", encoding="utf-8") as manual_file:
|
||||
markdown_text = manual_file.read()
|
||||
|
||||
markdown_text = rewrite_manual_asset_paths(markdown_text, language)
|
||||
tokens = renderer.parse(markdown_text)
|
||||
toc = add_heading_ids_and_toc(tokens)
|
||||
html = renderer.renderer.render(tokens, renderer.options, {})
|
||||
return Markup(html), toc
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@auth.login_required
|
||||
def index():
|
||||
@@ -127,6 +220,53 @@ def index():
|
||||
log_access(auth.current_user(), "/", "GET")
|
||||
return render_template("roll_calculator.html")
|
||||
|
||||
@app.route("/help", methods=["GET"])
|
||||
@auth.login_required
|
||||
def help_index():
|
||||
"""Help landing page - requires authentication."""
|
||||
log_access(auth.current_user(), "/help", "GET")
|
||||
return render_template("help_index.html")
|
||||
|
||||
@app.route("/help/user-manual", methods=["GET"])
|
||||
@auth.login_required
|
||||
def user_manual():
|
||||
"""Render the Markdown user manual inside the RollCalc layout."""
|
||||
log_access(auth.current_user(), "/help/user-manual", "GET")
|
||||
try:
|
||||
manual_html, toc = render_user_manual(DEFAULT_DOC_LANGUAGE)
|
||||
return render_template(
|
||||
"user_manual.html",
|
||||
manual_html=manual_html,
|
||||
toc=toc,
|
||||
render_error=None
|
||||
)
|
||||
except Exception:
|
||||
app.logger.exception("Could not render user manual")
|
||||
return render_template(
|
||||
"user_manual.html",
|
||||
manual_html=None,
|
||||
toc=[],
|
||||
render_error=(
|
||||
"The user manual could not be loaded. "
|
||||
"Please contact the RollCalc maintainer if the problem persists."
|
||||
)
|
||||
), 500
|
||||
|
||||
@app.route("/help/user-manual/screenshots/<path:filename>", methods=["GET"])
|
||||
@auth.login_required
|
||||
def manual_screenshot(filename, language=DEFAULT_DOC_LANGUAGE):
|
||||
"""Serve user-manual screenshots from the selected language directory."""
|
||||
log_access(
|
||||
auth.current_user(),
|
||||
f"/help/user-manual/screenshots/{filename}",
|
||||
"GET"
|
||||
)
|
||||
try:
|
||||
return send_from_directory(get_manual_screenshot_dir(language), filename)
|
||||
except Exception:
|
||||
app.logger.warning("Missing or inaccessible manual screenshot: %s", filename)
|
||||
abort(404)
|
||||
|
||||
@app.route("/static/<path:filename>", methods=["GET"])
|
||||
@auth.login_required
|
||||
def serve_static(filename):
|
||||
|
||||
@@ -5,3 +5,4 @@ click==8.1.7
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
markdown-it-py==3.0.0
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="RollCalc Help">
|
||||
<title>RollCalc Help</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #2f3437; min-height: 100vh; }
|
||||
.help-header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
||||
.help-header-inner { max-width: 980px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||
.help-header h1 { font-size: 24px; margin-bottom: 5px; }
|
||||
.help-header p { font-size: 13px; opacity: 0.9; }
|
||||
.help-nav { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.help-nav a { color: #fff; border: 1px solid rgba(255,255,255,0.58); border-radius: 5px; padding: 7px 13px; text-decoration: none; font-size: 13px; font-weight: 700; background: rgba(255,255,255,0.08); }
|
||||
.help-nav a:hover { background: rgba(255,255,255,0.18); }
|
||||
.help-container { max-width: 980px; margin: 30px auto; padding: 0 16px 60px; }
|
||||
.help-intro { margin-bottom: 22px; line-height: 1.55; color: #4b555a; }
|
||||
.help-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
|
||||
.help-card { background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
||||
.help-card h2 { color: #1F5438; font-size: 18px; margin-bottom: 10px; }
|
||||
.help-card p { color: #4f5b60; font-size: 14px; line-height: 1.55; margin-bottom: 18px; }
|
||||
.help-card a { display: inline-block; background: #2BAC70; color: #fff; text-decoration: none; border-radius: 6px; padding: 10px 16px; font-size: 14px; font-weight: 700; }
|
||||
.help-card a:hover { background: #1F5438; }
|
||||
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; }
|
||||
.footer-build-info { margin-top: 6px; color: #bbb; }
|
||||
@media (max-width: 640px) {
|
||||
.help-header { padding: 18px 16px; }
|
||||
.help-header-inner { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="help-header">
|
||||
<div class="help-header-inner">
|
||||
<div>
|
||||
<h1>RollCalc Help</h1>
|
||||
<p>Documentation for roll calculation and transport capacity analysis</p>
|
||||
</div>
|
||||
<nav class="help-nav" aria-label="Help navigation">
|
||||
<a href="{{ url_for('index') }}">Calculator</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="help-container">
|
||||
<p class="help-intro">
|
||||
This area provides internal technical documentation for RollCalc. The documents are rendered from the maintained Markdown sources.
|
||||
</p>
|
||||
|
||||
<section class="help-grid" aria-label="Available help documents">
|
||||
<article class="help-card">
|
||||
<h2>User Manual</h2>
|
||||
<p>
|
||||
Workflow-oriented manual for roll calculation, handling evaluation, Load Optimizer usage, and interpretation of Capacity Summary and Capacity Analysis.
|
||||
</p>
|
||||
<a href="{{ url_for('user_manual') }}">Open User Manual</a>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
RollCalc Beta © Naue
|
||||
<div class="footer-build-info">
|
||||
Version {{ build_info.version }} |
|
||||
Branch {{ build_info.branch }} |
|
||||
Commit {{ build_info.commit }} |
|
||||
Built {{ build_info.timestamp }}
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,8 +9,11 @@
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #333; min-height: 100vh; }
|
||||
.header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
||||
.header-inner { max-width: 900px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||
.header h1 { font-size: 24px; margin-bottom: 5px; }
|
||||
.header p { font-size: 13px; opacity: 0.9; }
|
||||
.header-help-link { color: #fff; border: 1px solid rgba(255,255,255,0.58); border-radius: 5px; padding: 7px 13px; text-decoration: none; font-size: 13px; font-weight: 700; background: rgba(255,255,255,0.08); }
|
||||
.header-help-link:hover { background: rgba(255,255,255,0.18); }
|
||||
.container { max-width: 900px; margin: 30px auto; padding: 0 16px 60px; }
|
||||
.card { background: #fff; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); margin-bottom: 24px; overflow: hidden; }
|
||||
.card-header { background: #f0f4fa; padding: 16px 22px; border-bottom: 1px solid #e0e8f0; }
|
||||
@@ -448,6 +451,8 @@
|
||||
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header { padding: 18px 16px; }
|
||||
.header-inner { align-items: flex-start; }
|
||||
.lo-accordion-header h3 { font-size: 14px; }
|
||||
.lo-preset-buttons { flex-direction: column; }
|
||||
.lo-preset-btn { width: 100%; }
|
||||
@@ -492,9 +497,14 @@
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-inner">
|
||||
<div>
|
||||
<h1>Naue Roll Diameter Calculator</h1>
|
||||
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
|
||||
</div>
|
||||
<a class="header-help-link" href="{{ url_for('help_index') }}">Help</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="RollCalc User Manual">
|
||||
<title>RollCalc User Manual</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #2f3437; min-height: 100vh; }
|
||||
.help-header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
||||
.help-header-inner { max-width: 1180px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||
.help-header h1 { font-size: 24px; margin-bottom: 5px; }
|
||||
.help-header p { font-size: 13px; opacity: 0.9; }
|
||||
.help-nav { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.help-nav a { color: #fff; border: 1px solid rgba(255,255,255,0.58); border-radius: 5px; padding: 7px 13px; text-decoration: none; font-size: 13px; font-weight: 700; background: rgba(255,255,255,0.08); }
|
||||
.help-nav a:hover { background: rgba(255,255,255,0.18); }
|
||||
.manual-shell { max-width: 1180px; margin: 28px auto; padding: 0 16px 60px; }
|
||||
.manual-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
|
||||
.manual-toc { position: sticky; top: 18px; background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.05); max-height: calc(100vh - 36px); overflow: auto; }
|
||||
.manual-toc h2 { color: #1F5438; font-size: 14px; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0; }
|
||||
.manual-toc ul { list-style: none; }
|
||||
.manual-toc li { margin: 0 0 8px; }
|
||||
.manual-toc a { color: #34443b; text-decoration: none; font-size: 13px; line-height: 1.35; }
|
||||
.manual-toc a:hover { color: #1F5438; text-decoration: underline; }
|
||||
.manual-toc .toc-level-2 { padding-left: 14px; }
|
||||
.manual-content { background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 34px 42px; box-shadow: 0 2px 12px rgba(0,0,0,0.05); line-height: 1.62; font-size: 15px; }
|
||||
.manual-content h1, .manual-content h2, .manual-content h3, .manual-content h4 { color: #1F5438; line-height: 1.25; scroll-margin-top: 18px; }
|
||||
.manual-content h1 { font-size: 30px; margin: 0 0 24px; padding-bottom: 14px; border-bottom: 2px solid #dfe7e2; }
|
||||
.manual-content h2 { font-size: 22px; margin: 34px 0 14px; padding-top: 4px; }
|
||||
.manual-content h3 { font-size: 17px; margin: 26px 0 10px; color: #244b39; }
|
||||
.manual-content h4 { font-size: 15px; margin: 20px 0 8px; color: #244b39; }
|
||||
.manual-content p { margin: 0 0 14px; }
|
||||
.manual-content ul, .manual-content ol { margin: 0 0 16px 24px; }
|
||||
.manual-content li { margin: 5px 0; }
|
||||
.manual-content a { color: #1F5438; font-weight: 600; }
|
||||
.manual-content code { background: #eef4f1; border: 1px solid #d7e4de; border-radius: 4px; padding: 1px 4px; font-family: Consolas, 'Courier New', monospace; font-size: 0.92em; }
|
||||
.manual-content pre { background: #f5f7f6; border: 1px solid #dfe7e2; border-radius: 6px; padding: 14px; margin: 16px 0; overflow-x: auto; }
|
||||
.manual-content pre code { background: transparent; border: 0; padding: 0; }
|
||||
.manual-content blockquote { margin: 18px 0; padding: 14px 18px; border-left: 4px solid #2BAC70; background: #f1f8f5; color: #34443b; }
|
||||
.manual-content blockquote p:last-child, .manual-content blockquote ul:last-child { margin-bottom: 0; }
|
||||
.manual-content table { width: 100%; border-collapse: collapse; margin: 18px 0; display: block; overflow-x: auto; }
|
||||
.manual-content th, .manual-content td { border: 1px solid #d7e4de; padding: 9px 11px; text-align: left; vertical-align: top; }
|
||||
.manual-content th { background: #eef4f1; color: #1F5438; }
|
||||
.manual-content img { max-width: 100%; height: auto; border: 1px solid #dfe7e2; border-radius: 6px; background: #f8fafa; display: block; margin: 18px 0 8px; }
|
||||
.manual-error { background: #fff; border: 1px solid #e6c7c7; border-left: 4px solid #b84a4a; border-radius: 8px; padding: 20px; color: #5a2d2d; box-shadow: 0 2px 12px rgba(0,0,0,0.05); }
|
||||
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; }
|
||||
.footer-build-info { margin-top: 6px; color: #bbb; }
|
||||
@media (max-width: 840px) {
|
||||
.help-header { padding: 18px 16px; }
|
||||
.help-header-inner { align-items: flex-start; flex-direction: column; }
|
||||
.manual-layout { grid-template-columns: 1fr; }
|
||||
.manual-toc { position: static; max-height: none; }
|
||||
.manual-content { padding: 24px 20px; }
|
||||
.manual-content h1 { font-size: 25px; }
|
||||
.manual-content h2 { font-size: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="help-header">
|
||||
<div class="help-header-inner">
|
||||
<div>
|
||||
<h1>User Manual</h1>
|
||||
<p>RollCalc technical user documentation</p>
|
||||
</div>
|
||||
<nav class="help-nav" aria-label="Help navigation">
|
||||
<a href="{{ url_for('index') }}">Calculator</a>
|
||||
<a href="{{ url_for('help_index') }}">Help</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="manual-shell">
|
||||
{% if render_error %}
|
||||
<section class="manual-error" role="alert">
|
||||
<strong>User Manual unavailable</strong>
|
||||
<p>{{ render_error }}</p>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="manual-layout">
|
||||
<nav class="manual-toc" aria-label="Table of contents">
|
||||
<h2>Contents</h2>
|
||||
<ul>
|
||||
{% for entry in toc %}
|
||||
<li class="toc-level-{{ entry.level }}">
|
||||
<a href="#{{ entry.id }}">{{ entry.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="manual-content">
|
||||
{{ manual_html }}
|
||||
</article>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
RollCalc Beta © Naue
|
||||
<div class="footer-build-info">
|
||||
Version {{ build_info.version }} |
|
||||
Branch {{ build_info.branch }} |
|
||||
Commit {{ build_info.commit }} |
|
||||
Built {{ build_info.timestamp }}
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user