diff --git a/nouri/main.py b/nouri/main.py index 18d5a9e..f7d1690 100644 --- a/nouri/main.py +++ b/nouri/main.py @@ -1482,7 +1482,12 @@ def fetch_plan_candidates(daypart_id: int, query: str | None = None): """, params, ).fetchall() - return decorate_items(rows) + items = decorate_items(rows) + return [ + item + for item in items + if item["kind"] != "meal" or meal_matches_daypart(item, daypart_id) + ] def fetch_home_food_ids() -> set[int]: @@ -1561,6 +1566,22 @@ def item_matches_daypart(item: dict, daypart_id: int | None) -> bool: return any(int(daypart["id"]) == int(daypart_id) for daypart in dayparts_meta) +def meal_matches_daypart(item: dict, daypart_id: int | None) -> bool: + if item.get("kind") != "meal" or daypart_id is None: + return True + + dayparts_meta = item.get("dayparts_meta") or [] + if dayparts_meta: + return any(int(daypart["id"]) == int(daypart_id) for daypart in dayparts_meta) + + raw_meal_type = (item.get("meal_type") or "").strip() + if not raw_meal_type: + return False + + expected_meal_type = meal_type_for_daypart(daypart_id) + return normalize_meal_type(raw_meal_type, "snack") == expected_meal_type + + def normalized_component_signature(component_ids: list[int]) -> tuple[int, ...]: return tuple(sorted({int(component_id) for component_id in component_ids})) diff --git a/nouri/static/css/styles.css b/nouri/static/css/styles.css index 2d6a694..4ff8cf3 100644 --- a/nouri/static/css/styles.css +++ b/nouri/static/css/styles.css @@ -661,50 +661,247 @@ h3 { .selected-components-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 0.75rem; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 0.7rem; } .selected-component-card { + position: relative; display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 0.85rem; - padding: 0.9rem 1rem; - border-radius: 18px; + gap: 0.65rem; + min-height: 138px; + padding: 0.9rem 0.85rem 0.8rem; + border-radius: 22px; border: 1px solid var(--line); - background: color-mix(in srgb, var(--accent-soft) 18%, var(--surface-strong) 82%); + background: color-mix(in srgb, var(--accent-soft) 10%, var(--surface-strong) 90%); + align-content: start; } .selected-component-main { display: grid; - gap: 0.2rem; + gap: 0.15rem; min-width: 0; + margin-top: auto; + text-align: center; } -.selected-component-main strong, -.selected-component-main small { +.selected-component-main strong { min-width: 0; overflow-wrap: anywhere; + font-size: 1rem; + line-height: 1.25; } -.selected-component-main small { - color: var(--muted); +.selected-component-visual { + display: flex; + align-items: center; + justify-content: center; + min-height: 4.4rem; +} + +.selected-component-visual img, +.selected-component-fallback { + width: 4rem; + height: 4rem; + border-radius: 18px; +} + +.selected-component-visual img { + object-fit: cover; + border: 1px solid color-mix(in srgb, var(--line) 76%, transparent 24%); + box-shadow: 0 8px 18px color-mix(in srgb, var(--accent) 10%, transparent 90%); +} + +.selected-component-fallback { + display: inline-flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--surface) 76%, #fff 24%); + border: 1px solid color-mix(in srgb, var(--line) 76%, transparent 24%); + color: color-mix(in srgb, var(--accent-strong) 70%, var(--text) 30%); +} + +.selected-component-fallback .ui-icon { + width: 1.45rem; + height: 1.45rem; } .selected-component-remove { - white-space: nowrap; + position: absolute; + top: 0.45rem; + right: 0.45rem; + width: 1.9rem; + height: 1.9rem; + min-width: 1.9rem; + padding: 0; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--line) 74%, transparent 26%); + background: transparent; + color: var(--muted); + box-shadow: none; + transform: none; + z-index: 1; +} + +.selected-component-remove:hover { + background: color-mix(in srgb, var(--accent-soft) 40%, transparent 60%); + color: var(--text); + border-color: color-mix(in srgb, var(--accent) 40%, var(--line) 60%); + box-shadow: none; + transform: none; +} + +.selected-component-remove span[aria-hidden="true"] { + font-size: 1.25rem; + line-height: 1; } [data-theme="dark"] .selected-component-card { background: linear-gradient( 180deg, - color-mix(in srgb, var(--surface-soft) 82%, #4d413d 18%), - color-mix(in srgb, var(--surface) 96%, #2c2523 4%) + color-mix(in srgb, var(--surface-soft) 76%, #4d413d 24%), + color-mix(in srgb, var(--surface) 94%, #2c2523 6%) ); border-color: color-mix(in srgb, rgba(243, 177, 125, 0.36) 48%, var(--line) 52%); } +[data-theme="dark"] .selected-component-fallback { + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--surface-soft) 72%, #4a403c 28%), + color-mix(in srgb, var(--surface-strong) 86%, #241f1d 14%) + ); + border-color: color-mix(in srgb, var(--line) 54%, rgba(243, 177, 125, 0.18) 46%); +} + +[data-theme="dark"] .selected-component-remove { + background: rgba(32, 27, 25, 0.16); + color: color-mix(in srgb, var(--muted) 90%, white 10%); +} + +[data-theme="dark"] .selected-component-remove:hover { + background: color-mix(in srgb, var(--accent-soft) 42%, rgba(32, 27, 25, 0.58) 58%); +} + +.package-option-grid { + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 0.7rem; +} + +.set-item-option { + position: relative; + display: block; +} + +.set-item-option input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.set-item-option-card { + display: grid; + gap: 0.65rem; + min-height: 138px; + padding: 0.9rem 0.85rem 0.8rem; + border-radius: 22px; + border: 1px solid var(--line); + background: color-mix(in srgb, var(--accent-soft) 10%, var(--surface-strong) 90%); + color: var(--muted); + align-content: start; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease; +} + +.set-item-option-visual { + display: flex; + align-items: center; + justify-content: center; + min-height: 4.4rem; +} + +.set-item-option-visual img, +.set-item-option-fallback { + width: 4rem; + height: 4rem; + border-radius: 18px; +} + +.set-item-option-visual img { + object-fit: cover; + border: 1px solid color-mix(in srgb, var(--line) 76%, transparent 24%); + box-shadow: 0 8px 18px color-mix(in srgb, var(--accent) 10%, transparent 90%); +} + +.set-item-option-fallback { + display: inline-flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--surface) 76%, #fff 24%); + border: 1px solid color-mix(in srgb, var(--line) 76%, transparent 24%); + color: color-mix(in srgb, var(--accent-strong) 70%, var(--text) 30%); +} + +.set-item-option-fallback .ui-icon { + width: 1.45rem; + height: 1.45rem; +} + +.set-item-option-label { + margin-top: auto; + min-width: 0; + overflow-wrap: anywhere; + text-align: center; + color: var(--text); + font-weight: 600; + line-height: 1.25; +} + +.set-item-option:hover .set-item-option-card { + border-color: var(--accent-soft); + transform: translateY(-1px); + color: var(--text); +} + +.set-item-option input:focus-visible + .set-item-option-card { + outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent 55%); + outline-offset: 3px; +} + +.set-item-option input:checked + .set-item-option-card { + border-color: color-mix(in srgb, var(--accent) 55%, var(--line) 45%); + background: color-mix(in srgb, var(--accent-soft) 18%, var(--surface-strong) 82%); + box-shadow: 0 12px 30px color-mix(in srgb, var(--accent) 14%, transparent 86%); + color: var(--text); +} + +.set-item-option input:checked + .set-item-option-card .set-item-option-fallback { + background: color-mix(in srgb, var(--accent) 16%, var(--surface) 84%); + border-color: color-mix(in srgb, var(--accent) 38%, var(--line) 62%); +} + +[data-theme="dark"] .set-item-option-card { + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--surface-soft) 76%, #4d413d 24%), + color-mix(in srgb, var(--surface) 94%, #2c2523 6%) + ); + border-color: color-mix(in srgb, rgba(243, 177, 125, 0.36) 48%, var(--line) 52%); + color: color-mix(in srgb, var(--muted) 92%, white 8%); +} + +[data-theme="dark"] .set-item-option-fallback { + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--surface-soft) 72%, #4a403c 28%), + color-mix(in srgb, var(--surface-strong) 86%, #241f1d 14%) + ); + border-color: color-mix(in srgb, var(--line) 54%, rgba(243, 177, 125, 0.18) 46%); +} + .quick-food-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -825,6 +1022,7 @@ h3 { .stack-form, .stack-sections, .planner-day-stack, +.planner-day-sidebar, .planner-entry-list, .week-entry-stack, .week-slot-stack { @@ -832,6 +1030,18 @@ h3 { gap: 1rem; } +.planner-day-layout { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); + gap: 1rem; + align-items: start; +} + +.planner-day-main, +.planner-day-sidebar { + min-width: 0; +} + .dashboard-spaced-panel > .panel-head + * { margin-top: 0.45rem; } @@ -2239,6 +2449,46 @@ legend { mask-image: url("../icons/fa/leaf.svg"); } +.icon-component-protein { + -webkit-mask-image: url("../icons/components/protein.svg"); + mask-image: url("../icons/components/protein.svg"); +} + +.icon-component-carb { + -webkit-mask-image: url("../icons/components/carb.svg"); + mask-image: url("../icons/components/carb.svg"); +} + +.icon-component-veg { + -webkit-mask-image: url("../icons/components/veg.svg"); + mask-image: url("../icons/components/veg.svg"); +} + +.icon-component-fruit { + -webkit-mask-image: url("../icons/components/fruit.svg"); + mask-image: url("../icons/components/fruit.svg"); +} + +.icon-component-dairy { + -webkit-mask-image: url("../icons/components/dairy.svg"); + mask-image: url("../icons/components/dairy.svg"); +} + +.icon-component-nuts { + -webkit-mask-image: url("../icons/components/nuts.svg"); + mask-image: url("../icons/components/nuts.svg"); +} + +.icon-component-seeds { + -webkit-mask-image: url("../icons/components/seeds.svg"); + mask-image: url("../icons/components/seeds.svg"); +} + +.icon-component-neutral { + -webkit-mask-image: url("../icons/components/neutral.svg"); + mask-image: url("../icons/components/neutral.svg"); +} + .mobile-sheet-backdrop { position: fixed; inset: 0; @@ -2321,6 +2571,7 @@ legend { .stats-grid, .two-column, + .planner-day-layout, .template-library-grid, .settings-grid, .inline-form, diff --git a/nouri/static/icons/components/carb.svg b/nouri/static/icons/components/carb.svg new file mode 100644 index 0000000..975650b --- /dev/null +++ b/nouri/static/icons/components/carb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/dairy.svg b/nouri/static/icons/components/dairy.svg new file mode 100644 index 0000000..cd98071 --- /dev/null +++ b/nouri/static/icons/components/dairy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/fruit.svg b/nouri/static/icons/components/fruit.svg new file mode 100644 index 0000000..28edfef --- /dev/null +++ b/nouri/static/icons/components/fruit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/neutral.svg b/nouri/static/icons/components/neutral.svg new file mode 100644 index 0000000..f16f039 --- /dev/null +++ b/nouri/static/icons/components/neutral.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/nuts.svg b/nouri/static/icons/components/nuts.svg new file mode 100644 index 0000000..09fb882 --- /dev/null +++ b/nouri/static/icons/components/nuts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/protein.svg b/nouri/static/icons/components/protein.svg new file mode 100644 index 0000000..922b536 --- /dev/null +++ b/nouri/static/icons/components/protein.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/seeds.svg b/nouri/static/icons/components/seeds.svg new file mode 100644 index 0000000..bc25c8b --- /dev/null +++ b/nouri/static/icons/components/seeds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/static/icons/components/veg.svg b/nouri/static/icons/components/veg.svg new file mode 100644 index 0000000..d9cc271 --- /dev/null +++ b/nouri/static/icons/components/veg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nouri/templates/items/form.html b/nouri/templates/items/form.html index 832319c..704adc2 100644 --- a/nouri/templates/items/form.html +++ b/nouri/templates/items/form.html @@ -210,15 +210,39 @@

Schon ausgewählt

{% for component in selected_components %} + {% set component_icon_class = { + 'protein': 'icon-component-protein', + 'carb': 'icon-component-carb', + 'veg': 'icon-component-veg', + 'fruit': 'icon-component-fruit', + 'dairy': 'icon-component-dairy', + 'nuts': 'icon-component-nuts', + 'seeds': 'icon-component-seeds', + 'neutral': 'icon-component-neutral', + }.get(component.primary_builder_key or component.base_type, 'icon-component-neutral') %}
+ +
+ {% if component.photo_filename %} + {{ component.name }} + {% else %} + + + + {% endif %} +
{{ component.name }} - {{ component.base_type_label }} · {{ component.visibility_label }}
-
{% endfor %}
diff --git a/nouri/templates/library/set_form.html b/nouri/templates/library/set_form.html index 9845bb8..e2cc8e1 100644 --- a/nouri/templates/library/set_form.html +++ b/nouri/templates/library/set_form.html @@ -51,11 +51,46 @@

{{ group["title"] }}

{{ group["items"]|length }} Einträge -
+
{% for item in group["items"] %} -
diff --git a/nouri/templates/planner/day.html b/nouri/templates/planner/day.html index c30b245..80de42f 100644 --- a/nouri/templates/planner/day.html +++ b/nouri/templates/planner/day.html @@ -14,90 +14,53 @@
-
-
-
-

Tagesvorlagen

- Als Vorlage speichern -
- {% if day_templates %} -
- {% for template in day_templates %} -
- {{ csrf_input() }} - -
- {{ template.name }} - {{ template.visibility_label }} · {{ template.owner_label }} -
- -
- {% endfor %} -
- {% else %} -

Wenn du einen Tag öfter wiederverwenden möchtest, kannst du ihn hier als Tagesvorlage speichern.

- {% endif %} -
- - {% if day_hints %} -
-
-

Heute im Blick

-
-
- {% for hint in day_hints %} -

{{ hint }}

- {% endfor %} -
-
- {% endif %} -
- -
- {% set hidden_snack_sections = sections | selectattr('is_snack_daypart') | rejectattr('visible_by_default') | list %} - {% if hidden_snack_sections %} -
-
-

Zwischenmahlzeit hinzufügen

-
-
- {% for section in hidden_snack_sections %} - - {% endfor %} -
-
- {% endif %} - - {% for section in sections %} -
- -
-
-
-

{{ section.daypart.name }}

- {% if section.summary_items %} -

{{ section.summary_items|join(', ') }}

- {% else %} -

Noch offen. Öffnen, wenn du etwas eintragen möchtest.

- {% endif %} +
+
+
+ {% set hidden_snack_sections = sections | selectattr('is_snack_daypart') | rejectattr('visible_by_default') | list %} + {% if hidden_snack_sections %} +
+
+

Zwischenmahlzeit hinzufügen

-
- {{ section.entries|length }} geplant -
+
+ {% for section in hidden_snack_sections %} + + {% endfor %} +
+
+ {% endif %} -
+ {% for section in sections %} +
+ +
+
+
+

{{ section.daypart.name }}

+ {% if section.summary_items %} +

{{ section.summary_items|join(', ') }}

+ {% else %} +

Noch offen. Öffnen, wenn du etwas eintragen möchtest.

+ {% endif %} +
+
+ {{ section.entries|length }} geplant +
+ +
{% if section.selected_quick_action %}
Schon ausgewählt @@ -301,8 +264,49 @@
{% endif %} {% endif %} +
+
+ {% endfor %} + +
+ + {% endblock %}