Linear design system: new admin UI + display design switcher (8 designs)
Admin UI: Linear.app dark theme — near-black canvas, indigo-violet accent, Inter font Display: Minimal, Minimal Color, Bold, Analog Clock, Magazine, Cards, Kiosk, Classic
This commit is contained in:
+272
@@ -0,0 +1,272 @@
|
||||
"""Neue Display-Designs für ACeP 7-Farben e-Paper.
|
||||
|
||||
Jedes Design definiert:
|
||||
- Hintergrund / Rahmen
|
||||
- Widget-Rahmen ( Frames)
|
||||
- content_fn: wie das Widget gerendert wird
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from palette import FG, BG, WHITE, BLACK, GREEN, BLUE, RED, YELLOW, ORANGE, OK, WARN, ALERT, INFO, ACCENT
|
||||
import math
|
||||
|
||||
DISPLAY_W = 800
|
||||
DISPLAY_H = 480
|
||||
COLS, ROWS = 4, 4
|
||||
CELL_W = DISPLAY_W // COLS
|
||||
CELL_H = DISPLAY_H // ROWS
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 5: WIREFRAME
|
||||
# Technisches Plan-Drawing. Blaue Linien, weiße Flächen,
|
||||
# schmale weiße Zellenrahmen. Blueprint-Ästhetik.
|
||||
# ============================================================================
|
||||
def render_wireframe(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (240, 248, 255)) # aliceblue
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for item, widget in zip(items, widgets):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
# Weißer Hintergrund pro Zelle
|
||||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), fill=WHITE)
|
||||
|
||||
# Dünner Rahmen (blau)
|
||||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=BLUE, width=1)
|
||||
|
||||
# Obere Akzent-Linie (breit)
|
||||
draw.rectangle((x + 2, y + 2, x + w - 3, y + 10), fill=BLUE)
|
||||
|
||||
# Widget-Name oben links
|
||||
label = (widget.label or widget.name or "").upper()
|
||||
draw.text((x + 10, y + 14), label, font=fonts.get("14", fonts.get("default")), fill=BLUE)
|
||||
|
||||
# Grid-Checkerboard im Hintergrund (subtle)
|
||||
for row in range(item.y, item.y + item.h):
|
||||
for col in range(item.x, item.x + item.w):
|
||||
if (row + col) % 2 == 0:
|
||||
cx = col * CELL_W + 4
|
||||
cy = row * CELL_H + 4
|
||||
cw = CELL_W - 6
|
||||
ch = CELL_H - 6
|
||||
draw.rectangle((cx, cy, cx + cw, cy + ch), fill=(240, 248, 255))
|
||||
|
||||
# Content
|
||||
widget.render(draw, fonts, x + 6, y + 32, w - 12, h - 38)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 6: BAUHAUS
|
||||
# Geometrische Primärfarben. Jedes Widget bekommt eine der
|
||||
# 7 ACeP-Farben als Hintergrund-Balken links. Reine Formen.
|
||||
# ============================================================================
|
||||
def render_bauhaus(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 245, 240)) # warm gray
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
PALETTE_7 = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 100, 200)]
|
||||
item_colors = {}
|
||||
|
||||
for i, (item, widget) in enumerate(zip(items, widgets)):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
color = PALETTE_7[i % len(PALETTE_7)]
|
||||
item_colors[item.id] = color
|
||||
|
||||
# Großer farbiger Block links (30% width)
|
||||
block_w = w // 3
|
||||
draw.rectangle((x + 2, y + 2, x + block_w - 2, y + h - 3), fill=color)
|
||||
|
||||
# Rest weiß
|
||||
draw.rectangle((x + block_w, y + 2, x + w - 3, y + h - 3), fill=WHITE)
|
||||
|
||||
# Rahmen
|
||||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=FG, width=2)
|
||||
|
||||
# Widget-Name in der farbigen Fläche
|
||||
label = (widget.label or widget.name or "").upper()
|
||||
draw.text((x + 6, y + 8), label, font=fonts.get("14", fonts.get("default")), fill=WHITE)
|
||||
|
||||
# Content (rechte Seite)
|
||||
widget.render(draw, fonts, x + block_w + 4, y + 4, w - block_w - 8, h - 8)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 7: POSTER
|
||||
# Überdimensionierte Typography. Jedes Widget großflächig.
|
||||
# Widget-Hintergrund = monochrome Farbe aus Palette.
|
||||
# Fast nur Text, kein UI-Chrome.
|
||||
# ============================================================================
|
||||
def render_poster(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), BLACK)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for item, widget in zip(items, widgets):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
# Zufällige kräftige Farbe pro widget (basierend auf name hash)
|
||||
c = _hash_color(widget.name)
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=c)
|
||||
|
||||
# Weißer Rahmen drum
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), outline=WHITE, width=3)
|
||||
|
||||
# Label oben Winzig
|
||||
label = (widget.label or widget.name or "").upper()
|
||||
draw.text((x + 8, y + 6), label[:18], font=fonts.get("12", fonts.get("default")), fill=WHITE)
|
||||
|
||||
# Content — das Widget bekommt 90% der Fläche
|
||||
# Wir übergeben ein invertiertes draw-Objekt (fg=WHITE, bg=c)
|
||||
widget.render(draw, fonts, x + 6, y + 24, w - 12, h - 30)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 8: RETRO LCD
|
||||
# Alte LCD/Uhren-Ästhetik. Schwarzer Hintergrund, Ziffern in
|
||||
# satten UnOld Green (LCD-STYLE). Rahmen mit abgerundeten Ecken,
|
||||
# die wie eingeritzte Rillen aussehen.
|
||||
# ============================================================================
|
||||
def render_retro_lcd(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (0, 20, 0)) # dunkles LCD-grün
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
LCD_GREEN = (0, 255, 120)
|
||||
LCD_DARK = (0, 60, 30)
|
||||
LCD_BG = (0, 30, 15)
|
||||
|
||||
for item, widget in zip(items, widgets):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
# LCD-Zelle: dunkler als Hintergrund
|
||||
draw.rectangle((x + 3, y + 3, x + w - 4, y + h - 4), fill=LCD_DARK)
|
||||
|
||||
# Rahmen mit Fake-3D (oben/links hell, unten/rechts dunkel)
|
||||
draw.rectangle((x + 1, y + 1, x + w - 2, y + h - 2), outline=LCD_GREEN, width=2)
|
||||
|
||||
# Label in kleiner Schrift oben
|
||||
label = (widget.label or widget.name or "").upper()
|
||||
draw.text((x + 8, y + 6), label[:16], font=fonts.get("11", fonts.get("default")), fill=LCD_GREEN)
|
||||
|
||||
# Separator-Linie
|
||||
draw.line((x + 8, y + 22, x + w - 8, y + 22), fill=LCD_GREEN, width=1)
|
||||
|
||||
# Content
|
||||
widget.render(draw, fonts, x + 6, y + 26, w - 12, h - 32)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 9: GLOSSY
|
||||
# Moderner Glossy-Style. Weiße Karten mit leichtem Farbverlauf oben,
|
||||
# ikonischer Rand, dezente Schatten durch mehrfache Ränder.
|
||||
# ============================================================================
|
||||
def render_glossy(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (220, 228, 235)) # cool gray
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
WIDGET_ACCENTS = {
|
||||
"clock": (80, 160, 255),
|
||||
"weather": (60, 200, 100),
|
||||
"system": (255, 160, 40),
|
||||
"hello": (200, 80, 80),
|
||||
"spotify": (0, 180, 80),
|
||||
"strava": (255, 120, 0),
|
||||
"gmail": (200, 60, 60),
|
||||
"minimax": (160, 80, 255),
|
||||
}
|
||||
|
||||
for item, widget in zip(items, widgets):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
accent = WIDGET_ACCENTS.get(widget.name, (100, 100, 100))
|
||||
|
||||
# Karte weiß
|
||||
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), fill=WHITE)
|
||||
|
||||
# Glossy-Effekt: heller Balken oben
|
||||
draw.rectangle((x + 2, y + 2, x + w - 2, y + 14), fill=accent)
|
||||
# Weißer Gradient-Overlay oben
|
||||
for i in range(6):
|
||||
alpha = int(80 - i * 12)
|
||||
c = tuple(min(255, accent[j] + alpha) for j in range(3))
|
||||
draw.line((x + 2, y + 2 + i, x + w - 2, y + 2 + i), fill=c, width=1)
|
||||
|
||||
# Rahmeneffekt: 3-lagig für Schatten
|
||||
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), outline=(200, 210, 220), width=1)
|
||||
draw.rectangle((x + 3, y + 3, x + w - 3, y + h - 3), outline=(220, 228, 235), width=1)
|
||||
draw.rectangle((x + 4, y + 4, x + w - 4, y + h - 4), outline=(180, 190, 200), width=1)
|
||||
|
||||
# Label in Accent-Farbe
|
||||
label = (widget.label or widget.name or "").title()
|
||||
draw.text((x + 10, y + 18), label, font=fonts.get("14", fonts.get("default")), fill=accent)
|
||||
|
||||
# Separator
|
||||
draw.line((x + 8, y + 36, x + w - 8, y + 36), fill=(220, 220, 220), width=1)
|
||||
|
||||
# Content
|
||||
widget.render(draw, fonts, x + 6, y + 40, w - 12, h - 46)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DESIGN 10: DATA BOARD
|
||||
# Kontrollraum-Ästhetik. Winzige Zellen, viele Daten.
|
||||
# Horizontale Balken, Tick-Marks, monochrome Palette.
|
||||
#===========================================================================
|
||||
def render_data_board(items, widgets, fonts) -> Image.Image:
|
||||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (10, 10, 15))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for item, widget in zip(items, widgets):
|
||||
x, y, w, h = _px(item)
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
# Weißer Rand
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=WHITE, width=2)
|
||||
|
||||
# Header-Balken (invertiert)
|
||||
draw.rectangle((x, y, x + w - 1, y + 22), fill=WHITE)
|
||||
draw.text((x + 6, y + 4), (widget.label or widget.name or "").upper(),
|
||||
font=fonts.get("12", fonts.get("default")), fill=BLACK)
|
||||
|
||||
# Kleine Tick-Marks oben (dekorativ)
|
||||
for tx in range(x + 10, x + w - 10, 20):
|
||||
draw.line((tx, y + 24, tx, y + 28), fill=(180, 180, 180), width=1)
|
||||
|
||||
# Content
|
||||
widget.render(draw, fonts, x + 4, y + 30, w - 8, h - 34)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper
|
||||
# ============================================================================
|
||||
def _px(item):
|
||||
return item.x * CELL_W, item.y * CELL_H, item.w * CELL_W, item.h * CELL_H
|
||||
|
||||
|
||||
def _hash_color(name: str):
|
||||
"""Consistent color from name using simple hash."""
|
||||
colors = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 0, 200)]
|
||||
h = sum(ord(c) for c in name)
|
||||
return colors[h % len(colors)]
|
||||
Reference in New Issue
Block a user