Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ on the page background; `ink-400` is decorative only.
- Color is never the only cue; live regions announce copy results and async
status.
- Plain-text rendering for model output and remote content.
- Motion is limited to state feedback and respects `prefers-reduced-motion`.
- Motion is limited to state feedback, dialog and menu entrances, and a short
cross-fade between pages in which the current-page underline slides to its
new link. All of it respects `prefers-reduced-motion`.

## Local preview

Expand Down
15 changes: 12 additions & 3 deletions routes/dashboard_security.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Dashboard single sign-on through Cloudflare Access, the audit log page and page hardening."""

import base64
import hashlib
import logging
from typing import Optional

Expand All @@ -13,12 +15,19 @@

logger = logging.getLogger(__name__)

# Templates load only same-origin scripts and styles and hold no inline code; JSON data
# Chromium misses a cross-document view-transition opt-in that arrives in a stylesheet still
# being revalidated (static files are no-cache), so base.html carries this one rule inline and the
# policy allows exactly it by hash. The transition's animations stay in shell.css.
VIEW_TRANSITION_STYLE = "@view-transition{navigation:auto}"
VIEW_TRANSITION_STYLE_SOURCE = "'sha256-%s'" % base64.b64encode(
hashlib.sha256(VIEW_TRANSITION_STYLE.encode()).digest()).decode()

# Templates load only same-origin scripts and styles and hold no other inline code; JSON data
# blocks are not executed. Fetches, the service worker and the manifest stay same-origin.
CONTENT_SECURITY_POLICY = "; ".join((
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
f"style-src 'self' {VIEW_TRANSITION_STYLE_SOURCE}",
"img-src 'self' data: blob:",
"font-src 'self'",
"connect-src 'self'",
Expand Down Expand Up @@ -130,7 +139,7 @@ def dashboard_sso_context():
"enabled": dashboard_sso.configured(),
"only": dashboard_sso.sso_only(),
"logout_url": dashboard_sso.logout_url(),
}}
}, "view_transition_style": VIEW_TRANSITION_STYLE}

@app.after_request
def dashboard_security_headers(response):
Expand Down
3 changes: 2 additions & 1 deletion static/css/auto-routes.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@
.auto-route-candidate-heading,
.auto-route-section-heading {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
gap: 0.5rem 1rem;
}

.auto-route-candidate-heading {
Expand Down
55 changes: 53 additions & 2 deletions static/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@
background: var(--rose-700);
}

/* Bare disclosures get a 40 px target; styled ones set their own. */
:where(.app-main) details:not(.disclosure, .endpoint-group) > summary {
padding-block: 0.6rem;
cursor: pointer;
}

/* Disclosure: a quiet expandable section for secondary detail. */
.disclosure > summary {
display: flex;
Expand Down Expand Up @@ -483,19 +489,57 @@
animation: spin 0.75s linear infinite;
}

/* Dialog */
/* Dialog: rises in over a darkening backdrop; closing is shorter and quieter. The base
reset zeroes every margin, so centring is restored here. */
.dialog {
width: min(100% - 2rem, 34rem);
max-height: calc(100dvh - 2rem);
margin: auto;
padding: 0;
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
color: var(--text);
box-shadow: var(--shadow-lg);
opacity: 0;
translate: 0 0.5rem;
scale: 0.98;
transition: opacity 150ms ease-in, translate 150ms ease-in, scale 150ms ease-in,
overlay 150ms allow-discrete, display 150ms allow-discrete;
}

.dialog[open] {
opacity: 1;
translate: 0;
scale: 1;
transition-duration: var(--motion);
transition-timing-function: var(--ease);
}

@starting-style {
.dialog[open] {
opacity: 0;
translate: 0 0.5rem;
scale: 0.98;
}
}

.dialog::backdrop {
background: rgb(7 19 31 / 62%);
background-color: rgb(7 19 31 / 0%);
transition: background-color 150ms ease-in, overlay 150ms allow-discrete, display 150ms allow-discrete;
}

.dialog[open]::backdrop {
background-color: rgb(7 19 31 / 62%);
transition-duration: var(--motion);
}

@starting-style {
.dialog[open]::backdrop {
background-color: rgb(7 19 31 / 0%);
}
}

.dialog__body {
Expand All @@ -516,11 +560,18 @@
line-height: 1.55;
}

/* Stays in view at the foot of a long dialog. */
.dialog__actions {
position: sticky;
bottom: 0;
display: flex;
margin: 0 -1.5rem -1.5rem;
padding: 0.85rem 1.5rem 1rem;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.55rem;
border-top: 1px solid var(--border-subtle);
background: var(--surface);
}

/* Notifications */
Expand Down
16 changes: 15 additions & 1 deletion static/css/controls.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: background var(--motion-fast) ease, border-color var(--motion-fast) ease, color var(--motion-fast) ease;
transition: background var(--motion-fast) ease, border-color var(--motion-fast) ease, color var(--motion-fast) ease,
scale var(--motion-fast) var(--ease);
}

@media (prefers-reduced-motion: no-preference) {
.button:active:not(:disabled, [aria-disabled="true"], [aria-busy="true"]) {
scale: 0.97;
}
}

.button:hover {
Expand Down Expand Up @@ -163,6 +170,13 @@
resize: vertical;
}

/* JSON and other structured text. */
.field-textarea--code {
font-family: var(--font-mono);
font-size: var(--text-sm);
tab-size: 2;
}

:is(.app-main, dialog) :where(input, select, textarea):focus-visible,
.field:focus-visible,
.field-select:focus-visible,
Expand Down
79 changes: 79 additions & 0 deletions static/css/shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ html {
font-family: var(--font-sans);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
scroll-padding-top: calc(var(--header-height) + 4rem);
}
Expand Down Expand Up @@ -282,6 +284,12 @@ samp {
background: var(--surface);
}

/* Holds still while the page beneath it cross-fades. The public header is left out because it
scrolls away on small screens. */
.app-header {
view-transition-name: masthead;
}

.app-header__inner,
.site-header__inner {
display: flex;
Expand All @@ -304,6 +312,7 @@ samp {

.primary-nav__link,
.site-nav__link {
position: relative;
display: inline-flex;
padding: 0 0.8rem;
align-items: center;
Expand All @@ -327,6 +336,21 @@ samp {
box-shadow: inset 0 -3px 0 var(--accent-fill);
}

/* The current page's underline is its own element so it can slide to the next page's link. */
.primary-nav__link[aria-current="page"] {
box-shadow: none;
}

.primary-nav__indicator {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3px;
background: var(--accent-fill);
view-transition-name: nav-indicator;
}

.app-header__actions,
.site-header__actions {
display: flex;
Expand Down Expand Up @@ -384,6 +408,17 @@ samp {
border-radius: var(--radius-sm);
background: var(--surface);
cursor: pointer;
transition: background-color var(--motion-fast) ease, scale var(--motion-fast) var(--ease);
}

.mobile-menu-button:hover {
background: var(--surface-muted);
}

@media (prefers-reduced-motion: no-preference) {
.mobile-menu-button:active {
scale: 0.94;
}
}

.mobile-menu-button span:not(.sr-only) {
Expand All @@ -405,6 +440,17 @@ samp {
background: var(--surface);
}

.mobile-nav:not([hidden]) {
transition: opacity var(--motion) var(--ease), translate var(--motion) var(--ease);
}

@starting-style {
.mobile-nav:not([hidden]) {
opacity: 0;
translate: 0 -0.35rem;
}
}

.mobile-nav a {
display: flex;
min-height: 2.75rem;
Expand Down Expand Up @@ -633,6 +679,33 @@ samp {
}
}

/* Page transitions: same-origin navigations cross-fade, the header holds still and the
current-page underline slides to its new link. The opt-in is inline in base.html so it is
present before stylesheets revalidate; reduced motion cancels these animations. */
::view-transition-old(root) {
animation: 120ms ease-in both page-fade-out;
}

::view-transition-new(root) {
animation: var(--motion) var(--ease) both page-fade-in;
}

::view-transition-group(nav-indicator) {
animation-duration: 280ms;
animation-timing-function: var(--ease);
}

@keyframes page-fade-out {
to { opacity: 0; }
}

@keyframes page-fade-in {
from {
opacity: 0;
translate: 0 0.25rem;
}
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
Expand All @@ -642,4 +715,10 @@ samp {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}

::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
3 changes: 2 additions & 1 deletion static/js/key-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
/** The PUT /users/<name>/controls body for the dialog's field values. */
function controlsPayload(fields) {
return {
official_only: fields.official_only === "on" || fields.official_only === true,
// A clear box stores no restriction (null): an explicit false would still mark the key controlled.
official_only: fields.official_only === "on" || fields.official_only === true ? true : null,
forced_provider_preferences: fields.forced_provider_preferences ? JSON.parse(fields.forced_provider_preferences) : null,
secret_scan_mode: fields.secret_scan_mode || null,
shadow_eval_rate: amount(fields.shadow_eval_rate),
Expand Down
10 changes: 5 additions & 5 deletions static/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const CACHE_NAME = 'multillm-proxy-v13';
const CACHE_NAME = 'multillm-proxy-v14';
const OFFLINE_URL = '/static/offline.html';
const PRECACHE_URLS = [
OFFLINE_URL,
'/static/css/style.css',
'/static/css/shell.css?v=2',
'/static/css/controls.css?v=1',
'/static/css/components.css?v=1',
'/static/css/shell.css?v=3',
'/static/css/controls.css?v=2',
'/static/css/components.css?v=2',
'/static/css/content.css?v=1',
'/static/css/public.css?v=2',
'/static/css/auto-routes.css?v=8',
'/static/css/auto-routes.css?v=9',
'/static/css/documentation.css?v=10',
'/static/css/operations.css?v=2',
'/static/css/surfaces.css?v=2',
Expand Down
11 changes: 7 additions & 4 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
{# Opts in to cross-document view-transitions; the page CSP allows it by hash (routes/dashboard_security.py). #}
{% if view_transition_style %}<style>{{ view_transition_style }}</style>{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/shell.css', v='2') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/controls.css', v='1') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css', v='1') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/shell.css', v='3') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/controls.css', v='2') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css', v='2') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/content.css', v='1') }}">
{% block styles %}{% endblock %}
</head>
Expand Down Expand Up @@ -51,7 +53,8 @@

<nav class="primary-nav" aria-label="Primary">
{% for endpoint, label, admin_only, current_for in nav_items if is_admin or not admin_only %}
<a href="{{ url_for(endpoint) }}" class="primary-nav__link"{% if request.endpoint in current_for %} aria-current="page"{% endif %}>{{ label }}</a>
{% set is_current = request.endpoint in current_for %}
<a href="{{ url_for(endpoint) }}" class="primary-nav__link"{% if is_current %} aria-current="page"{% endif %}>{{ label }}{% if is_current %}<span class="primary-nav__indicator" aria-hidden="true"></span>{% endif %}</a>
{% endfor %}
</nav>

Expand Down
Loading
Loading