Roll out "modern" HTML/CSS render style to every widget except photos
Extends weather's experimental Chromium+Jinja2 render style to battery, text, tasks, static image, whiteboard, and calendar (all four view modes -- agenda/today_tomorrow/week/month), and gives the photos widget its own genuinely independent palette + dithering strength. Photos: Frame.photo_palette_rgb/photo_dither_strength (mirroring the existing palette_rgb/dither_strength), with a second "Photos configuration" card in Advanced Configuration. widgets/photos.py's render() quantizes itself against these before returning -- no render_panel changes needed, since photos is the only widget that genuinely needs a different reference palette and can carry that itself, the same way modern-style widgets already self-dither via ordered_dither. Battery/text/tasks/static image/whiteboard: same render_style pattern weather established (render_style column, html_render.py build function, Jinja2 template, dialog toggle). Static image/whiteboard get their first-ever visual chrome (a rounded-corner shadowed card, shared framed_image.html.jinja) since classic draws them with zero frame at all. Fixed the same "preview endpoint bypasses render_style" bug weather originally shipped with, for tasks/static/whiteboard/ calendar's preview endpoints. Calendar: own module (app/calendar_html_render.py, mirroring calendar_render.py's separation from the simpler widgets) covering all four view modes, not just agenda -- reuses calendar_render's own private helpers so event colors/times/weather/month-grid math match classic exactly. Found and fixed two real cross-day layout bugs along the way: a per-day header height that varied based on whether that specific day had a weather entry (misaligning where every other day's event rows started across the week/month grid), and regular-weight small text being fragile under Bayer ordered dithering (out-of-month day numbers degraded into unrecognizable speckle) -- fixed by using bold everywhere and de-emphasizing via size instead of weight/gray, since gray text has the same dithering fragility this project's PIL renderers already avoid for exactly this reason. Migrations 32-38 (Frame's two new columns, then one render_style column per widget config table). 452 tests passing, including new dispatch/ migration coverage per widget type and a dedicated photos test proving photo_palette_rgb produces genuinely independent quantization from the frame's main palette_rgb.
This commit is contained in:
@@ -11,6 +11,12 @@
|
||||
<option value="detailed" {% if not battery_cfg or battery_cfg.mode == 'detailed' %}selected{% endif %}>Detailed (+ estimated time left, last report)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Render style
|
||||
<select id="battery_render_style">
|
||||
<option value="classic" {% if not battery_cfg or battery_cfg.render_style == 'classic' %}selected{% endif %}>Classic (hand-drawn icon)</option>
|
||||
<option value="modern" {% if battery_cfg and battery_cfg.render_style == 'modern' %}selected{% endif %}>Modern (experimental)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
<p class="sub" style="margin-top: 4px;">0 = starts today, negative = starts in the
|
||||
past, positive = starts in the future. Only used when Days to show isn't 7.</p>
|
||||
</div>
|
||||
<label>Render style
|
||||
<select id="calendar_render_style">
|
||||
<option value="classic" {% if calendar_cfg.render_style == 'classic' %}selected{% endif %}>Classic (hand-drawn)</option>
|
||||
<option value="modern" {% if calendar_cfg.render_style == 'modern' %}selected{% endif %}>Modern (experimental, all views)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
exactly without cropping (an image with a different aspect ratio
|
||||
looks stretched); <strong>Shrink to fit</strong> shows the whole
|
||||
image, letterboxed if needed.</p>
|
||||
<label>Render style
|
||||
<select id="static_render_style">
|
||||
<option value="classic" {% if not static_cfg or static_cfg.render_style == 'classic' %}selected{% endif %}>Classic (no frame)</option>
|
||||
<option value="modern" {% if static_cfg and static_cfg.render_style == 'modern' %}selected{% endif %}>Modern (rounded-corner card, experimental)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
<input type="checkbox" id="tasks_show_completed" {% if task_cfg.show_completed %}checked{% endif %}>
|
||||
<label for="tasks_show_completed">Also show tasks completed in the last 24 hours</label>
|
||||
</div>
|
||||
<label>Render style
|
||||
<select id="tasks_render_style">
|
||||
<option value="classic" {% if task_cfg.render_style == 'classic' %}selected{% endif %}>Classic (hand-drawn checklist)</option>
|
||||
<option value="modern" {% if task_cfg.render_style == 'modern' %}selected{% endif %}>Modern (experimental)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
<label>Background color
|
||||
<input type="color" id="text_background_color" value="{{ text_cfg.background_color if text_cfg else '#ffffff' }}">
|
||||
</label>
|
||||
<label>Render style
|
||||
<select id="text_render_style">
|
||||
<option value="classic" {% if not text_cfg or text_cfg.render_style == 'classic' %}selected{% endif %}>Classic (hand-drawn text)</option>
|
||||
<option value="modern" {% if text_cfg and text_cfg.render_style == 'modern' %}selected{% endif %}>Modern (experimental)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -50,6 +50,19 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 20px;">
|
||||
<h2 class="card-title">Settings</h2>
|
||||
<form id="whiteboard-config-form">
|
||||
<label>Render style
|
||||
<select id="whiteboard_render_style">
|
||||
<option value="classic" {% if not whiteboard_cfg or whiteboard_cfg.render_style == 'classic' %}selected{% endif %}>Classic (no frame)</option>
|
||||
<option value="modern" {% if whiteboard_cfg and whiteboard_cfg.render_style == 'modern' %}selected{% endif %}>Modern (rounded-corner card, experimental)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% include "_widget_border_fields.html" %}
|
||||
|
||||
{% include "_widget_button_fields.html" %}
|
||||
|
||||
@@ -139,11 +139,12 @@
|
||||
|
||||
<details class="card">
|
||||
<summary class="card-title">Advanced configuration</summary>
|
||||
<p class="sub">Color-quantization values used when dithering photos
|
||||
for this panel -- approximations by default, since exact primaries
|
||||
aren't published. Tune them by comparing a rendered photo against
|
||||
the physical panel; different panel units can vary enough to be
|
||||
worth calibrating per frame.</p>
|
||||
<p class="sub">Color-quantization values used for every widget
|
||||
<strong>except photos</strong> (which has its own separate
|
||||
settings below) -- approximations by default, since exact
|
||||
primaries aren't published. Tune them by comparing a rendered
|
||||
widget against the physical panel; different panel units can
|
||||
vary enough to be worth calibrating per frame.</p>
|
||||
<div class="palette-table-wrap">
|
||||
<table class="palette-table">
|
||||
<thead><tr><th></th><th>Color</th><th>Hex</th><th>R</th><th>G</th><th>B</th></tr></thead>
|
||||
@@ -195,6 +196,45 @@
|
||||
Compare against the physical panel before keeping it.</p>
|
||||
</details>
|
||||
|
||||
<details class="card">
|
||||
<summary class="card-title">Photos configuration</summary>
|
||||
<p class="sub">Palette and dithering used <strong>only</strong> by
|
||||
the photos widget, independent of Advanced configuration above --
|
||||
lets you tune the rest of this frame's widgets (e.g. a "modern"
|
||||
HTML-rendered look) without changing what looks best for actual
|
||||
photographs, or vice versa.</p>
|
||||
<div class="palette-table-wrap">
|
||||
<table class="palette-table">
|
||||
<thead><tr><th></th><th>Color</th><th>Hex</th><th>R</th><th>G</th><th>B</th></tr></thead>
|
||||
<tbody>
|
||||
{% set current_photo_palette = frame.photo_palette_rgb or default_palette_rgb %}
|
||||
{% set current_photo_hex = palette_to_hex(current_photo_palette) %}
|
||||
{% for label in palette_labels %}
|
||||
<tr>
|
||||
<td><span class="photo-palette-swatch-preview" data-index="{{ loop.index0 }}"
|
||||
style="background: {{ current_photo_hex[loop.index0] }};"></span></td>
|
||||
<td>{{ label }}</td>
|
||||
<td><input type="text" class="photo-palette-hex" id="photo_palette_{{ loop.index0 }}" data-index="{{ loop.index0 }}"
|
||||
value="{{ current_photo_hex[loop.index0] }}" maxlength="7" pattern="#[0-9a-fA-F]{6}"
|
||||
spellcheck="false" autocomplete="off"></td>
|
||||
<td><input type="number" class="photo-palette-rgb photo-palette-r" data-index="{{ loop.index0 }}"
|
||||
min="0" max="255" value="{{ current_photo_palette[loop.index0][0] }}"></td>
|
||||
<td><input type="number" class="photo-palette-rgb photo-palette-g" data-index="{{ loop.index0 }}"
|
||||
min="0" max="255" value="{{ current_photo_palette[loop.index0][1] }}"></td>
|
||||
<td><input type="number" class="photo-palette-rgb photo-palette-b" data-index="{{ loop.index0 }}"
|
||||
min="0" max="255" value="{{ current_photo_palette[loop.index0][2] }}"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<label>Dithering strength <span class="slider-value" id="photo_dither_strength_value">{{ "%.2f" | format(frame.photo_dither_strength) }}</span>
|
||||
<input type="range" id="photo_dither_strength" min="0" max="1" step="0.05" value="{{ frame.photo_dither_strength }}">
|
||||
</label>
|
||||
<button type="button" class="secondary" id="photo-palette-save" style="margin-top: 16px;">Save</button>
|
||||
<button type="button" class="secondary" id="photo-palette-reset">Reset to defaults</button>
|
||||
</details>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card-title">Preview</h2>
|
||||
{% if photo_widget_id %}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% macro day_section(header, weather_entries, weather_size, unit_suffix, rows, more_count, row_h, body_size, title_size, accent_start, accent_end, header_h) %}
|
||||
<div class="day-section">
|
||||
{#- Fixed height (not auto) -- every stacked day-section's header
|
||||
must be exactly this tall regardless of whether THIS particular
|
||||
day has a weather entry, or days with/without weather misalign
|
||||
where their event rows start (see calendar_week_horizontal's
|
||||
identical fix/reasoning). #}
|
||||
<div class="day-header" style="background: linear-gradient(135deg, {{ accent_start }} 0%, {{ accent_end }} 100%); font-size: {{ title_size }}px; height: {{ header_h }}px;">
|
||||
<div class="day-header-title">{{ header }}</div>
|
||||
{% if weather_entries %}
|
||||
<div class="day-weather-row" style="font-size: {{ weather_size }}px;">
|
||||
{% for we in weather_entries %}
|
||||
<div class="day-weather-entry"><span class="day-weather-icon" style="font-size: {{ weather_size * 1.3 }}px;">{{ we.emoji }}</span><span>{{ we.high }}°/{{ we.low }}°{{ unit_suffix }}</span></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="day-rows">
|
||||
{% if not rows and not more_count %}
|
||||
<div class="day-empty" style="font-size: {{ body_size }}px;">Nothing scheduled</div>
|
||||
{% endif %}
|
||||
{% for row in rows %}
|
||||
<div class="day-row" style="height: {{ row_h }}px;">
|
||||
<div class="day-chip">{% for c in row.colors %}<span style="background:{{ c }};"></span>{% endfor %}</div>
|
||||
<div class="day-time" style="font-size: {{ body_size }}px;">{{ row.time }}</div>
|
||||
<div class="day-summary" style="font-size: {{ body_size }}px;">{{ row.summary }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if more_count %}<div class="day-more" style="font-size: {{ body_size }}px;">+{{ more_count }} more</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.icon-wrap { display: flex; align-items: center; }
|
||||
.icon-body {
|
||||
width: {{ icon_w }}px;
|
||||
height: {{ icon_h }}px;
|
||||
border: {{ stroke }}px solid #000000;
|
||||
border-radius: {{ icon_radius }}px;
|
||||
padding: {{ stroke }}px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.icon-fill {
|
||||
width: {{ fill_pct }}%;
|
||||
height: 100%;
|
||||
border-radius: {{ fill_radius }}px;
|
||||
background: linear-gradient(180deg, {{ fill_color }}, {{ fill_color_dark }});
|
||||
}
|
||||
.icon-nub {
|
||||
width: {{ nub_w }}px;
|
||||
height: {{ nub_h }}px;
|
||||
background: #000000;
|
||||
border-radius: 0 {{ nub_radius }}px {{ nub_radius }}px 0;
|
||||
}
|
||||
.pct { font-weight: 700; font-size: {{ pct_size }}px; line-height: 1; color: {{ fill_color }}; }
|
||||
.line { font-weight: 400; font-size: {{ line_size }}px; line-height: 1; color: #5b6674; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon-wrap">
|
||||
<div class="icon-body"><div class="icon-fill"></div></div>
|
||||
<div class="icon-nub"></div>
|
||||
</div>
|
||||
<div class="pct">{{ percent }}%</div>
|
||||
{% for line in lines %}<div class="line">{{ line }}</div>{% endfor %}
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.2);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.day-section { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
|
||||
.day-header { flex: 0 0 auto; color: #ffffff; font-weight: 700; line-height: 1.2; padding: 10px 16px; overflow: hidden; }
|
||||
.day-weather-row { display: flex; gap: 14px; margin-top: 6px; }
|
||||
.day-weather-entry { display: flex; align-items: center; gap: 4px; color: rgba(255,255,255,0.9); }
|
||||
.day-weather-icon { line-height: 1; }
|
||||
.day-rows { flex: 1 1 auto; padding: 8px 14px; overflow: hidden; }
|
||||
.day-row { display: flex; align-items: center; gap: 8px; }
|
||||
.day-chip { display: flex; height: 12px; width: 10px; border-radius: 3px; overflow: hidden; flex: 0 0 auto; }
|
||||
.day-chip span { flex: 1 1 0; }
|
||||
.day-time { color: #5b6674; flex: 0 0 auto; white-space: nowrap; }
|
||||
.day-summary { color: #17233b; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.day-empty, .day-more { color: #5b6674; padding-top: 4px; }
|
||||
</style></head>
|
||||
<body>
|
||||
{% import "_calendar_day_section.html.jinja" as ds %}
|
||||
<div class="card">
|
||||
{{ ds.day_section(header, weather_entries, weather_size, unit_suffix, rows, more_count, row_h, body_size, title_size, accent_start, accent_end, header_h) }}
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.2);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.weekday-row { display: flex; flex: 0 0 auto; background: {{ accent_start }}; }
|
||||
.weekday-cell { flex: 1 1 0; color: #ffffff; font-weight: 700; font-size: {{ header_size }}px; text-align: center; padding: 4px 0; }
|
||||
.weeks { flex: 1 1 auto; display: flex; flex-direction: column; }
|
||||
.week-row { flex: 1 1 0; display: flex; }
|
||||
.day-cell { flex: 1 1 0; border: 1px solid #e2e6ec; padding: 4px; min-width: 0; overflow: hidden; }
|
||||
{#- Bold everywhere, including out-of-month -- de-emphasis is via
|
||||
smaller size only, not weight or a gray color. Regular-weight and
|
||||
gray text are both individually fragile under Bayer ordered
|
||||
dithering at small sizes (thin/low-contrast anti-aliased edges
|
||||
have little "mass" to survive the bias+threshold step), and this
|
||||
cell combined both, which degraded out-of-month day numbers into
|
||||
unrecognizable speckle -- classic PIL's own de-emphasis trick
|
||||
(weight instead of gray, see calendar_render._build_month's
|
||||
docstring) doesn't transfer safely to this render path. #}
|
||||
.day-num { font-size: {{ day_size }}px; font-weight: 700; color: #17233b; }
|
||||
.day-num.out-of-month { font-size: {{ day_size * 0.8 }}px; }
|
||||
.day-num.today {
|
||||
display: inline-block; background: {{ accent_start }}; color: #ffffff;
|
||||
border-radius: 4px; padding: 0 4px;
|
||||
}
|
||||
.dots { display: flex; gap: 3px; margin-top: 3px; align-items: center; flex-wrap: wrap; }
|
||||
.dot { width: {{ dot_size }}px; height: {{ dot_size }}px; border-radius: 50%; flex: 0 0 auto; }
|
||||
.dot-more { font-size: {{ day_size * 0.8 }}px; color: #5b6674; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="weekday-row">
|
||||
{% for name in day_names %}<div class="weekday-cell">{{ name }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="weeks">
|
||||
{% for week in weeks %}
|
||||
<div class="week-row">
|
||||
{% for day in week %}
|
||||
<div class="day-cell">
|
||||
<span class="day-num {% if day.is_today %}today{% elif not day.in_month %}out-of-month{% endif %}">{{ day.day_num }}</span>
|
||||
{% if day.dots %}
|
||||
<div class="dots">
|
||||
{% for c in day.dots %}<div class="dot" style="background:{{ c }};"></div>{% endfor %}
|
||||
{% if day.more_count %}<span class="dot-more">+{{ day.more_count }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.2);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.day-section { display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; overflow: hidden; }
|
||||
.day-section + .day-section { border-top: 1px solid #e2e6ec; }
|
||||
.day-header { flex: 0 0 auto; color: #ffffff; font-weight: 700; line-height: 1.2; padding: 8px 16px; overflow: hidden; }
|
||||
.day-weather-row { display: flex; gap: 14px; margin-top: 4px; }
|
||||
.day-weather-entry { display: flex; align-items: center; gap: 4px; color: rgba(255,255,255,0.9); }
|
||||
.day-weather-icon { line-height: 1; }
|
||||
.day-rows { flex: 1 1 auto; padding: 6px 14px; overflow: hidden; }
|
||||
.day-row { display: flex; align-items: center; gap: 8px; }
|
||||
.day-chip { display: flex; height: 12px; width: 10px; border-radius: 3px; overflow: hidden; flex: 0 0 auto; }
|
||||
.day-chip span { flex: 1 1 0; }
|
||||
.day-time { color: #5b6674; flex: 0 0 auto; white-space: nowrap; }
|
||||
.day-summary { color: #17233b; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.day-empty, .day-more { color: #5b6674; padding-top: 2px; }
|
||||
</style></head>
|
||||
<body>
|
||||
{% import "_calendar_day_section.html.jinja" as ds %}
|
||||
<div class="card">
|
||||
{% for day in days %}
|
||||
{{ ds.day_section(day.header, day.weather_entries, weather_size, unit_suffix, day.rows, day.more_count, row_h, body_size, title_size, accent_start, accent_end, header_h) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.2);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
}
|
||||
.col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
|
||||
.col + .col { border-left: 1px solid #e2e6ec; }
|
||||
.col-header {
|
||||
/* Fixed height (not auto) -- every column must be exactly this tall
|
||||
regardless of whether THIS particular day has a weather entry, or
|
||||
columns with/without weather misalign their event rows to
|
||||
different starting Y positions across the week grid. */
|
||||
height: {{ header_h }}px;
|
||||
flex: 0 0 auto;
|
||||
background: linear-gradient(135deg, {{ accent_start }} 0%, {{ accent_end }} 100%);
|
||||
color: #ffffff; font-weight: 700; font-size: {{ header_size }}px;
|
||||
padding: 6px 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.col-header .label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.col-weather { display: flex; align-items: center; gap: 3px; color: rgba(255,255,255,0.9); font-size: {{ weather_size }}px; margin-top: 2px; }
|
||||
.col-rows { flex: 1 1 auto; padding: 4px; overflow: hidden; }
|
||||
.col-row { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
||||
.col-chip { width: 7px; height: 7px; border-radius: 2px; flex: 0 0 auto; }
|
||||
.col-summary {
|
||||
flex: 1 1 0; min-width: 0;
|
||||
font-size: {{ chip_size }}px; color: #17233b; line-height: 1.3;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.col-more { font-size: {{ chip_size }}px; color: #5b6674; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
{% for col in cols %}
|
||||
<div class="col">
|
||||
<div class="col-header">
|
||||
<div class="label">{{ col.label }}</div>
|
||||
{% if col.weather %}<div class="col-weather"><span>{{ col.weather.emoji }}</span><span>{{ col.weather.high }}°/{{ col.weather.low }}°{{ unit_suffix }}</span></div>{% endif %}
|
||||
</div>
|
||||
<div class="col-rows">
|
||||
{% for row in col.rows %}
|
||||
<div class="col-row"><div class="col-chip" style="background:{{ row.color }};"></div><div class="col-summary">{{ row.summary }}</div></div>
|
||||
{% endfor %}
|
||||
{% if col.more_count %}<div class="col-more">+{{ col.more_count }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.25);
|
||||
background: #ffffff;
|
||||
}
|
||||
.card img {
|
||||
width: 100%; height: 100%; display: block;
|
||||
/* "contain", not "cover" -- the source image already went through
|
||||
compose_into's own crop/fit (e.g. whiteboard's deliberate
|
||||
letterbox-never-crop mode), so this card must not re-crop it;
|
||||
the gutter inset is small relative to typical widget sizes, so
|
||||
"contain" leaves at most a sliver of background visible, not a
|
||||
real letterbox. */
|
||||
object-fit: contain;
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card"><img src="data:image/png;base64,{{ image_b64 }}"></div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Regular.ttf"); font-weight: 400; }
|
||||
@font-face { font-family: "Inter"; src: url("file://{{ font_dir }}/Inter-Bold.ttf"); font-weight: 700; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "Inter", sans-serif; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: #ffffff; }
|
||||
.card {
|
||||
width: {{ w - gutter * 2 }}px;
|
||||
height: {{ h - gutter * 2 }}px;
|
||||
margin: {{ gutter }}px;
|
||||
border-radius: {{ radius }}px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 14px rgba(0, 20, 60, 0.2);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
height: {{ header_h }}px;
|
||||
flex: 0 0 auto;
|
||||
background: linear-gradient(135deg, {{ accent_start }} 0%, {{ accent_end }} 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.header .title { color: #ffffff; font-weight: 700; font-size: {{ title_size }}px; line-height: 1; }
|
||||
.rows { flex: 1 1 auto; padding: 8px 14px; overflow: hidden; }
|
||||
.row { display: flex; align-items: center; gap: 8px; height: {{ row_h }}px; }
|
||||
.chip { display: flex; height: 12px; width: 10px; border-radius: 3px; overflow: hidden; flex: 0 0 auto; }
|
||||
.chip span { flex: 1 1 0; }
|
||||
.box {
|
||||
width: {{ box_size }}px; height: {{ box_size }}px; border-radius: 3px; flex: 0 0 auto;
|
||||
border: 2px solid #17233b;
|
||||
}
|
||||
.box.done { border-color: {{ accent_start }}; background: {{ accent_start }}; }
|
||||
.due { font-size: {{ body_size }}px; color: #5b6674; flex: 0 0 auto; white-space: nowrap; }
|
||||
.summary {
|
||||
font-size: {{ body_size }}px; color: #17233b; line-height: 1.2;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.empty { font-size: {{ body_size }}px; color: #5b6674; padding-top: 4px; }
|
||||
.more { font-size: {{ body_size }}px; color: #5b6674; padding-top: 2px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="header"><div class="title">{{ title }}</div></div>
|
||||
<div class="rows">
|
||||
{% if not rows %}
|
||||
<div class="empty">Nothing outstanding</div>
|
||||
{% endif %}
|
||||
{% for row in rows %}
|
||||
<div class="row">
|
||||
<div class="chip">{% for c in row.colors %}<span style="background:{{ c }};"></span>{% endfor %}</div>
|
||||
<div class="box {% if row.done %}done{% endif %}"></div>
|
||||
{% if row.due %}<div class="due">{{ row.due }}</div>{% endif %}
|
||||
<div class="summary">{{ row.summary }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if more_count %}<div class="more">+{{ more_count }} more</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html><head><style>
|
||||
@font-face { font-family: "TextFont"; src: url("file://{{ font_regular }}"); font-weight: 400; font-style: normal; }
|
||||
@font-face { font-family: "TextFont"; src: url("file://{{ font_bold }}"); font-weight: 700; font-style: normal; }
|
||||
@font-face { font-family: "TextFont"; src: url("file://{{ font_italic }}"); font-weight: 400; font-style: italic; }
|
||||
@font-face { font-family: "TextFont"; src: url("file://{{ font_bold_italic }}"); font-weight: 700; font-style: italic; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { width: {{ w }}px; height: {{ h }}px; background: {{ bg_color }}; }
|
||||
.wrap {
|
||||
width: {{ w - margin * 2 }}px;
|
||||
min-height: {{ h - margin * 2 }}px;
|
||||
margin: {{ margin }}px;
|
||||
font-family: "TextFont", sans-serif;
|
||||
font-size: {{ size }}px;
|
||||
line-height: {{ line_height }};
|
||||
text-align: {{ align }};
|
||||
color: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
p { min-height: 1em; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
{% for paragraph in paragraphs %}
|
||||
<p>
|
||||
{% if not paragraph %} {% endif %}
|
||||
{% for run in paragraph %}<span style="{% if run.bold %}font-weight:700;{% endif %}{% if run.italic %}font-style:italic;{% endif %}{% if run.underline %}text-decoration:underline;{% endif %}{% if run.color %}color:{{ run.color }};{% endif %}{% if run.bg %}background:{{ run.bg }};{% endif %}">{{ run.text }}</span>{% endfor %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user