Compare commits

...
5 Commits
Author SHA1 Message Date
lukasandClaude Opus 5.5 f82c7eb444 Loading splash: animated logo over a dark overlay on desktop
index.html carries the overlay markup and styles inline so it paints before
any script; public/splash.js (external, for the CSP) plays the logo
animation from icons/ultimo-splash.html, holds, fades out after ~3.25 s and
removes itself. Skipped at phone sizes (same query as useIsMobile) and in
pop-out compose windows; a click skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:23:56 +01:00
lukasandClaude Opus 5.5 05db376b89 Updated logo, carried to every derived icon
New ultimo-logo and signet SVGs, re-rendered into the PWA icons (any and
maskable), apple-touch icon, favicon, the public and calendar-plugin
signet copies, the BIMI Tiny-PS logo and the system-email logo PNG, each
at its previous size, placement and background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:13:15 +01:00
lukasandClaude Opus 5.5 77e9c25353 Admin: allow changing your own quota
The self-protection guard refused every change to the admin's own account.
It now only refuses blocking and deleting yourself; a quota change is fine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:39:33 +01:00
lukasandClaude Opus 5.5 a4581ab199 Interface size setting: Default, +10%, +20%
Mailbox display and Settings → Appearance get an interface-size picker,
stored per device in localStorage and applied as CSS zoom on <html>.
Viewport-height layouts divide by --ui-zoom, and code that turns mouse
coordinates or bounding rects into CSS lengths (resizers, compose dock,
emoji picker, editor popups and image handles, swipe-to-delete) divides
by the zoom factor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:20:39 +01:00
lukasandClaude Opus 5.5 d8e6cca66b Spam banner: inset card, red title, plain-language reasons, translated
The banner sits inside the message body's 16px gutter with room below,
"Looks like spam!" is red, and the rspamd symbols are replaced by up to
three human-readable reasons (neutral symbols are hidden). All strings go
through ctx.t with English and Polish catalog entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:20:39 +01:00
32 changed files with 876 additions and 193 deletions
+11 -6
View File
@@ -3,8 +3,9 @@
The backend seam for the admin panel: the registration gate — the first account
on a fresh install becomes the administrator and closes signup behind itself,
and this is how that gets reopened — the default quota, and managing users
(search, per-user quota, block, delete). An administrator cannot block or delete
their own account, so the instance can never lock out its last admin that way.
(search, per-user quota, block, delete). An administrator can change their own
quota but cannot block or delete their own account, so the instance can never
lock out its last admin that way.
"""
from __future__ import annotations
@@ -280,12 +281,15 @@ def _user_out(user: User, used_bytes: int, accounts: list[str]) -> AdminUserOut:
)
async def _target(db: AsyncSession, admin: User, user_id: uuid.UUID) -> User:
"""The user an admin action is aimed at; never the admin themselves."""
async def _target(
db: AsyncSession, admin: User, user_id: uuid.UUID, *, allow_self: bool = False
) -> User:
"""The user an admin action is aimed at. Blocking and deleting are never
allowed on the admin themselves; a quota change is (``allow_self``)."""
user = await db.scalar(select(User).where(User.id == user_id))
if user is None:
raise_api_error(404, "not_found", "user not found")
if user.id == admin.id:
if user.id == admin.id and not allow_self:
raise_api_error(409, "cannot_modify_self", "you cannot do this to your own account")
return user
@@ -303,7 +307,8 @@ async def patch_user(
user_id: uuid.UUID, body: AdminUserPatch, admin: CurrentAdmin, db: DbSession
) -> AdminUserOut:
"""Change a user's quota and/or block or unblock them."""
user = await _target(db, admin, user_id)
# Your own quota is fine to change; blocking yourself is not.
user = await _target(db, admin, user_id, allow_self=body.blocked is None)
if body.quota_bytes is not None:
user.quota_bytes = body.quota_bytes
if body.blocked is not None:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+14
View File
@@ -168,6 +168,20 @@ async def test_admin_cannot_block_or_delete_themselves(world: World) -> None:
assert (await c.delete(f"/api/admin/users/{admin.id}")).status_code == 409
async def test_admin_can_change_their_own_quota(world: World) -> None:
admin = await world.user(admin=True)
async with await world.client(admin) as c:
resp = await c.patch(f"/api/admin/users/{admin.id}", json={"quota_bytes": 5 * 2**30})
assert resp.status_code == 200
assert resp.json()["quota_bytes"] == 5 * 2**30
# Quota together with a block is still refused: the block is the problem.
assert (
await c.patch(
f"/api/admin/users/{admin.id}", json={"quota_bytes": 2**30, "blocked": True}
)
).status_code == 409
async def test_delete_removes_the_user_and_their_data(world: World, bus: _Bus) -> None:
admin = await world.user(admin=True)
target = await world.user()
+44 -44
View File
@@ -26,11 +26,11 @@
inkscape:document-units="mm"
inkscape:zoom="2.5488772"
inkscape:cx="89.451151"
inkscape:cy="-10.20057"
inkscape:cy="-10.004405"
inkscape:window-width="2560"
inkscape:window-height="1403"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-height="1376"
inkscape:window-x="2560"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><defs
id="defs1"><linearGradient
@@ -84,61 +84,61 @@
style="fill:url(#linearGradient1789);fill-opacity:1;stroke-width:0.2;stroke-linecap:round"
d="m 18.20332,107.02297 c 0,0 1.930731,1.59986 4.989281,4.12846 1.725315,1.42637 3.809507,3.14827 6.108453,5.04523 5.657506,4.74616 33.20207,27.48506 33.558689,27.18811 0.241178,-0.20083 -16.275384,9.86387 -16.275384,9.86387 L 17.979141,140.69463 Z"
id="path1776-1"
sodipodi:nodetypes="cccsccc" /></mask><linearGradient
sodipodi:nodetypes="cccsccc" /></mask><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient13-6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-18.692802,-93.278335)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
id="radialGradient816"
xlink:href="#linearGradient8-5"
id="radialGradient816-3"
cx="8.0343275"
cy="13.452971"
fx="8.0343275"
fy="13.452971"
r="16.678839"
gradientTransform="matrix(-0.29271702,0.00410386,-0.01334442,-0.95181978,11.840139,23.950941)"
gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient8"
id="linearGradient8-5"
inkscape:collect="always"><stop
style="stop-color:#fa7367;stop-opacity:1;"
style="stop-color:#fa4435;stop-opacity:1;"
offset="0"
id="stop8" /><stop
id="stop8-6" /><stop
style="stop-color:#fa7367;stop-opacity:0;"
offset="1"
id="stop9" /></linearGradient><radialGradient
id="stop9-2" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient6"
id="radialGradient815"
cx="35.709007"
cy="12.042645"
fx="35.709007"
fy="12.042645"
xlink:href="#linearGradient6-1"
id="radialGradient815-9"
cx="34.523952"
cy="11.253304"
fx="34.523952"
fy="11.253304"
r="16.740065"
gradientTransform="matrix(0.25712152,0.00260463,-0.00969379,0.95694375,26.644192,0.97301292)"
gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient6"
id="linearGradient6-1"
inkscape:collect="always"><stop
style="stop-color:#fab23b;stop-opacity:1;"
style="stop-color:#fadc56;stop-opacity:1;"
offset="0"
id="stop6" /><stop
style="stop-color:#fab23a;stop-opacity:0;"
id="stop6-2" /><stop
style="stop-color:#faae3a;stop-opacity:0;"
offset="1"
id="stop7" /></linearGradient></defs><g
id="g816"
transform="translate(0.128676,-0.079965)"><path
id="path1-2-1-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient13-6);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 37.175189,4.174075 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592601,-4.12477 -4.1103801,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.76402 7.0382001,7.38257 14.4526701,7.38257 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.44221 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 V 8.300605 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.85901,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -0.001,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247011,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"
id="stop7-7" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /></defs><g
id="g1"
transform="translate(1.425541,43.741704)"><path
id="path1-2-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient1);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"
sodipodi:nodetypes="cccccccccccssssccccccccssssssccccccccccccscc" /><path
id="path1-2-1-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 3.6452555,7.14375 C 3.5159759,7.5278714 3.4447807,7.9357538 3.444751,8.3586629 V 23.909652 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.76402 7.038395,7.382495 14.452865,7.382495 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.218257 -3.218664,-2.595334 -5.013648,-4.05505 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.859257,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249919,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 C 16.15064,17.512631 11.679239,13.804108 9.750826,12.186336 7.4531167,10.290397 5.3700283,8.5696704 3.6452555,7.14375 Z" /><path
id="path1-2-1-7"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 41.226941,7.09104 c -1.718284,1.4205409 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.805553,1.467965 -3.513225,2.849693 -5.0307002,4.067452 2.6425302,2.518552 6.6894502,4.460193 12.1212242,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.441923 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 V 8.3007853 C 41.429125,7.8798785 41.357396,7.4736084 41.226941,7.09104 Z" /></g></svg>
id="path1-2-1-9-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816-3);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z" /><path
id="path1-2-1-7-3"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815-9);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z" /></g></svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

+26 -27
View File
@@ -24,19 +24,19 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.9327415"
inkscape:cx="187.29872"
inkscape:cy="135.81744"
inkscape:zoom="0.96637075"
inkscape:cx="84.853562"
inkscape:cy="115.89755"
inkscape:window-width="2560"
inkscape:window-height="1376"
inkscape:window-x="2560"
inkscape:window-y="27"
inkscape:window-height="1403"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><defs
id="defs1"><linearGradient
id="linearGradient8"
inkscape:collect="always"><stop
style="stop-color:#fa7367;stop-opacity:1;"
style="stop-color:#fa4435;stop-opacity:1;"
offset="0"
id="stop8" /><stop
style="stop-color:#fa7367;stop-opacity:0;"
@@ -44,10 +44,10 @@
id="stop9" /></linearGradient><linearGradient
id="linearGradient6"
inkscape:collect="always"><stop
style="stop-color:#fab23b;stop-opacity:1;"
style="stop-color:#fadc56;stop-opacity:1;"
offset="0"
id="stop6" /><stop
style="stop-color:#fab23a;stop-opacity:0;"
style="stop-color:#faae3a;stop-opacity:0;"
offset="1"
id="stop7" /></linearGradient><linearGradient
id="linearGradient11"><stop
@@ -105,12 +105,12 @@
inkscape:collect="always"
xlink:href="#linearGradient6"
id="radialGradient815"
cx="35.709007"
cy="12.042645"
fx="35.709007"
fy="12.042645"
cx="34.523952"
cy="11.253304"
fx="34.523952"
fy="11.253304"
r="16.740065"
gradientTransform="matrix(0.25712152,0.00260463,-0.00969379,0.95694375,26.644192,0.97301292)"
gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,25.754108,1.2533273)"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
@@ -120,19 +120,18 @@
fx="8.0343275"
fy="13.452971"
r="16.678839"
gradientTransform="matrix(-0.29271702,0.00410386,-0.01334442,-0.95181978,11.840139,23.950941)"
gradientUnits="userSpaceOnUse" /></defs><g
id="g816"><path
id="path1-2-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient13);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 37.175189,4.174075 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592601,-4.12477 -4.1103801,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.76402 7.0382001,7.38257 14.4526701,7.38257 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.44221 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 V 8.300605 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.85901,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -0.001,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247011,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"
sodipodi:nodetypes="cccccccccccssssccccccccssssssccccccccccccscc" /><path
id="path1-2-1-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 3.6452555 7.14375 C 3.5159759 7.5278714 3.4447807 7.9357538 3.444751 8.3586629 L 3.444751 23.909652 C 3.444711 23.914652 3.4152451 27.753282 5.617745 31.514872 C 7.821705 35.278892 12.65614 38.897367 20.07061 38.897367 L 24.651208 38.897367 C 30.085144 38.897367 34.141887 36.930057 36.802405 34.39325 C 35.285228 33.174993 33.583741 31.797916 31.788757 30.3382 C 31.529773 30.540194 31.247156 30.732346 30.938163 30.908708 C 29.465893 31.749008 27.477765 32.239376 24.666195 32.239376 L 20.085079 32.239376 C 17.261269 32.239376 15.286427 31.759973 13.828097 30.935063 C 12.369767 30.110163 11.51428 28.931371 10.96884 27.999841 C 9.7721994 25.956191 9.7389404 23.928255 9.7389404 23.928255 L 9.7379069 23.914303 L 9.7379069 23.898283 L 9.7379069 12.201839 L 9.7379069 12.187887 L 9.7415243 12.179618 C 9.7928043 12.228318 9.92951 12.342748 9.98337 12.387358 L 15.408362 16.932817 L 20.923271 21.504114 C 21.007674 21.573954 21.108583 21.634775 21.221444 21.686532 C 16.15064 17.512631 11.679239 13.804108 9.750826 12.186336 C 7.4531167 10.290397 5.3700283 8.5696704 3.6452555 7.14375 z " /><path
id="path1-2-1-7"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 41.226941 7.09104 C 39.508657 8.5115809 37.437441 10.222955 35.154443 12.106755 C 33.219453 13.730045 28.724331 17.458018 23.632149 21.649325 C 23.714466 21.60631 23.788932 21.557823 23.853841 21.504114 L 29.36875 16.932817 L 34.85627 12.358936 C 34.87617 12.342536 34.897015 12.325776 34.916215 12.310876 L 34.94412 12.289689 L 35.166846 12.09952 L 35.166846 23.697778 L 35.166846 23.726717 C 35.166846 23.726717 35.100288 25.808681 33.855298 27.910441 C 33.283278 28.876161 32.409906 30.068398 30.937646 30.908708 C 29.465376 31.749008 27.477248 32.239376 24.665678 32.239376 L 20.084562 32.239376 C 17.260752 32.239376 15.28591 31.759973 13.82758 30.935063 C 13.518898 30.760458 13.236992 30.570319 12.979569 30.369722 C 11.174016 31.837687 9.466344 33.219415 7.9488688 34.437174 C 10.591399 36.955726 14.638319 38.897367 20.070093 38.897367 L 24.650692 38.897367 C 32.068392 38.897367 36.920103 35.231484 39.156783 31.455444 C 41.393463 27.679404 41.426929 23.820251 41.426929 23.820251 L 41.428996 23.786145 L 41.428996 8.3007853 C 41.429125 7.8798785 41.357396 7.4736084 41.226941 7.09104 z " /></g><text
gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,11.509024,28.421367)"
gradientUnits="userSpaceOnUse" /></defs><path
id="path1-2-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient13);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 37.175189,4.174075 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592601,-4.12477 -4.1103801,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.76402 7.0382001,7.38257 14.4526701,7.38257 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.44221 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 V 8.300605 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.85901,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -0.001,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247011,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"
sodipodi:nodetypes="cccccccccccssssccccccccssssssccccccccccccscc" /><path
id="path1-2-1-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 3.6452555,7.14375 C 3.5159759,7.5278714 3.4447807,7.9357538 3.444751,8.3586629 V 23.909652 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.76402 7.038395,7.382495 14.452865,7.382495 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.218257 -3.218664,-2.595334 -5.013648,-4.05505 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.859257,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249919,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 C 16.15064,17.512631 11.679239,13.804108 9.750826,12.186336 7.4531167,10.290397 5.3700283,8.5696704 3.6452555,7.14375 Z" /><path
id="path1-2-1-7"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 41.226941,7.09104 c -1.718284,1.4205409 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.805553,1.467965 -3.513225,2.849693 -5.0307002,4.067452 2.6425302,2.518552 6.6894502,4.460193 12.1212242,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.441923 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 V 8.3007853 C 41.429125,7.8798785 41.357396,7.4736084 41.226941,7.09104 Z" /><text
xml:space="preserve"
style="font-size:44.7887px;line-height:1.5;font-family:Roboto;-inkscape-font-specification:Roboto;letter-spacing:0.223944px;writing-mode:lr-tb;direction:ltr;fill:#f74e6a;fill-opacity:1;stroke:#ff0000;stroke-width:56.4265;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
x="43.547977"

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

+237
View File
@@ -0,0 +1,237 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ultimo Splash</title>
<style>
/* ---- Splash styles (copy into your site) ---------------------------- */
:root { --splash-bg: #0f0f12; }
#ultimo-splash {
position: fixed; inset: 0; z-index: 9999;
display: grid; place-items: center;
background: var(--splash-bg);
transition: opacity .35s ease, visibility 0s linear .35s;
cursor: pointer;
}
#ultimo-splash.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
#ultimo-splash svg { width: min(46vmin, 340px); height: auto; overflow: visible; }
/* ---- Demo page only (remove on your site) ---------------------------- */
html, body { margin: 0; height: 100%; }
body { font-family: system-ui, sans-serif; background: #f6f6f7; color: #1b1b1f;
display: grid; place-items: center; }
.demo { text-align: center; }
.demo button { font: inherit; padding: 12px 20px; border-radius: 10px; border: 1px solid #c9c9cf;
background: #fff; cursor: pointer; min-height: 44px; }
</style>
</head>
<body>
<!-- ======================= SPLASH MARKUP (copy) ======================= -->
<div id="ultimo-splash" aria-hidden="true">
<svg viewBox="-10 -6 65 56" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="ul-g1" x1="54.340946" y1="122.84999" x2="54.340946" y2="162.66415" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)">
<stop offset="0.20198774" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<radialGradient id="ul-r1" cx="8.0343275" cy="13.452971" fx="8.0343275" fy="13.452971" r="16.678839" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)">
<stop offset="0" stop-color="#fa4435" stop-opacity="1"/><stop offset="1" stop-color="#fa7367" stop-opacity="0"/>
</radialGradient>
<radialGradient id="ul-r2" cx="34.523952" cy="11.253304" fx="34.523952" fy="11.253304" r="16.740065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)">
<stop offset="0" stop-color="#fadc56" stop-opacity="1"/><stop offset="1" stop-color="#faae3a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="ul-gs" x1="0" y1="5.11" x2="0" y2="37.01" gradientUnits="userSpaceOnUse">
<stop offset="0.202" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<filter id="ul-sh" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="0.35" stdDeviation="0.5" flood-color="#000" flood-opacity="0.42"/>
</filter>
<filter id="ul-blur" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="1.1"/>
</filter>
<g id="ul-logo" transform="translate(1.425541,43.741704)">
<path id="ul-main" fill="url(#ul-g1)" d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"/>
<path fill="url(#ul-r1)" d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z"/>
<path fill="url(#ul-r2)" d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z"/>
</g>
<!-- Mask that reveals the logo's U as the stroke draws -->
<mask id="ul-mask-u" maskUnits="userSpaceOnUse" x="-20" y="-20" width="100" height="100">
<path id="ul-stroke" d="M7.73,8.31 L7.73,23.8 C7.73,30.6 13.6,34.65 22.57,34.65 C31.5,34.65 37.4,30.6 37.4,23.8 L37.4,8.31"
pathLength="1" fill="none" stroke="#fff" stroke-width="9.6" stroke-linecap="round"
stroke-dasharray="1 2" stroke-dashoffset="1"/>
</mask>
<!-- Logo colours limited to the top-flap area -->
<!-- Area between the U's arms: inside it the logo's own fill is exactly the flap -->
<clipPath id="ul-clip-flap"><path d="M10.093,11.066 L35.296,11.066 L35.296,12.1 L22.69,23.6 L10.093,12.1 Z"/></clipPath>
<!-- Darkens the coloured flap while it is mid-fold -->
<filter id="ul-dark" x="0" y="0" width="1" height="1">
<feComponentTransfer>
<feFuncR id="ul-dr" type="linear" slope="1"/><feFuncG id="ul-dg" type="linear" slope="1"/><feFuncB id="ul-db" type="linear" slope="1"/>
</feComponentTransfer>
</filter>
</defs>
<!-- Envelope (white/grey paper) -->
<g id="ul-env">
<g id="ul-rest">
<!-- Thin paper margin tucked under the U so no background shows along its inner edge -->
<rect id="ul-under" x="9.8" y="12.1" width="25.8" height="20.4" fill="rgb(250,250,253)" opacity="0" mask="url(#ul-mask-u)"/>
<rect x="10.093" y="11.066" width="25.203" height="21.064" fill="rgb(226,226,230)"/>
<path id="ul-fl" filter="url(#ul-sh)"/>
<path id="ul-fr" filter="url(#ul-sh)"/>
<path id="ul-fb" filter="url(#ul-sh)"/>
</g>
<!-- Top flap: exact outline of the logo's V flap (shown folded); flipped about the top edge while folding -->
<!-- Inside of the flap = white paper; outside = the logo's colours (clipped to the flap) -->
<g id="ul-ftg" filter="url(#ul-sh)">
<path id="ul-ft"
d="M10.093,11.066 L35.296,11.066 L35.296,12.019 L35.208,12.089 L24.206,21.234 C23.554,21.773 21.927,21.773 21.275,21.234 L10.335,12.117 L10.093,11.910 Z"/>
<g id="ul-fto" opacity="0" filter="url(#ul-dark)">
<use href="#ul-logo" clip-path="url(#ul-clip-flap)"/>
</g>
</g>
</g>
<!-- U drawn over the envelope: the real logo artwork (with its side gradients) revealed by a growing stroke -->
<g id="ul-ug" opacity="0">
<use href="#ul-logo" mask="url(#ul-mask-u)"/>
</g>
<!-- Final logo + glow -->
<use id="ul-glow" href="#ul-logo" filter="url(#ul-blur)" opacity="0"/>
<use id="ul-final" href="#ul-logo" opacity="0"/>
</svg>
</div>
<!-- ===================================================================== -->
<!-- Demo page content (remove on your site) -->
<main class="demo">
<p>Your website content</p>
<button type="button" id="replay">Replay intro</button>
</main>
<script>
/* ======================= SPLASH SCRIPT (copy) ======================== */
(function () {
var ONCE_PER_SESSION = false; // true = show only on the first page view per browser session
var SPEED = 1; // 1 = ~2.6 s to final logo; 0.8 = faster, 1.2 = slower
var HOLD_MS = 450; // how long the finished logo stays before the splash fades
var root = document.getElementById('ultimo-splash');
if (!root) return;
var $ = function (id) { return document.getElementById(id); };
var env = $('ul-env'), rest = $('ul-rest'), fl = $('ul-fl'), fr = $('ul-fr'), fb = $('ul-fb'), ft = $('ul-ft');
var stroke = $('ul-stroke'), glow = $('ul-glow'), fin = $('ul-final');
var ug = $('ul-ug'), under = $('ul-under');
var ftg = $('ul-ftg'), fto = $('ul-fto'), dr = $('ul-dr'), dg = $('ul-dg'), db = $('ul-db');
// Envelope body edges (logo coordinates) and the four flaps.
var L = 10.093, R = 35.296, T = 11.066, B = 32.13, CX = 22.69, CY = 21.6;
var FLAPS = {
l: { axis: 'x', h: L, a: T, b: B, mid: CY, depth: 12.2, dir: -1, r: 3.2 },
r: { axis: 'x', h: R, a: T, b: B, mid: CY, depth: 12.2, dir: 1, r: 3.2 },
b: { axis: 'y', h: B, a: L, b: R, mid: CX, depth: 14.0, dir: 1, r: 3.6 }
};
var END = 2450; // ms (at SPEED 1) when the logo is fully formed
function clamp(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
function seg(t, a, b) { return clamp((t - a) / (b - a)); }
function ease(x) { return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
function easeOut(x) { return 1 - Math.pow(1 - x, 3); }
function n(v) { return Math.round(v * 1000) / 1000; }
// Flap outline: base fixed exactly on the body edge, apex swings over the hinge, tip rounded.
function flapD(f, p) {
var s = Math.cos(Math.PI * p), ax, ay, A, C;
if (f.axis === 'x') { ax = f.h + f.dir * f.depth * s; ay = f.mid; A = [f.h, f.a]; C = [f.h, f.b]; }
else { ax = f.mid; ay = f.h + f.dir * f.depth * s; A = [f.a, f.h]; C = [f.b, f.h]; }
function toward(P) {
var dx = P[0] - ax, dy = P[1] - ay, l = Math.hypot(dx, dy) || 1, r = Math.min(f.r, l * 0.45);
return n(ax + dx * r / l) + ',' + n(ay + dy * r / l);
}
return 'M' + A[0] + ',' + A[1] + ' L' + toward(A) + ' Q' + n(ax) + ',' + n(ay) + ' ' + toward(C) +
' L' + C[0] + ',' + C[1] + ' Z';
}
// Paper shading: inner face (grey) before the flip, outer face (white) after it.
function faceC(p) {
var lift = Math.sin(Math.PI * p);
var c = Math.round(p < .5 ? 230 - 42 * lift : 250 - 36 * lift);
return 'rgb(' + c + ',' + c + ',' + (c + 3) + ')';
}
function setFlap(el, f, p) {
el.setAttribute('d', flapD(f, p));
el.setAttribute('fill', faceC(p));
}
function render(ms) {
var intro = easeOut(seg(ms, 0, 220));
setFlap(fl, FLAPS.l, ease(seg(ms, 180, 560)));
setFlap(fr, FLAPS.r, ease(seg(ms, 240, 620)));
setFlap(fb, FLAPS.b, ease(seg(ms, 560, 920)));
var pt = ease(seg(ms, 880, 1260)), st = -Math.cos(Math.PI * pt);
if (Math.abs(st) < 0.001) st = 0.001;
// Top flap: white inside while open; once it flips past vertical its outside (logo colours) shows
ftg.setAttribute('transform', 'translate(0 ' + T + ') scale(1 ' + n(st) + ') translate(0 ' + (-T) + ')');
ft.setAttribute('fill', faceC(pt));
ft.setAttribute('opacity', pt >= 0.5 ? 0 : 1); // white inside face only while it faces the viewer
fto.setAttribute('opacity', pt >= 0.5 ? 1 : 0);
var slope = n(1 - 0.35 * Math.sin(Math.PI * pt));
dr.setAttribute('slope', slope); dg.setAttribute('slope', slope); db.setAttribute('slope', slope);
var draw = ease(seg(ms, 1180, 1800));
var fade = ease(seg(ms, 2150, 2450)); // white envelope (all but the top flap) vanishes after the logo has formed
var dis = ease(seg(ms, 1800, 2150)); // dissolve: U stroke + top flap -> final logo
var sc = 0.9 + 0.1 * intro;
env.setAttribute('transform', 'translate(22.5 22) scale(' + sc + ') translate(-22.5 -22)');
env.setAttribute('opacity', intro);
rest.setAttribute('opacity', 1 - fade);
ftg.setAttribute('opacity', 1 - fade); // coloured flap sits under the logo until the envelope goes
stroke.setAttribute('stroke-dashoffset', 1 - draw);
// Stroke stays solid under the logo while it dissolves in, then is removed once fully covered
ug.setAttribute('opacity', draw > 0 && dis < 1 ? 1 : 0);
under.setAttribute('opacity', draw > 0 ? 1 : 0);
fin.setAttribute('opacity', dis);
glow.setAttribute('opacity', 0.35 * Math.sin(Math.PI * seg(ms, 1900, 2450)));
}
var raf = 0, start = 0, hideTimer = 0;
function finish() {
cancelAnimationFrame(raf);
render(99999);
root.classList.add('is-done');
document.dispatchEvent(new CustomEvent('ultimo-splash:done'));
}
function tick(now) {
if (!start) start = now;
var ms = (now - start) / SPEED;
render(ms);
if (ms < END) raf = requestAnimationFrame(tick);
else hideTimer = setTimeout(finish, HOLD_MS);
}
function play() {
cancelAnimationFrame(raf); clearTimeout(hideTimer);
root.classList.remove('is-done');
start = 0; render(0);
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) {
render(99999); hideTimer = setTimeout(finish, 700); return;
}
raf = requestAnimationFrame(tick);
}
try {
if (ONCE_PER_SESSION && sessionStorage.getItem('ultimo-splash-seen')) { root.classList.add('is-done'); return; }
sessionStorage.setItem('ultimo-splash-seen', '1');
} catch (e) {}
root.addEventListener('click', finish); // click / tap skips the intro
window.ultimoSplash = { play: play, skip: finish, seek: function (ms) { cancelAnimationFrame(raf); clearTimeout(hideTimer); root.classList.remove('is-done'); render(ms); } };
play();
})();
/* ===================================================================== */
// Demo only
document.getElementById('replay').addEventListener('click', function () { window.ultimoSplash.play(); });
</script>
</body>
</html>
+91
View File
@@ -13,8 +13,99 @@
<meta name="apple-mobile-web-app-title" content="Ultimo" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>Ultimo Email</title>
<style>
/* Loading splash (public/splash.js). Dark grey like the app's dark theme. */
#ultimo-splash {
position: fixed; inset: 0; z-index: 9999;
display: grid; place-items: center;
background: #1b1e20;
transition: opacity .35s ease, visibility 0s linear .35s;
cursor: pointer;
}
#ultimo-splash.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
#ultimo-splash svg { width: min(46vmin, 340px); height: auto; overflow: visible; }
/* Never on a phone (same query as useIsMobile). */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
#ultimo-splash { display: none; }
}
</style>
</head>
<body>
<div id="ultimo-splash" aria-hidden="true">
<svg viewBox="-10 -6 65 56" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="ul-g1" x1="54.340946" y1="122.84999" x2="54.340946" y2="162.66415" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)">
<stop offset="0.20198774" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<radialGradient id="ul-r1" cx="8.0343275" cy="13.452971" fx="8.0343275" fy="13.452971" r="16.678839" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)">
<stop offset="0" stop-color="#fa4435" stop-opacity="1"/><stop offset="1" stop-color="#fa7367" stop-opacity="0"/>
</radialGradient>
<radialGradient id="ul-r2" cx="34.523952" cy="11.253304" fx="34.523952" fy="11.253304" r="16.740065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)">
<stop offset="0" stop-color="#fadc56" stop-opacity="1"/><stop offset="1" stop-color="#faae3a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="ul-gs" x1="0" y1="5.11" x2="0" y2="37.01" gradientUnits="userSpaceOnUse">
<stop offset="0.202" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<filter id="ul-sh" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="0.35" stdDeviation="0.5" flood-color="#000" flood-opacity="0.42"/>
</filter>
<filter id="ul-blur" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="1.1"/>
</filter>
<g id="ul-logo" transform="translate(1.425541,43.741704)">
<path id="ul-main" fill="url(#ul-g1)" d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"/>
<path fill="url(#ul-r1)" d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z"/>
<path fill="url(#ul-r2)" d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z"/>
</g>
<!-- Mask that reveals the logo's U as the stroke draws -->
<mask id="ul-mask-u" maskUnits="userSpaceOnUse" x="-20" y="-20" width="100" height="100">
<path id="ul-stroke" d="M7.73,8.31 L7.73,23.8 C7.73,30.6 13.6,34.65 22.57,34.65 C31.5,34.65 37.4,30.6 37.4,23.8 L37.4,8.31"
pathLength="1" fill="none" stroke="#fff" stroke-width="9.6" stroke-linecap="round"
stroke-dasharray="1 2" stroke-dashoffset="1"/>
</mask>
<!-- Logo colours limited to the top-flap area -->
<!-- Area between the U's arms: inside it the logo's own fill is exactly the flap -->
<clipPath id="ul-clip-flap"><path d="M10.093,11.066 L35.296,11.066 L35.296,12.1 L22.69,23.6 L10.093,12.1 Z"/></clipPath>
<!-- Darkens the coloured flap while it is mid-fold -->
<filter id="ul-dark" x="0" y="0" width="1" height="1">
<feComponentTransfer>
<feFuncR id="ul-dr" type="linear" slope="1"/><feFuncG id="ul-dg" type="linear" slope="1"/><feFuncB id="ul-db" type="linear" slope="1"/>
</feComponentTransfer>
</filter>
</defs>
<!-- Envelope (white/grey paper) -->
<g id="ul-env">
<g id="ul-rest">
<!-- Thin paper margin tucked under the U so no background shows along its inner edge -->
<rect id="ul-under" x="9.8" y="12.1" width="25.8" height="20.4" fill="rgb(250,250,253)" opacity="0" mask="url(#ul-mask-u)"/>
<rect x="10.093" y="11.066" width="25.203" height="21.064" fill="rgb(226,226,230)"/>
<path id="ul-fl" filter="url(#ul-sh)"/>
<path id="ul-fr" filter="url(#ul-sh)"/>
<path id="ul-fb" filter="url(#ul-sh)"/>
</g>
<!-- Top flap: exact outline of the logo's V flap (shown folded); flipped about the top edge while folding -->
<!-- Inside of the flap = white paper; outside = the logo's colours (clipped to the flap) -->
<g id="ul-ftg" filter="url(#ul-sh)">
<path id="ul-ft"
d="M10.093,11.066 L35.296,11.066 L35.296,12.019 L35.208,12.089 L24.206,21.234 C23.554,21.773 21.927,21.773 21.275,21.234 L10.335,12.117 L10.093,11.910 Z"/>
<g id="ul-fto" opacity="0" filter="url(#ul-dark)">
<use href="#ul-logo" clip-path="url(#ul-clip-flap)"/>
</g>
</g>
</g>
<!-- U drawn over the envelope: the real logo artwork (with its side gradients) revealed by a growing stroke -->
<g id="ul-ug" opacity="0">
<use href="#ul-logo" mask="url(#ul-mask-u)"/>
</g>
<!-- Final logo + glow -->
<use id="ul-glow" href="#ul-logo" filter="url(#ul-blur)" opacity="0"/>
<use id="ul-final" href="#ul-logo" opacity="0"/>
</svg>
</div>
<script src="/splash.js"></script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny-ps" viewBox="0 0 512 512">
<title>Ultimo Email</title>
<desc>Ultimo Email logo, for BIMI (SVG Tiny Portable/Secure)</desc>
<defs><linearGradient id="g0" gradientUnits="userSpaceOnUse" x1="22.4348" y1="5.1873" x2="22.4348" y2="37.0988"><stop offset="0.2020" stop-color="#f89629" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#f74176" stop-opacity="1.0000"/></linearGradient><radialGradient id="g1" gradientUnits="userSpaceOnUse" cx="8.0343" cy="13.4530" r="16.6788"><stop offset="0.0000" stop-color="#fa7367" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#fa7367" stop-opacity="0.0000"/></radialGradient><radialGradient id="g2" gradientUnits="userSpaceOnUse" cx="35.7090" cy="12.0426" r="16.7401"><stop offset="0.0000" stop-color="#fab23b" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#fab23a" stop-opacity="0.0000"/></radialGradient></defs>
<defs><linearGradient id="g0" gradientUnits="userSpaceOnUse" x1="54.3409" y1="122.8500" x2="54.3409" y2="162.6642"><stop offset="0.2020" stop-color="#f89629" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#f74176" stop-opacity="1.0000"/></linearGradient><radialGradient id="g1" gradientUnits="userSpaceOnUse" cx="8.0343" cy="13.4530" r="16.6788"><stop offset="0.0000" stop-color="#fa4435" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#fa7367" stop-opacity="0.0000"/></radialGradient><radialGradient id="g2" gradientUnits="userSpaceOnUse" cx="34.5240" cy="11.2533" r="16.7401"><stop offset="0.0000" stop-color="#fadc56" stop-opacity="1.0000"/><stop offset="1.0000" stop-color="#faae3a" stop-opacity="0.0000"/></radialGradient></defs>
<rect width="512" height="512" fill="#ffffff"/>
<g transform="matrix(7.081810,0,0,7.081810,96.1936,104.0546)"><g transform="matrix(1,0,0,1,0.128676,-0.079965)"><path fill="url(#g0)" d="M37.1752,4.1741C35.0228,4.1733 33.1074,6.0213 33.1081,8.3007L33.1081,11.1528C33.0351,11.1478 32.9615,11.1458 32.8879,11.1458L22.3878,11.1458L11.8892,11.1458C11.8474,11.1457 11.8057,11.1464 11.7641,11.1478L11.7641,8.3586C11.7639,6.0805 9.8048,4.2338 7.6537,4.2337C5.5026,4.2339 3.4449,6.0805 3.4448,8.3586L3.4448,23.9099C3.4447,23.9149 3.4153,27.7533 5.6178,31.5149C7.8217,35.2789 12.6560,38.8974 20.0704,38.8974L24.6511,38.8974C32.0688,38.8974 36.9203,35.2313 39.1570,31.4552C41.3937,27.6792 41.4274,23.8204 41.4274,23.8204L41.4294,23.7860L41.4294,8.3006C41.4301,6.0219 39.3273,4.1742 37.1756,4.1740ZM35.1673,12.0993L35.1673,23.6979L35.1673,23.7265C35.1673,23.7265 35.1009,25.8089 33.8559,27.9106C33.2839,28.8763 32.4105,30.0682 30.9382,30.9085C29.4659,31.7488 27.4775,32.2393 24.6660,32.2393L20.0849,32.2393C17.2611,32.2393 15.2863,31.7600 13.8279,30.9350C12.3696,30.1101 11.5144,28.9316 10.9689,28.0001C9.7723,25.9564 9.7389,23.9282 9.7389,23.9282L9.7379,23.9141L9.7379,23.8984L9.7379,12.2020L9.7381,12.1878L9.7416,12.1798C9.7929,12.2285 9.9297,12.3428 9.9835,12.3874L15.4082,16.9326L20.9233,21.5040C21.5752,22.0435 23.2023,22.0435 23.8542,21.5040L29.3693,16.9326L34.8566,12.3591C34.8765,12.3427 34.8975,12.3260 34.9167,12.3111L34.9443,12.2896Z"/><g transform="matrix(-0.29271702,0.00410386,-0.01334442,-0.95181978,11.840139,23.950941)"><path fill="url(#g1)" d="M27.1856,17.7752C27.6455,17.3736 27.9083,16.9462 27.9286,16.5020L28.6733,0.1670C28.6737,0.1618 28.9581,-3.8700 21.6154,-7.8536C14.2678,-11.8398 -2.0714,-15.7119 -27.3962,-15.8211L-43.0417,-15.8886C-61.6019,-15.9686 -75.5523,-13.9619 -84.7610,-11.3363C-79.6373,-10.0343 -73.8916,-8.5628 -67.8306,-7.0030C-66.9363,-7.2114 -65.9618,-7.4091 -64.8980,-7.5898C-59.8291,-8.4508 -53.0149,-8.9366 -43.4117,-8.8952L-27.7645,-8.8277C-18.1195,-8.7861 -11.3972,-8.2535 -6.4556,-7.3655C-1.5141,-6.4775 1.3515,-5.2267 3.1699,-4.2402C7.1593,-2.0759 7.1758,0.0548 7.1758,0.0548L7.1786,0.0694L7.1778,0.0863L6.6177,12.3724L6.6171,12.3870L6.6043,12.3957C6.4315,12.3438 5.9700,12.2216 5.7882,12.1739L-12.5237,7.3194L-31.1415,2.4364C-31.4264,2.3618 -31.7682,2.2965 -32.1512,2.2404C-15.0312,6.6994 0.0637,10.6607 6.5729,12.3885C14.3301,14.4138 21.3627,16.2520 27.1856,17.7752Z"/></g><g transform="matrix(0.25712152,0.00260463,-0.00969379,0.95694375,26.644192,0.973013)"><path fill="url(#g2)" d="M56.9506,6.2383C50.3245,7.7408 42.3373,9.5509 33.5334,11.5434C26.0725,13.2601 8.7387,17.2029 -10.8988,21.6363C-10.5803,21.5905 -10.2927,21.5390 -10.0424,21.4822L11.2240,16.6473L32.3838,11.8101C32.4605,11.7927 32.5409,11.7750 32.6150,11.7592L32.7226,11.7368L33.5813,11.5357L34.0382,23.6546L34.0393,23.6848C34.0393,23.6848 33.8625,25.8609 29.1038,28.0702C26.9173,29.0853 23.5679,30.3403 17.8757,31.2340C12.1834,32.1276 4.4713,32.6610 -6.4624,32.6907L-24.2775,32.7392C-35.2588,32.7691 -42.9575,32.2891 -48.6611,31.4426C-49.8684,31.2634 -50.9722,31.0677 -51.9812,30.8609C-58.9448,32.4138 -65.5312,33.8756 -71.3844,35.1641C-61.0089,37.7678 -45.1947,39.7537 -24.0715,39.6962L-6.2584,39.6477C22.5876,39.5692 41.3106,35.6874 49.8599,31.7182C58.4092,27.7490 58.3873,23.7163 58.3873,23.7163L58.3941,23.6806L57.7841,7.5002C57.7679,7.0604 57.4730,6.6366 56.9506,6.2383Z"/></g></g></g>
<g transform="matrix(5.359577,0.000000,0.000000,5.675894,-34.596697,-557.282863)"><path fill="url(#g0)" d="M73.8171,121.5859C70.9733,121.5849 68.4425,123.8906 68.4434,126.7344L68.4434,130.2928C68.3469,130.2866 68.2497,130.2841 68.1524,130.2841L54.2789,130.2841L40.4074,130.2841C40.3521,130.2840 40.2970,130.2848 40.2420,130.2866L40.2420,126.8067C40.2418,123.9644 37.6533,121.6604 34.8110,121.6602C31.9688,121.6604 29.2501,123.9644 29.2499,126.8067L29.2499,146.2091C29.2498,146.2153 29.2109,151.0043 32.1210,155.6974C35.0331,160.3936 41.4205,164.9082 51.2170,164.9082L57.2694,164.9082C67.0702,164.9082 73.4804,160.3342 76.4356,155.6230C79.3909,150.9119 79.4355,146.0975 79.4355,146.0975L79.4381,146.0546L79.4381,126.7343C79.4390,123.8913 76.6608,121.5860 73.8177,121.5858ZM71.1641,131.4737L71.1641,145.9446L71.1641,145.9803C71.1641,145.9803 71.0764,148.5783 69.4314,151.2006C68.6756,152.4055 67.5216,153.8925 65.5764,154.9409C63.6311,155.9893 61.0039,156.6013 57.2890,156.6013L51.2362,156.6013C47.5051,156.6013 44.8958,156.0032 42.9690,154.9740C41.0421,153.9448 39.9121,152.4744 39.1914,151.3122C37.6103,148.7625 37.5662,146.2319 37.5662,146.2319L37.5649,146.2143L37.5649,146.1947L37.5649,131.6018L37.5651,131.5841L37.5697,131.5741C37.6375,131.6349 37.8182,131.7775 37.8894,131.8332L45.0569,137.5039L52.3438,143.2074C53.2052,143.8805 55.3551,143.8805 56.2165,143.2074L63.5034,137.5039L70.7537,131.7978C70.7800,131.7773 70.8077,131.7565 70.8330,131.7379L70.8696,131.7111Z"/></g><g transform="matrix(-2.072996,-0.018264,0.079870,-9.065222,179.277271,304.532491)"><path fill="url(#g1)" d="M27.5014,16.5661C27.9315,16.2651 28.1624,15.9460 28.1497,15.6157L27.6817,3.4686C27.6817,3.4647 27.6668,0.4661 20.0303,-2.4569C12.3888,-5.3819 -4.2336,-8.1750 -29.5599,-8.1240L-45.2064,-8.0925C-63.7676,-8.0551 -77.5654,-6.4905 -86.5769,-4.4906C-81.3578,-3.5495 -75.5044,-2.4855 -69.3292,-1.3577C-68.4507,-1.5173 -67.4911,-1.6693 -66.4409,-1.8092C-61.4372,-2.4757 -54.6609,-2.8724 -45.0572,-2.8917L-29.4090,-2.9233C-19.7634,-2.9427 -13.0033,-2.5818 -7.9971,-1.9475C-2.9910,-1.3132 -0.0333,-0.3983 1.8578,0.3255C6.0068,1.9136 6.1815,3.4974 6.1815,3.4974L6.1854,3.5083L6.1859,3.5208L6.5379,12.6571L6.5383,12.6680L6.5262,12.6745C6.3496,12.6368 5.8792,12.5483 5.6939,12.5139L-12.9736,9.0007L-31.9490,5.4680C-32.2394,5.4140 -32.5860,5.3672 -32.9730,5.3275C-15.5265,8.5529 -0.1415,11.4189 6.4943,12.6693C14.3998,14.1344 21.5670,15.4641 27.5014,16.5661Z"/></g><g transform="matrix(1.952557,-0.039466,0.136950,6.775553,280.153710,112.142252)"><path fill="url(#g2)" d="M55.6659,6.4255C49.3325,7.8733 41.6983,9.6175 33.2837,11.5374C26.1499,13.1924 9.5805,16.9922 -9.1873,21.2634C-8.8857,21.2202 -8.6122,21.1712 -8.3729,21.1164L11.9552,16.4571L32.1843,11.7945C32.2576,11.7778 32.3344,11.7607 32.4051,11.7456L32.5078,11.7240L33.3292,11.5301L32.4793,23.6471L32.4772,23.6773C32.4772,23.6773 32.0833,25.8510 27.4159,28.0205C25.2714,29.0173 22.0178,30.2444 16.6188,31.0912C11.2198,31.9380 3.9763,32.4083 -6.2164,32.3490L-22.8244,32.2522C-33.0615,32.1926 -40.1858,31.6500 -45.4122,30.7574C-46.5185,30.5685 -47.5265,30.3639 -48.4451,30.1489C-55.0983,31.6444 -61.3904,33.0519 -66.9809,34.2921C-57.5855,36.9791 -43.0565,39.0930 -23.3647,39.2077L-6.7586,39.3044C20.1327,39.4611 37.9902,35.7337 46.3756,31.8360C54.7609,27.9383 55.1650,23.9072 55.1650,23.9072L55.1751,23.8716L56.3098,7.6937C56.3410,7.2539 56.1108,6.8280 55.6659,6.4255Z"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+120
View File
@@ -0,0 +1,120 @@
// Loading splash: the animated Ultimo logo over a dark overlay while the app
// starts (markup and styles inline in index.html, so it paints before any script
// runs; this file is separate because the CSP allows no inline scripts). About
// 3.25 s in all: ~2.45 s to the finished logo, a short hold, then a .35 s fade.
// Desktop and tablet only; a click skips it. Source: frontend/icons/ultimo-splash.html.
(function () {
var SPEED = 1; // 1 = ~2.6 s to final logo; 0.8 = faster, 1.2 = slower
var HOLD_MS = 450; // how long the finished logo stays before the splash fades
var root = document.getElementById('ultimo-splash');
if (!root) return;
// Not on a phone (same query as useIsMobile), and not in a pop-out compose window.
var PHONE = '(max-width: 767px), (pointer: coarse) and (max-height: 500px)';
if ((window.matchMedia && matchMedia(PHONE).matches) || location.pathname.indexOf('/compose/') === 0) {
root.parentNode.removeChild(root);
return;
}
var $ = function (id) { return document.getElementById(id); };
var env = $('ul-env'), rest = $('ul-rest'), fl = $('ul-fl'), fr = $('ul-fr'), fb = $('ul-fb'), ft = $('ul-ft');
var stroke = $('ul-stroke'), glow = $('ul-glow'), fin = $('ul-final');
var ug = $('ul-ug'), under = $('ul-under');
var ftg = $('ul-ftg'), fto = $('ul-fto'), dr = $('ul-dr'), dg = $('ul-dg'), db = $('ul-db');
// Envelope body edges (logo coordinates) and the four flaps.
var L = 10.093, R = 35.296, T = 11.066, B = 32.13, CX = 22.69, CY = 21.6;
var FLAPS = {
l: { axis: 'x', h: L, a: T, b: B, mid: CY, depth: 12.2, dir: -1, r: 3.2 },
r: { axis: 'x', h: R, a: T, b: B, mid: CY, depth: 12.2, dir: 1, r: 3.2 },
b: { axis: 'y', h: B, a: L, b: R, mid: CX, depth: 14.0, dir: 1, r: 3.6 }
};
var END = 2450; // ms (at SPEED 1) when the logo is fully formed
function clamp(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
function seg(t, a, b) { return clamp((t - a) / (b - a)); }
function ease(x) { return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
function easeOut(x) { return 1 - Math.pow(1 - x, 3); }
function n(v) { return Math.round(v * 1000) / 1000; }
// Flap outline: base fixed exactly on the body edge, apex swings over the hinge, tip rounded.
function flapD(f, p) {
var s = Math.cos(Math.PI * p), ax, ay, A, C;
if (f.axis === 'x') { ax = f.h + f.dir * f.depth * s; ay = f.mid; A = [f.h, f.a]; C = [f.h, f.b]; }
else { ax = f.mid; ay = f.h + f.dir * f.depth * s; A = [f.a, f.h]; C = [f.b, f.h]; }
function toward(P) {
var dx = P[0] - ax, dy = P[1] - ay, l = Math.hypot(dx, dy) || 1, r = Math.min(f.r, l * 0.45);
return n(ax + dx * r / l) + ',' + n(ay + dy * r / l);
}
return 'M' + A[0] + ',' + A[1] + ' L' + toward(A) + ' Q' + n(ax) + ',' + n(ay) + ' ' + toward(C) +
' L' + C[0] + ',' + C[1] + ' Z';
}
// Paper shading: inner face (grey) before the flip, outer face (white) after it.
function faceC(p) {
var lift = Math.sin(Math.PI * p);
var c = Math.round(p < .5 ? 230 - 42 * lift : 250 - 36 * lift);
return 'rgb(' + c + ',' + c + ',' + (c + 3) + ')';
}
function setFlap(el, f, p) {
el.setAttribute('d', flapD(f, p));
el.setAttribute('fill', faceC(p));
}
function render(ms) {
var intro = easeOut(seg(ms, 0, 220));
setFlap(fl, FLAPS.l, ease(seg(ms, 180, 560)));
setFlap(fr, FLAPS.r, ease(seg(ms, 240, 620)));
setFlap(fb, FLAPS.b, ease(seg(ms, 560, 920)));
var pt = ease(seg(ms, 880, 1260)), st = -Math.cos(Math.PI * pt);
if (Math.abs(st) < 0.001) st = 0.001;
// Top flap: white inside while open; once it flips past vertical its outside (logo colours) shows
ftg.setAttribute('transform', 'translate(0 ' + T + ') scale(1 ' + n(st) + ') translate(0 ' + (-T) + ')');
ft.setAttribute('fill', faceC(pt));
ft.setAttribute('opacity', pt >= 0.5 ? 0 : 1); // white inside face only while it faces the viewer
fto.setAttribute('opacity', pt >= 0.5 ? 1 : 0);
var slope = n(1 - 0.35 * Math.sin(Math.PI * pt));
dr.setAttribute('slope', slope); dg.setAttribute('slope', slope); db.setAttribute('slope', slope);
var draw = ease(seg(ms, 1180, 1800));
var fade = ease(seg(ms, 2150, 2450)); // white envelope (all but the top flap) vanishes after the logo has formed
var dis = ease(seg(ms, 1800, 2150)); // dissolve: U stroke + top flap -> final logo
var sc = 0.9 + 0.1 * intro;
env.setAttribute('transform', 'translate(22.5 22) scale(' + sc + ') translate(-22.5 -22)');
env.setAttribute('opacity', intro);
rest.setAttribute('opacity', 1 - fade);
ftg.setAttribute('opacity', 1 - fade); // coloured flap sits under the logo until the envelope goes
stroke.setAttribute('stroke-dashoffset', 1 - draw);
// Stroke stays solid under the logo while it dissolves in, then is removed once fully covered
ug.setAttribute('opacity', draw > 0 && dis < 1 ? 1 : 0);
under.setAttribute('opacity', draw > 0 ? 1 : 0);
fin.setAttribute('opacity', dis);
glow.setAttribute('opacity', 0.35 * Math.sin(Math.PI * seg(ms, 1900, 2450)));
}
var raf = 0, start = 0, hideTimer = 0;
function finish() {
cancelAnimationFrame(raf);
render(99999);
root.classList.add('is-done');
// Gone from the page once faded out (the .35 s opacity transition).
setTimeout(function () { if (root.parentNode) root.parentNode.removeChild(root); }, 400);
}
function tick(now) {
if (!start) start = now;
var ms = (now - start) / SPEED;
render(ms);
if (ms < END) raf = requestAnimationFrame(tick);
else hideTimer = setTimeout(finish, HOLD_MS);
}
function play() {
cancelAnimationFrame(raf); clearTimeout(hideTimer);
root.classList.remove('is-done');
start = 0; render(0);
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) {
render(99999); hideTimer = setTimeout(finish, 700); return;
}
raf = requestAnimationFrame(tick);
}
root.addEventListener('click', finish); // a click skips the intro
play();
})();
+39 -39
View File
@@ -58,55 +58,55 @@
id="mask1776-6"><path
style="fill:url(#linearGradient1789);fill-opacity:1;stroke-width:0.2;stroke-linecap:round"
d="m 18.20332,107.02297 c 0,0 1.930731,1.59986 4.989281,4.12846 1.725315,1.42637 3.809507,3.14827 6.108453,5.04523 5.657506,4.74616 33.20207,27.48506 33.558689,27.18811 0.241178,-0.20083 -16.275384,9.86387 -16.275384,9.86387 L 17.979141,140.69463 Z"
id="path1776-1" /></mask><linearGradient
xlink:href="#linearGradient11"
id="linearGradient13-6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-18.692802,-93.278335)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /><radialGradient
xlink:href="#linearGradient8"
id="radialGradient816"
id="path1776-1" /></mask><radialGradient
xlink:href="#linearGradient8-5"
id="radialGradient816-3"
cx="8.0343275"
cy="13.452971"
fx="8.0343275"
fy="13.452971"
r="16.678839"
gradientTransform="matrix(-0.29271702,0.00410386,-0.01334442,-0.95181978,11.840139,23.950941)"
gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient8"><stop
style="stop-color:#fa7367;stop-opacity:1;"
id="linearGradient8-5"><stop
style="stop-color:#fa4435;stop-opacity:1;"
offset="0"
id="stop8" /><stop
id="stop8-6" /><stop
style="stop-color:#fa7367;stop-opacity:0;"
offset="1"
id="stop9" /></linearGradient><radialGradient
xlink:href="#linearGradient6"
id="radialGradient815"
cx="35.709007"
cy="12.042645"
fx="35.709007"
fy="12.042645"
id="stop9-2" /></linearGradient><radialGradient
xlink:href="#linearGradient6-1"
id="radialGradient815-9"
cx="34.523952"
cy="11.253304"
fx="34.523952"
fy="11.253304"
r="16.740065"
gradientTransform="matrix(0.25712152,0.00260463,-0.00969379,0.95694375,26.644192,0.97301292)"
gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient6"><stop
style="stop-color:#fab23b;stop-opacity:1;"
id="linearGradient6-1"><stop
style="stop-color:#fadc56;stop-opacity:1;"
offset="0"
id="stop6" /><stop
style="stop-color:#fab23a;stop-opacity:0;"
id="stop6-2" /><stop
style="stop-color:#faae3a;stop-opacity:0;"
offset="1"
id="stop7" /></linearGradient></defs><g
id="g816"
transform="translate(0.128676,-0.079965)"><path
id="path1-2-1-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient13-6);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 37.175189,4.174075 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592601,-4.12477 -4.1103801,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.76402 7.0382001,7.38257 14.4526701,7.38257 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.44221 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 V 8.300605 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.85901,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -0.001,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247011,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z" /><path
id="path1-2-1-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 3.6452555,7.14375 C 3.5159759,7.5278714 3.4447807,7.9357538 3.444751,8.3586629 V 23.909652 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.76402 7.038395,7.382495 14.452865,7.382495 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.218257 -3.218664,-2.595334 -5.013648,-4.05505 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.859257,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249919,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 C 16.15064,17.512631 11.679239,13.804108 9.750826,12.186336 7.4531167,10.290397 5.3700283,8.5696704 3.6452555,7.14375 Z" /><path
id="path1-2-1-7"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 41.226941,7.09104 c -1.718284,1.4205409 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.805553,1.467965 -3.513225,2.849693 -5.0307002,4.067452 2.6425302,2.518552 6.6894502,4.460193 12.1212242,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.441923 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 V 8.3007853 C 41.429125,7.8798785 41.357396,7.4736084 41.226941,7.09104 Z" /></g></svg>
id="stop7-7" /></linearGradient><linearGradient
xlink:href="#linearGradient11"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /></defs><g
id="g1"
transform="translate(1.425541,43.741704)"><path
id="path1-2-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient1);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z" /><path
id="path1-2-1-9-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816-3);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z" /><path
id="path1-2-1-7-3"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815-9);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z" /></g></svg>

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

+3 -1
View File
@@ -1,3 +1,5 @@
import { uiZoom } from "../lib/uiScale";
// A drag handle between two panels. Vertical (the default) sits between panels
// side by side and reports horizontal movement; horizontal sits between panels
// stacked one above the other (the "reading pane at the bottom" layout) and
@@ -17,7 +19,7 @@ export function Resizer({
let last = vertical ? e.clientX : e.clientY;
const onMove = (ev: MouseEvent) => {
const now = vertical ? ev.clientX : ev.clientY;
onDelta(now - last);
onDelta((now - last) / uiZoom()); // screen px → CSS px under the interface zoom
last = now;
};
const onUp = () => {
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { useT } from "../../i18n";
import { uiZoom } from "../../lib/uiScale";
import { Composer } from "./Composer";
import { CHIP_W, type ComposeWin, DOCK_INSET, layoutDock } from "./windows";
@@ -8,7 +9,7 @@ function useViewportSize() {
const read = () =>
typeof window === "undefined"
? { vw: 1280, vh: 800 }
: { vw: window.innerWidth, vh: window.innerHeight };
: { vw: window.innerWidth / uiZoom(), vh: window.innerHeight / uiZoom() };
const [size, setSize] = useState(read);
useEffect(() => {
const onResize = () => setSize(read());
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
import { useT } from "../../i18n";
import { useTheme } from "../../lib/theme";
import { uiZoom } from "../../lib/uiScale";
// emoji-mart, loaded on first use: its emoji data is a few hundred KB that the
// rest of the app does without. The data is bundled — nothing is fetched from
@@ -82,13 +83,18 @@ export function EmojiButton({
const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
const open = () => {
const r = btnRef.current?.getBoundingClientRect();
if (!r) return;
const sr = btnRef.current?.getBoundingClientRect();
if (!sr) return;
onOpen();
// Screen px → CSS px under the interface zoom, so the picker lands on the button.
const z = uiZoom();
const r = { left: sr.left / z, top: sr.top / z, bottom: sr.bottom / z };
const vw = window.innerWidth / z;
const vh = window.innerHeight / z;
// Below the button, or above it when the window ends first; inside the viewport.
const below = r.bottom + 6 + PICKER_H <= window.innerHeight;
const below = r.bottom + 6 + PICKER_H <= vh;
setPos({
left: Math.max(8, Math.min(r.left, window.innerWidth - PICKER_W - 8)),
left: Math.max(8, Math.min(r.left, vw - PICKER_W - 8)),
top: below ? r.bottom + 6 : Math.max(8, r.top - 6 - PICKER_H),
});
};
@@ -11,6 +11,7 @@ import {
import { useT } from "../../i18n";
import { api, ApiError } from "../../lib/api";
import { uiZoom } from "../../lib/uiScale";
import { notify } from "../AppDialog";
import { EmojiButton } from "./EmojiButton";
import {
@@ -581,7 +582,8 @@ export const RichTextEditor = forwardRef<
if (!img || !ed) return null;
const ir = img.getBoundingClientRect();
const er = ed.getBoundingClientRect();
return { left: ir.left - er.left, top: ir.top - er.top, width: ir.width, height: ir.height };
const z = uiZoom(); // rects are screen px; the handles are placed in CSS px
return { left: (ir.left - er.left) / z, top: (ir.top - er.top) / z, width: ir.width / z, height: ir.height / z };
}, [selImg]);
useEffect(() => setBox(computeBox()), [selImg, computeBox]);
@@ -610,10 +612,11 @@ export const RichTextEditor = forwardRef<
const ed = ref.current;
if (!img || !ed) return;
const startX = e.clientX;
const startW = img.getBoundingClientRect().width;
const z = uiZoom();
const startW = img.getBoundingClientRect().width / z;
const maxW = ed.clientWidth - 24; // minus px-3 padding
const onMove = (ev: MouseEvent) => {
const dx = ev.clientX - startX;
const dx = (ev.clientX - startX) / z;
const grow = corner === "se" || corner === "ne" ? dx : -dx;
img.style.width = `${Math.round(Math.max(24, Math.min(startW + grow, maxW)))}px`;
img.style.height = "auto";
@@ -663,8 +666,9 @@ export const RichTextEditor = forwardRef<
if (!ed || !spellIssue || !stillValid(spellIssue)) return null;
const r = spellIssue.range.getBoundingClientRect();
const er = ed.getBoundingClientRect();
const z = uiZoom();
return {
left: Math.max(8, Math.min(r.left - er.left, er.width - 300)),
left: Math.max(8, Math.min((r.left - er.left) / z, er.width / z - 300)),
top: popupTop(r, spellPopupRef.current?.offsetHeight || 160),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -714,14 +718,17 @@ export const RichTextEditor = forwardRef<
// A popup's top edge, relative to the editor: just under its anchor, or just
// above it when there is no room left below — inside the editor's visible
// box and the window alike — so it is never cut off at the bottom.
const popupTop = (anchor: DOMRect, popupH: number) => {
const popupTop = (anchor: DOMRect, cssPopupH: number) => {
// Worked out in screen px (the rects'), returned in CSS px for the style.
const z = uiZoom();
const popupH = cssPopupH * z;
const er = ref.current!.getBoundingClientRect();
const bottom = Math.min(er.bottom, window.innerHeight) - 4;
const top = Math.max(er.top, 0) + 4;
const below = anchor.bottom + 4;
const above = anchor.top - 4 - popupH;
const fitsBelow = below + popupH <= bottom;
return (fitsBelow || above < top ? below : above) - er.top;
return ((fitsBelow || above < top ? below : above) - er.top) / z;
};
// Where the link popup goes: just under the link (or the text becoming one),
@@ -738,7 +745,8 @@ export const RichTextEditor = forwardRef<
}
const er = ed.getBoundingClientRect();
if (!ar || (!ar.width && !ar.height && !ar.top)) return newLinkRange ? { left: 12, top: 12 } : null;
const left = Math.max(8, Math.min(ar.left - er.left, er.width - 320));
const z = uiZoom();
const left = Math.max(8, Math.min((ar.left - er.left) / z, er.width / z - 320));
return { left, top: popupTop(ar, linkPopupRef.current?.offsetHeight || 44) };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selLink, newLinkRange]);
+4 -3
View File
@@ -9,6 +9,7 @@ import { avatarColor } from "../../lib/avatarColor";
import { useSavedThreads } from "../../lib/offline";
import { formatClock, getDateLocale } from "../../lib/timeFormat";
import type { Address, ThreadSummary } from "../../lib/types";
import { uiZoom } from "../../lib/uiScale";
import { Icon } from "../Icon";
import { Clock } from "../Offline";
import { AvatarImage } from "../SenderAvatar";
@@ -554,8 +555,8 @@ function useSwipeToDelete(onDelete?: (id: string) => void) {
onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => {
const s = start.current;
if (!s || s.id !== id) return;
const dx = e.clientX - s.x;
const dy = e.clientY - s.y;
const dx = (e.clientX - s.x) / uiZoom(); // CSS px, like the row's offsetWidth
const dy = (e.clientY - s.y) / uiZoom();
if (!s.active) {
if (Math.abs(dy) > SWIPE_START_PX && Math.abs(dy) > Math.abs(dx)) {
start.current = null; // a scroll
@@ -572,7 +573,7 @@ function useSwipeToDelete(onDelete?: (id: string) => void) {
start.current = null;
if (!s || !s.active || s.id !== id) return;
swipedAt.current = Date.now();
const dx = Math.max(0, e.clientX - s.x);
const dx = Math.max(0, (e.clientX - s.x) / uiZoom());
if (dx > s.width * SWIPE_DELETE_SHARE) {
setState({ id, dx: s.width, dragging: false }); // off it goes
window.setTimeout(() => {
@@ -3,11 +3,13 @@ import { useEffect, useRef } from "react";
import { useT } from "../../i18n";
import { ListDividersOption } from "./ListDividersOption";
import { ReadingPanePicker } from "./ReadingPanePicker";
import { UiScaleOption } from "./UiScaleOption";
/**
* "Mailbox display": the reading-pane choice and the row dividers, opened from
* "Mailbox display": the reading-pane choice, the row dividers and the interface
* size, opened from
* the list's ⋯ menu. Anchored below that menu. Picking a layout applies it and
* closes the popover; the dividers switch applies at once and leaves it open;
* closes the popover; the dividers switch and the size apply at once and leave it open;
* ✕, Escape or a click elsewhere closes it.
*/
export function MailboxDisplay({
@@ -59,12 +61,16 @@ export function MailboxDisplay({
<>
<p className="mb-2 text-[13px] font-medium text-app-fg">{t("display.readingPane")}</p>
<ReadingPanePicker onPick={onClose} />
<div className="mt-4 border-t border-app-border pt-3">
<div className="mt-4 space-y-3 border-t border-app-border pt-3">
<ListDividersOption />
<UiScaleOption />
</div>
</>
) : (
<ListDividersOption />
<div className="space-y-3">
<ListDividersOption />
<UiScaleOption />
</div>
)}
</div>
);
@@ -0,0 +1,30 @@
import { useT } from "../../i18n";
import { setUiScale, UI_SCALES, useUiScale } from "../../lib/uiScale";
/** "Interface size" — Default / +10% / +20%, in the ⋯ menu's Mailbox display
* and in Settings → Appearance. Applies at once. */
export function UiScaleOption() {
const t = useT();
const scale = useUiScale();
return (
<div className="flex items-center justify-between gap-3 text-[14px] text-app-fg">
<span>{t("display.size")}</span>
<div role="radiogroup" aria-label={t("display.size")} className="flex gap-1 rounded-lg border border-app-border p-0.5">
{UI_SCALES.map((value) => (
<button
key={value}
type="button"
role="radio"
aria-checked={scale === value}
onClick={() => setUiScale(value)}
className={`rounded px-2.5 py-0.5 ${
scale === value ? "bg-app-active font-medium text-app-fg" : "text-app-muted hover:bg-app-hover"
}`}
>
{value === 1 ? t("display.sizeDefault") : `+${Math.round((value - 1) * 100)}%`}
</button>
))}
</div>
</div>
);
}
+21
View File
@@ -670,6 +670,8 @@ export const en = {
"display.bottom": "Bottom",
"display.off": "Off",
"display.dividers": "Show a dividing line between rows",
"display.size": "Interface size",
"display.sizeDefault": "Default",
"admin.mail": "Outgoing system mail",
"admin.mailHelp":
"The mail account the server sends password-reset links from. Without it, “Forgot password?” sends nothing.",
@@ -876,6 +878,25 @@ export const en = {
"mail.translatedFrom": "Translated from {language}",
"mail.translatedFromMany": "Translated from {languages}",
"mail.translateFailed": "Couldn't translate this message. The translation service may be busy or unavailable.",
"antispam.looksLikeSpam": "Looks like spam!",
"antispam.score": "score {score}",
"antispam.moveToSpam": "Move to Spam",
"antispam.notSpam": "Not spam",
"antispam.trustSender": "Always trust sender",
"antispam.why.bayes": "resembles mail you marked as spam",
"antispam.why.fuzzy": "matches known spam",
"antispam.why.reputation": "sender has a poor reputation",
"antispam.why.blocklist": "sending server is on a spam blocklist",
"antispam.why.badLinks": "links to known spam or phishing sites",
"antispam.why.phishing": "link text hides its real destination",
"antispam.why.authFail": "sender failed authenticity checks",
"antispam.why.forged": "sender address looks forged",
"antispam.why.replyTo": "replies would go to a different address",
"antispam.why.headers": "message is missing standard headers",
"antispam.why.hiddenText": "contains hidden text",
"antispam.why.suspiciousServer": "sending server looks suspicious",
"antispam.why.shouting": "subject written in capitals",
"antispam.why.date": "message date is implausible",
} as const;
export type MessageKey = keyof typeof en;
+21
View File
@@ -680,6 +680,8 @@ export const pl: Catalog = {
"display.bottom": "Na dole",
"display.off": "Wyłączone",
"display.dividers": "Pokazuj linię między wierszami",
"display.size": "Rozmiar interfejsu",
"display.sizeDefault": "Domyślny",
"admin.mail": "Wychodząca poczta systemowa",
"admin.mailHelp":
"Konto pocztowe, z którego serwer wysyła linki do resetowania hasła. Bez niego „Nie pamiętasz hasła?” niczego nie wyśle.",
@@ -902,4 +904,23 @@ export const pl: Catalog = {
"mail.translatedFrom": "Przetłumaczono z języka: {language}",
"mail.translatedFromMany": "Przetłumaczono z języków: {languages}",
"mail.translateFailed": "Nie udało się przetłumaczyć tej wiadomości. Usługa tłumaczenia może być zajęta lub niedostępna.",
"antispam.looksLikeSpam": "Wygląda na spam!",
"antispam.score": "wynik {score}",
"antispam.moveToSpam": "Przenieś do spamu",
"antispam.notSpam": "To nie spam",
"antispam.trustSender": "Zawsze ufaj nadawcy",
"antispam.why.bayes": "przypomina wiadomości oznaczone przez Ciebie jako spam",
"antispam.why.fuzzy": "pasuje do znanego spamu",
"antispam.why.reputation": "nadawca ma złą reputację",
"antispam.why.blocklist": "serwer nadawcy jest na liście spamerów",
"antispam.why.badLinks": "zawiera linki do stron ze spamem lub phishingiem",
"antispam.why.phishing": "tekst linku ukrywa jego prawdziwy adres",
"antispam.why.authFail": "nadawca nie przeszedł weryfikacji autentyczności",
"antispam.why.forged": "adres nadawcy wygląda na podrobiony",
"antispam.why.replyTo": "odpowiedzi trafiłyby na inny adres",
"antispam.why.headers": "brakuje standardowych nagłówków",
"antispam.why.hiddenText": "zawiera ukryty tekst",
"antispam.why.suspiciousServer": "serwer nadawcy wygląda podejrzanie",
"antispam.why.shouting": "temat napisany wielkimi literami",
"antispam.why.date": "data wiadomości jest nieprawdopodobna",
};
+75
View File
@@ -0,0 +1,75 @@
// Interface size (Mailbox display / Settings → Appearance): Default, +10% or
// +20%. Per device, in localStorage, like the reading-pane layout.
//
// Applied as CSS `zoom` on <html>, which scales everything evenly — px and rem
// sizes, inline styles, SVG icons and the message iframes alike. Two things zoom
// does NOT hide, and the app compensates for:
// - viewport units are zoomed too, so a 100vh box would overflow; `h-screen`
// and the dvh heights divide by --ui-zoom (tailwind.config.js, index.css).
// - mouse coordinates, getBoundingClientRect() and innerWidth/innerHeight are
// in unzoomed screen px, while a style written from them is zoomed again;
// divide by uiZoom() before turning them into a CSS length.
import { useSyncExternalStore } from "react";
export const UI_SCALES = [1, 1.1, 1.2] as const;
export type UiScale = (typeof UI_SCALES)[number];
const KEY = "ui.scale";
const EVENT = "webmail:ui-scale";
function read(): UiScale {
try {
const v = Number(localStorage.getItem(KEY));
return (UI_SCALES as readonly number[]).includes(v) ? (v as UiScale) : 1;
} catch {
return 1;
}
}
/** The current zoom factor, for converting screen px into CSS px. */
export function uiZoom(): number {
return read();
}
let applied = 1;
function apply(): void {
const z = read();
if (z === applied) return; // the storage event fires for every key
applied = z;
const root = document.documentElement;
root.style.zoom = z === 1 ? "" : String(z);
root.style.setProperty("--ui-zoom", String(z));
// The viewport in CSS px just changed size; let everything that measures it
// on "resize" (panel clamps, the compose dock, popups) measure again.
window.dispatchEvent(new Event("resize"));
}
function subscribe(onChange: () => void): () => void {
// Our own writes (same tab) and other tabs' writes (the storage event).
window.addEventListener(EVENT, onChange);
window.addEventListener("storage", onChange);
return () => {
window.removeEventListener(EVENT, onChange);
window.removeEventListener("storage", onChange);
};
}
export function useUiScale(): UiScale {
return useSyncExternalStore(subscribe, read, () => 1);
}
export function setUiScale(scale: UiScale): void {
try {
localStorage.setItem(KEY, String(scale));
} catch {
/* private mode: the choice simply does not stick */
}
window.dispatchEvent(new Event(EVENT));
}
// Applied before the first render (imported from main.tsx), so the app never
// paints at the wrong size, and kept in step with later changes from any tab.
apply();
subscribe(apply);
+1
View File
@@ -8,6 +8,7 @@ import { I18nProvider } from "./i18n";
import { isSessionGone, signedOut } from "./lib/auth";
import "./lib/install"; // catches the install event, which can come before the app renders
import { ThemeProvider } from "./lib/theme";
import "./lib/uiScale"; // applies the chosen interface size before the first paint
import "./index.css";
import "./plugins/plugin-ui.css"; // .wm-* classes for plugin bundles (SPEC §11.3)
+9 -7
View File
@@ -60,6 +60,7 @@ import type { AccountOut, QuotaOut, SavedSearch, ThreadSummary } from "../lib/ty
import { useReadingPane } from "../lib/readingPane";
import { useIsMobile } from "../lib/useIsMobile";
import { useKeyboardShortcuts } from "../lib/useKeyboardShortcuts";
import { uiZoom } from "../lib/uiScale";
import { subscribeWs } from "../lib/ws";
import { PluginSlot } from "../plugins/PluginSlot";
import { usePluginPages } from "../plugins/manifest";
@@ -213,14 +214,15 @@ export function MailLayout() {
if (!el) return;
const measure = () => {
const r = el.getBoundingClientRect();
const z = uiZoom(); // the rect is in screen px; the dock lays out in CSS px
// `right` is the gap to the viewport edge, which is what the dock needs
// to line an expanded window up with the pane — it is non-zero whenever
// the right panel is open. `height` only under the list: everywhere else
// the pane runs the full height of the screen.
setPane({
width: Math.round(r.width),
right: Math.round(window.innerWidth - r.right),
height: bottom ? Math.round(r.height) : undefined,
width: Math.round(r.width / z),
right: Math.round((window.innerWidth - r.right) / z),
height: bottom ? Math.round(r.height / z) : undefined,
});
};
measure();
@@ -240,7 +242,7 @@ export function MailLayout() {
// The list's height when the reading pane sits under it.
// Until dragged, the list takes 45% of the height and the reading pane 55%.
const [listH, setListH] = useState(
() => Number(localStorage.getItem("panel.listH")) || Math.round(window.innerHeight * 0.45),
() => Number(localStorage.getItem("panel.listH")) || Math.round((window.innerHeight / uiZoom()) * 0.45),
);
// Collapsed sidebar shows icons only (persisted). The mobile drawer is always full.
const [collapsedPref, setCollapsed] = useState(
@@ -253,13 +255,13 @@ export function MailLayout() {
const [rightW, setRightW] = useState(() => Number(localStorage.getItem("panel.rightW")) || 340);
const clampSidebar = (w: number) => Math.min(Math.max(w, 180), 420);
// The list can be dragged up to 75% of the window width.
const clampList = (w: number) => Math.min(Math.max(w, 260), window.innerWidth * 0.75);
const clampList = (w: number) => Math.min(Math.max(w, 260), (window.innerWidth / uiZoom()) * 0.75);
const clampRight = (w: number) => Math.min(Math.max(w, 280), 560);
useEffect(() => localStorage.setItem("panel.sidebar", String(sidebarW)), [sidebarW]);
useEffect(() => localStorage.setItem("panel.list", String(listW)), [listW]);
useEffect(() => localStorage.setItem("panel.listH", String(listH)), [listH]);
// Leave both the list and the reading pane at least a usable strip.
const clampListH = (h: number) => Math.min(Math.max(h, 160), window.innerHeight - 220);
const clampListH = (h: number) => Math.min(Math.max(h, 160), window.innerHeight / uiZoom() - 220);
useEffect(
() => localStorage.setItem("panel.collapsed", collapsedPref ? "1" : "0"),
[collapsedPref],
@@ -1166,7 +1168,7 @@ export function MailLayout() {
const paneShown = Boolean(paneDraft || threadId || selectedSub);
return (
<div className={`flex bg-app-bg text-app-fg ${mobile ? "h-[100dvh]" : "h-screen"}`}>
<div className={`flex bg-app-bg text-app-fg ${mobile ? "h-[calc(100dvh/var(--ui-zoom,1))]" : "h-screen"}`}>
<BackfillBanner />
{mobile && (
// Tapping outside the drawer closes it.
+5 -1
View File
@@ -7,6 +7,7 @@ import { RichTextEditor } from "../../components/compose/RichTextEditor";
import { TypedPasswordInput } from "../../components/TypedPasswordInput";
import { ListDividersOption } from "../../components/mail/ListDividersOption";
import { ReadingPanePicker } from "../../components/mail/ReadingPanePicker";
import { UiScaleOption } from "../../components/mail/UiScaleOption";
import { LANGUAGES, translate, useLocale, useT } from "../../i18n";
import type { MessageKey } from "../../i18n/en";
import { api, ApiError } from "../../lib/api";
@@ -190,7 +191,7 @@ function MobileSettings({
};
return (
<div className="relative h-[100dvh] overflow-hidden bg-app-bg text-app-fg">
<div className="relative h-[calc(100dvh/var(--ui-zoom,1))] overflow-hidden bg-app-bg text-app-fg">
<div className="flex h-full flex-col pt-[env(safe-area-inset-top)]">
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-app-border px-2">
<Link to={from} className="flex items-center rounded-lg px-2 py-2 text-app-accent" aria-label={t("nav.back")}>
@@ -1444,6 +1445,9 @@ function AppearanceSection() {
<div className="max-w-md pt-2">
<ListDividersOption />
</div>
<div className="max-w-md pt-2">
<UiScaleOption />
</div>
</div>
<SpellcheckSetting />
<CompletionSetting />
+4
View File
@@ -4,6 +4,10 @@ export default {
content: ["./index.html", "./admin.html", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
// The interface-size setting zooms <html>, and zoom scales viewport units
// too: divide them back so a full-height layout still fits (lib/uiScale.ts).
height: { screen: "calc(100vh / var(--ui-zoom, 1))" },
minHeight: { screen: "calc(100vh / var(--ui-zoom, 1))" },
fontFamily: {
sans: ['"Lato"', "system-ui", "-apple-system", '"Segoe UI"', "Roboto", "sans-serif"],
},
@@ -6,6 +6,37 @@
// mounts on every message, and a spam warning on ordinary mail is worse than no
// warning at all.
// Engine symbols mean nothing to a reader, so the banner explains only the ones
// that point AT spam, in plain words (app catalog keys antispam.why.*). Neutral
// or reassuring symbols (ARC_NA, DKIM_TRACE, R_SPF_ALLOW…) are not reasons and
// stay hidden. Ordered by how telling the reason is; first match wins per symbol.
const REASONS = [
[/^BAYES_SPAM$/, "bayes"],
[/^FUZZY_(DENIED|PROB|BLACK)/, "fuzzy"],
[/^PHISH(ING|ED)/, "phishing"],
[/^(PH_SURBL|DBL_PHISH|URIBL_|SURBL_|DBL_|SEM_URIBL|RSPAMD_URIBL|MSBL_)/, "badLinks"],
[/^(RBL_|RECEIVED_SPAMHAUS|RCVD_IN_(?!DNSWL)|RSPAMD_EMAILBL|MAILSPIKE_BAD)/, "blocklist"],
[/^(BAD_REP_|RWL_.*_BAD|SENDER_REP_SPAM|IP_REPUTATION_SPAM)/, "reputation"],
[/^(R_SPF_(FAIL|SOFTFAIL)|R_DKIM_REJECT|DMARC_POLICY_(REJECT|QUARANTINE|SOFTFAIL)|ARC_REJECT)$/, "authFail"],
[/^(FORGED_|SPOOF_)/, "forged"],
[/^(FREEMAIL_REPLYTO_NEQ_FROM|REPLYTO_DOM_NEQ_FROM_DOM|REPLYTO_ADDR_EQ_TO_ADDR)/, "replyTo"],
[/^(ZERO_FONT|MANY_INVISIBLE_PARTS|R_WHITE_ON_WHITE|HTML_SHORT_LINK_IMG)/, "hiddenText"],
[/^HFILTER_/, "suspiciousServer"],
[/^MISSING_(DATE|MID|FROM|TO)$/, "headers"],
[/^(DATE_IN_FUTURE|DATE_IN_PAST)$/, "date"],
[/^SUBJ_ALL_CAPS$/, "shouting"],
];
/** Distinct plain-language reasons for a verdict's symbols, most telling first. */
function reasons(symbols) {
const found = new Set();
for (const s of symbols || []) {
const hit = REASONS.find(([re]) => re.test(s));
if (hit) found.add(hit[1]);
}
return REASONS.map(([, key]) => key).filter((key) => found.has(key));
}
function esc(s) {
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
}
@@ -14,6 +45,7 @@ export default function mount(el, ctx, props) {
const messageId = props && props.messageId;
if (!messageId) return;
let disposed = false;
const t = ctx.t;
async function act(path, body) {
await ctx.api.pluginFetch(path, { method: "POST", body: JSON.stringify(body) });
@@ -31,15 +63,18 @@ export default function mount(el, ctx, props) {
}
if (disposed || !v || v.action !== "banner") return;
const why = (v.symbols || []).slice(0, 4).join(", ");
const why = reasons(v.symbols).slice(0, 3).map((key) => t(`antispam.why.${key}`));
const meta = [t("antispam.score", { score: Number(v.score).toFixed(1) }), ...why].join(" · ");
// Inset like the rest of the message body (px-4) with air below, so the card
// does not run edge to edge into the remote-images notice under it.
el.innerHTML = `
<div class="wm-card" role="status">
<strong>Looks like spam</strong>
<span class="wm-muted">score ${Number(v.score).toFixed(1)}${why ? ` · ${esc(why)}` : ""}</span>
<div class="wm-row">
<button class="wm-btn" data-a="junk">Move to Spam</button>
<button class="wm-btn-ghost" data-a="ham">Not spam</button>
<button class="wm-btn-ghost" data-a="trust">Always trust sender</button>
<div class="wm-card" role="status" style="margin: 0 16px 10px">
<strong style="color: rgb(239 68 68)">${esc(t("antispam.looksLikeSpam"))}</strong>
<span class="wm-muted">${esc(meta)}</span>
<div class="wm-row" style="margin-top: 6px">
<button class="wm-btn" data-a="junk">${esc(t("antispam.moveToSpam"))}</button>
<button class="wm-btn-ghost" data-a="ham">${esc(t("antispam.notSpam"))}</button>
<button class="wm-btn-ghost" data-a="trust">${esc(t("antispam.trustSender"))}</button>
</div>
</div>`;
@@ -58,55 +58,55 @@
id="mask1776-6"><path
style="fill:url(#linearGradient1789);fill-opacity:1;stroke-width:0.2;stroke-linecap:round"
d="m 18.20332,107.02297 c 0,0 1.930731,1.59986 4.989281,4.12846 1.725315,1.42637 3.809507,3.14827 6.108453,5.04523 5.657506,4.74616 33.20207,27.48506 33.558689,27.18811 0.241178,-0.20083 -16.275384,9.86387 -16.275384,9.86387 L 17.979141,140.69463 Z"
id="path1776-1" /></mask><linearGradient
xlink:href="#linearGradient11"
id="linearGradient13-6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-18.692802,-93.278335)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /><radialGradient
xlink:href="#linearGradient8"
id="radialGradient816"
id="path1776-1" /></mask><radialGradient
xlink:href="#linearGradient8-5"
id="radialGradient816-3"
cx="8.0343275"
cy="13.452971"
fx="8.0343275"
fy="13.452971"
r="16.678839"
gradientTransform="matrix(-0.29271702,0.00410386,-0.01334442,-0.95181978,11.840139,23.950941)"
gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient8"><stop
style="stop-color:#fa7367;stop-opacity:1;"
id="linearGradient8-5"><stop
style="stop-color:#fa4435;stop-opacity:1;"
offset="0"
id="stop8" /><stop
id="stop8-6" /><stop
style="stop-color:#fa7367;stop-opacity:0;"
offset="1"
id="stop9" /></linearGradient><radialGradient
xlink:href="#linearGradient6"
id="radialGradient815"
cx="35.709007"
cy="12.042645"
fx="35.709007"
fy="12.042645"
id="stop9-2" /></linearGradient><radialGradient
xlink:href="#linearGradient6-1"
id="radialGradient815-9"
cx="34.523952"
cy="11.253304"
fx="34.523952"
fy="11.253304"
r="16.740065"
gradientTransform="matrix(0.25712152,0.00260463,-0.00969379,0.95694375,26.644192,0.97301292)"
gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient6"><stop
style="stop-color:#fab23b;stop-opacity:1;"
id="linearGradient6-1"><stop
style="stop-color:#fadc56;stop-opacity:1;"
offset="0"
id="stop6" /><stop
style="stop-color:#fab23a;stop-opacity:0;"
id="stop6-2" /><stop
style="stop-color:#faae3a;stop-opacity:0;"
offset="1"
id="stop7" /></linearGradient></defs><g
id="g816"
transform="translate(0.128676,-0.079965)"><path
id="path1-2-1-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient13-6);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 37.175189,4.174075 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592601,-4.12477 -4.1103801,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.76402 7.0382001,7.38257 14.4526701,7.38257 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.44221 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 V 8.300605 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.85901,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -0.001,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247011,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z" /><path
id="path1-2-1-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="M 3.6452555,7.14375 C 3.5159759,7.5278714 3.4447807,7.9357538 3.444751,8.3586629 V 23.909652 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.76402 7.038395,7.382495 14.452865,7.382495 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.218257 -3.218664,-2.595334 -5.013648,-4.05505 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.859257,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249919,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 C 16.15064,17.512631 11.679239,13.804108 9.750826,12.186336 7.4531167,10.290397 5.3700283,8.5696704 3.6452555,7.14375 Z" /><path
id="path1-2-1-7"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 41.226941,7.09104 c -1.718284,1.4205409 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.805553,1.467965 -3.513225,2.849693 -5.0307002,4.067452 2.6425302,2.518552 6.6894502,4.460193 12.1212242,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.441923 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 V 8.3007853 C 41.429125,7.8798785 41.357396,7.4736084 41.226941,7.09104 Z" /></g></svg>
id="stop7-7" /></linearGradient><linearGradient
xlink:href="#linearGradient11"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)"
x1="54.340946"
y1="122.84999"
x2="54.340946"
y2="162.66415" /></defs><g
id="g1"
transform="translate(1.425541,43.741704)"><path
id="path1-2-1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#linearGradient1);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z" /><path
id="path1-2-1-9-9"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient816-3);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z" /><path
id="path1-2-1-7-3"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:url(#radialGradient815-9);fill-opacity:1;stroke-width:0.778856;stroke-linecap:round;enable-background:accumulate;stop-color:#000000"
d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z" /></g></svg>

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB