Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Desktop" row) on each side means "use it".

It reskins through Adminer's own `--bg/--fg/--dim/--lit` plus our `--ad-*`, and is split
into components pulled in by `@import`: `tokens`, `base`, `tables`, `forms`, `sidebar`,
`settings`. `light-dark()` is colour-only, so the one non-colour scheme difference —
`settings`, `window`, and `Theme::cssMap()` adds `mac-os.css` or `windows-os.css` after them. `light-dark()` is colour-only, so the one non-colour scheme difference —
inverting adminer's sprite icons on the dark surface — lives in `base.css` and keys off the
`theme-auto`/`theme-dark` body class instead. `system-ui` gives the native OS font with no
branching; the `theme-*`, `mac-os`/`windows-os`/`linux-os` and `density-compact`/`-cozy`/
Expand Down
10 changes: 8 additions & 2 deletions app/src/Settings/Theme/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
class Theme {
private const string OWN_DESIGN = "src/Settings/Theme/designs/adminer-desktop/adminer.css";
private const string MAC_SHEET = "src/Settings/Theme/designs/adminer-desktop/mac-os.css";
private const string WINDOWS_SHEET = "src/Settings/Theme/designs/adminer-desktop/windows-os.css";

private \AdminerDesktop $desktop;
Expand Down Expand Up @@ -65,7 +66,12 @@ function designs(Mode $mode): array {
*/
function cssMap(): array {
$map = $this->designMap();
if (Os::look() !== Os::Windows) {
$sheet = match (Os::look()) {
Os::Mac => self::MAC_SHEET,
Os::Windows => self::WINDOWS_SHEET,
Os::Linux => null,
};
if ($sheet === null) {
return $map;
}
// Right after ours and tagged the same, so it applies wherever ours does and never over a
Expand All @@ -74,7 +80,7 @@ function cssMap(): array {
foreach ($map as $path => $mode) {
$return[$path] = $mode;
if ($path === self::OWN_DESIGN) {
$return[self::WINDOWS_SHEET] = $mode;
$return[$sheet] = $mode;
}
}
return $return;
Expand Down
74 changes: 48 additions & 26 deletions app/src/Settings/Theme/designs/adminer-desktop/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
Adminer Desktop — the app's own default theme, not a downloaded design.

Unlike the gallery designs beside it (single upstream `adminer.css` / `adminer-dark.css`
files, pinned and never edited), this one is ours to change. It is split into components
pulled in by native `@import` from `adminer.css`:

- `tokens.css` — the variable system (Adminer's `--bg/--fg/--dim/--lit` + our `--ad-*`) as `light-dark()` pairs, density levels, font stacks
- `base.css` — type, links, headings, focus, messages, the dark icon invert
- `tables.css` — the data grid
- `forms.css` — inputs and buttons
- `sidebar.css` — the menu and breadcrumb
- `windows-os.css` — Windows 11's tokens and controls under `windows-os`, with values from WinUI's resources; `Theme::cssMap()` loads it on Windows only, not this file

One file carrying both schemes: every scheme difference is a `light-dark(light, dark)`
token, and the used `color-scheme` picks the side. Adminer sets that from its
`<meta name="color-scheme">`, which `Theme::cssMap()` drives from the appearance
preference — `light dark` for Auto (follows the OS), or pinned to one side for a Light /
Dark override. The lone colour-only exception, inverting Adminer's sprite icons on the
dark surface, keys off the `theme-auto` / `theme-dark` body class instead (`base.css`).
It is excluded from the gallery (`Theme::designs()`) because it is the default, offered as
the empty "Adminer Desktop" option on each side rather than one entry among the rest.

Per-OS adaptation is mostly `system-ui` (the OS UI font, no branching); the `mac-os` /
`windows-os` / `linux-os` body class from `AdminerDesktop::bodyClass()` is the hook for
the rest. Row density (`density-compact` / `-cozy` / `-comfortable`) comes from the same
place.
# Adminer Desktop theme

The app's own default look. The gallery designs beside it are downloaded from upstream, pinned and never edited; this one is ours to change.

- It is the empty "Adminer Desktop" option on each side of the settings, so `Theme::designs()` keeps it out of the gallery.
- It reskins Adminer through Adminer's `--bg`, `--fg`, `--dim` and `--lit`, plus our own `--ad-*`.

## Files

`adminer.css` is the entry point. It pulls these in with `@import`, for every OS:

| File | What it holds |
|------|---------------|
| `tokens.css` | the variables as `light-dark()` pairs, density levels, scaling, font stacks |
| `layout.css` | the two panels, the sidebar's drag handle, the breadcrumb as a sticky header |
| `base.css` | type, links, headings, messages, the dark icon invert, the page transition |
| `tables.css` | the data grid |
| `forms.css` | inputs and buttons |
| `sidebar.css` | the menu and the table list |
| `settings.css` | the settings dialog |
| `window.css` | the frameless macOS window, laid out as Mail is, under `.ad-frameless` |

`Theme::cssMap()` adds one more right after `adminer.css`, by OS, and never over a gallery design:

| File | Loaded on | What it holds |
|------|-----------|---------------|
| `mac-os.css` | macOS | the system accent, neutral greys, corners, overlay scrollbars |
| `windows-os.css` | Windows | Windows 11's tokens and controls, with values from WinUI's resources |

Linux has no file of its own.

## Light and dark

- Every scheme difference is a `light-dark(light, dark)` token, and the used `color-scheme` picks the side.
- Adminer sets that from `<meta name="color-scheme">`, which `Theme::cssMap()` drives from the appearance preference: `light dark` for Auto, one side for a Light or Dark override.
- So no scheme media query, and no `color-scheme` in the CSS: a value there would beat the meta and defeat the override.
- The one exception is inverting Adminer's sprite icons, which is not a colour. It keys off the `theme-auto` and `theme-dark` body classes (`base.css`).

## Hooks on the body

All of them come from `AdminerDesktop::bodyClass()`:

- `mac-os`, `windows-os`, `linux-os`: the OS. `make debug` can simulate another one.
- `density-compact`, `density-cozy`, `density-comfortable`: row density.
- `scale-100` to `scale-200`: the UI scale.
- `theme-auto`, `theme-light`, `theme-dark`: the appearance preference.

A token an OS file sets on the body has to restate what `html` derives from it. A `var()` resolves where it is declared, so `--ad-accent-text` on `html` keeps `html`'s accent.
36 changes: 0 additions & 36 deletions app/src/Settings/Theme/designs/adminer-desktop/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ kbd,
font-family: var(--ad-mono);
}

/* macOS renders system-ui (San Francisco) best antialiased; Windows and Linux stay on
the OS default, where forcing this only thins the text. .os-* comes from bodyClass. */
.mac-os body {
-webkit-font-smoothing: antialiased;
}

/* Links. */
a,
a:visited {
Expand Down Expand Up @@ -201,36 +195,6 @@ body.theme-dark .icon {
}
}

/* Scrollbars that show only while the pointer is over what they scroll, as macOS draws them in
* its own apps. WebKit here keeps them on screen for good — Safari too, on a bare page, with the
* system set to hide them — so the panels wore two grey bars at all times. Transparent until
* hovered (`:hover` holds for every element under the pointer, so the scroller it is in), and
* thin, as an overlay scrollbar is, and while it scrolls (.ad-scrolling, scrollers.js), as an
* overlay one appears then. Windows has the same in windows-os.css; Linux keeps its own on screen.
*
* Only while the system draws overlay scrollbars. With a mouse plugged in, or System Settings set
* to Always, macOS draws legacy ones that stay on screen, and so does every native app — Mail
* included; the launcher says which (ad-legacy-scrollers on <html>, installScrollerStyle), and then
* WebKit's own are left as they are, except the panels' (below). A plain browser is never told,
* and keeps the overlay behaviour. */
:root:not(.ad-legacy-scrollers) .mac-os * {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}

:root:not(.ad-legacy-scrollers) .mac-os :is(:hover, .ad-scrolling) {
scrollbar-color: color-mix(in srgb, var(--fg) 35%, transparent) transparent;
}

/* Even transparent, that bar keeps its width, so every rule in the two panels stopped one
scrollbar short of the right edge. A legacy one takes the same column, and the toolbar's
hairline and search stopped short of it. So the panels' bars are scrollers.js's in both modes;
the scrollers inside them keep theirs. */
.mac-os #menu,
.mac-os #content {
scrollbar-width: none;
}

/* The panels' overlay thumbs (scrollers.js): a rounded bar over the content's edge, in macOS's
overlay grey, there while the panel scrolls and fading after. It takes no room, so the
content does not move when it shows. */
Expand Down
136 changes: 136 additions & 0 deletions app/src/Settings/Theme/designs/adminer-desktop/mac-os.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* What macOS changes in our design: its accent and neutral greys, its corners, and its overlay
* scrollbars. Theme::cssMap() loads it after adminer.css, on macOS only. The frameless window has
* its own file, window.css, because that follows from the launcher and not from the OS class. */

/* On macOS the accent is the system's (System Settings > Appearance > Colour), read through
WebKit's own keyword, so links, icons and the selected row follow it and change with it. Other
engines have no such keyword and keep the theme's indigo below. Text on it is white on a dark
accent and black on a light one (yellow, orange, green), chosen from the accent's own lightness:
white on the yellow one is about 1.6:1. */
@supports (color: -apple-system-control-accent) {
/* :root, not html: it outranks the html block below, which sets the default. */
:root {
--ad-accent: -apple-system-control-accent;
--ad-accent-fg: oklch(
from var(--ad-accent) clamp(0, (0.72 - l) * 1000, 1) 0 0
);
/* The buttons stay out of it: graphite, so a form's Save does not shout in the accent
every link already wears. Lighter on the dark surface, or it sinks into it; white on both. */
--ad-button: light-dark(#333336, #5a5a5f);
--ad-button-fg: #ffffff;
/* The dark side in macOS's own neutral greys, as Mail and the system's windows are, not
the theme's blue-tinted ones, which beside the system's sidebar material read as
another app's. The light side keeps the theme's. */
--bg: light-dark(#ffffff, #1e1e1e);
--fg: light-dark(#1d2127, #dcdcdc);
--dim: light-dark(#f1f3f6, #2a2a2a);
--ad-panel: light-dark(#ffffff, #1e1e1e);
--ad-menu-bg: light-dark(#f7f8fa, #262626);
/* Adminer's highlight (a row it marks, :target, a checked row) tinted by the accent. */
--lit: light-dark(
color-mix(in srgb, var(--ad-accent) 12%, #ffffff),
color-mix(in srgb, var(--ad-accent) 22%, #1e1e1e)
);
}
}

/* macOS 26+ rounds a titled window with a continuous curve, not a circle: 16.5pt at its apex,
easing into the edges 24pt from the corner (measured off a screenshot of ours). A circular
corner cannot follow both; 20 sits between them and is the one that reads as parallel. */
.mac-os {
--ad-window-radius: 20px;
/* What stands on a surface, a table or a card: as round as a message in Mail. */
--ad-card-radius: 12px;
}

/* macOS renders system-ui (San Francisco) best antialiased; Windows and Linux stay on
the OS default, where forcing this only thins the text. .mac-os comes from bodyClass. */
.mac-os body {
-webkit-font-smoothing: antialiased;
}

/* Scrollbars that show only while the pointer is over what they scroll, as macOS draws them in
* its own apps. WebKit here keeps them on screen for good — Safari too, on a bare page, with the
* system set to hide them — so the panels wore two grey bars at all times. Transparent until
* hovered (`:hover` holds for every element under the pointer, so the scroller it is in), and
* thin, as an overlay scrollbar is, and while it scrolls (.ad-scrolling, scrollers.js), as an
* overlay one appears then. Windows has the same in windows-os.css; Linux keeps its own on screen.
*
* Only while the system draws overlay scrollbars. With a mouse plugged in, or System Settings set
* to Always, macOS draws legacy ones that stay on screen, and so does every native app — Mail
* included; the launcher says which (ad-legacy-scrollers on <html>, installScrollerStyle), and then
* WebKit's own are left as they are, except the panels' (below). A plain browser is never told,
* and keeps the overlay behaviour. */
:root:not(.ad-legacy-scrollers) .mac-os * {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}

:root:not(.ad-legacy-scrollers) .mac-os :is(:hover, .ad-scrolling) {
scrollbar-color: color-mix(in srgb, var(--fg) 35%, transparent) transparent;
}

/* Even transparent, that bar keeps its width, so every rule in the two panels stopped one
scrollbar short of the right edge. A legacy one takes the same column, and the toolbar's
hairline and search stopped short of it. So the panels' bars are scrollers.js's in both modes;
the scrollers inside them keep theirs. */
.mac-os #menu,
.mac-os #content {
scrollbar-width: none;
}

/* Rounded corners on the grid, macOS only.
*
* It costs nothing structurally: adminer already draws its tables with border-spacing: 0 and
* split borders — `table { border-width: 1px 0 0 1px }`, `td, th { border-width: 0 1px 1px 0 }`
* — which is the separated-border model a radius needs. No overflow: hidden, which would make
* the table its own scroll container and quietly kill the sticky <thead>.
*
* The corner cells are rounded as well as the table: the cell backgrounds paint over the
* table's own rounded corner otherwise, and square off exactly the corner being rounded.
* `> :first-child` is the section (thead/tbody — the browser inserts one either way), so this
* finds the true first and last row whether or not the table has a header.
*
* macOS only, because a rounded grid is a macOS idiom rather than a house style: Windows and
* Linux keep the square corners their own tables have, which is what mac-os not matching
* leaves them with. */
.mac-os table {
border-radius: var(--ad-radius);
}

.mac-os table > :first-child > tr:first-child > :first-child {
border-top-left-radius: var(--ad-radius);
}

.mac-os table > :first-child > tr:first-child > :last-child {
border-top-right-radius: var(--ad-radius);
}

.mac-os table > :last-child > tr:last-child > :first-child {
border-bottom-left-radius: var(--ad-radius);
}

.mac-os table > :last-child > tr:last-child > :last-child {
border-bottom-right-radius: var(--ad-radius);
}

/* A settings table's corners, which everything drawing them reads from --ad-radius. */
.mac-os #desktop-panels table {
--ad-radius: var(--ad-card-radius);
}

/* A notice inside the settings dialog, rounded as its tables are. */
.mac-os dialog#desktop-settings :is(.message, .error) {
border-radius: var(--ad-card-radius);
padding: 1.2em 1.5em;
}

/* A select in a dialog's body only: the sidebar's and the pages' keep the UI's tighter corners. */
.mac-os .ad-modal-body select {
border-radius: 10px;
}

/* The saved connections on the index. */
.mac-os :is(.ad-card, #ad-new-connection) {
border-radius: var(--ad-card-radius);
}
56 changes: 0 additions & 56 deletions app/src/Settings/Theme/designs/adminer-desktop/tables.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,6 @@ th,
border-color: var(--ad-border);
}

/* Rounded corners on the grid, macOS only.
*
* It costs nothing structurally: adminer already draws its tables with border-spacing: 0 and
* split borders — `table { border-width: 1px 0 0 1px }`, `td, th { border-width: 0 1px 1px 0 }`
* — which is the separated-border model a radius needs. No overflow: hidden, which would make
* the table its own scroll container and quietly kill the sticky <thead>.
*
* The corner cells are rounded as well as the table: the cell backgrounds paint over the
* table's own rounded corner otherwise, and square off exactly the corner being rounded.
* `> :first-child` is the section (thead/tbody — the browser inserts one either way), so this
* finds the true first and last row whether or not the table has a header.
*
* macOS only, because a rounded grid is a macOS idiom rather than a house style: Windows and
* Linux keep the square corners their own tables have, which is what mac-os not matching
* leaves them with. */
.mac-os table {
border-radius: var(--ad-radius);
}

.mac-os table > :first-child > tr:first-child > :first-child {
border-top-left-radius: var(--ad-radius);
}

.mac-os table > :first-child > tr:first-child > :last-child {
border-top-right-radius: var(--ad-radius);
}

.mac-os table > :last-child > tr:last-child > :first-child {
border-bottom-left-radius: var(--ad-radius);
}

.mac-os table > :last-child > tr:last-child > :last-child {
border-bottom-right-radius: var(--ad-radius);
}

/* A settings table's corners, which everything drawing them reads from --ad-radius. */
.mac-os #desktop-panels table {
--ad-radius: var(--ad-card-radius);
}

/* Stuck to the top of the dialog's body, where rows would show in round corners
(settings-table-stuck.js). */
#desktop-panels table.ad-stuck th {
Expand Down Expand Up @@ -369,19 +329,3 @@ span.ad-page-step {
-webkit-mask: url("/icons/edit.svg") center / 0.85rem no-repeat;
mask: url("/icons/edit.svg") center / 0.85rem no-repeat;
}

/* A notice inside the settings dialog, rounded as its tables are. */
.mac-os dialog#desktop-settings :is(.message, .error) {
border-radius: var(--ad-card-radius);
padding: 1.2em 1.5em;
}

/* A select in a dialog's body only: the sidebar's and the pages' keep the UI's tighter corners. */
.mac-os .ad-modal-body select {
border-radius: 10px;
}

/* The saved connections on the index. */
.mac-os :is(.ad-card, #ad-new-connection) {
border-radius: var(--ad-card-radius);
}
Loading
Loading