chore: remove unused files and duplicate assets
Delete orphaned Python screenshot generators, social-preview template, and duplicate icon files in public/assets/ (superseded by public/icons/). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,758 +0,0 @@
|
||||
"""
|
||||
Generate mock screenshots for Oikos family planner PWA.
|
||||
Creates mobile-style mockups matching the app's design system.
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
# --- Paths ---
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(BASE, "screenshots")
|
||||
LOGO_PATH = os.path.join(BASE, "..", "public", "assets", "oikos-icon-1024.png")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
# --- Fonts ---
|
||||
FONT_BOLD = "/usr/share/fonts/google-noto/NotoSans-Bold.ttf"
|
||||
FONT_SEMI = "/usr/share/fonts/google-noto/NotoSans-SemiBold.ttf"
|
||||
FONT_MED = "/usr/share/fonts/google-noto/NotoSans-Medium.ttf"
|
||||
FONT_REG = "/usr/share/fonts/google-noto/NotoSans-Regular.ttf"
|
||||
|
||||
|
||||
def font(style="regular", size=14):
|
||||
paths = {"bold": FONT_BOLD, "semi": FONT_SEMI, "medium": FONT_MED, "regular": FONT_REG}
|
||||
return ImageFont.truetype(paths.get(style, FONT_REG), size)
|
||||
|
||||
|
||||
# --- Design Tokens ---
|
||||
C = {
|
||||
"bg": "#FAFAF8",
|
||||
"surface": "#FFFFFF",
|
||||
"border": "#E8E7E3",
|
||||
"text": "#1C1C1A",
|
||||
"text2": "#4A4A46",
|
||||
"text3": "#8E8D89",
|
||||
"accent": "#2563EB",
|
||||
"accent_light": "#EFF6FF",
|
||||
"accent_dark": "#1D4ED8",
|
||||
"green": "#15803D",
|
||||
"green_light": "#DCFCE7",
|
||||
"orange": "#B45309",
|
||||
"orange_light": "#FFF4D4",
|
||||
"red": "#DC2626",
|
||||
"red_light": "#FEE2E2",
|
||||
"purple": "#8250DF",
|
||||
"purple_light": "#F3EEFF",
|
||||
"shopping_accent": "#D4511E",
|
||||
"shopping_light": "#FFECE3",
|
||||
"teal": "#1A7F5A",
|
||||
"gold": "#BF8700",
|
||||
"white": "#FFFFFF",
|
||||
"nav_bg": "#FAFAF8",
|
||||
}
|
||||
|
||||
# Dark theme
|
||||
CD = {
|
||||
"bg": "#1C1C1A",
|
||||
"surface": "#2A2A28",
|
||||
"border": "#3A3A38",
|
||||
"text": "#F5F4F1",
|
||||
"text2": "#D1D0CB",
|
||||
"text3": "#8E8D89",
|
||||
"accent": "#4B8BFF",
|
||||
"accent_light": "#1E3A5F",
|
||||
"accent_dark": "#6BA1FF",
|
||||
"green": "#22C55E",
|
||||
"green_light": "#1A3D2A",
|
||||
"orange": "#F59E0B",
|
||||
"orange_light": "#3D2E0A",
|
||||
"red": "#EF4444",
|
||||
"red_light": "#3D1A1A",
|
||||
"purple": "#A371F7",
|
||||
"purple_light": "#2D1F4E",
|
||||
"shopping_accent": "#F97316",
|
||||
"shopping_light": "#3D2010",
|
||||
"teal": "#34D399",
|
||||
"gold": "#EAB308",
|
||||
"white": "#F5F4F1",
|
||||
"nav_bg": "#222220",
|
||||
}
|
||||
|
||||
# Phone dimensions (iPhone-like)
|
||||
W = 393
|
||||
H = 852
|
||||
NAV_H = 56
|
||||
STATUS_H = 44
|
||||
CORNER = 40
|
||||
PAD = 16
|
||||
|
||||
|
||||
def hex_to_rgb(h):
|
||||
h = h.lstrip("#")
|
||||
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def c(name, dark=False):
|
||||
"""Get color as RGB tuple."""
|
||||
palette = CD if dark else C
|
||||
return hex_to_rgb(palette[name])
|
||||
|
||||
|
||||
def new_screen(dark=False):
|
||||
"""Create a blank phone screen."""
|
||||
img = Image.new("RGB", (W, H), c("bg", dark))
|
||||
return img, ImageDraw.Draw(img)
|
||||
|
||||
|
||||
def draw_status_bar(draw, dark=False):
|
||||
"""Draw iOS-style status bar."""
|
||||
y = 14
|
||||
# Time
|
||||
draw.text((24, y), "09:41", fill=c("text", dark), font=font("semi", 15))
|
||||
# Signal dots
|
||||
for i in range(4):
|
||||
x = W - 80 + i * 8
|
||||
draw.ellipse([x, y + 4, x + 5, y + 9], fill=c("text", dark))
|
||||
# Battery
|
||||
draw.rounded_rectangle([W - 36, y + 2, W - 12, y + 12], radius=2,
|
||||
outline=c("text", dark), width=1)
|
||||
draw.rectangle([W - 34, y + 4, W - 18, y + 10], fill=c("green", dark))
|
||||
|
||||
|
||||
def draw_bottom_nav(draw, active_idx, dark=False):
|
||||
"""Draw bottom navigation bar."""
|
||||
y = H - NAV_H
|
||||
# Background
|
||||
draw.rectangle([0, y, W, H], fill=c("nav_bg", dark))
|
||||
draw.line([(0, y), (W, y)], fill=c("border", dark), width=1)
|
||||
|
||||
labels = ["Übersicht", "Aufgaben", "Kalender", "Einkauf", "Essen"]
|
||||
icons = ["⊞", "☑", "📅", "🛒", "🍽"]
|
||||
module_colors_light = [
|
||||
c("accent", dark), c("green", dark), c("purple", dark),
|
||||
c("shopping_accent", dark), c("orange", dark)
|
||||
]
|
||||
|
||||
tab_w = W // 5
|
||||
for i, (label, icon) in enumerate(zip(labels, icons)):
|
||||
cx = tab_w * i + tab_w // 2
|
||||
color = module_colors_light[i] if i == active_idx else c("text3", dark)
|
||||
|
||||
# Simple icon placeholder (circle with letter)
|
||||
draw.ellipse([cx - 10, y + 8, cx + 10, y + 28], fill=color if i == active_idx else None,
|
||||
outline=color, width=2)
|
||||
# Active dot
|
||||
if i == active_idx:
|
||||
draw.ellipse([cx - 2, y + 32, cx + 2, y + 36], fill=color)
|
||||
|
||||
# Label
|
||||
bbox = draw.textbbox((0, 0), label, font=font("medium", 10))
|
||||
lw = bbox[2] - bbox[0]
|
||||
draw.text((cx - lw // 2, y + 38), label, fill=color, font=font("medium", 10))
|
||||
|
||||
|
||||
def draw_card(draw, x, y, w, h, dark=False, radius=12):
|
||||
"""Draw a card with subtle border."""
|
||||
draw.rounded_rectangle([x, y, x + w, y + h], radius=radius,
|
||||
fill=c("surface", dark), outline=c("border", dark), width=1)
|
||||
|
||||
|
||||
def draw_pill(draw, x, y, text_str, bg_color, text_color, dark=False, f=None):
|
||||
"""Draw a pill badge."""
|
||||
if f is None:
|
||||
f = font("semi", 11)
|
||||
bbox = draw.textbbox((0, 0), text_str, font=f)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
pw = tw + 14
|
||||
ph = th + 6
|
||||
draw.rounded_rectangle([x, y, x + pw, y + ph], radius=ph // 2,
|
||||
fill=bg_color)
|
||||
draw.text((x + 7, y + 2), text_str, fill=text_color, font=f)
|
||||
return pw
|
||||
|
||||
|
||||
def draw_checkbox(draw, x, y, checked=False, dark=False, size=20):
|
||||
"""Draw a checkbox."""
|
||||
if checked:
|
||||
draw.rounded_rectangle([x, y, x + size, y + size], radius=4,
|
||||
fill=c("green", dark))
|
||||
# Checkmark
|
||||
draw.line([(x + 4, y + size // 2), (x + size // 3 + 1, y + size - 5)],
|
||||
fill=c("white"), width=2)
|
||||
draw.line([(x + size // 3 + 1, y + size - 5), (x + size - 4, y + 4)],
|
||||
fill=c("white"), width=2)
|
||||
else:
|
||||
draw.rounded_rectangle([x, y, x + size, y + size], radius=4,
|
||||
outline=c("border", dark), width=2)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SCREENSHOT 1: DASHBOARD
|
||||
# ============================================================
|
||||
def make_dashboard(dark=False):
|
||||
img, draw = new_screen(dark)
|
||||
draw_status_bar(draw, dark)
|
||||
|
||||
y = STATUS_H + 8
|
||||
|
||||
# Greeting card (blue gradient)
|
||||
grad_top = c("accent_dark", dark)
|
||||
grad_bot = c("accent", dark)
|
||||
card_h = 100
|
||||
draw.rounded_rectangle([PAD, y, W - PAD, y + card_h], radius=16, fill=grad_bot)
|
||||
# Gradient effect (top portion darker)
|
||||
for i in range(card_h // 3):
|
||||
alpha_ratio = 1 - (i / (card_h // 3))
|
||||
r = int(grad_top[0] * alpha_ratio + grad_bot[0] * (1 - alpha_ratio))
|
||||
g = int(grad_top[1] * alpha_ratio + grad_bot[1] * (1 - alpha_ratio))
|
||||
b = int(grad_top[2] * alpha_ratio + grad_bot[2] * (1 - alpha_ratio))
|
||||
draw.line([(PAD + 1, y + i), (W - PAD - 1, y + i)], fill=(r, g, b))
|
||||
# Round top corners
|
||||
draw.rounded_rectangle([PAD, y, W - PAD, y + card_h], radius=16, outline=None)
|
||||
|
||||
draw.text((PAD + 16, y + 16), "Guten Morgen, Lisa 👋", fill=(255, 255, 255),
|
||||
font=font("bold", 20))
|
||||
draw.text((PAD + 16, y + 44), "Freitag, 28. März 2026", fill=(220, 230, 255),
|
||||
font=font("regular", 13))
|
||||
# Weather mini
|
||||
draw.text((PAD + 16, y + 68), "☀️ 17 °C — Berlin", fill=(200, 215, 255),
|
||||
font=font("medium", 13))
|
||||
|
||||
y += card_h + 16
|
||||
|
||||
# Section: Anstehende Termine
|
||||
draw.text((PAD, y), "Anstehende Termine", fill=c("text", dark), font=font("semi", 16))
|
||||
y += 26
|
||||
|
||||
events = [
|
||||
("Zahnarzt – Lisa", "10:00 – 11:00", c("red", dark)),
|
||||
("Schulabholung – Max", "15:30", c("accent", dark)),
|
||||
("Sportverein", "18:00 – 19:30", c("purple", dark)),
|
||||
]
|
||||
for title, time_str, color in events:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 52, dark)
|
||||
draw.rectangle([PAD + 4, y + 8, PAD + 7, y + 44], fill=color)
|
||||
draw.text((PAD + 16, y + 10), title, fill=c("text", dark), font=font("medium", 14))
|
||||
draw.text((PAD + 16, y + 30), time_str, fill=c("text3", dark), font=font("regular", 12))
|
||||
y += 60
|
||||
|
||||
y += 8
|
||||
|
||||
# Section: Dringende Aufgaben
|
||||
draw.text((PAD, y), "Dringende Aufgaben", fill=c("text", dark), font=font("semi", 16))
|
||||
y += 26
|
||||
|
||||
tasks = [
|
||||
("Steuererklärung einreichen", "Heute", c("red", dark)),
|
||||
("Kühlschrank reparieren", "Morgen", c("orange", dark)),
|
||||
("Schulbuch bestellen", "Fr", c("text3", dark)),
|
||||
]
|
||||
for title, due, prio_color in tasks:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 44, dark)
|
||||
draw.ellipse([PAD + 12, y + 16, PAD + 19, y + 23], fill=prio_color)
|
||||
draw.text((PAD + 28, y + 12), title, fill=c("text", dark), font=font("regular", 13))
|
||||
bbox = draw.textbbox((0, 0), due, font=font("regular", 12))
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((W - PAD - 12 - tw, y + 14), due, fill=c("text3", dark), font=font("regular", 12))
|
||||
y += 52
|
||||
|
||||
y += 8
|
||||
|
||||
# Section: Essen heute
|
||||
draw.text((PAD, y), "Essen heute", fill=c("text", dark), font=font("semi", 16))
|
||||
y += 26
|
||||
|
||||
meals = [
|
||||
("Frühstück", "Haferflocken mit Beeren", c("orange", dark), c("orange_light", dark)),
|
||||
("Mittagessen", "Spaghetti Bolognese", c("green", dark), c("green_light", dark)),
|
||||
("Abendessen", "Gemüsesuppe", c("accent", dark), c("accent_light", dark)),
|
||||
]
|
||||
for meal_type, meal_name, color, bg in meals:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 44, dark)
|
||||
draw_pill(draw, PAD + 8, y + 12, meal_type, bg, color, dark, font("semi", 10))
|
||||
draw.text((PAD + 100, y + 13), meal_name, fill=c("text", dark), font=font("regular", 13))
|
||||
y += 52
|
||||
|
||||
draw_bottom_nav(draw, 0, dark)
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SCREENSHOT 2: TASKS
|
||||
# ============================================================
|
||||
def make_tasks(dark=False):
|
||||
img, draw = new_screen(dark)
|
||||
draw_status_bar(draw, dark)
|
||||
|
||||
y = STATUS_H + 8
|
||||
|
||||
# Title
|
||||
draw.text((PAD, y), "Aufgaben", fill=c("text", dark), font=font("bold", 24))
|
||||
y += 40
|
||||
|
||||
# Search bar
|
||||
draw.rounded_rectangle([PAD, y, W - PAD, y + 38], radius=10,
|
||||
fill=c("surface", dark), outline=c("border", dark), width=1)
|
||||
draw.text((PAD + 14, y + 10), "🔍 Aufgaben suchen...", fill=c("text3", dark),
|
||||
font=font("regular", 13))
|
||||
y += 50
|
||||
|
||||
# Filter pills
|
||||
pill_x = PAD
|
||||
for label, active in [("Alle", True), ("Offen", False), ("Heute", False), ("Meine", False)]:
|
||||
if active:
|
||||
bg, fg = c("accent", dark), (255, 255, 255)
|
||||
else:
|
||||
bg, fg = c("surface", dark), c("text2", dark)
|
||||
pw = draw_pill(draw, pill_x, y, label, bg, fg, dark, font("medium", 12))
|
||||
# Border for inactive
|
||||
if not active:
|
||||
bbox2 = draw.textbbox((0, 0), label, font=font("medium", 12))
|
||||
tw = bbox2[2] - bbox2[0]
|
||||
draw.rounded_rectangle([pill_x, y, pill_x + tw + 14, y + bbox2[3] - bbox2[1] + 6],
|
||||
radius=10, outline=c("border", dark), width=1)
|
||||
pill_x += pw + 8
|
||||
y += 36
|
||||
|
||||
# Task groups
|
||||
groups = [
|
||||
("🔴 Dringend", [
|
||||
("Steuererklärung einreichen", "Heute", c("red", dark), False, "L"),
|
||||
]),
|
||||
("🟠 Hoch", [
|
||||
("Kühlschrank reparieren", "Morgen", c("orange", dark), False, "L"),
|
||||
("Arzttermin vereinbaren", "Fr, 28.03.", c("orange", dark), False, "M"),
|
||||
]),
|
||||
("🔵 Normal", [
|
||||
("Schulbuch bestellen", "Fr, 28.03.", c("accent", dark), True, "L"),
|
||||
("Garage aufräumen", "Sa, 29.03.", c("accent", dark), False, "T"),
|
||||
("Geburtstagsgeschenk kaufen", "So, 30.03.", c("accent", dark), False, "L"),
|
||||
]),
|
||||
]
|
||||
|
||||
for group_title, items in groups:
|
||||
draw.text((PAD, y), group_title, fill=c("text2", dark), font=font("semi", 13))
|
||||
y += 24
|
||||
for title, due, prio_color, done, avatar_letter in items:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 52, dark)
|
||||
# Checkbox
|
||||
draw_checkbox(draw, PAD + 12, y + 16, checked=done, dark=dark)
|
||||
# Title
|
||||
title_color = c("text3", dark) if done else c("text", dark)
|
||||
draw.text((PAD + 40, y + 10), title, fill=title_color, font=font("regular", 14))
|
||||
if done:
|
||||
# Strikethrough
|
||||
bbox2 = draw.textbbox((PAD + 40, y + 10), title, font=font("regular", 14))
|
||||
mid_y = (bbox2[1] + bbox2[3]) // 2
|
||||
draw.line([(bbox2[0], mid_y), (bbox2[2], mid_y)], fill=c("text3", dark), width=1)
|
||||
draw.text((PAD + 40, y + 30), due, fill=c("text3", dark), font=font("regular", 11))
|
||||
|
||||
# Avatar circle
|
||||
avatar_colors = {"L": c("accent", dark), "M": c("green", dark), "T": c("orange", dark)}
|
||||
ac = avatar_colors.get(avatar_letter, c("accent", dark))
|
||||
ax = W - PAD - 32
|
||||
draw.ellipse([ax, y + 14, ax + 24, y + 38], fill=ac)
|
||||
bbox_a = draw.textbbox((0, 0), avatar_letter, font=font("semi", 12))
|
||||
aw = bbox_a[2] - bbox_a[0]
|
||||
draw.text((ax + 12 - aw // 2, y + 18), avatar_letter,
|
||||
fill=(255, 255, 255), font=font("semi", 12))
|
||||
|
||||
y += 60
|
||||
y += 4
|
||||
|
||||
draw_bottom_nav(draw, 1, dark)
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SCREENSHOT 3: CALENDAR
|
||||
# ============================================================
|
||||
def make_calendar(dark=False):
|
||||
img, draw = new_screen(dark)
|
||||
draw_status_bar(draw, dark)
|
||||
|
||||
y = STATUS_H + 8
|
||||
|
||||
# Title + navigation
|
||||
draw.text((PAD, y), "März 2026", fill=c("text", dark), font=font("bold", 24))
|
||||
# Nav arrows
|
||||
draw.text((W - PAD - 50, y + 4), "◀ ▶", fill=c("accent", dark), font=font("regular", 18))
|
||||
y += 40
|
||||
|
||||
# View toggle pills
|
||||
pill_x = PAD
|
||||
for label, active in [("Monat", True), ("Woche", False), ("Tag", False), ("Agenda", False)]:
|
||||
if active:
|
||||
bg, fg = c("accent", dark), (255, 255, 255)
|
||||
else:
|
||||
bg, fg = c("surface", dark), c("text2", dark)
|
||||
pw = draw_pill(draw, pill_x, y, label, bg, fg, dark, font("medium", 11))
|
||||
if not active:
|
||||
bbox2 = draw.textbbox((0, 0), label, font=font("medium", 11))
|
||||
tw = bbox2[2] - bbox2[0]
|
||||
draw.rounded_rectangle([pill_x, y, pill_x + tw + 14, y + bbox2[3] - bbox2[1] + 6],
|
||||
radius=9, outline=c("border", dark), width=1)
|
||||
pill_x += pw + 6
|
||||
y += 36
|
||||
|
||||
# Calendar grid
|
||||
cell_w = (W - PAD * 2) // 7
|
||||
cell_h = 58
|
||||
|
||||
# Day headers
|
||||
days = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
||||
for i, day in enumerate(days):
|
||||
dx = PAD + i * cell_w
|
||||
color = c("red", dark) if i >= 5 else c("text3", dark)
|
||||
bbox2 = draw.textbbox((0, 0), day, font=font("semi", 12))
|
||||
dw = bbox2[2] - bbox2[0]
|
||||
draw.text((dx + cell_w // 2 - dw // 2, y), day, fill=color, font=font("semi", 12))
|
||||
y += 24
|
||||
|
||||
# Calendar days (March 2026: starts on Sunday)
|
||||
# Week 1: - - - - - - 1
|
||||
# Week 2: 2-8, Week 3: 9-15, Week 4: 16-22, Week 5: 23-29, Week 6: 30-31
|
||||
month_data = [
|
||||
[0, 0, 0, 0, 0, 0, 1],
|
||||
[2, 3, 4, 5, 6, 7, 8],
|
||||
[9, 10, 11, 12, 13, 14, 15],
|
||||
[16, 17, 18, 19, 20, 21, 22],
|
||||
[23, 24, 25, 26, 27, 28, 29],
|
||||
[30, 31, 0, 0, 0, 0, 0],
|
||||
]
|
||||
today = 28
|
||||
event_days = {3: c("red", dark), 7: c("green", dark), 12: c("purple", dark),
|
||||
18: c("accent", dark), 25: c("orange", dark), 28: c("accent", dark)}
|
||||
|
||||
for week in month_data:
|
||||
for i, day_num in enumerate(week):
|
||||
dx = PAD + i * cell_w
|
||||
if day_num == 0:
|
||||
continue
|
||||
day_str = str(day_num)
|
||||
bbox2 = draw.textbbox((0, 0), day_str, font=font("regular", 14))
|
||||
dw = bbox2[2] - bbox2[0]
|
||||
cx = dx + cell_w // 2
|
||||
cy = y + 10
|
||||
|
||||
if day_num == today:
|
||||
draw.ellipse([cx - 16, cy - 4, cx + 16, cy + 24], fill=c("accent", dark))
|
||||
draw.text((cx - dw // 2, cy), day_str, fill=(255, 255, 255), font=font("bold", 14))
|
||||
else:
|
||||
color = c("text3", dark) if i >= 5 else c("text", dark)
|
||||
draw.text((cx - dw // 2, cy), day_str, fill=color, font=font("regular", 14))
|
||||
|
||||
# Event dot
|
||||
if day_num in event_days and day_num != today:
|
||||
draw.ellipse([cx - 3, cy + 24, cx + 3, cy + 30], fill=event_days[day_num])
|
||||
y += cell_h
|
||||
|
||||
y += 8
|
||||
|
||||
# Today's events
|
||||
draw.text((PAD, y), "Heute — 28. März", fill=c("text", dark), font=font("semi", 16))
|
||||
y += 28
|
||||
|
||||
events = [
|
||||
("Zahnarzt – Lisa", "10:00 – 11:00", c("red", dark)),
|
||||
("Schulabholung – Max", "15:30", c("accent", dark)),
|
||||
("Sportverein", "18:00 – 19:30", c("purple", dark)),
|
||||
]
|
||||
for title, time_str, color in events:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 52, dark)
|
||||
draw.rectangle([PAD + 4, y + 8, PAD + 7, y + 44], fill=color)
|
||||
draw.text((PAD + 16, y + 10), title, fill=c("text", dark), font=font("medium", 14))
|
||||
draw.text((PAD + 16, y + 30), time_str, fill=c("text3", dark), font=font("regular", 12))
|
||||
y += 60
|
||||
|
||||
draw_bottom_nav(draw, 2, dark)
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SCREENSHOT 4: SHOPPING
|
||||
# ============================================================
|
||||
def make_shopping(dark=False):
|
||||
img, draw = new_screen(dark)
|
||||
draw_status_bar(draw, dark)
|
||||
|
||||
y = STATUS_H + 8
|
||||
|
||||
# Title
|
||||
draw.text((PAD, y), "Einkauf", fill=c("text", dark), font=font("bold", 24))
|
||||
y += 40
|
||||
|
||||
# Store tabs
|
||||
tabs = [("REWE", True), ("dm", False), ("Baumarkt", False)]
|
||||
tab_x = PAD
|
||||
for label, active in tabs:
|
||||
f_tab = font("semi", 13)
|
||||
bbox2 = draw.textbbox((0, 0), label, font=f_tab)
|
||||
tw = bbox2[2] - bbox2[0]
|
||||
pw = tw + 20
|
||||
ph = 30
|
||||
if active:
|
||||
draw.rounded_rectangle([tab_x, y, tab_x + pw, y + ph], radius=8,
|
||||
fill=c("green", dark))
|
||||
draw.text((tab_x + 10, y + 6), label, fill=(255, 255, 255), font=f_tab)
|
||||
else:
|
||||
draw.rounded_rectangle([tab_x, y, tab_x + pw, y + ph], radius=8,
|
||||
outline=c("border", dark), width=1)
|
||||
draw.text((tab_x + 10, y + 6), label, fill=c("text2", dark), font=f_tab)
|
||||
tab_x += pw + 8
|
||||
y += 42
|
||||
|
||||
# Progress
|
||||
draw.text((PAD, y), "7 von 14 Artikeln erledigt", fill=c("text3", dark), font=font("regular", 12))
|
||||
y += 20
|
||||
# Progress bar
|
||||
bar_w = W - PAD * 2
|
||||
draw.rounded_rectangle([PAD, y, PAD + bar_w, y + 4], radius=2, fill=c("border", dark))
|
||||
draw.rounded_rectangle([PAD, y, PAD + bar_w // 2, y + 4], radius=2, fill=c("green", dark))
|
||||
y += 16
|
||||
|
||||
# Shopping list by category
|
||||
categories = [
|
||||
("Obst & Gemüse", [
|
||||
("Äpfel", "1 kg", True),
|
||||
("Bananen", "6 Stück", False),
|
||||
("Tomaten", "500 g", True),
|
||||
("Salat", "1 Kopf", False),
|
||||
]),
|
||||
("Milchprodukte", [
|
||||
("Milch", "2 L", False),
|
||||
("Butter", "250 g", False),
|
||||
("Joghurt", "400 g", True),
|
||||
]),
|
||||
("Backwaren", [
|
||||
("Brot", "1 Laib", True),
|
||||
("Brötchen", "6 Stück", False),
|
||||
]),
|
||||
("Getränke", [
|
||||
("Mineralwasser", "6er Pack", True),
|
||||
("Apfelsaft", "1 L", True),
|
||||
]),
|
||||
]
|
||||
|
||||
for cat_name, items in categories:
|
||||
draw.text((PAD, y), cat_name, fill=c("text3", dark), font=font("semi", 11))
|
||||
y += 22
|
||||
for name, qty, checked in items:
|
||||
draw_card(draw, PAD, y, W - PAD * 2, 40, dark)
|
||||
draw_checkbox(draw, PAD + 10, y + 10, checked=checked, dark=dark)
|
||||
name_color = c("text3", dark) if checked else c("text", dark)
|
||||
draw.text((PAD + 38, y + 11), name, fill=name_color, font=font("regular", 13))
|
||||
if checked:
|
||||
bbox2 = draw.textbbox((PAD + 38, y + 11), name, font=font("regular", 13))
|
||||
mid = (bbox2[1] + bbox2[3]) // 2
|
||||
draw.line([(bbox2[0], mid), (bbox2[2], mid)], fill=c("text3", dark), width=1)
|
||||
# Quantity right-aligned
|
||||
bbox_q = draw.textbbox((0, 0), qty, font=font("regular", 12))
|
||||
qw = bbox_q[2] - bbox_q[0]
|
||||
draw.text((W - PAD - 12 - qw, y + 12), qty, fill=c("text3", dark),
|
||||
font=font("regular", 12))
|
||||
y += 46
|
||||
y += 4
|
||||
|
||||
draw_bottom_nav(draw, 3, dark)
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SCREENSHOT 5: MEALS
|
||||
# ============================================================
|
||||
def make_meals(dark=False):
|
||||
img, draw = new_screen(dark)
|
||||
draw_status_bar(draw, dark)
|
||||
|
||||
y = STATUS_H + 8
|
||||
|
||||
# Title
|
||||
draw.text((PAD, y), "Essensplan", fill=c("text", dark), font=font("bold", 24))
|
||||
y += 32
|
||||
draw.text((PAD, y), "KW 13 — 23.–29. März 2026", fill=c("text3", dark),
|
||||
font=font("regular", 13))
|
||||
y += 28
|
||||
|
||||
# Day tabs (horizontal)
|
||||
days_short = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
||||
day_nums = [23, 24, 25, 26, 27, 28, 29]
|
||||
active_day = 5 # Friday 28.
|
||||
tab_w = (W - PAD * 2) // 7
|
||||
for i, (ds, dn) in enumerate(zip(days_short, day_nums)):
|
||||
cx = PAD + i * tab_w + tab_w // 2
|
||||
if i == active_day:
|
||||
draw.ellipse([cx - 18, y, cx + 18, y + 36], fill=c("accent", dark))
|
||||
draw.text((cx - 6, y + 2), ds, fill=(255, 255, 255), font=font("semi", 11))
|
||||
draw.text((cx - 6, y + 18), str(dn), fill=(255, 255, 255), font=font("bold", 12))
|
||||
else:
|
||||
draw.text((cx - 6, y + 2), ds, fill=c("text3", dark), font=font("regular", 11))
|
||||
draw.text((cx - 6, y + 18), str(dn), fill=c("text", dark), font=font("medium", 12))
|
||||
y += 48
|
||||
|
||||
# Today's meals
|
||||
draw.text((PAD, y), "Freitag, 28. März", fill=c("text", dark), font=font("semi", 16))
|
||||
y += 28
|
||||
|
||||
meal_slots = [
|
||||
("Frühstück", "Haferflocken mit Beeren",
|
||||
["Haferflocken: 80 g", "Milch: 200 ml"],
|
||||
c("orange", dark), c("orange_light", dark)),
|
||||
("Mittagessen", "Spaghetti Bolognese",
|
||||
["Spaghetti: 200 g", "Hackfleisch: 300 g"],
|
||||
c("green", dark), c("green_light", dark)),
|
||||
("Abendessen", "Gemüsesuppe",
|
||||
["Karotten: 200 g", "Kartoffeln: 300 g"],
|
||||
c("accent", dark), c("accent_light", dark)),
|
||||
]
|
||||
|
||||
for meal_type, title, ingredients, color, bg_color in meal_slots:
|
||||
card_h = 110
|
||||
draw_card(draw, PAD, y, W - PAD * 2, card_h, dark)
|
||||
|
||||
# Meal type pill
|
||||
draw_pill(draw, PAD + 12, y + 12, meal_type, bg_color, color, dark, font("semi", 11))
|
||||
|
||||
# "Einkaufsliste" link
|
||||
bbox_link = draw.textbbox((0, 0), "+ Einkaufsliste", font=font("medium", 11))
|
||||
lw = bbox_link[2] - bbox_link[0]
|
||||
draw.text((W - PAD - 12 - lw, y + 14), "+ Einkaufsliste",
|
||||
fill=c("accent", dark), font=font("medium", 11))
|
||||
|
||||
# Title
|
||||
draw.text((PAD + 12, y + 38), title, fill=c("text", dark), font=font("semi", 15))
|
||||
|
||||
# Ingredients
|
||||
iy = y + 60
|
||||
for ing in ingredients:
|
||||
draw.text((PAD + 16, iy), "• " + ing, fill=c("text3", dark), font=font("regular", 12))
|
||||
iy += 20
|
||||
|
||||
y += card_h + 12
|
||||
|
||||
draw_bottom_nav(draw, 4, dark)
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PHONE FRAME
|
||||
# ============================================================
|
||||
def add_phone_frame(screen_img, dark=False):
|
||||
"""Wrap screenshot in a phone bezel."""
|
||||
bezel = 12
|
||||
frame_w = W + bezel * 2
|
||||
frame_h = H + bezel * 2
|
||||
frame_color = (30, 30, 30) if not dark else (60, 60, 58)
|
||||
|
||||
frame = Image.new("RGBA", (frame_w + 8, frame_h + 8), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
# Shadow
|
||||
draw.rounded_rectangle([4, 6, frame_w + 4, frame_h + 6], radius=CORNER + bezel,
|
||||
fill=(0, 0, 0, 40))
|
||||
|
||||
# Bezel
|
||||
draw.rounded_rectangle([0, 0, frame_w, frame_h], radius=CORNER + bezel, fill=frame_color)
|
||||
|
||||
# Screen area
|
||||
draw.rounded_rectangle([bezel, bezel, bezel + W, bezel + H], radius=CORNER, fill=(255, 255, 255))
|
||||
|
||||
# Paste screen
|
||||
screen_rgba = screen_img.convert("RGBA")
|
||||
# Mask with rounded corners
|
||||
mask = Image.new("L", (W, H), 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
mask_draw.rounded_rectangle([0, 0, W, H], radius=CORNER, fill=255)
|
||||
frame.paste(screen_rgba, (bezel, bezel), mask)
|
||||
|
||||
# Dynamic island
|
||||
island_w = 100
|
||||
island_h = 28
|
||||
ix = frame_w // 2 - island_w // 2
|
||||
iy = bezel + 6
|
||||
draw.rounded_rectangle([ix, iy, ix + island_w, iy + island_h],
|
||||
radius=island_h // 2, fill=(0, 0, 0))
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SHOWCASE IMAGE
|
||||
# ============================================================
|
||||
def make_showcase(screens, dark=False):
|
||||
"""Create a showcase image with all framed screenshots in a row."""
|
||||
framed = [add_phone_frame(s, dark) for s in screens]
|
||||
|
||||
gap = 32
|
||||
pad = 80
|
||||
total_w = sum(f.width for f in framed) + gap * (len(framed) - 1) + pad * 2
|
||||
|
||||
# Header space
|
||||
header_h = 100
|
||||
total_h = header_h + max(f.height for f in framed) + pad
|
||||
|
||||
bg = (28, 28, 26) if dark else (248, 250, 248)
|
||||
canvas = Image.new("RGBA", (total_w, total_h), bg)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# Header: logo + text
|
||||
try:
|
||||
logo = Image.open(LOGO_PATH).convert("RGBA")
|
||||
logo = logo.resize((64, 64), Image.LANCZOS)
|
||||
logo_x = total_w // 2 - 160
|
||||
logo_y = pad // 2
|
||||
canvas.paste(logo, (logo_x, logo_y), logo)
|
||||
|
||||
text_x = logo_x + 76
|
||||
title_color = (245, 244, 241) if dark else (28, 28, 26)
|
||||
sub_color = (142, 141, 137) if dark else (74, 74, 70)
|
||||
draw.text((text_x, logo_y + 4), "Oikos", fill=title_color, font=font("bold", 36))
|
||||
draw.text((text_x, logo_y + 42), "Dein Familienplaner für Zuhause",
|
||||
fill=sub_color, font=font("regular", 16))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Place framed screenshots
|
||||
x = pad
|
||||
y = header_h
|
||||
for f in framed:
|
||||
canvas.paste(f, (x, y), f)
|
||||
x += f.width + gap
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
def main():
|
||||
generators = [
|
||||
("dashboard", make_dashboard),
|
||||
("tasks", make_tasks),
|
||||
("calendar", make_calendar),
|
||||
("shopping", make_shopping),
|
||||
("meals", make_meals),
|
||||
]
|
||||
|
||||
for theme_name, dark in [("light", False), ("dark", True)]:
|
||||
screens = []
|
||||
for name, gen_fn in generators:
|
||||
screen = gen_fn(dark)
|
||||
suffix = "-dark" if dark else ""
|
||||
path = os.path.join(OUT, f"{name}{suffix}.png")
|
||||
screen.save(path, "PNG")
|
||||
print(f" ✓ {path}")
|
||||
screens.append(screen)
|
||||
|
||||
showcase = make_showcase(screens, dark)
|
||||
suffix = "-dark" if dark else ""
|
||||
showcase_path = os.path.join(OUT, f"showcase{suffix}.png")
|
||||
showcase.save(showcase_path, "PNG")
|
||||
print(f" ✓ {showcase_path} ({showcase.width}x{showcase.height})")
|
||||
|
||||
print("\nFertig! Alle Screenshots und Showcase-Bilder generiert.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,166 +0,0 @@
|
||||
"""Generate a showcase image for Oikos with screenshots in a row."""
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
import os
|
||||
|
||||
# --- Config ---
|
||||
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
|
||||
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "public", "assets")
|
||||
OUTPUT_DIR = os.path.dirname(__file__)
|
||||
|
||||
CANVAS_WIDTH = 2400
|
||||
BG_COLOR = "#F8FAFC" # Light gray background
|
||||
ACCENT_COLOR = "#2563EB" # Blue accent matching the logo
|
||||
TEXT_COLOR = "#1E293B" # Dark text
|
||||
SUBTITLE_COLOR = "#64748B" # Gray subtitle
|
||||
|
||||
SCREENSHOT_ORDER = ["dashboard.png", "tasks.png", "calendar.png", "shopping.png", "meals.png"]
|
||||
SCREENSHOT_HEIGHT = 700 # Target height for each screenshot
|
||||
PADDING = 60 # Padding around edges
|
||||
SCREENSHOT_GAP = 32 # Gap between screenshots
|
||||
CORNER_RADIUS = 24 # Rounded corners for screenshots
|
||||
SHADOW_OFFSET = 8
|
||||
SHADOW_BLUR = 20
|
||||
|
||||
APP_NAME = "Oikos"
|
||||
TAGLINE = "Dein Familienplaner für Zuhause"
|
||||
|
||||
# Font paths (Fedora)
|
||||
FONT_BOLD = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Bold.otf"
|
||||
FONT_REGULAR = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Regular.otf"
|
||||
FONT_FALLBACK_BOLD = "/usr/share/fonts/google-carlito-fonts/Carlito-Bold.ttf"
|
||||
FONT_FALLBACK_REG = "/usr/share/fonts/google-carlito-fonts/Carlito-Regular.ttf"
|
||||
|
||||
|
||||
def load_font(bold=True, size=48):
|
||||
"""Load best available font."""
|
||||
paths = [FONT_BOLD, FONT_FALLBACK_BOLD] if bold else [FONT_REGULAR, FONT_FALLBACK_REG]
|
||||
for path in paths:
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def add_rounded_corners(img, radius):
|
||||
"""Add rounded corners to an image."""
|
||||
mask = Image.new("L", img.size, 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle([(0, 0), img.size], radius=radius, fill=255)
|
||||
result = img.copy()
|
||||
result.putalpha(mask)
|
||||
return result
|
||||
|
||||
|
||||
def create_shadow(size, radius, offset=8, blur=20):
|
||||
"""Create a drop shadow."""
|
||||
shadow_size = (size[0] + blur * 4, size[1] + blur * 4)
|
||||
shadow = Image.new("RGBA", shadow_size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(shadow)
|
||||
x_off = blur * 2
|
||||
y_off = blur * 2 + offset
|
||||
draw.rounded_rectangle(
|
||||
[(x_off, y_off), (x_off + size[0], y_off + size[1])],
|
||||
radius=radius,
|
||||
fill=(0, 0, 0, 50),
|
||||
)
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
||||
return shadow
|
||||
|
||||
|
||||
def main():
|
||||
# Load and resize screenshots
|
||||
screenshots = []
|
||||
for name in SCREENSHOT_ORDER:
|
||||
path = os.path.join(SCREENSHOTS_DIR, name)
|
||||
img = Image.open(path).convert("RGBA")
|
||||
ratio = SCREENSHOT_HEIGHT / img.height
|
||||
new_width = int(img.width * ratio)
|
||||
img = img.resize((new_width, SCREENSHOT_HEIGHT), Image.LANCZOS)
|
||||
screenshots.append(img)
|
||||
|
||||
# Calculate layout
|
||||
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
|
||||
|
||||
# Scale screenshots if they don't fit
|
||||
available_width = CANVAS_WIDTH - PADDING * 2
|
||||
if total_screenshots_width > available_width:
|
||||
scale = available_width / total_screenshots_width
|
||||
new_screenshots = []
|
||||
for s in screenshots:
|
||||
new_w = int(s.width * scale)
|
||||
new_h = int(s.height * scale)
|
||||
new_screenshots.append(s.resize((new_w, new_h), Image.LANCZOS))
|
||||
screenshots = new_screenshots
|
||||
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
|
||||
|
||||
screenshot_h = screenshots[0].height
|
||||
|
||||
# Header area: logo + text
|
||||
logo_size = 72
|
||||
title_font = load_font(bold=True, size=52)
|
||||
tagline_font = load_font(bold=False, size=28)
|
||||
|
||||
header_height = max(logo_size, 52 + 28 + 8) # logo or text stack height
|
||||
top_section = PADDING + header_height + 48 # padding + header + gap to screenshots
|
||||
canvas_height = top_section + screenshot_h + PADDING + SHADOW_BLUR * 2
|
||||
|
||||
# Create canvas
|
||||
canvas = Image.new("RGBA", (CANVAS_WIDTH, canvas_height), BG_COLOR)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# --- Draw subtle gradient accent at top ---
|
||||
for y in range(min(6, canvas_height)):
|
||||
alpha = int(180 * (1 - y / 6))
|
||||
draw.line([(0, y), (CANVAS_WIDTH, y)], fill=(37, 99, 235, alpha))
|
||||
|
||||
# --- Draw logo ---
|
||||
logo_path = os.path.join(ASSETS_DIR, "oikos-icon-1024.png")
|
||||
logo = Image.open(logo_path).convert("RGBA")
|
||||
logo = logo.resize((logo_size, logo_size), Image.LANCZOS)
|
||||
|
||||
# Center header: logo + name + tagline
|
||||
title_bbox = draw.textbbox((0, 0), APP_NAME, font=title_font)
|
||||
title_w = title_bbox[2] - title_bbox[0]
|
||||
tagline_bbox = draw.textbbox((0, 0), TAGLINE, font=tagline_font)
|
||||
tagline_w = tagline_bbox[2] - tagline_bbox[0]
|
||||
|
||||
header_total_w = logo_size + 20 + max(title_w, tagline_w)
|
||||
header_x = (CANVAS_WIDTH - header_total_w) // 2
|
||||
header_y = PADDING
|
||||
|
||||
canvas.paste(logo, (header_x, header_y), logo)
|
||||
|
||||
text_x = header_x + logo_size + 20
|
||||
draw.text((text_x, header_y - 4), APP_NAME, fill=TEXT_COLOR, font=title_font)
|
||||
draw.text((text_x, header_y + 48), TAGLINE, fill=SUBTITLE_COLOR, font=tagline_font)
|
||||
|
||||
# --- Draw screenshots ---
|
||||
start_x = (CANVAS_WIDTH - total_screenshots_width) // 2
|
||||
x = start_x
|
||||
y = top_section
|
||||
|
||||
for s in screenshots:
|
||||
# Shadow
|
||||
shadow = create_shadow(s.size, CORNER_RADIUS, SHADOW_OFFSET, SHADOW_BLUR)
|
||||
sx = x - SHADOW_BLUR * 2
|
||||
sy = y - SHADOW_BLUR * 2
|
||||
canvas.paste(shadow, (sx, sy), shadow)
|
||||
|
||||
# Screenshot with rounded corners
|
||||
rounded = add_rounded_corners(s, CORNER_RADIUS)
|
||||
canvas.paste(rounded, (x, y), rounded)
|
||||
|
||||
x += s.width + SCREENSHOT_GAP
|
||||
|
||||
# Save
|
||||
output_path = os.path.join(OUTPUT_DIR, "showcase.png")
|
||||
final = canvas.convert("RGB")
|
||||
final.save(output_path, "PNG", optimize=True)
|
||||
print(f"Showcase image saved: {output_path}")
|
||||
print(f"Size: {CANVAS_WIDTH}x{canvas_height}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,193 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Oikos — Social Preview</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
width: 1280px;
|
||||
height: 640px;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #0a0f1a 0%, #141c2e 50%, #1a2540 100%);
|
||||
display: flex;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 64px 56px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(0, 122, 255, 0.15);
|
||||
border: 1px solid rgba(0, 122, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #5ac8fa;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 24px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -2px;
|
||||
line-height: 1;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #c8d6e5 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: #8899b0;
|
||||
line-height: 1.5;
|
||||
max-width: 440px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #a0b0c4;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 560px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.screenshot-wrapper {
|
||||
position: relative;
|
||||
transform: perspective(1200px) rotateY(-8deg) rotateX(2deg);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 24px 80px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.screenshot-wrapper img {
|
||||
display: block;
|
||||
width: 500px;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Gradient overlay on the screenshot edges */
|
||||
.screenshot-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(90deg, rgba(10, 15, 26, 0.4) 0%, transparent 30%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Subtle glow behind screenshot */
|
||||
.right::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(0, 122, 255, 0.12) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
left: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: #556680;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #556680;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="left">
|
||||
<div class="badge">Self-Hosted · Open Source</div>
|
||||
<div class="title">Oikos</div>
|
||||
<p class="tagline">The family planner that respects your privacy. Tasks, calendars, shopping, meals, budget — on your own server.</p>
|
||||
<div class="features">
|
||||
<span class="feature"><span class="feature-icon">✅</span> Tasks</span>
|
||||
<span class="feature"><span class="feature-icon">📅</span> Calendar</span>
|
||||
<span class="feature"><span class="feature-icon">🛒</span> Shopping</span>
|
||||
<span class="feature"><span class="feature-icon">🍝</span> Meals</span>
|
||||
<span class="feature"><span class="feature-icon">💰</span> Budget</span>
|
||||
<span class="feature"><span class="feature-icon">📌</span> Notes</span>
|
||||
<span class="feature"><span class="feature-icon">👥</span> Contacts</span>
|
||||
<span class="feature"><span class="feature-icon">🔒</span> Encrypted</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<!-- Replace with your actual tablet screenshot path -->
|
||||
<div class="screenshot-wrapper">
|
||||
<img src="../docs/screenshots/tablet-light/tablet-light-dashboard.png" alt="Dashboard">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>MIT License</span>
|
||||
<div class="dot"></div>
|
||||
<span>Docker · Express · SQLite · Vanilla JS</span>
|
||||
<div class="dot"></div>
|
||||
<span>github.com/ulsklyc/oikos</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user