diff --git a/CLAUDE.md b/CLAUDE.md
index 8f1fbd1..37c3bd2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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`/
diff --git a/app/src/Settings/Theme/Theme.php b/app/src/Settings/Theme/Theme.php
index d7cfba4..e31d7de 100644
--- a/app/src/Settings/Theme/Theme.php
+++ b/app/src/Settings/Theme/Theme.php
@@ -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;
@@ -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
@@ -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;
diff --git a/app/src/Settings/Theme/designs/adminer-desktop/README.md b/app/src/Settings/Theme/designs/adminer-desktop/README.md
index 6de23cf..591eeec 100644
--- a/app/src/Settings/Theme/designs/adminer-desktop/README.md
+++ b/app/src/Settings/Theme/designs/adminer-desktop/README.md
@@ -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
-``, 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 ``, 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.
diff --git a/app/src/Settings/Theme/designs/adminer-desktop/base.css b/app/src/Settings/Theme/designs/adminer-desktop/base.css
index 8028167..07980d9 100644
--- a/app/src/Settings/Theme/designs/adminer-desktop/base.css
+++ b/app/src/Settings/Theme/designs/adminer-desktop/base.css
@@ -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 {
@@ -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 , 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. */
diff --git a/app/src/Settings/Theme/designs/adminer-desktop/mac-os.css b/app/src/Settings/Theme/designs/adminer-desktop/mac-os.css
new file mode 100644
index 0000000..d20cd3a
--- /dev/null
+++ b/app/src/Settings/Theme/designs/adminer-desktop/mac-os.css
@@ -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 , 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 .
+ *
+ * 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);
+}
diff --git a/app/src/Settings/Theme/designs/adminer-desktop/tables.css b/app/src/Settings/Theme/designs/adminer-desktop/tables.css
index b35a451..2c8e617 100644
--- a/app/src/Settings/Theme/designs/adminer-desktop/tables.css
+++ b/app/src/Settings/Theme/designs/adminer-desktop/tables.css
@@ -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 .
- *
- * 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 {
@@ -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);
-}
diff --git a/app/src/Settings/Theme/designs/adminer-desktop/tokens.css b/app/src/Settings/Theme/designs/adminer-desktop/tokens.css
index 52f99a1..1240293 100644
--- a/app/src/Settings/Theme/designs/adminer-desktop/tokens.css
+++ b/app/src/Settings/Theme/designs/adminer-desktop/tokens.css
@@ -12,38 +12,6 @@
* inverting Adminer's sprite icons on the dark surface — is in base.css.
*/
-/* 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)
- );
- }
-}
-
/* Sidebar width. Registered, so it becomes px once, on , and everything inherits that:
unregistered it is text, resolved against each user's own font size, and the gear (font-size
0) sat at 0. On because the text scale is set there, so the 19em default grows with
@@ -77,7 +45,7 @@ html {
oklch(from var(--ad-accent) min(l, 0.52) c h),
oklch(from var(--ad-accent) max(l, 0.72) c h)
);
- /* A form's buttons; the accent unless a platform says otherwise (the macOS block above). */
+ /* A form's buttons; the accent unless a platform says otherwise (mac-os.css). */
--ad-button: var(--ad-accent);
--ad-button-fg: var(--ad-accent-fg);
--ad-accent-quiet: light-dark(
@@ -162,15 +130,6 @@ html {
accent-color: var(--ad-accent);
}
-/* 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;
-}
-
/* Row density. These set variables only; padding and line-height read them from
tables.css and base.css, so the whole grid retightens from one class on . */
.density-compact {