feat: implement night mode functionality with UI updates and journey tracking
- Added NightScreen component for displaying night mode information and alerts. - Introduced NextStops component to show upcoming stops during a journey. - Implemented logic for determining night mode activation based on schedule and user settings. - Created journey utility functions to manage onward journey data and stop information. - Updated tests to cover new night mode features and journey logic. - Added fixture for testing night mode scenarios with specific timestamps. - Removed QR code from night screen for a cleaner UI experience. - Enhanced admin settings to manage night mode configurations.
This commit is contained in:
102
src/styles.css
102
src/styles.css
@@ -101,33 +101,82 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
||||
.route-card-name { flex: 1 1 0; min-width: 0; height: 100%; display: flex; flex-direction: column; justify-content: center; }
|
||||
.route-list .destination-heading { width: 100%; font-size: clamp(22px, 3cqw, 30px); line-height: 1.2; margin: 0; font-weight: 550; letter-spacing: -.025em; overflow-wrap: anywhere; }
|
||||
.route-list-pages { display: flex; justify-content: flex-end; color: var(--muted); font-size: 16px; padding-top: 10px; }
|
||||
.service-info { container-name: service-card; }
|
||||
.service-window { flex: 1 1 0; min-height: 0; display: grid; grid-template-rows: repeat(2, minmax(0, 1fr)); gap: 20px; }
|
||||
.service-boundary { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: center; gap: 12px; }
|
||||
.service-boundary + .service-boundary { border-top: 1px solid var(--line); padding-top: 20px; }
|
||||
.service-label { display: flex; flex-wrap: wrap; gap: 4px 10px; color: var(--secondary); font-size: clamp(17px, 3cqw, 24px); line-height: 1.25; }
|
||||
.service-label span { color: var(--muted); font-size: .8em; align-self: center; }
|
||||
.service-boundary > time { display: block; color: var(--accent); font-size: clamp(44px, min(17cqw, 19cqh), 112px); font-weight: 600; line-height: 1; }
|
||||
.service-trip { min-width: 0; display: flex; align-items: center; gap: 12px; font-size: clamp(18px, 4cqw, 26px); line-height: 1.2; }
|
||||
.service-trip .route-badge { flex-shrink: 0; width: 72px; height: 48px; border-radius: 14px; font-size: 32px; padding: 0 8px; }
|
||||
.service-trip > span:last-child { overflow-wrap: anywhere; }
|
||||
.service-info .service-empty { margin: 0; font-size: 20px; }
|
||||
.service-info .service-unavailable { margin-block: auto; text-align: center; font-size: 22px; }
|
||||
@container service-card (min-width: 600px) {
|
||||
.service-window { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: minmax(0, 1fr); gap: 32px; }
|
||||
.service-boundary + .service-boundary { border-top: 0; border-left: 1px solid var(--line); padding-top: 0; padding-left: 32px; }
|
||||
/* A moving line and one large stop, inspired by over-door transit displays. */
|
||||
.service-info { container-name: journey-card; overflow: hidden; }
|
||||
.journey-route { display: flex; align-items: center; gap: 18px; min-width: 0; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
|
||||
.journey-route .route-badge { flex-shrink: 0; width: 80px; height: 62px; border-radius: 18px; font-size: 36px; }
|
||||
.journey-trip { min-width: 0; flex: 1; display: flex; align-items: center; gap: 18px; }
|
||||
.journey-route-name { min-width: 0; flex: 1; }
|
||||
.service-info .journey-trip .destination-heading { margin: 0; font-size: clamp(22px, 3.8cqw, 36px); line-height: 1.2; overflow-wrap: anywhere; }
|
||||
.journey-trip time { display: flex; gap: 8px; align-items: baseline; color: var(--accent); font-size: clamp(22px, 4.5cqw, 40px); font-weight: 600; }
|
||||
.journey-trip .journey-day { color: var(--secondary); font-size: 17px; font-weight: 400; }
|
||||
.service-info .journey-empty { margin-block: auto; padding: 20px 0; text-align: center; font-size: 22px; }
|
||||
.journey-metro { --station-width: clamp(180px, 22cqw, 260px); --focused-width: min(clamp(300px, 60cqw, 560px), calc(100cqw - 48px)); flex: 1 1 0; min-height: 0; min-width: 0; display: flex; flex-direction: column; padding-top: clamp(16px, 4cqh, 32px); }
|
||||
.journey-tour-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; color: var(--secondary); font-size: clamp(17px, 3.3cqw, 24px); line-height: 1.3; }
|
||||
.journey-order { color: var(--accent); font-size: .85em; white-space: nowrap; }
|
||||
.journey-map-space { flex: 1 1 0; min-height: 0; display: flex; align-items: center; margin: 18px -8px 0; }
|
||||
.journey-rail-window { width: 100%; height: min(100%, 320px); position: relative; overflow: hidden; }
|
||||
.journey-rail-window::after { content: ''; position: absolute; inset: 0 0 0 auto; width: 36px; background: linear-gradient(90deg, transparent, var(--background)); pointer-events: none; }
|
||||
.journey-rail { display: flex; height: 100%; margin: 0; padding: 0 0 0 24px; list-style: none; transform: translateX(calc(-1 * var(--journey-focus) * var(--station-width))); transition: transform 900ms cubic-bezier(.22,.72,.18,1); }
|
||||
.journey-station { flex: 0 0 var(--station-width); min-width: 0; position: relative; display: grid; grid-template-rows: 34px minmax(0, 1fr) 36px; gap: 10px; padding-right: 28px; transition: flex-basis 900ms cubic-bezier(.22,.72,.18,1); }
|
||||
.journey-station[data-focused=true] { flex-basis: var(--focused-width); }
|
||||
.journey-station::after { content: ''; position: absolute; bottom: 15.5px; left: 0; width: 100%; height: 5px; background: var(--accent); transition: opacity 700ms ease; }
|
||||
.journey-station:last-child::after { width: calc(var(--station-width) - 36px); }
|
||||
.journey-metro[data-more=false] .journey-station:last-child::after { display: none; }
|
||||
.journey-station[data-before=true]::after { opacity: .25; }
|
||||
.journey-station-name { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: 8px; transition: opacity 500ms ease; }
|
||||
.journey-station[data-before=true] .journey-station-name, .journey-station[data-before=true] .journey-duration { opacity: 0; }
|
||||
.service-info .journey-station-name .destination-heading { flex-shrink: 0; margin: 0; font-size: calc(clamp(22px, 4cqw, 32px) * var(--reading-scale, 1)); line-height: 1.13; font-weight: 600; letter-spacing: -.035em; overflow-wrap: anywhere; transition: color 600ms ease; }
|
||||
.service-info .journey-station[data-focused=true] .destination-heading { font-size: calc(clamp(48px, 11cqw, 96px) * var(--reading-scale, 1)); font-weight: 650; letter-spacing: -.055em; color: var(--foreground); }
|
||||
.journey-station[data-focused=true] .destination-current { transform-origin: left bottom; animation: journey-name-in 900ms cubic-bezier(.22,.72,.18,1) both; }
|
||||
@keyframes journey-name-in { from { opacity: 0; transform: translateX(20px) scale(.84); } to { opacity: 1; transform: translateX(0) scale(1); } }
|
||||
.journey-duration { align-self: start; display: flex; align-items: baseline; gap: 8px; color: var(--muted); font-size: clamp(18px, 3.4cqw, 26px); font-weight: 550; white-space: nowrap; transition: color 600ms ease, opacity 500ms ease; }
|
||||
.journey-duration > span { display: inline-flex; align-items: baseline; gap: 5px; }
|
||||
.journey-duration small { font-size: .72em; font-weight: 500; }
|
||||
.journey-station[data-focused=true] .journey-duration { color: var(--accent); font-size: clamp(24px, 4.5cqw, 36px); }
|
||||
.journey-node { align-self: center; display: block; width: 18px; height: 18px; border: 4px solid var(--accent); border-radius: 50%; background: var(--background); margin-left: -9px; z-index: 1; transition: background 600ms ease, box-shadow 600ms ease; }
|
||||
.journey-station[data-focused=true] .journey-node { background: var(--accent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 18%, transparent); }
|
||||
.journey-station[data-before=true] .journey-node { border-color: var(--outline); }
|
||||
.journey-continuation { flex: 0 0 var(--station-width); min-width: 0; display: flex; align-items: center; position: relative; padding: 34px 20px 36px 0; }
|
||||
.journey-continuation-label { color: var(--muted); font-size: clamp(20px, 4cqw, 28px); line-height: 1.2; max-width: 180px; }
|
||||
.journey-continuation-line { position: absolute; bottom: 15.5px; left: 0; right: 24px; border-top: 5px dashed var(--accent); opacity: .5; }
|
||||
.journey-direction-arrow { position: absolute; left: 1px; bottom: 1px; color: var(--accent); font-size: 42px; line-height: 36px; font-weight: 700; }
|
||||
.journey-caption { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: space-between; color: var(--muted); font-size: 16px; line-height: 1.3; padding-top: 16px; }
|
||||
.journey-page { color: var(--accent); margin-left: auto; }
|
||||
.journey-metro[data-calm=true] .journey-rail { transition: transform 300ms ease-out; }
|
||||
.journey-metro[data-calm=true] .journey-station { transition: flex-basis 300ms ease-out; }
|
||||
.journey-metro[data-calm=true] .destination-heading { transition: color 250ms ease-out; }
|
||||
.journey-metro[data-calm=true] .destination-current { animation: none; }
|
||||
.journey-metro[data-loop=true] .journey-rail { transition: none; animation: journey-overview-reset 600ms ease-out both; }
|
||||
.journey-metro[data-loop=true] .journey-station { transition: none; }
|
||||
@keyframes journey-overview-reset { from { opacity: 0; } to { opacity: 1; } }
|
||||
@container journey-card (max-width: 380px) {
|
||||
.journey-trip { display: block; }
|
||||
.journey-trip time { margin-top: 7px; }
|
||||
.journey-metro { --station-width: 140px; }
|
||||
.service-info .journey-station[data-focused=true] .destination-heading { font-size: calc(clamp(34px, 11cqw, 46px) * var(--reading-scale, 1)); }
|
||||
.journey-station { padding-right: 22px; }
|
||||
.journey-tour-heading { font-size: 17px; }
|
||||
.journey-caption { font-size: 14px; gap: 4px 10px; }
|
||||
}
|
||||
@container service-card (max-height: 380px) {
|
||||
.service-window { gap: 12px; }
|
||||
.service-boundary { gap: 8px; }
|
||||
.service-boundary + .service-boundary { padding-top: 12px; }
|
||||
.service-label { font-size: 17px; }
|
||||
.service-boundary > time { font-size: clamp(40px, min(14cqw, 25cqh), 72px); }
|
||||
.service-trip { font-size: 18px; }
|
||||
.service-trip .route-badge { width: 60px; height: 40px; font-size: 28px; border-radius: 12px; }
|
||||
@container journey-card (max-height: 380px) {
|
||||
.journey-route { gap: 12px; padding-bottom: 12px; }
|
||||
.journey-route .route-badge { width: 72px; height: 48px; border-radius: 14px; font-size: 30px; }
|
||||
.service-info .journey-trip .destination-heading { font-size: 24px; }
|
||||
.journey-trip time { font-size: 24px; }
|
||||
.journey-metro { padding-top: 12px; }
|
||||
.journey-map-space { margin-top: 10px; }
|
||||
.journey-station { grid-template-rows: 26px minmax(0, 1fr) 28px; gap: 6px; }
|
||||
.journey-station-name { padding-bottom: 4px; }
|
||||
.journey-station::after { bottom: 12px; height: 4px; }
|
||||
.journey-direction-arrow { bottom: 0; font-size: 36px; line-height: 28px; }
|
||||
.journey-node { width: 16px; height: 16px; margin-left: -8px; border-width: 3px; }
|
||||
.journey-continuation-line { bottom: 12px; border-top-width: 4px; }
|
||||
.journey-caption { font-size: 14px; padding-top: 10px; }
|
||||
}
|
||||
@container service-card (min-width: 600px) and (max-height: 380px) {
|
||||
.service-boundary + .service-boundary { padding-top: 0; padding-left: 20px; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.journey-rail, .service-info .journey-station .destination-heading, .journey-station, .journey-station-name, .journey-duration, .journey-station::after, .journey-node { transition: none; }
|
||||
.journey-metro .destination-current, .journey-metro[data-loop=true] .journey-rail { animation: none; }
|
||||
}
|
||||
.weather-info { container-name: weather-card; }
|
||||
.weather-body { flex: 1 1 0; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; align-items: center; justify-items: center; gap: 24px; }
|
||||
@@ -518,3 +567,6 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
||||
|
||||
/* Device onboarding keeps the same black/lime typography as the board. */
|
||||
.device-setup{min-height:100dvh;background:#000;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:clamp(24px,4vw,64px);text-align:center;gap:clamp(22px,4vh,48px);--accent:#d5ff70}.device-setup-brand{display:flex;align-items:center;gap:12px;font-size:32px;font-weight:800;letter-spacing:-.07em}.device-setup-brand>span{display:block;width:48px;color:var(--accent)}.device-setup-brand svg{width:100%;height:auto}.device-setup-card{width:min(850px,100%);border:1px solid #38402a;border-radius:32px;padding:clamp(22px,4vw,52px);background:radial-gradient(ellipse at top,#d5ff7011,transparent 65%)}.device-setup-card h1{font-size:clamp(30px,4.4vw,62px);margin:12px 0 18px;letter-spacing:-.055em}.device-setup-card p{font-size:clamp(18px,2vw,26px);color:#b3b9aa;max-width:640px;margin:0 auto;line-height:1.4}.device-pair-code{display:flex;justify-content:center;font-weight:750;font-size:clamp(64px,min(14vw,18vh),150px);letter-spacing:.08em;margin:22px 0;color:var(--accent)}.device-pair-expiry{display:flex;justify-content:center;align-items:center;gap:10px;font-size:clamp(18px,2vw,24px);color:#b3b9aa}.device-pair-expiry .numeric-text{color:var(--accent)}.device-setup button,.managed-panel button{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:14px 20px;font-size:18px;border:1px solid #555;border-radius:14px;color:#fff;background:#161616;min-height:50px}.device-setup-card button{margin-top:24px}.device-setup-card a{display:block;margin-top:20px;color:var(--accent);font-size:20px}.device-setup-status{color:#ffd479;margin-top:20px;font-size:18px}.managed-panel{color:var(--text);background:var(--background,#000);border:1px solid var(--border,#555);border-radius:26px;padding:32px;width:min(600px,calc(100% - 32px));max-height:calc(100dvh - 32px);overflow:auto}.managed-panel::backdrop{background:#000a;backdrop-filter:blur(8px)}.managed-panel>svg{color:var(--accent)}.managed-panel h2{font-size:28px;margin:16px 0}.managed-panel strong{font-size:22px;color:var(--accent)}.managed-panel p{font-size:18px;line-height:1.5;margin:16px 0;color:var(--muted,#aaa)}.managed-panel dl{display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:16px;margin:20px 0}.managed-panel>div{display:flex;gap:12px;flex-wrap:wrap}.managed-offline{color:var(--warning);font-size:14px;position:fixed;top:8px;left:50%;transform:translateX(-50%);background:var(--background,#000);padding:6px 12px;border-radius:10px;z-index:3;max-width:90vw;text-align:center}@media(max-height:650px){.device-setup{gap:16px;padding:20px}.device-setup-card{padding:24px}.device-pair-code{margin:14px 0}.device-setup-brand{font-size:26px}.device-setup-brand>span{width:36px}}
|
||||
|
||||
/* Quiet schedule-based night screen. The saved daytime theme remains intact. */
|
||||
.board[data-night=true] .cycle-progress{opacity:0}.night-screen{height:100%;min-height:0;display:flex;flex-direction:column;gap:clamp(14px,2.4vh,30px);padding:clamp(14px,2.2vh,28px) 0;container-type:size}.night-intro{flex-shrink:0}.night-intro .eyebrow{display:flex;align-items:center;gap:10px;color:var(--accent)}.night-intro h2{font-size:clamp(26px,3.3vw,54px);letter-spacing:-.05em;font-weight:650;margin:12px 0 0;line-height:1.15;max-width:1000px}.night-content{flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr);align-items:center}.night-service{min-width:0;display:flex;flex-direction:column;gap:clamp(10px,2cqh,20px)}.night-service-label{font-size:clamp(20px,2vw,32px);color:var(--muted)}.night-service-time{font-size:clamp(64px,min(13vw,28cqh),180px);font-weight:650;line-height:1;color:var(--accent);letter-spacing:-.055em;display:block}.night-route{display:flex;align-items:center;gap:clamp(14px,2vw,26px);min-width:0}.night-route .route-badge{width:clamp(72px,8vw,130px);height:clamp(58px,10cqh,100px);border-radius:22px;flex-shrink:0;font-size:clamp(28px,4vw,60px)}.night-route h3{font-size:clamp(24px,3vw,46px);font-weight:600;line-height:1.2;letter-spacing:-.045em;overflow-wrap:anywhere;margin:0}.night-alerts{flex:0 1 30%;min-height:100px;max-height:200px;overflow:auto;position:relative;scrollbar-width:none}.night-alerts .notice{grid-template-columns:minmax(0,1fr);margin:0;padding:16px 20px;border-radius:18px}.night-alerts .notice h3{font-size:clamp(20px,1.8vw,28px)}.night-alerts .notice p{font-size:clamp(18px,1.5vw,24px);line-height:1.35}.night-alerts .notice-routes{display:flex;gap:8px;margin:8px 0}.night-alerts .notice-routes .route-badge{width:62px;height:38px;border-radius:10px;font-size:22px}.night-alerts .notice-graphic{display:none}.night-alert-page{display:block;text-align:right;color:var(--muted);font-size:14px;padding:6px}.board[data-night=true] .numeric-old,.board[data-night=true] .numeric-next,.board[data-night=true] .numeric-previous{animation:none!important}.board[data-night=true] .numeric-current{animation:none!important}@media(max-width:700px){.night-screen{gap:16px;padding:18px 0}.night-intro h2{font-size:clamp(26px,6.7vw,36px)}.night-content{grid-template-columns:1fr;gap:18px;align-content:center}.night-service{gap:12px}.night-service-time{font-size:clamp(68px,min(22vw,24cqh),115px)}.night-route h3{font-size:clamp(23px,5.7vw,32px)}.night-alerts{max-height:150px;flex-basis:28%}.night-alerts .notice{padding:14px}.night-alerts .notice-meta{flex-wrap:wrap}.night-alerts .notice-period{font-size:13px}.night-alerts .notice p{font-size:18px}}@media(max-height:620px) and (min-width:701px){.night-screen{gap:12px;padding:12px 0}.night-intro h2{font-size:clamp(25px,3vw,36px);margin-top:8px}.night-service-time{font-size:clamp(64px,28cqh,120px)}.night-route h3{font-size:clamp(23px,3vw,36px)}.night-route .route-badge{height:56px;width:80px}.night-alerts{min-height:90px;max-height:110px;flex-basis:28%}.night-service-label{font-size:22px}}
|
||||
|
||||
Reference in New Issue
Block a user