feat(ui): white settings rail in light mode, unify shadows, mobile sheet polish

- Settings rail (application-settings-navigation) is now white in light mode
  instead of a gray fill; separation comes from the hairline ring + soft lift.
- Mobile slide-over: replace the heavy shadow-modal with a soft left-casting
  shadow (scrim handles content dimming, shadcn-style) + rounded-l corners.
- Unify scattered shadow-lg usages onto the shadow-dropdown / shadow-modal
  tokens across dropdowns, tooltips, popups and slide-over.
- Light-mode semantic text + tertiary/nav contrast tuning for WCAG AA.
- status-summary: support right-aligned dropdown; use it on the app heading.
This commit is contained in:
Aditya Tripathi
2026-09-08 07:05:01 +02:00
committed by Andras Bacsai
parent fd928537e3
commit 160be6f571
10 changed files with 81 additions and 25 deletions
+58 -9
View File
@@ -1023,8 +1023,34 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .termina
the same neutral temperature as the content text, so the sidebar no longer
reads cooler than the panels/cards. */
--color-nav-text: oklch(73.8% 0 0);
--color-nav-muted: oklch(58.97% 0 0);
/* Raised from 58.97% (4.47:1 on panel, just under AA) to 61% (4.86:1) so the
11px sidebar section labels clear 4.5:1. */
--color-nav-muted: oklch(61% 0 0);
--color-nav-active: oklch(96.12% 0 0);
/* Tertiary text. Base token (59.31%) only clears 4.5:1 on app/panel; it
failed on surface/raised/selected (4.46/4.20/3.73). Raised to 64% so the
313 dark:text-fg-faint usages clear AA across the whole surface ladder,
while staying clearly dimmer than fg-dim (76.99%). */
--color-fg-faint: oklch(65% 0 0);
/* Error text. The base error red (57.71%) failed on every dark surface
(4.10/3.75/3.53). Brightened for dark mode so text-error clears AA on the
card ladder while staying unmistakably red. */
--color-error: oklch(66% 0.2 27.33);
}
/* Light-mode semantic text.
warning / success / error double as the bright brand accent and as solid
fills, which only appear under `dark:` — so in light mode these tokens are
used almost entirely as text, translucent tints (bg-warning/10) and borders.
The bright values (yellow 88%, green 72%) and the base red (57.71%) fail AA
as text on white and the light surface ladder (warning 1.43, success 2.28,
error 4.43 on canvas). Scoped to `:not(.dark)` so dark keeps the brand
colors and the custom theme keeps its own overrides. Every value clears
4.5:1 on white, canvas, elevated, recessed and fill, and is in sRGB gamut. */
html:not(.dark) {
--color-warning: oklch(50% 0.105 80);
--color-success: oklch(48% 0.135 149.58);
--color-error: oklch(52% 0.2 27.33);
}
/* Theme surfaces are derived from one brand color. Changing
@@ -1493,7 +1519,14 @@ html[data-theme="custom"] textarea:disabled {
font-weight: 500;
line-height: 1.25rem;
letter-spacing: 0;
color: inherit;
/* Card title is a heading, so give it a readable strength instead of the
muted --coollabs-subtle (5.66:1 light / 6.98:1 dark). ~8.7:1 in both
modes (comfortable AAA) while descriptions/labels stay muted. */
color: oklch(40% 0 0);
}
.dark .application-settings-section > :is(header, .application-settings-section-header) :is(h2, h3) {
color: var(--color-fg-dim);
}
.application-settings-section > .application-settings-section-header p {
@@ -1503,12 +1536,16 @@ html[data-theme="custom"] textarea:disabled {
color: var(--coollabs-subtle);
}
/* Layer card body: nested rounded panel with base bg + fill ring */
/* Layer card body: nested rounded panel with base bg. Ring uses the same
--coollabs-hairline as the card shell (line ~1456) so the body edge and the
header edge read as one continuous border instead of two different grays
(fill was lighter than hairline, so the header sides and body sides did not
match). */
.application-settings-section-body {
position: relative;
border-radius: 8px;
background: var(--coollabs-base);
box-shadow: 0 0 0 1px var(--coollabs-fill);
box-shadow: 0 0 0 1px var(--coollabs-hairline);
padding: 1rem;
}
@@ -1549,9 +1586,12 @@ html[data-theme="custom"] textarea:disabled {
@media (min-width: 1280px) {
.application-settings-navigation {
position: sticky;
top: calc(3rem + 1rem);
/* Clear the fixed 3rem topbar and match <main>'s pt (3rem + 1.75rem) so
the pinned rail aligns with the content and has breathing room at the
top instead of hugging the header when the page scrolls. */
top: calc(3rem + 1.75rem);
align-self: start;
max-height: calc(100dvh - 5rem);
max-height: calc(100dvh - 5.75rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
@@ -1560,9 +1600,10 @@ html[data-theme="custom"] textarea:disabled {
scrollbar-width: thin;
scrollbar-color: var(--coollabs-fill) transparent;
scrollbar-gutter: stable;
/* Contained card: a distinct elevated surface with a hairline ring and a
restrained lift, so the settings nav reads as a structured panel that
floats above the content canvas instead of loose links on the page. */
/* Contained card: a distinct surface with a hairline ring and a restrained
lift, so the settings nav reads as a structured panel that stands off
the content canvas. In dark mode --coollabs-elevated is a clear lighter
step above the canvas; the light-mode fill is overridden below. */
padding: 0.5rem;
background: var(--coollabs-elevated);
border: 1px solid var(--coollabs-hairline);
@@ -1570,6 +1611,14 @@ html[data-theme="custom"] textarea:disabled {
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
/* Light mode: a bright white rail on the light-gray page canvas (neutral-50,
~98.5%). Fill contrast to the canvas is small, so separation comes from the
hairline ring + soft lift rather than a darker gray fill — the standard
"white rail on gray canvas" hierarchy (Linear/Vercel), not a gray rail. */
html:not(.dark) .application-settings-navigation {
background: var(--coollabs-base);
}
.application-settings-navigation::-webkit-scrollbar {
width: 6px;
}
+8 -5
View File
@@ -15,7 +15,7 @@
}
@utility apexcharts-tooltip-custom {
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-dropdown p-3 text-sm;
min-width: 160px;
}
@@ -162,7 +162,7 @@
}
@utility auth-tooltip {
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-lg dark:text-fg dark:bg-raised dark:border-white/10;
@apply fixed z-[10000] px-2.5 py-1.5 text-xs font-medium rounded-lg pointer-events-none whitespace-nowrap text-white bg-neutral-900 border border-neutral-700 shadow-dropdown dark:text-fg dark:bg-raised dark:border-white/10;
}
@utility alert-success {
@@ -222,7 +222,10 @@
}
@utility menu-item {
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
/* Translucent hover (like the active pill) so it darkens against any surface,
including the light-mode settings rail's gray fill; on white it matches the
old neutral-100. */
@apply relative flex gap-2.5 items-center h-8 px-2.5 w-full text-[13px] font-medium rounded-md truncate min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
}
@utility menu-item-icon {
@apply shrink-0 size-[18px] opacity-90;
@@ -245,7 +248,7 @@
/* Indented child rows in a collapsible nav group */
@utility menu-subitem {
/* Label owns text ellipsis; keep this row overflow-visible so the focus ring is not clipped. */
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-neutral-100 hover:text-nav-active dark:hover:bg-white/[0.05];
@apply relative flex gap-2.5 items-center h-8 pl-3 pr-2.5 w-full text-[13px] font-medium rounded-md min-w-0 transition-colors text-nav-text hover:bg-black/[0.04] hover:text-nav-active dark:hover:bg-white/[0.05];
}
@utility menu-subitem-active {
@apply rounded-md bg-black/[0.05] text-nav-active hover:bg-black/[0.05] dark:bg-white/[0.06] dark:hover:bg-white/[0.06];
@@ -344,7 +347,7 @@
}
@utility info-helper-popup {
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-modal whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
@apply rounded-lg border border-neutral-200 bg-white text-neutral-600 shadow-dropdown whitespace-normal break-words dark:border-white/10 dark:bg-raised dark:text-fg-dim;
}
@utility buyme {
@@ -132,7 +132,7 @@
{{-- Dropdown Options --}}
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
<template x-if="filteredOptions.length === 0">
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
@@ -277,7 +277,7 @@
{{-- Dropdown Options --}}
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-dropdown max-h-60 overflow-auto scrollbar">
<template x-if="filteredOptions.length === 0">
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
@@ -75,6 +75,6 @@
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
:style="`left: ${x}px; top: ${y}px;`"
:class="[below ? '' : '-translate-y-full', positioned ? 'visible' : 'invisible']"
class="pointer-events-none fixed z-[10000] whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-lg dark:border-white/10 dark:bg-raised">
class="pointer-events-none fixed z-[10000] whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-dropdown dark:border-white/10 dark:bg-raised">
</div>
</div>
+1 -1
View File
@@ -240,6 +240,6 @@
</div>
<div x-show="collapsed && tooltip.show" x-cloak x-transition.opacity.duration.100ms
:style="`left: ${tooltip.x}px; top: ${tooltip.y}px;`"
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-lg border border-neutral-700 dark:border-white/10"
class="fixed z-[10000] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-lg bg-neutral-900 dark:bg-raised text-white whitespace-nowrap pointer-events-none shadow-dropdown border border-neutral-700 dark:border-white/10"
x-text="tooltip.text"></div>
</nav>
+1 -1
View File
@@ -11,7 +11,7 @@
{{ $customActions }}
@else
<div
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
class="flex lg:items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-modal lg:border-t dark:border-coolgray-300 border-neutral-200 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded-sm">
<div
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
@if (isset($icon))
@@ -35,7 +35,7 @@ x-init="$watch('slideOverOpen', value => {
'max-w-4xl w-screen' => $fullScreen,
])>
<div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-modal bg-neutral-50 dark:bg-base dark:border-neutral-800 border-neutral-200">
<div class="px-4 pb-4 sm:px-5">
<div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title">
@@ -1,4 +1,4 @@
@props(['status', 'title' => 'Application status', 'containerName' => 'Container'])
@props(['status', 'title' => 'Application status', 'containerName' => 'Container', 'align' => 'left'])
@php
$rawStatus = str((string) $status)->lower()->trim()->value();
@@ -62,8 +62,12 @@
</span>
</x-status-badge>
<div x-cloak x-show="open" x-transition.origin.top.left
class="listbox-panel top-8! right-auto! left-0! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!" role="menu">
<div x-cloak x-show="open" x-transition.origin.top.{{ $align === 'right' ? 'right' : 'left' }}
@class([
'listbox-panel top-8! z-[90]! w-[min(16rem,calc(100vw-1.5rem))]! min-w-0! sm:w-64! sm:min-w-64!',
'right-auto! left-0!' => $align !== 'right',
'left-auto! right-0!' => $align === 'right',
]) role="menu">
<div class="px-3 py-2 text-[11px] font-medium text-neutral-400 dark:text-fg-faint">{{ $title }}</div>
<div class="listbox-option cursor-default! gap-2.5!">
<span @class([
+1 -1
View File
@@ -87,7 +87,7 @@
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in duration-200"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="relative flex h-full w-72 max-w-[85vw] min-w-0 flex-col border-l border-neutral-200 bg-white shadow-2xl dark:border-white/[0.12] dark:bg-panel">
class="relative flex h-full w-72 max-w-[85vw] min-w-0 flex-col overflow-hidden rounded-l-2xl border-l border-neutral-200 bg-white shadow-[-8px_0_30px_-6px_rgba(0,0,0,0.18)] dark:border-white/[0.12] dark:bg-panel dark:shadow-[-8px_0_30px_-4px_rgba(0,0,0,0.5)]">
<div data-mobile-sidebar-brand
class="flex h-12 shrink-0 items-center justify-between gap-1.5 border-b border-neutral-200 px-4 dark:border-white/[0.06]">
<div class="flex min-w-0 items-baseline gap-1.5">
@@ -32,7 +32,7 @@
{{ $application->name }}
</h1>
<div class="flex shrink-0 items-center gap-2">
<x-status-summary :status="$application->status" />
<x-status-summary :status="$application->status" align="right" />
<x-applications.links :application="$application" compact />
</div>
<div class="flex w-full flex-wrap gap-1">