Web frontend: touch layout for phones
Phones got the kiosk's keyboard-first layout squeezed down: shelves cut off by the tab rail and a header taking a fifth of the screen. They now get their own layout, chosen by a media query (narrow, or short landscape on a touch screen); tablets and desktop are unchanged. - Pick a group (Musik/Hörbücher/Podcasts), then a category, then scroll through a grid of smaller albums. - A floating glass dock: mini player, Musik/Zimmer tabs (no typing tab - a phone has no keyboard for it) and a search button that turns into a real search field, raised above the on-screen keyboard. - Album sheet and room page get compact phone variants. Browse state stays in the shared UiState, so resizing or rotating keeps your place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
561
web/src/styles/mobile.css
Normal file
561
web/src/styles/mobile.css
Normal file
@@ -0,0 +1,561 @@
|
||||
/* The phone layout (components/mobile/). Every class is `m-` prefixed and only rendered
|
||||
when `usePhoneLayout` says so, so none of this reaches the kiosk or a tablet. */
|
||||
|
||||
:root {
|
||||
/* Liquid-glass surfaces over the sea gradient: a dark translucent fill, a strong
|
||||
blur with a saturation boost so colour behind it still glows through, and a thin
|
||||
bright rim along the top edge. */
|
||||
--m-glass-fill: oklch(32% 0.04 210 / 0.42);
|
||||
--m-glass-rim: oklch(100% 0 0 / 0.22);
|
||||
--m-glass-shadow: 0 10px 30px oklch(10% 0.04 210 / 0.45);
|
||||
--m-gutter: 16px;
|
||||
/* Height of the floating dock, so scrolling content can clear it. */
|
||||
--m-dock-clearance: calc(96px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.m-glass {
|
||||
background: var(--m-glass-fill);
|
||||
backdrop-filter: blur(14px) saturate(1.7);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.7);
|
||||
border: 1px solid var(--m-glass-rim);
|
||||
box-shadow:
|
||||
var(--m-glass-shadow),
|
||||
inset 0 1px 0 oklch(100% 0 0 / 0.28),
|
||||
inset 0 -1px 0 oklch(100% 0 0 / 0.06);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.m-page {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: env(safe-area-inset-top) 0 calc(var(--m-dock-clearance) + 84px);
|
||||
animation: viewEnter 180ms ease-out;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ header -- */
|
||||
|
||||
.m-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px var(--m-gutter) 4px;
|
||||
}
|
||||
|
||||
.m-title-mascot {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 3px 8px oklch(15% 0.05 210 / 0.5));
|
||||
}
|
||||
|
||||
.m-title {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: var(--paper);
|
||||
letter-spacing: -0.01em;
|
||||
text-shadow: 0 2px 10px oklch(15% 0.05 210 / 0.45);
|
||||
}
|
||||
|
||||
.m-status {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--paper);
|
||||
background: oklch(97% 0.01 210 / 0.16);
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-status[data-offline="true"] {
|
||||
color: oklch(80% 0.16 30);
|
||||
}
|
||||
|
||||
/* The bar that stays put while the page scrolls under it. Fades from the top of the
|
||||
sea into transparency rather than ending in a hard edge. */
|
||||
.m-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 10px var(--m-gutter) 14px;
|
||||
background: linear-gradient(180deg, #2f6f7aee 0%, #2f6f7a99 65%, #2f6f7a00 100%);
|
||||
}
|
||||
|
||||
.m-navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.m-navbar-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
font-size: 19px;
|
||||
font-weight: 900;
|
||||
color: var(--paper);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.m-subtitle {
|
||||
text-align: center;
|
||||
margin: -6px var(--m-gutter) 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: oklch(90% 0.02 210 / 0.75);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- segmented -- */
|
||||
|
||||
.m-segmented {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 999px;
|
||||
background: oklch(20% 0.04 210 / 0.38);
|
||||
backdrop-filter: blur(12px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(1.6);
|
||||
border: 1px solid oklch(100% 0 0 / 0.14);
|
||||
box-shadow: inset 0 1px 2px oklch(10% 0.04 210 / 0.35);
|
||||
}
|
||||
|
||||
.m-segment {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: oklch(95% 0.01 210 / 0.85);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
padding: 9px 6px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.m-segment[data-active="true"] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 210 / 0.35);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ grids -- */
|
||||
|
||||
.m-category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 14px;
|
||||
padding: 0 var(--m-gutter);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.m-category-grid > .card {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 230px;
|
||||
}
|
||||
|
||||
.m-album-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 18px 12px;
|
||||
padding: 0 var(--m-gutter);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.m-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
color: var(--paper);
|
||||
cursor: pointer;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 170px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.m-album:active .m-album-cover {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.m-album-cover {
|
||||
box-shadow: 0 6px 16px oklch(10% 0.04 210 / 0.45);
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
.m-album[data-current="true"] .m-album-cover {
|
||||
outline: 3px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.m-album-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.m-album-sub {
|
||||
margin-top: -3px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- songs -- */
|
||||
|
||||
.m-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 0 var(--m-gutter);
|
||||
}
|
||||
|
||||
.m-song {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
color: var(--paper);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-song:active,
|
||||
.m-song[data-current="true"] {
|
||||
background: oklch(97% 0.01 210 / 0.14);
|
||||
}
|
||||
|
||||
.m-song-cover {
|
||||
height: 44px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.m-song-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m-song-title,
|
||||
.m-song-sub {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.m-song-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.m-song-sub {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: oklch(88% 0.02 210 / 0.65);
|
||||
}
|
||||
|
||||
.m-song-time {
|
||||
font: 700 12px ui-monospace, Menlo, monospace;
|
||||
color: oklch(88% 0.02 210 / 0.6);
|
||||
}
|
||||
|
||||
.m-hint {
|
||||
text-align: center;
|
||||
margin-top: 48px;
|
||||
padding: 0 var(--m-gutter);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: oklch(92% 0.02 210 / 0.8);
|
||||
}
|
||||
|
||||
.m-hint img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: contain;
|
||||
opacity: 0.9;
|
||||
display: block;
|
||||
margin: 0 auto 8px;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- dock -- */
|
||||
|
||||
.m-dock {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0 12px calc(10px + env(safe-area-inset-bottom));
|
||||
pointer-events: none;
|
||||
transition: bottom 0.2s ease;
|
||||
}
|
||||
|
||||
/* With the keyboard up the home indicator is hidden behind it, so no safe-area gap. */
|
||||
.m-dock[data-keyboard="true"] {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.m-dock > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.m-dock-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
/* Lets the row itself ignore taps between its pills. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.m-dock-row > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.m-circle {
|
||||
flex: none;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.m-circle-small {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.m-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
border-radius: 999px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.m-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: oklch(95% 0.01 210 / 0.8);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.m-tab[data-active="true"] {
|
||||
background: oklch(97% 0.01 210 / 0.2);
|
||||
color: var(--paper);
|
||||
box-shadow: inset 0 1px 0 oklch(100% 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.m-tab-icon {
|
||||
font-size: 19px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.m-tab[data-active="true"] .m-tab-icon {
|
||||
color: var(--accent-dim);
|
||||
}
|
||||
|
||||
.m-tab-label {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.m-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 58px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 10px 0 18px;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.m-search > svg {
|
||||
flex: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.m-search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--paper);
|
||||
font: inherit;
|
||||
/* 16px or more, or iOS zooms the page into the field on focus. */
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.m-search input::placeholder {
|
||||
color: oklch(95% 0.01 210 / 0.55);
|
||||
}
|
||||
|
||||
.m-search input::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m-search-closed {
|
||||
cursor: pointer;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: oklch(95% 0.01 210 / 0.7);
|
||||
}
|
||||
|
||||
.m-search-clear {
|
||||
flex: none;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: oklch(97% 0.01 210 / 0.3);
|
||||
color: var(--sea-deep);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- mini player -- */
|
||||
|
||||
.m-mini {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 60px;
|
||||
padding: 0 8px 0 8px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.m-mini-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-mini-cover {
|
||||
height: 44px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 210 / 0.4);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.m-mini-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m-mini-title,
|
||||
.m-mini-sub {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.m-mini-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.m-mini-sub {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: oklch(90% 0.02 210 / 0.7);
|
||||
}
|
||||
|
||||
.m-mini-button {
|
||||
flex: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--paper);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.m-mini-button:active {
|
||||
background: oklch(97% 0.01 210 / 0.15);
|
||||
}
|
||||
|
||||
.m-mini-progress {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
transform-origin: left;
|
||||
transition: transform 0.5s linear;
|
||||
}
|
||||
Reference in New Issue
Block a user