diff --git a/cauldron/server.py b/cauldron/server.py index a871047..b0b274d 100644 --- a/cauldron/server.py +++ b/cauldron/server.py @@ -471,6 +471,20 @@ def create_app() -> Flask: def list_view(): return render_template("stub.html", title="list", coming="aggregated shopping list from this week's plan", active="list") + @app.get("/api/recipes/.json") + @require_session + def recipe_detail_json(slug: str): + client = current_user_mealie() + if not client: + return jsonify({"error": "not connected"}), 409 + u = session["user"] + try: + recipe = client.get_recipe(slug) + except Exception as e: + return jsonify({"error": str(e)}), 502 + recipe["picked"] = slug in db.list_household_pick_slugs(current_household_id() or 0) if current_household_id() else False + return jsonify({"recipe": recipe, "public_url": cfg.mealie_public_url}) + @app.get("/recipes/") @require_session def recipe_detail(slug: str): diff --git a/cauldron/templates/_base.html b/cauldron/templates/_base.html index 97d9833..5c75e6e 100644 --- a/cauldron/templates/_base.html +++ b/cauldron/templates/_base.html @@ -374,11 +374,96 @@ ol li, ul li { margin: .35em 0; } .recipe-card .rname { font-size: .95em; } } +/* Recipe modal */ +.modal-backdrop { + position: fixed; inset: 0; z-index: 100; + background: rgba(5, 5, 8, .82); + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); + display: none; align-items: flex-end; justify-content: center; + animation: backdropIn .2s ease-out forwards; + overscroll-behavior: contain; +} +.modal-backdrop.open { display: flex; } +@media (min-width: 720px) { .modal-backdrop { align-items: center; padding: 30px; } } +.modal { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px 8px 0 0; + width: 100%; max-width: 720px; + max-height: 92vh; max-height: 92dvh; + display: flex; flex-direction: column; + box-shadow: 0 -10px 60px -10px rgba(0,0,0,.7), 0 0 0 1px rgba(155,95,232,.15); + animation: modalIn .25s ease-out forwards; + overscroll-behavior: contain; +} +@media (min-width: 720px) { .modal { border-radius: 8px; max-height: 86vh; } } +.modal-head { + padding: 14px 18px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; gap: 10px; + background: var(--bg-2); + border-radius: 8px 8px 0 0; +} +.modal-head .title { + flex: 1; min-width: 0; + color: var(--bone); font-family: var(--serif); font-weight: 600; font-size: 1.15em; + letter-spacing: .02em; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.modal-close { + width: 40px; height: 40px; border-radius: 50%; + background: var(--surface-2); border: 1px solid var(--line); + color: var(--bone-dim); font-size: 22px; line-height: 1; + cursor: pointer; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +} +.modal-close:active { transform: scale(.92); background: var(--surface-3); } +.modal-body { + padding: 18px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + flex: 1; +} +.modal-body h3 { + margin: 1.2em 0 .4em 0; padding-bottom: .3em; + border-bottom: 1px solid var(--line-soft); + color: var(--purple-bright); font-family: var(--serif); font-weight: 600; + font-size: 1em; letter-spacing: .15em; text-transform: uppercase; +} +.modal-body h3:first-child { margin-top: 0; } +.modal-body .ing-list { list-style: none; padding: 0; margin: 0; } +.modal-body .ing-list li { padding: 6px 0; border-bottom: 1px dashed var(--line-soft); color: var(--bone); } +.modal-body .ing-list li:last-child { border-bottom: none; } +.modal-body ol.steps { padding-left: 1.4em; margin: 0; } +.modal-body ol.steps li { padding: 6px 0; color: var(--bone); } +.modal-foot { + padding: 14px 18px; + border-top: 1px solid var(--line); + background: var(--bg-2); + display: flex; gap: 10px; flex-wrap: wrap; align-items: center; +} +.modal-foot .btn { flex: 1; min-width: 120px; text-align: center; } +.modal-foot .btn.full { flex: 1 0 100%; } +.modal-meta { + display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; + color: var(--muted); font-size: 11px; letter-spacing: .15em; + text-transform: uppercase; font-family: var(--mono); +} +.modal-meta .m { padding: 3px 10px; border: 1px solid var(--line); border-radius: 999px; } + /* Animations */ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +@keyframes backdropIn { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes modalIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} form { margin: 0; } button { font-family: inherit; } @@ -409,5 +494,172 @@ button { font-family: inherit; } {% block content %}{% endblock %} + + + +