17 Commits
Author SHA1 Message Date
Gabriel Brown d61792433c Merge the Panama roadmap completion work
Six commits, rebased onto main and integrated with where main had moved in the
sixty-two commits since the branch was cut.

What lands: a theme-profile system, so a curated accent and a saved custom theme
become the same kind of record; a dock context menu; contextual Settings routes
from the dock, notifications and the OSD; overview-thumbnail capture reliability;
and wallpaper and lock-screen hardening -- which needed the shipped wallpaper
that the fresh-install work made exist, and was the dependency that kept this
branch parked.

Where main had moved further along the same path, main won: the focused border
driven by the chosen accent, the gradient built through the shared serializer,
the multi-edge dock. The branch's work sits on top of those. The one thing that
had to be carried across by hand was the `gnome` member on each accent, which is
what recolors libadwaita applications; it moved into the curated table rather
than being lost with the table it replaced.

One behaviour change worth knowing: the OSD now accepts pointer input, where it
previously accepted none. A secondary click on a visible OSD opens its settings,
and a Wayland region cannot admit one button and refuse another. The region stays
bounded to the OSD's own card.

124 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 22:36:41 -04:00
Gabriel Brown 6016efa436 Meet main where it has moved since this branch was written
The rebase itself is the previous five commits replayed onto main; this is what
they needed once they landed there, kept separate so the replay stays readable.

The shell did not start. A Column in this branch's notification settings menu
assigned its own implicitWidth and implicitHeight, which a Qt 6 positioner
computes and does not let you set. That took out every contract that launches a
shell -- six of the seven failures were this one line, and none of them said so
until the error was read to the bottom. A Column already measures itself from its
children, so the bindings are simply gone.

Three assertions pinned an implementation main has since replaced, and each is
updated to pin the intent rather than the mechanism:

  - The display picker now reads primaryFirstMonitors, which is monitors sorted
    with the primary first. Still populated from what is connected, which is what
    the contract is for; the sorted list is the point, so the picker opens on the
    display somebody is most likely to mean. This branch made that change and
    broke its own contract without noticing.
  - The accent swatches come from the accentName schema rather than
    Object.keys(Theme.accents). Same swatches, same order, one source shared with
    every other enum row.
  - The OSD used to take no pointer input at all. It takes some now, because this
    branch's own design calls for a secondary click on a visible OSD to open its
    settings, and a Wayland input region cannot admit one button and refuse
    another. The rule that survives is that the region stays bounded to the OSD's
    own card: it floats over other windows for a couple of seconds, and a region
    bigger than the card would swallow clicks meant for something underneath.

Theme's accent table moved to ThemeProfiles, which is this branch's point -- a
curated accent and a custom profile become the same kind of record. main had
meanwhile given each accent a `gnome` member, the nearest name in GNOME's fixed
accent-color enum, which is what makes libadwaita applications recolor instead of
staying in GNOME blue. That member moved into the curated table rather than being
dropped, and adwaita-accent-contract now reads it where it lives.

Where main had simply moved further along the same path, main won: the focused
border driven by the chosen accent rather than a hardcoded pair, the gradient
built through the shared serializer rather than a hand-rolled string, the
multi-edge dock geometry. This branch's context menu, keyboard focus and
accessibility work sit on top of those rather than beside them.

Three new contracts arrived carrying .sh and lost it, along with the references
in this branch's own plan.

124 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 22:23:27 -04:00
Gabriel Brown ed428e87c4 Complete the Panama theme system 2026-08-20 22:03:08 -04:00
Gabriel Brown 69ecec7ecf Complete contextual desktop controls 2026-08-20 22:00:53 -04:00
Gabriel Brown a90f6eb357 Fix wallpaper scan cap exit status 2026-08-20 22:00:26 -04:00
Gabriel Brown c4642919f7 Close desktop safety gaps 2026-08-20 22:00:26 -04:00
Gabriel Brown c4733e0624 Plan Panama roadmap completion 2026-08-20 21:58:25 -04:00
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec.

159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor
contracts. A shebang and the executable bit already select the interpreter. The
extension only ever added something that had to stay in sync, and the rename
proved the point twice over in the space of an hour.

The spec's stated risk was Vicinae's script discovery. One script was renamed and
reloaded on its own before the other 46 followed; it came back as
scripts:panama.capture and all 47 resolve. What the probe turned up instead is
that the extension was never only a filename: Vicinae's command IDs embed it, so
every ID changed. Nothing in this repository refers to them, so nothing breaks.
The only trace is Vicinae's metadata.json, whose visited map had two Panama
entries that are now orphaned -- two commands lost their usage ranking and will
earn it back. Worth knowing before anyone renames these again on a machine that
has a keybind pointing at one.

Rewriting the references by exact filename missed two things it structurally
could not see: a name built from a variable, settings-$page.sh, and a glob,
-name '*.sh'. Both were in the contract that counts the generated commands, which
promptly reported 47 expected and 0 found. The mechanical part of a rename is the
part that looks finished.

The three subcommands. panama doctor fronts a health check that already existed
and already ran at the end of every install but could not be reached from a
terminal. panama upgrade re-runs the installer from anywhere. panama test runs
the suite, which had no entry point at all -- 121 files that were the main safety
net in this repository and were invisible in it.

Writing that runner found three tests nothing was running.
calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test
are unittest suites without the executable bit, so no contract invoked them and
the first draft of the runner skipped them silently. All three pass, and have
passed unobserved for weeks. The runner collects *_test.py as well now, because a
runner with a blind spot is worse than no runner for the same reason a dependency
checker with one is: it reports PASS.

Six worktrees pruned. Each was re-checked rather than trusted to the spec's list,
and two needed it: panama-commands is not on feat/panama-commands but on
feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead
of its remote, not of main, with every commit patch-equivalent to landed work.
roadmap-completion stays; it has five commits that are genuinely unlanded. The
branches are left alone: pruning a worktree costs nothing, deleting a branch is a
decision.

121 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:55:55 -04:00
Gabriel Brown 47f29f9fa9 Stop describing a desktop this repository does not install
Phase 5. The README advertised two desktops that coexist -- GNOME with Forge,
Dash-to-Dock, Openbar and Vitals, alongside Hyprland -- and nothing in setup/
installed or configured any of the first one. Panama configures one desktop, and
now says so.

config/dot/forge is deleted along with its entry in link-dotfiles. It was the
hedge from when the GNOME session was still the fallback and Hyprland was being
built beside it; the hedge has been paid off. Nine files, six of which were
Forge's own editor backups that should never have been committed.

Searching for the rest of GNOME found nothing else to cut, which is recorded in
the spec so nobody goes looking again. change-settings never enabled an
extension. The mentions of Dash-to-Dock, Openbar and Vitals through the shell are
comments saying what a component was modelled on -- which intellihide behaviour
the dock reproduces, where a colour came from -- and DESKTOP-PARITY.md is the
table of what replaced what. That is provenance, and it is the reason those
components behave the way they do. Vitals in services/ is Panama's own bar
service and merely shares a name with the extension it replaced.

The handoff panel list was wrong in two places. It named Wacom, which nothing
hands off to, and Region, which is a subpage of System rather than a panel. The
nine real ones are read off the call sites and the allow-list that gates them.
The spec said it, the comment on gnome-control-center repeated it, and the README
would have made it three.

One test gap turned up and is closed. The assets contract caught a directory
under config/dot/ that nothing links, but not the inverse: a name left in the
dirs array with nothing behind it, which makes link-dotfiles point ~/.config/<name>
at a path that does not exist. Deleting Forge is the exact move that introduces
that, and nothing would have failed if the array entry had been left. A dangling
symlink is worse than a missing one, because everything that looks there finds
something. Verified by putting the entry back and watching it fail.

The audit of docs/settings.md this phase asked for needed nothing: it is generated
from PreferenceSchema.qml, a contract already fails when it goes stale, and it
carries no claim about GNOME or Forge. The README gained the section it was
missing instead -- the 121 contracts under tests/ were the main safety net in this
repository and went entirely unmentioned in it.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:33:57 -04:00
Gabriel Brown 88497826ec Let a machine say what it is for, and give Firefox its face back
Phase 4: the optional application categories, and the Firefox chrome.

Everything Panama installed until now was what every machine gets, which meant a
work laptop acquired emulators and a desktop that wanted Steam had to be told
about it by hand. The interview now offers the categories in
setup/packages/extras/ as a checklist -- gaming, creative, communication,
virtualization -- and nothing is preselected, because a default here installs
applications nobody chose on a machine whose owner answered a question they
thought was about something else.

A category is one file, and a category mixes both package managers because the
applications do: Steam is in RPM Fusion, Slack publishes only a flatpak. So a
bare line is a dnf package and a flatpak: line is a Flathub ID, and one file
holds the whole answer rather than splitting each category across two. The menu
is read from the directory rather than written down, so adding a category is
adding a file. Every name in all four was resolved against the actual
repositories before being written down, and the contract re-resolves them --
the point of admitting applications one at a time is that they stay installable,
and a typo here fails on somebody else's machine, not this one.

Firefox is declared, and its chrome is Edge-Frfox, vendored into config/firefox.
sunhat carried that theme with no license and no attribution; it is MIT, and now
it says so and says whose it is.

It is the only piece of Panama's configuration that does not go to a path this
repository chooses. Firefox owns the profile directory, names it with a random
salt, and does not create one until the browser has been run -- so link-dotfiles
finds or creates a profile and links both halves into it. Both, or neither works:
chrome/ is the CSS and user.js sets the preference that makes Firefox read chrome/
at all, without which the theme is a directory of dead files.

Two assumptions there were wrong, and the contract exists for both. Firefox has
moved to the XDG directories -- the profile root is ~/.config/mozilla/firefox on
this build, not ~/.mozilla/firefox, and writing to the wrong one themes nothing
and says nothing about it. And -CreateProfile turns out to be non-interactive, so
a fresh machine gets the theme on the first install rather than the second. The
contract runs link-dotfiles for real against a throwaway home with no profile in
it and looks at what came out; it was checked by pointing the search at the
legacy path only and watching it fail.

Also: the enrolment/enrollment spellings from the last commit are corrected. This
repository is US-spelled everywhere else -- color 1131 times against colour never
-- and consistency in prose is worth as much as it is in code.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:24:15 -04:00
Gabriel Brown b319d1a5e1 Stop handing dnf the comments that explain the package lists
Every list in setup/packages/ is annotated -- which package exists for which
settings page, why an exception was made -- and install-packages passed the whole
file to dnf, comment lines included.

dnf does not ignore an argument it cannot match. It reports "No match for
argument: #" and exits 1, and with set -euo pipefail at the top of that script
the first annotated list ends the stage. initial-packages carries four comments
and is the first list read, so a fresh machine got the repositories, the group
updates, and then nothing.

Two things hid it. On a machine that already has everything, a re-run matches
every real name and fails only on the comments, so the failure looks like noise
rather than the stage dying. And every contract that reads these lists strips
comments with sed before comparing -- the tests were reading a file the installer
was not, which is why a repository with a dependency contract, an assets contract
and a doctor still reported PASS across the board.

The fix is one filter used at all five call sites. The contract lifts that filter
out of the script and runs it, rather than describing what it should do, so
deleting or renaming it fails here instead of passing quietly. It also checks the
inverse -- that stripping comments does not strip packages -- because trading a
loud failure for a silent one would be worse than the bug.

Found while adding the extras lists for phase 4, which are annotated the same way
and would have hit the same wall.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:11:44 -04:00
Gabriel Brown 13f3648e4d Install the driver, enrol the key, and still never ask twice
Phase 3 of the fresh-install spec: the parts of a run that depend on what the
machine actually is. NVIDIA, Secure Boot, Fedora's preinstalled extras, firmware.

Two of these looked like they would force a compromise, and neither did.

sunhat opened an editor in the middle of its run so grub could be hand-corrected,
and that single step is why walking away from an install did not work. The step
existed to delete duplicated kernel arguments -- and grubby replaces an argument
that already exists rather than appending a second copy, so the duplicates cannot
accumulate and there is nothing to correct. The editor was load-bearing for a
problem that a different tool does not have.

MOK enrolment needs a password now and the same password at the next boot's blue
screen, which reads like a prompt that has to happen mid-run. mokutil has
--generate-hash and --hash-file for exactly this: the interview asks, hashes it
on the spot, and records only the hash. The plaintext never reaches the answers
file, the environment, or a command line, and the stage runs without asking.

The stage runs last rather than fourth as the spec's table had it. The constraint
was always "late" and fourth of eight is not late: enrolment arms a prompt for the
next boot and firmware may want a reboot, so a machine that reboots out of this
stage should already be completely configured.

Every question names what was found -- the card, the packages actually installed
-- and is not asked at all on a machine it would do nothing to. sunhat's debloat
list no longer describes Fedora 44: totem became showtime and LibreOffice is not
preinstalled, so the list is curated and a package that is not installed is never
passed to dnf, which is what lets it outlive a release.

This stage cannot be verified by running it. It installs a proprietary driver and
queues a Secure Boot enrolment, and this machine is an AMD desktop. So every
privileged command is stood in on PATH and the contract asserts which answer led
to which call: that no answers means no commands, that a failed driver install is
not followed by arguments and services for a driver that is not there, that the
hash reaches mokutil through a file and never a command line, and that removal is
offered only for packages that are installed. The contract was checked by breaking
the stage three ways and confirming it caught each. It does not verify that
akmod-nvidia builds, and says so where a reader would otherwise assume it did.

The README's stage table listed three of seven stages; the interview and identity
work never reached it. Corrected rather than extended, since a table that lists
three of seven is worse than one that lists none. The Desktops section still
describes a GNOME session nothing installs -- that is phase 5.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 19:36:53 -04:00
Gabriel Brown 359fb922aa Install the four applications this desktop assumed you had
Helium was already declared. Podman Desktop is on Flathub, so it joins the
flatpak list beside the podman it fronts.

RustDesk was the interesting one. panama-doctor has checked `rustdesk.service`
for as long as it has existed, and autostart.lua works around the tray that
service spawns -- so RustDesk was already part of this desktop, installed by
nothing. The flatpak cannot register a root-owned system service, so unattended
access needs the RPM.

Claude Code has no RPM and no flatpak either, so it takes the official installer
and keeps itself updated afterwards.

Neither pins a version. sunhat pinned upscayl 2.11.5, LACT 0.5.4 and a fedora-40
RPM, and every one of those was a 404 within a release cycle; the RustDesk URL is
resolved from whatever the latest release happens to be. Both are skipped when
already present, and a failure is logged and stepped over rather than aborting a
stage that has already installed the desktop.

That leaves three exceptions to the dnf-or-flatpak rule, all named in one place
with a reason each. The dependency contract now knows they are installed out of
band, so probing for them with `command -v` is not read as an undeclared
dependency -- narrowly, per command, so a genuine omission still fails.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-20 19:25:30 -04:00
Gabriel Brown b6448c9876 Give the revealed Dock back the pointer that revealed it
The bottom dock came up when the cursor reached the edge and hid again a
quarter-second later with the cursor still sitting there.

The input region has two shapes: a three-pixel strip along the whole edge while
hidden, and a region over the body once revealed. Teaching the dock about left
and right rewrote both, and the bottom case was folded into the branch that
serves a left dock -- x 0. That is right for a dock that hugs the left edge and
wrong for one that is centred on the bottom: the region landed on the left third
of the screen while the pointer that summoned the dock was in the middle. Hover
dropped on the very frame the dock arrived, and the hide timer did the rest.
Approaching from the far left worked, which is the only reason it looked
intermittent rather than broken.

Bottom is centred, so the region starts where the body starts.

Nothing measured the input region, which is why "bottom is unchanged" passed
while bottom was broken -- the contract read the window and the window was
fine. It now probes the mask in both states on all three edges and asserts the
point a hand actually aims at, the middle of the edge the dock lives on, is
still inside the region after the dock arrives. It fails on the old binding
with the coordinates that were wrong.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 19:23:02 -04:00
Gabriel Brown 15d54b16f6 Ask everything first, then run without needing anybody
sunhat's failure mode was a question twenty minutes into a run. Walking away
from an install meant coming back to a prompt that had been waiting an hour.

So the questions move to the front. A new interview stage asks what Panama needs
to be told -- hostname, git identity, whether to sign in to GitHub, whether to
make an SSH key -- shows the answers back, and asks once to proceed. After that
nothing asks again. gum is bootstrapped before it runs, because the interview is
built on gum and gum arrives with a stage that has not run yet.

Answers reach the stages through a mktemp file that install sources and the
existing trap deletes, since a child process cannot export into its siblings.
They are not remembered between runs: there is no state file to go stale, and
one of the answers is an email address.

The interview asks only what a stage in this repository actually consumes.
Extras, hardware and debloat questions arrive with the stages that act on them
-- a prompt whose answer nothing reads is a control that lies. The new contract
pins that in both directions, and four deliberate mutations confirmed it catches
a question nobody reads, a stage reading something nobody asks, an answers file
left on disk, and a declined interview that fails to stop the run.

The run now ends with panama-doctor, because a failed-stage count says nothing
about a service that did not start. It never changes the exit code: on a fresh
machine, unconfigured is the honest answer, not a failure.

espanso and oh-my-posh stop being exceptions -- Terra packages espanso-wayland
and Fedora packages oh-my-posh, so the curl installer is gone. bun is now the
only remaining one.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-20 19:22:36 -04:00
Gabriel Brown 96e4085919 Install the desktop this repository already describes
The shell named a font, a pointer theme and a wallpaper that no package list
installed and no stage placed. It went unnoticed because this machine collected
all three under sunhat and never lost them; a fresh Fedora box would have come
up with tofu for every shell glyph, the default pointer, and no wallpaper --
while Wallpaper.qml called that missing file `shippedPath`.

The dependency contract reported PASS throughout, because it reads commands that
scripts invoke and none of these are one. The new assets contract covers what it
structurally cannot: fonts and pointer themes named in configs and gsettings, a
shipped wallpaper that must exist, commands launched from QML, and directories
nothing puts into service. Written against the broken tree first, where it found
ten faults.

Four of those were packages nobody had noticed were missing -- gnome-calendar,
podman, pipewire-utils and flatpak -- alongside gnome-control-center, which backs
fifteen rows of Panama's own Settings and is commented so it is not mistaken for
GNOME-session residue later.

Fonts turned out to need no install stage at all. Terra, which install-packages
already enables, packages every Nerd Font, so sunhat's wget-and-unzip is replaced
by five lines in a package list. The pointer theme had no such luck: it is
packaged nowhere, so it is vendored rather than downloaded from a URL that can
rot.

espanso stays undeclared. It is in no enabled repository, and building it from
source is the install method this repository is trying to stop using.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-20 18:57:47 -04:00
Gabriel Brown 6bcc351e07 Write down what a fresh install actually lacks
Panama installs a desktop it does not fully provision. The shell names fonts
nothing installs, a cursor theme nothing installs, and a wallpaper that does not
exist; five packages that settings pages depend on are declared nowhere. None of
it shows here, because this machine accumulated those pieces under sunhat and
never lost them.

The spec covers closing that gap, moving every question the installer needs to
the front of the run, and porting the parts of sunhat that still matter.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-20 18:38:50 -04:00
466 changed files with 10849 additions and 1422 deletions
+86 -15
View File
@@ -8,29 +8,57 @@ git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
~/.local/share/Panama/install ~/.local/share/Panama/install
``` ```
`install` runs everything in `setup/scripts/` in order: `install` asks its questions first and then runs the stages in `setup/scripts/`
in order, without stopping again:
| Script | Does | | Script | Does |
|---|---| |---|---|
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), then the four package lists in `setup/packages/` | | `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>` | | `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services | | `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
The run ends with a health summary from `panama-doctor`, which reports what is
actually running rather than what was attempted. It never fails the install: on a
fresh machine it legitimately reports things as not yet configured.
### Optional applications
Every machine gets the lists in `setup/packages/`. The interview also offers the
categories in `setup/packages/extras/` as a checklist, so a work laptop need not
acquire emulators and a desktop need not skip Steam. Nothing is preselected.
A category is one file. A bare line is a dnf package and a `flatpak:` line is a
Flathub ID, because the applications in a category do not all come from one
place. Adding a category is adding a file — the menu is read from the directory,
not written down anywhere.
Existing configs are moved to `config/old/` rather than overwritten. Existing configs are moved to `config/old/` rather than overwritten.
## Desktops ## The desktop
Panama configures **two** desktops that coexist. Both sessions stay available in Hyprland, with a shell written from scratch. It began as a replacement for a
GDM, so you can switch back and forth while you settle in. GNOME session — Forge for tiling, Dash-to-Dock, Openbar, Vitals — and was built
to reproduce it closely enough that muscle memory transferred: same keybinds,
same panel contents, same dock, same Tokyo Night Moon palette.
### GNOME That is history now rather than a second option. Panama installs and configures
The original setup: Forge for tiling, Dash-to-Dock, Openbar, Vitals, one desktop, and the GNOME session it grew out of is neither installed nor
AppIndicator support. Configured through `config/dot/forge/` and gsettings. configured here. What each piece replaced is recorded in
[`config/dot/hypr/DESKTOP-PARITY.md`](config/dot/hypr/DESKTOP-PARITY.md) and in
the comments of the components themselves, because knowing what a thing was
modelled on explains why it behaves the way it does.
### Hyprland GNOME is not gone from the machine: `gnome-control-center` is a declared
A from-scratch replacement built to reproduce the GNOME setup closely enough dependency, and Panama's own Settings hands off to it for the panels it
that muscle memory transfers — same keybinds, same panel contents, same dock, deliberately does not own — Online Accounts, Color, Sound, Network, Keyboard,
same Tokyo Night Moon palette. Privacy, Wellbeing, Accessibility, and System for users, date and time, region
and remote desktop. The allow-list in
[`services/SystemSettings.qml`](config/dot/quickshell/services/SystemSettings.qml)
is what decides; anything not on it is a panel Panama owns itself.
| Piece | What it is | | Piece | What it is |
|---|---| |---|---|
@@ -50,13 +78,56 @@ Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
## Layout ## Layout
``` ```
bin/ Small user-facing commands on PATH bin/ Small user-facing commands on PATH; `panama` is the entry point
config/ config/
bash/ .bashrc, aliases, env (env is gitignored) bash/ .bashrc, aliases, env (env is gitignored)
copy/ Files copied verbatim over / (needs sudo) copy/ Files copied verbatim over / (needs sudo)
dot/ Symlinked into ~/.config dot/ Symlinked into ~/.config
firefox/ Vendored Firefox chrome, linked into the browser profile
local/ Icons and the cursor theme, linked into ~/.local/share
old/ Backups of whatever was replaced (gitignored) old/ Backups of whatever was replaced (gitignored)
wallpapers/ Copied into ~/Pictures/Wallpapers when absent
setup/ setup/
packages/ One package per line packages/ One package per line; extras/ holds the optional categories
scripts/ Run in order by ./install scripts/ Run in order by ./install
tests/ Contracts. See below
docs/ Settings reference, and the design specs behind the work
``` ```
## Tests
121 of them, under `tests/`. Run the lot, or a subset by pattern:
```sh
panama test # everything
panama test dock # just the ones matching "dock"
tests/setup/interview-contract # or one directly; they are plain executables
```
They are called contracts rather than unit tests because that is what they are:
each one pins a decision that was expensive to get right and is cheap to undo by
accident. Most read or measure the real thing — launching a shell to measure a
surface's geometry, standing stub commands on `PATH` to see what a stage would
have installed, running a script against a throwaway `HOME` — rather than
asserting things about source text, because the bugs worth catching here have all
been ones that source text looked fine for.
```
tests/setup/ The installer: the interview, package lists, hardware, extras
tests/quickshell/ The shell and its settings pages
tests/hypr/ The compositor config
```
## The `panama` command
```sh
panama update # review, commit and sync this repo
panama edit # open it in Neovim
panama doctor # what is actually running, not what was installed
panama test # every contract, or a subset by pattern
panama upgrade # re-run ./install from anywhere
```
None of the scripts in this repository carry a `.sh` extension. A shebang and
the executable bit already select the interpreter, and the extension only
becomes something to keep in sync — which it did not stay.
+109
View File
@@ -7,6 +7,9 @@
# Commands: # Commands:
# update Commit & sync local changes (or just pull if clean) # update Commit & sync local changes (or just pull if clean)
# edit Open the Panama repo in Neovim # edit Open the Panama repo in Neovim
# doctor Report what is actually running on this machine
# test Run every contract under tests/
# upgrade Re-run the installer from anywhere
# help Show this help # help Show this help
# #
# Designed to grow: add new subcommands as cmd_<name> functions and # Designed to grow: add new subcommands as cmd_<name> functions and
@@ -62,6 +65,12 @@ ${BOLD}Commands:${RESET}
${GREEN}update${RESET} Review, commit & sync local changes. If the working tree is ${GREEN}update${RESET} Review, commit & sync local changes. If the working tree is
clean it simply runs 'git pull'. clean it simply runs 'git pull'.
${GREEN}edit${RESET} Open the Panama repo in Neovim. ${GREEN}edit${RESET} Open the Panama repo in Neovim.
${GREEN}doctor${RESET} Report what is actually running on this machine, rather
than what was installed. Takes --summary for one line per check.
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
a subset: 'panama test dock' runs the ones matching 'dock'.
${GREEN}upgrade${RESET} Re-run ./install from anywhere. Safe: every stage is
idempotent and this is the documented upgrade path.
${GREEN}help${RESET} Show this help (also -h, --help). ${GREEN}help${RESET} Show this help (also -h, --help).
${BOLD}Options:${RESET} ${BOLD}Options:${RESET}
@@ -71,6 +80,9 @@ ${BOLD}Options:${RESET}
${BOLD}Examples:${RESET} ${BOLD}Examples:${RESET}
$PROGRAM update $PROGRAM update
$PROGRAM edit $PROGRAM edit
$PROGRAM doctor --summary
$PROGRAM test dock
$PROGRAM upgrade
EOF EOF
} }
@@ -205,6 +217,100 @@ cmd_edit() {
exec nvim . exec nvim .
} }
# ----------------------------------------------------------------------------
# Command: doctor
# ----------------------------------------------------------------------------
#
# The health check already exists and the installer already runs it; what it did
# not have was a way to reach it from a terminal. Everything is passed straight
# through, so --summary and anything added later work without this knowing about
# them.
cmd_doctor() {
local doctor="$PANAMA_DIR/config/dot/quickshell/scripts/panama-doctor"
if [[ ! -x "$doctor" ]]; then
err "panama-doctor is missing from $doctor"
exit 1
fi
exec "$doctor" "$@"
}
# ----------------------------------------------------------------------------
# Command: test
# ----------------------------------------------------------------------------
#
# The contracts are the main safety net in this repository and had no entry
# point: 121 executables with no runner and no mention in the README, which is
# most of the way to not having them.
#
# Each runs in its own process and a failure does not stop the rest, because the
# useful output is the whole list of what is broken rather than the first thing
# that broke. The exit code is what a caller can act on.
cmd_test() {
local pattern="${1:-}"
local -a suite=()
# Executables, plus the Python suites. Those are unittest files rather than
# executables, and collecting only what has the executable bit would skip them
# without saying so -- which is how all three came to be run by nothing at all.
# A runner with a blind spot is worse than no runner, because it reports PASS.
while IFS= read -r path; do
[[ -x "$path" || "$path" == *_test.py ]] || continue
[[ -z "$pattern" || "$path" == *"$pattern"* ]] && suite+=("$path")
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
if (( ${#suite[@]} == 0 )); then
err "No contracts match '${pattern}'"
exit 1
fi
info "Running ${#suite[@]} contract(s)"
local -a failed=()
local path name
local -a runner
for path in "${suite[@]}"; do
name="${path#"$PANAMA_DIR"/tests/}"
if [[ "$path" == *_test.py ]]; then
runner=(python3 "$path")
else
runner=("$path")
fi
if "${runner[@]}" >/dev/null 2>&1; then
ok "$name"
else
err "$name"
failed+=("$name")
fi
done
header "Result"
if (( ${#failed[@]} == 0 )); then
ok "${#suite[@]} contract(s) passed"
return 0
fi
err "${#failed[@]} of ${#suite[@]} failed:"
printf ' %s\n' "${failed[@]}" >&2
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
return 1
}
# ----------------------------------------------------------------------------
# Command: upgrade
# ----------------------------------------------------------------------------
#
# ./install is the upgrade path -- every stage is idempotent and re-running is
# the documented way to repair a machine. This only saves remembering where the
# repository lives.
cmd_upgrade() {
local installer="$PANAMA_DIR/install"
if [[ ! -x "$installer" ]]; then
err "The installer is missing from $installer"
exit 1
fi
info "Re-running ${BOLD}${installer}${RESET}"
cd "$PANAMA_DIR"
exec "$installer" "$@"
}
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Dispatcher # Dispatcher
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@@ -213,6 +319,9 @@ main() {
case "$cmd" in case "$cmd" in
update) shift; cmd_update "$@" ;; update) shift; cmd_update "$@" ;;
edit) shift; cmd_edit "$@" ;; edit) shift; cmd_edit "$@" ;;
doctor) shift; cmd_doctor "$@" ;;
test) shift; cmd_test "$@" ;;
upgrade) shift; cmd_upgrade "$@" ;;
help|-h|--help|"") usage ;; help|-h|--help|"") usage ;;
--version) printf '%s %s\n' "$PROGRAM" "$VERSION" ;; --version) printf '%s %s\n' "$PROGRAM" "$VERSION" ;;
*) *)
+4 -5
View File
@@ -24,8 +24,7 @@ else
fi fi
export PATH export PATH
fi fi
. "$HOME/.cargo/env" # rustup writes this file, and initial-packages installs rustup rather than
# running rustup-init -- so on a fresh machine it does not exist yet and an
# >>> Codex installer >>> # unguarded source made every single shell start with an error.
export PATH="/home/gib/.local/bin:$PATH" [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
# <<< Codex installer <<<
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
@@ -1,72 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "Emulator",
"mode": "float"
}
]
}
-76
View File
@@ -1,76 +0,0 @@
{
"overrides": [
{
"wmClass": "jetbrains-toolbox",
"mode": "float"
},
{
"wmClass": "Com.github.amezin.ddterm",
"mode": "float"
},
{
"wmClass": "Com.github.donadigo.eddy",
"mode": "float"
},
{
"wmClass": "Conky",
"mode": "float"
},
{
"wmClass": "Gnome-initial-setup",
"mode": "float"
},
{
"wmClass": "org.gnome.Calculator",
"mode": "float"
},
{
"wmClass": "gnome-terminal-preferences",
"mode": "float"
},
{
"wmClass": "Guake",
"mode": "float"
},
{
"wmClass": "zoom",
"mode": "float"
},
{
"wmClass": "Bitwarden",
"mode": "float"
},
{
"wmClass": "Hidamari",
"mode": "float"
},
{
"wmClass": "com.mattjakeman.ExtensionManager",
"mode": "float"
},
{
"wmClass": "Cider",
"mode": "float"
},
{
"wmClass": "Ulauncher",
"mode": "float"
},
{
"wmClass": "com.nextcloud.desktopclient.nextcloud",
"mode": "float"
},
{
"wmClass": "mpv",
"mode": "float"
},
{
"wmClass": "Spotify",
"mode": "float"
},
{
"wmClass": "bitwarden",
"mode": "float"
}
]
}
@@ -1,190 +0,0 @@
.tiled {
color: rgba(236, 94, 94, 1);
opacity: 1;
border-width: 3px;
}
.split {
color: rgba(255, 246, 108, 1);
opacity: 1;
border-width: 3px;
}
.stacked {
color: rgba(247, 162, 43, 1);
opacity: 1;
border-width: 3px;
}
.tabbed {
color: rgba(17, 199, 224, 1);
opacity: 1;
border-width: 3px;
}
.floated {
color: rgba(180, 167, 214, 1);
border-width: 3px;
opacity: 1;
}
.window-tiled-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-split-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
/* forge-36le: St does not paint an asymmetric (single-side) border when a
* border-radius is set, so the directional hint never rendered. Drop the radius
* on the one-edge variants; St paints a straight single-side border reliably. */
.window-split-horizontal {
border-left-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
.window-split-vertical {
border-left-width: 0;
border-top-width: 0;
border-right-width: 0;
border-radius: 0;
}
.window-stacked-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tabbed-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tabbed-bg {
border-radius: 8px;
}
.window-tabbed-tab {
background-color: rgba(54, 47, 45, 1);
border-color: rgba(98,160,234,0.6);
border-width: 1px;
border-radius: 8px;
color: white;
margin: 1px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
}
.window-tabbed-tab-active {
background-color: rgb(98,160,234);
color: black;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
}
.window-tabbed-tab-close {
padding: 3px;
margin: 4px;
border-radius: 16px;
width: 16px;
background-color: #e06666;
}
.window-tabbed-tab-icon {
margin: 3px;
}
.window-floated-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tilepreview-tiled {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
.window-tilepreview-stacked {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
.window-tilepreview-swap {
border-width: 1px;
border-color: rgba(162, 247, 43, 0.4);
border-style: solid;
border-radius: 14px;
background-color: rgba(162, 247, 43, 0.4);
}
.window-tilepreview-tabbed {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
/* Keybindings Cheatsheet Overlay */
.forge-cheatsheet {
background-color: #2d2d2d;
border-radius: 16px;
padding: 24px 32px;
border: 1px solid #4d4d4d;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.forge-cheatsheet-title {
font-size: 16px;
font-weight: bold;
color: #ffffff;
margin-bottom: 20px;
text-align: center;
}
.forge-cheatsheet-category {
font-size: 13px;
font-weight: bold;
color: #78aeed;
margin-top: 16px;
margin-bottom: 8px;
}
.forge-cheatsheet-row {
font-size: 12px;
color: #eeeeec;
margin: 4px 0;
padding: 2px 0;
}
.forge-cheatsheet-key {
font-family: monospace;
font-weight: bold;
color: #f5c211;
min-width: 180px;
}
.forge-cheatsheet-desc {
color: #d0cfcc;
}
@@ -1,190 +0,0 @@
.tiled {
color: rgba(236, 94, 94, 1);
opacity: 1;
border-width: 3px;
}
.split {
color: rgba(255, 246, 108, 1);
opacity: 1;
border-width: 3px;
}
.stacked {
color: rgba(247, 162, 43, 1);
opacity: 1;
border-width: 3px;
}
.tabbed {
color: rgba(17, 199, 224, 1);
opacity: 1;
border-width: 3px;
}
.floated {
color: rgba(180, 167, 214, 1);
border-width: 3px;
opacity: 1;
}
.window-tiled-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-split-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
/* forge-36le: St does not paint an asymmetric (single-side) border when a
* border-radius is set, so the directional hint never rendered. Drop the radius
* on the one-edge variants; St paints a straight single-side border reliably. */
.window-split-horizontal {
border-left-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
.window-split-vertical {
border-left-width: 0;
border-top-width: 0;
border-right-width: 0;
border-radius: 0;
}
.window-stacked-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tabbed-border {
border-width: 3px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tabbed-bg {
border-radius: 8px;
}
.window-tabbed-tab {
background-color: rgba(54, 47, 45, 1);
border-color: rgba(98,160,234,0.6);
border-width: 1px;
border-radius: 8px;
color: white;
margin: 1px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
}
.window-tabbed-tab-active {
background-color: rgb(98,160,234);
color: black;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
}
.window-tabbed-tab-close {
padding: 3px;
margin: 4px;
border-radius: 16px;
width: 16px;
background-color: #e06666;
}
.window-tabbed-tab-icon {
margin: 3px;
}
.window-floated-border {
border-width: 1px;
border-color: rgb(98,160,234);
border-style: solid;
border-radius: 14px;
}
.window-tilepreview-tiled {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
.window-tilepreview-stacked {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
.window-tilepreview-swap {
border-width: 1px;
border-color: rgba(162, 247, 43, 0.4);
border-style: solid;
border-radius: 14px;
background-color: rgba(162, 247, 43, 0.4);
}
.window-tilepreview-tabbed {
border-width: 1px;
border-color: rgba(98,160,234,0.3);
border-style: solid;
border-radius: 14px;
background-color: rgba(98,160,234,0.2);
}
/* Keybindings Cheatsheet Overlay */
.forge-cheatsheet {
background-color: #2d2d2d;
border-radius: 16px;
padding: 24px 32px;
border: 1px solid #4d4d4d;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.forge-cheatsheet-title {
font-size: 16px;
font-weight: bold;
color: #ffffff;
margin-bottom: 20px;
text-align: center;
}
.forge-cheatsheet-category {
font-size: 13px;
font-weight: bold;
color: #78aeed;
margin-top: 16px;
margin-bottom: 8px;
}
.forge-cheatsheet-row {
font-size: 12px;
color: #eeeeec;
margin: 4px 0;
padding: 2px 0;
}
.forge-cheatsheet-key {
font-family: monospace;
font-weight: bold;
color: #f5c211;
min-width: 180px;
}
.forge-cheatsheet-desc {
color: #d0cfcc;
}
+2 -2
View File
@@ -88,7 +88,7 @@ entry.
`prefs.lua` never raises. A missing, empty, truncated, malformed, or `prefs.lua` never raises. A missing, empty, truncated, malformed, or
wrong-typed settings file costs you your customizations and nothing else; wrong-typed settings file costs you your customizations and nothing else;
`tests/hypr/prefs-fallback-contract.sh` pins that, including that Hyprland still `tests/hypr/prefs-fallback-contract` pins that, including that Hyprland still
accepts the config in each of those states. accepts the config in each of those states.
### Adding a keybind: use `bind`, not `hl.bind` ### Adding a keybind: use `bind`, not `hl.bind`
@@ -120,7 +120,7 @@ Every `hl.bind` must pass a `description`. Hyprland reports Lua-defined binds
with dispatcher `__lua` and a bytecode offset as the argument, so a bind without with dispatcher `__lua` and a bytecode offset as the argument, so a bind without
one has nothing readable beside its chord, and the Settings app drops it from the one has nothing readable beside its chord, and the Settings app drops it from the
Input & Shortcuts page rather than showing a mystery row. Input & Shortcuts page rather than showing a mystery row.
`tests/quickshell/keybinds-contract.sh` fails if any bind lacks a description, so `tests/quickshell/keybinds-contract` fails if any bind lacks a description, so
this cannot regress silently. this cannot regress silently.
```lua ```lua
@@ -0,0 +1,41 @@
import Quickshell
import Quickshell.Io
import QtQuick
import qs.modules.settings
import qs.services
ShellRoot {
Item {
width: 680
height: editor.implicitHeight
AccentEditor {
id: editor
width: parent.width
}
ThemeProfilePicker {
width: parent.width
visible: false
}
AccentPicker {
width: parent.width
visible: false
}
}
IpcHandler {
target: "accent-controls-test"
function status(): string {
return JSON.stringify(ThemeProfiles.activeProfile);
}
function adjust(target: string, channel: string, ratio: real): string {
editor.changeChannel(target, channel, ratio);
return status();
}
}
}
@@ -975,6 +975,19 @@ Singleton {
{ value: "slate", label: "Slate" } { value: "slate", label: "Slate" }
] ]
}, },
{
key: "themeProfileId", type: "string", def: "moon", group: "appearance",
pattern: "^[a-z0-9][a-z0-9-]{0,63}$",
label: "Selected theme profile",
detail: "The shipped or saved theme currently applied to the desktop",
internal: true
},
{
key: "themeProfiles", type: "json", def: [], group: "appearance",
label: "Saved theme profiles",
detail: "Named custom colour schemes and accent pairs",
internal: true
},
// ── Application themes ───────────────────────────────────────────── // ── Application themes ─────────────────────────────────────────────
// ColorScheme owns GTK's light/dark theme. These are the two theme // ColorScheme owns GTK's light/dark theme. These are the two theme
+12 -21
View File
@@ -14,6 +14,7 @@ import Quickshell
// QtQuick is required even though nothing visual is declared here: `color` is a // QtQuick is required even though nothing visual is declared here: `color` is a
// QtQuick value type, and Qt.rgba() lives in its JS namespace. // QtQuick value type, and Qt.rgba() lives in its JS namespace.
import QtQuick import QtQuick
import qs.services
Singleton { Singleton {
id: root id: root
@@ -60,30 +61,20 @@ Singleton {
// Blue is the shipped Prism -- blue leading, orchid following -- and stays // Blue is the shipped Prism -- blue leading, orchid following -- and stays
// the default. // the default.
// //
// `gnome` is the nearest member of GNOME's own accent-color enum, which is // The table itself lives in services/ThemeProfileModel.js so a curated
// a fixed list of nine we do not get to extend. It is what libadwaita // accent and a custom profile are the same kind of record. Its `gnome`
// member is the nearest name in GNOME's own accent-color enum, which is a
// fixed list of nine we do not get to extend; it is what libadwaita
// applications -- Files, Papers, Loupe -- are told to use, so choosing an // applications -- Files, Papers, Loupe -- are told to use, so choosing an
// accent here recolors them too instead of leaving them in GNOME blue. // accent here recolors them too instead of leaving them in GNOME blue.
// Nearest by hue, not by name: "rose" maps to red rather than pink because readonly property var accents: ThemeProfiles.curatedAccents
// it is the red role in this palette. readonly property var activeProfile: ThemeProfiles.activeProfile
readonly property var accents: ({
"blue": { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue", gnome: "blue" },
"orchid": { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid", gnome: "purple" },
"teal": { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal", gnome: "teal" },
"green": { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green", gnome: "green" },
"amber": { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber", gnome: "yellow" },
"orange": { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange", gnome: "orange" },
"rose": { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose", gnome: "red" },
"slate": { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate", gnome: "slate" }
})
// Falls back to blue for an unknown name, so a settings file written by a // ThemeProfiles validates every persisted record before it can become
// newer Panama -- or edited by hand -- degrades to the shipped identity // active, so these bindings are both reactive and safe to expose as the
// rather than to an undefined color. // shell-wide colour roles.
readonly property var accentPair: root.accents[DesktopPreferences.get("accentName")] ?? root.accents["blue"] readonly property color accent: root.activeProfile.accent
readonly property color accentSecondary: root.activeProfile.secondary
readonly property color accent: root.dark ? root.accentPair.dark : root.accentPair.light
readonly property color accentSecondary: root.dark ? root.accentPair.darkSecondary : root.accentPair.lightSecondary
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
readonly property color cyan: root.dark ? "#86e1fc" : "#007197" readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
readonly property color teal: root.dark ? "#4fd6be" : "#118c74" readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
@@ -6,6 +6,10 @@ import qs.config
import qs.services import qs.services
ShellRoot { ShellRoot {
// The isolated screen model begins with both fixture outputs so changing
// it below exercises the same reactive topology path as a real hotplug.
Component.onCompleted: Displays.screenOverride = ["DP-2", "HDMI-A-1"]
IpcHandler { IpcHandler {
target: "displays-test" target: "displays-test"
@@ -42,6 +46,7 @@ ShellRoot {
function transactionStatus(): string { function transactionStatus(): string {
return JSON.stringify({ return JSON.stringify({
layout: Displays.currentLayout(), layout: Displays.currentLayout(),
primaryFirst: Displays.primaryFirstMonitors.map(monitor => monitor.name),
pending: Displays.pendingRequestedLayout, pending: Displays.pendingRequestedLayout,
previous: Displays.pendingPreviousLayout, previous: Displays.pendingPreviousLayout,
reverting: Displays.revertExpectedLayout, reverting: Displays.revertExpectedLayout,
@@ -141,5 +146,8 @@ ShellRoot {
if (monitor) Displays.forget(monitor.name); if (monitor) Displays.forget(monitor.name);
} }
function refresh(): void { Displays.refresh(); } function refresh(): void { Displays.refresh(); }
function setScreenModel(names: string): void {
Displays.screenOverride = JSON.parse(names);
}
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
// Exercises Migrations.applyWith against fixture steps and prints a verdict per // Exercises Migrations.applyWith against fixture steps and prints a verdict per
// case. Run by tests/quickshell/migrations-contract.sh. // case. Run by tests/quickshell/migrations-contract.
// //
// Fixture steps rather than the real list: the real one is empty until the // Fixture steps rather than the real list: the real one is empty until the
// first breaking schema change, and a mechanism that has never been run against // first breaking schema change, and a mechanism that has never been run against
+20 -1
View File
@@ -177,12 +177,21 @@ PanelWindow {
x: { x: {
if (!root.revealed) if (!root.revealed)
return root.position === "right" ? surface.width - root.revealStripHeight : 0; return root.position === "right" ? surface.width - root.revealStripHeight : 0;
// A horizontal dock is centred on its edge, so the region has
// to start where the body starts -- starting at 0 puts it over
// the left of the screen while the pointer that summoned the
// dock is in the middle, and the hover drops on the very frame
// the dock arrives. A vertical dock reaches from the body out
// to its own edge, which is x 0 on the left and the body on
// the right.
if (!root.vertical)
return body.x;
return root.position === "right" ? body.x : 0; return root.position === "right" ? body.x : 0;
} }
y: { y: {
if (!root.revealed) if (!root.revealed)
return root.vertical ? 0 : surface.height - root.revealStripHeight; return root.vertical ? 0 : surface.height - root.revealStripHeight;
return root.vertical ? body.y : body.y; return body.y;
} }
width: { width: {
if (!root.revealed) if (!root.revealed)
@@ -201,6 +210,12 @@ PanelWindow {
DockBody { DockBody {
id: body id: body
onContextMenuRequested: (anchorItem, entry) => {
dockContextMenu.anchorItem = anchorItem;
dockContextMenu.entry = entry;
dockContextMenu.visible = true;
}
vertical: root.vertical vertical: root.vertical
leftSide: root.position === "left" leftSide: root.position === "left"
@@ -251,4 +266,8 @@ PanelWindow {
} }
} }
} }
DockContextMenu {
id: dockContextMenu
}
} }
@@ -116,6 +116,7 @@ Rectangle {
// The item the tooltip is currently describing, or null. // The item the tooltip is currently describing, or null.
property Item hoveredItem: null property Item hoveredItem: null
signal contextMenuRequested(Item anchorItem, var entry)
// Set by the Dock. A side dock runs the same strip down the screen instead // Set by the Dock. A side dock runs the same strip down the screen instead
// of across it. // of across it.
@@ -170,6 +171,7 @@ Rectangle {
onEntered: root.hoveredItem = dockItem onEntered: root.hoveredItem = dockItem
onExited: if (root.hoveredItem === dockItem) onExited: if (root.hoveredItem === dockItem)
root.hoveredItem = null root.hoveredItem = null
onContextMenuRequested: root.contextMenuRequested(dockItem, dockItem.entry)
} }
} }
} }
@@ -0,0 +1,84 @@
// The dock's app menu. Desktop-entry actions stay first; the shell-owned
// configuration route is deliberately last so it never displaces app actions.
import Quickshell
import QtQuick
import qs.config
import qs.modules.bar
import qs.services
import qs.widgets
PopupWindow {
id: root
property Item anchorItem: null
property var entry: null
anchor.item: root.anchorItem
anchor.edges: Edges.Top | Edges.Left
anchor.gravity: Edges.Top | Edges.Right
anchor.margins.bottom: 8
implicitWidth: Math.max(menu.implicitWidth + Theme.popoverPadding * 2, 240)
implicitHeight: menu.implicitHeight + Theme.popoverPadding * 2
color: "transparent"
visible: false
grabFocus: true
Rectangle {
anchors.fill: parent
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.08)
PrismEdge {
anchors.top: parent.top
anchors.topMargin: 1
anchors.left: parent.left
anchors.right: parent.right
inset: parent.radius
}
Column {
id: menu
anchors.fill: parent
anchors.margins: Theme.popoverPadding
spacing: 2
Repeater {
id: applicationActions
model: root.entry ? root.entry.actions : []
delegate: TrayMenuRow {
required property var modelData
width: parent.width
label: modelData.name
onActivated: {
modelData.execute();
root.visible = false;
}
}
}
Rectangle {
width: parent.width
height: 1
anchors.margins: 3
visible: applicationActions.count > 0
border.width: 0
color: Theme.alpha(Theme.fg, 0.1)
}
TrayMenuRow {
width: parent.width
label: "Dock settings"
onActivated: {
ShellState.openSettings("desktop");
root.visible = false;
}
}
}
}
}
@@ -24,6 +24,7 @@ Item {
// Emitted so DockBody can drive the single shared tooltip. // Emitted so DockBody can drive the single shared tooltip.
signal entered signal entered
signal exited signal exited
signal contextMenuRequested
// The icon may grow past the cell on hover; the cell itself stays a fixed // The icon may grow past the cell on hover; the cell itself stays a fixed
// size so the row doesn't reflow. // size so the row doesn't reflow.
@@ -115,7 +116,7 @@ Item {
id: mouse id: mouse
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onEntered: root.entered() onEntered: root.entered()
onExited: root.exited() onExited: root.exited()
@@ -126,6 +127,10 @@ Item {
root.launch(); root.launch();
return; return;
} }
if (mev.button === Qt.RightButton) {
root.contextMenuRequested();
return;
}
if (root.running) if (root.running)
root.focusNext(); root.focusNext();
else else
@@ -14,6 +14,7 @@ import Quickshell.Services.Notifications
import qs.config import qs.config
import qs.services import qs.services
import qs.modules.quicksettings import qs.modules.quicksettings
import qs.modules.bar
import qs.widgets import qs.widgets
Rectangle { Rectangle {
@@ -127,11 +128,44 @@ Rectangle {
onClicked: root.dismissed() onClicked: root.dismissed()
} }
IconButton {
id: settingsMenuButton
anchors.right: closeButton.left
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 6
size: 24
iconSize: 14
tint: Theme.fgDim
icon: "view-more-symbolic"
iconFallback: "open-menu-symbolic"
onClicked: settingsMenu.visible = !settingsMenu.visible
}
Popover {
id: settingsMenu
anchorItem: settingsMenuButton
// A Column already measures itself from its children, and in Qt 6 both
// implicit sizes are read-only on a positioner -- assigning them makes
// the whole shell fail to load rather than just this menu.
Column {
TrayMenuRow {
id: notificationSettings
label: "Notification settings"
onActivated: {
ShellState.openSettings("notifications");
settingsMenu.visible = false;
}
}
}
}
Column { Column {
id: layout id: layout
anchors.left: appIcon.visible ? appIcon.right : parent.left anchors.left: appIcon.visible ? appIcon.right : parent.left
anchors.leftMargin: appIcon.visible ? 10 : 14 anchors.leftMargin: appIcon.visible ? 10 : 14
anchors.right: image.visible ? image.left : closeButton.left anchors.right: image.visible ? image.left : settingsMenuButton.left
anchors.rightMargin: 8 anchors.rightMargin: 8
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 12 anchors.topMargin: 12
+19 -1
View File
@@ -34,7 +34,14 @@ PanelWindow {
implicitWidth: root.desiredWidth implicitWidth: root.desiredWidth
implicitHeight: 64 implicitHeight: 64
color: "transparent" color: "transparent"
mask: Region {} mask: Region {
item: inputMask
}
Item {
id: inputMask
anchors.fill: parent
}
WlrLayershell.namespace: "qs-popover-osd" WlrLayershell.namespace: "qs-popover-osd"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
@@ -145,6 +152,17 @@ PanelWindow {
font.weight: Font.DemiBold font.weight: Font.DemiBold
} }
} }
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: event => {
if (event.button !== Qt.RightButton)
return;
ShellState.openSettings("accessibility");
OsdState.hide();
}
}
} }
TextMetrics { TextMetrics {
@@ -176,34 +176,34 @@ Item {
height: aspect > 0 ? width / aspect : parent.height height: aspect > 0 ? width / aspect : parent.height
opacity: hasContent ? 1 : 0 opacity: hasContent ? 1 : 0
// The capture context isn't ready the instant the layer surface is // A ScreencopyView cannot tell us that its recording context is
// told to become visible — it needs the surface to actually map, // ready before a capture. The enclosing overview gets that
// which takes a frame or two. Calling captureFrame() before then // context only after its first rendered frame, so defer the first
// logs "no recording context is ready" and yields nothing, so // one-shot capture to that event. Retrying before then only emits
// retry a few times and then give up quietly (the caption and app // "no recording context is ready" warnings; if a capture later
// icon are still shown, so a missing thumbnail is cosmetic). // cannot produce content, the app icon remains the fallback.
property int captureAttempts: 0 property bool recordingReady: false
function tryCapture(): void { function tryCapture(): void {
captureAttempts = 0; if (shot.hasContent)
captureRetry.restart(); return;
} if (!shot.recordingReady) {
frameReady.restart();
// `shot`, not `parent`: Timer is a QtObject, so `parent` does not
// resolve to the enclosing ScreencopyView.
Timer {
id: captureRetry
interval: 80
repeat: true
running: false
onTriggered: {
if (shot.hasContent || shot.captureAttempts >= 6) {
stop();
return; return;
} }
shot.captureAttempts++;
shot.captureFrame(); shot.captureFrame();
} }
FrameAnimation {
id: frameReady
running: false
onTriggered: {
running = false;
if (shot.hasContent)
return;
shot.recordingReady = true;
shot.tryCapture();
}
} }
Component.onCompleted: tryCapture() Component.onCompleted: tryCapture()
@@ -0,0 +1,218 @@
// Advanced accent editing remains a labelled, keyboard-operable extension of
// the named fast path. Hue is always accompanied by saturation, value, a
// numeric readout, and the two-colour preview supplied by Appearance.
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
import qs.services
import qs.widgets
import "../../services/ThemeProfileModel.js" as ThemeProfileModel
Column {
id: editor
width: parent ? parent.width : 620
spacing: 0
property string pickerTarget: "primary"
property string lastError: ""
readonly property var primaryHsv: ThemeProfileModel.hexToHsv(
ThemeProfiles.activeProfile.accent) || ({ h: 0, s: 0, v: 0 })
readonly property var secondaryHsv: ThemeProfileModel.hexToHsv(
ThemeProfiles.activeProfile.secondary) || ({ h: 0, s: 0, v: 0 })
function changeChannel(target: string, channel: string, ratio: real): void {
const source = target === "primary" ? editor.primaryHsv : editor.secondaryHsv;
const next = { h: source.h, s: source.s, v: source.v };
next[channel] = Math.round(Math.max(0, Math.min(1, ratio))
* (channel === "h" ? 360 : 100));
const changed = ThemeProfileModel.hsvToHex(next.h, next.s, next.v);
const primary = target === "primary" ? changed : ThemeProfiles.activeProfile.accent;
const secondary = target === "secondary" ? changed : ThemeProfiles.activeProfile.secondary;
ThemeProfiles.setAccentPair(primary, secondary);
}
function pick(target: string): void {
if (screenPicker.running)
return;
editor.pickerTarget = target;
editor.lastError = "";
screenPicker.exec(["hyprpicker", "--format=hex", "--lowercase-hex", "--quiet", "--no-fancy"]);
}
function acceptPicked(value: string): void {
const picked = String(value).trim().toLowerCase();
if (ThemeProfileModel.hexToHsv(picked) === null) {
editor.lastError = "The sampled colour was not valid.";
return;
}
const primary = editor.pickerTarget === "primary"
? picked : ThemeProfiles.activeProfile.accent;
const secondary = editor.pickerTarget === "secondary"
? picked : ThemeProfiles.activeProfile.secondary;
ThemeProfiles.setAccentPair(primary, secondary);
}
component HsvRow: SettingRow {
id: root
required property string target
required property string channel
required property real channelValue
required property real channelMaximum
property string suffix: "%"
controlWidth: 280
Item {
id: keyboardSlider
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 270
height: 32
activeFocusOnTab: true
Accessible.role: Accessible.Slider
Accessible.name: root.label
Accessible.description: Math.round(root.channelValue) + root.suffix
+ ", range 0 to " + root.channelMaximum
Accessible.focusable: true
Accessible.focused: activeFocus
Accessible.onIncreaseAction: keyboardSlider.step(1)
Accessible.onDecreaseAction: keyboardSlider.step(-1)
function step(direction: int): void {
const increment = root.channel === "h" ? 1 : 1;
const value = Math.max(0, Math.min(root.channelMaximum,
root.channelValue + direction * increment));
editor.changeChannel(root.target, root.channel, value / root.channelMaximum);
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Left || event.key === Qt.Key_Down) {
keyboardSlider.step(-1);
event.accepted = true;
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Up) {
keyboardSlider.step(1);
event.accepted = true;
} else if (event.key === Qt.Key_Home) {
editor.changeChannel(root.target, root.channel, 0);
event.accepted = true;
} else if (event.key === Qt.Key_End) {
editor.changeChannel(root.target, root.channel, 1);
event.accepted = true;
}
}
Rectangle {
anchors.left: parent.left
anchors.right: readout.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
height: 24
radius: 9
color: "transparent"
border.width: keyboardSlider.activeFocus ? 2 : 1
border.color: keyboardSlider.activeFocus ? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.08)
ValueSlider {
anchors.fill: parent
anchors.margins: 4
value: root.channelMaximum > 0 ? root.channelValue / root.channelMaximum : 0
onMoved: ratio => editor.changeChannel(root.target, root.channel, ratio)
}
}
Text {
id: readout
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 56
horizontalAlignment: Text.AlignRight
text: Math.round(root.channelValue) + root.suffix
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
}
HsvRow {
target: "primary"; channel: "h"; channelValue: editor.primaryHsv.h; channelMaximum: 360
suffix: "°"; label: "Primary hue"; detail: "Colour family, measured from 0 to 360 degrees"
}
HsvRow {
target: "primary"; channel: "s"; channelValue: editor.primaryHsv.s; channelMaximum: 100
label: "Primary saturation"; detail: "Colour intensity from grey to vivid"
}
HsvRow {
target: "primary"; channel: "v"; channelValue: editor.primaryHsv.v; channelMaximum: 100
label: "Primary value"; detail: "Brightness from black to full colour"
}
HsvRow {
target: "secondary"; channel: "h"; channelValue: editor.secondaryHsv.h; channelMaximum: 360
suffix: "°"; label: "Secondary hue"; detail: "Colour family at the far end of the Prism gradient"
}
HsvRow {
target: "secondary"; channel: "s"; channelValue: editor.secondaryHsv.s; channelMaximum: 100
label: "Secondary saturation"; detail: "Colour intensity from grey to vivid"
}
HsvRow {
target: "secondary"; channel: "v"; channelValue: editor.secondaryHsv.v; channelMaximum: 100
label: "Secondary value"; detail: "Brightness from black to full colour"
}
SettingRow {
label: "Pick colour from screen"
detail: editor.lastError !== ""
? editor.lastError
: "Sample either end of the accent gradient with hyprpicker"
divider: false
controlWidth: 330
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 7
SettingsButton {
text: "Pick primary from screen"
enabled: !screenPicker.running
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: editor.pick("primary")
Keys.onReturnPressed: if (enabled) editor.pick("primary")
Keys.onSpacePressed: if (enabled) editor.pick("primary")
}
SettingsButton {
text: "Pick secondary from screen"
enabled: !screenPicker.running
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: editor.pick("secondary")
Keys.onReturnPressed: if (enabled) editor.pick("secondary")
Keys.onSpacePressed: if (enabled) editor.pick("secondary")
}
}
}
Process {
id: screenPicker
stdout: StdioCollector {
onStreamFinished: editor.acceptPicked(this.text)
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
editor.lastError = "Screen colour picking was cancelled or unavailable.";
}
}
}
@@ -18,7 +18,7 @@ Flow {
spacing: 10 spacing: 10
readonly property string current: DesktopPreferences.get("accentName") || "blue" readonly property string current: ThemeProfiles.activeAccentName
// The schema's own option list, not Theme.accents directly -- two lists // The schema's own option list, not Theme.accents directly -- two lists
// hand-kept in sync is how they drift. This is the same pattern // hand-kept in sync is how they drift. This is the same pattern
@@ -60,6 +60,8 @@ Flow {
} }
Rectangle { Rectangle {
id: swatch
width: 46 width: 46
height: 46 height: 46
radius: 23 radius: 23
@@ -67,8 +69,25 @@ Flow {
color: "transparent" color: "transparent"
// The ring sits outside the gradient rather than over it, so a // The ring sits outside the gradient rather than over it, so a
// selected swatch still shows its true colors. // selected swatch still shows its true colors.
border.width: entry.selected ? 2 : 1 border.width: swatch.activeFocus || entry.selected ? 2 : 1
border.color: entry.selected ? Theme.fg : Theme.alpha(Theme.fg, 0.14) border.color: swatch.activeFocus
? Theme.accentSecondary
: (entry.selected ? Theme.fg : Theme.alpha(Theme.fg, 0.14))
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: entry.pair.label + " accent"
Accessible.description: String(entry.start) + " to " + String(entry.end)
Accessible.focusable: true
Accessible.focused: activeFocus
function choose(): void {
if (!ThemeProfiles.useCuratedAccent(entry.modelData))
console.warn("AccentPicker: accent was rejected", entry.modelData);
}
Keys.onReturnPressed: swatch.choose()
Keys.onSpacePressed: swatch.choose()
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -81,6 +100,9 @@ Flow {
GradientStop { position: 1.0; color: entry.end } GradientStop { position: 1.0; color: entry.end }
} }
} }
HoverHandler { cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: swatch.choose() }
} }
// Always shown, not a tooltip. Telling swatches apart by color is // Always shown, not a tooltip. Telling swatches apart by color is
@@ -129,18 +129,26 @@ SettingsPage {
SettingsCard { SettingsCard {
visible: root.tab === "theme" visible: root.tab === "theme"
title: "Color scheme" // Not "Color scheme" any more: the card holds the saved themes as well
// as the scheme, and a theme carries both ends of the accent with it.
title: "Theme"
subtitle: ColorScheme.lastError !== "" subtitle: ColorScheme.lastError !== ""
? ColorScheme.lastError ? ColorScheme.lastError
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow." : "Start with Moon, Moon Rose, or Day; saved themes capture the scheme and both ends of the Prism accent."
ChoiceRow { setting: "colorScheme" } ThemeProfilePicker {
width: parent.width
}
// Drawn as the gradient each accent produces rather than a flat dot, // Drawn as the gradient each accent produces rather than a flat dot,
// because the gradient is what is being chosen. // because the gradient is what is being chosen.
AccentPicker { AccentPicker {
width: parent.width width: parent.width
} }
AccentEditor {
width: parent.width
}
} }
SettingsCard { SettingsCard {
@@ -22,7 +22,7 @@ SettingsPage {
property string selectedOutput: "" property string selectedOutput: ""
readonly property var monitor: Displays.monitorNamed(root.selectedOutput) readonly property var monitor: Displays.monitorNamed(root.selectedOutput)
?? (Displays.monitors.length > 0 ? Displays.monitors[0] : null) ?? (Displays.primaryFirstMonitors.length > 0 ? Displays.primaryFirstMonitors[0] : null)
readonly property string currentMode: root.monitor readonly property string currentMode: root.monitor
? root.monitor.mode ? root.monitor.mode
: "" : ""
@@ -38,7 +38,8 @@ SettingsPage {
function syncSelectedOutput(): void { function syncSelectedOutput(): void {
if (!Displays.monitorNamed(root.selectedOutput)) if (!Displays.monitorNamed(root.selectedOutput))
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : ""; root.selectedOutput = Displays.primaryFirstMonitors.length > 0
? Displays.primaryFirstMonitors[0].name : "";
} }
// Probing I2C for DDC-capable monitors takes on the order of a second, so // Probing I2C for DDC-capable monitors takes on the order of a second, so
@@ -137,7 +138,7 @@ SettingsPage {
ChoiceGrid { ChoiceGrid {
width: parent.width width: parent.width
label: "Display" label: "Display"
options: Displays.monitors.map(monitor => ({ options: Displays.primaryFirstMonitors.map(monitor => ({
value: monitor.name, value: monitor.name,
label: monitor.description || monitor.name label: monitor.description || monitor.name
})) }))
@@ -91,7 +91,7 @@ layout.
Mirrors must remain the same schema-backed control, never a second preference Mirrors must remain the same schema-backed control, never a second preference
or a copied default. Additions to this table require a concrete discoverability or a copied default. Additions to this table require a concrete discoverability
reason and an update to `tests/quickshell/settings-ownership-contract.sh`. reason and an update to `tests/quickshell/settings-ownership-contract`.
Window border color follows the same ownership rule. The inactive border is a Window border color follows the same ownership rule. The inactive border is a
**scheme-relative role** owned by `ColorScheme.qml`: it changes only to retain **scheme-relative role** owned by `ColorScheme.qml`: it changes only to retain
@@ -127,7 +127,7 @@ store. A row never needs to know which kind it holds.
the value in a different JSON field per type — `int`, `bool`, `float`, `str`, the value in a different JSON field per type — `int`, `bool`, `float`, `str`,
and `css` for gaps (a four-value box). Declaring the wrong one does not fail and `css` for gaps (a four-value box). Declaring the wrong one does not fail
loudly: it makes every write to that key look *rejected*, and the user sees an loudly: it makes every write to that key look *rejected*, and the user sees an
error for a change that worked. `tests/quickshell/schema-hypr-shape-contract.sh` error for a change that worked. `tests/quickshell/schema-hypr-shape-contract`
asks the compositor for the real shape of every mapped option. asks the compositor for the real shape of every mapped option.
**Never trust an exit code from `hyprctl`.** `keyword` refuses to work on a **Never trust an exit code from `hyprctl`.** `keyword` refuses to work on a
@@ -0,0 +1,123 @@
// Named themes are the first layer of Appearance: shipped profiles stay
// immutable, while saved profiles can be selected or removed in place.
import QtQuick
import qs.config
import qs.services
Column {
id: root
width: parent ? parent.width : 620
spacing: 0
Repeater {
model: ThemeProfiles.profiles
SettingRow {
id: profileRow
required property var modelData
label: profileRow.modelData.name
detail: (profileRow.modelData.scheme === "light" ? "Light" : "Dark")
+ " · " + profileRow.modelData.accent + " → " + profileRow.modelData.secondary
controlWidth: profileRow.modelData.shipped ? 88 : 170
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
SettingsButton {
id: useButton
text: profileRow.modelData.id === ThemeProfiles.activeProfile.id ? "Selected" : "Use"
enabled: profileRow.modelData.id !== ThemeProfiles.activeProfile.id
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : (tone === "accent" ? 0 : 1)
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: ThemeProfiles.selectProfile(profileRow.modelData.id)
Keys.onReturnPressed: if (enabled) ThemeProfiles.selectProfile(profileRow.modelData.id)
Keys.onSpacePressed: if (enabled) ThemeProfiles.selectProfile(profileRow.modelData.id)
}
SettingsButton {
id: deleteButton
visible: !profileRow.modelData.shipped
text: "Delete"
activeFocusOnTab: visible
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.danger : Theme.alpha(Theme.fg, 0.08)
onClicked: ThemeProfiles.deleteProfile(profileRow.modelData.id)
Keys.onReturnPressed: ThemeProfiles.deleteProfile(profileRow.modelData.id)
Keys.onSpacePressed: ThemeProfiles.deleteProfile(profileRow.modelData.id)
}
}
}
}
SettingRow {
label: "Save current theme"
detail: "Stores this scheme and accent pair under a unique name"
divider: false
controlWidth: 292
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 7
Rectangle {
width: 196
height: 31
radius: 8
color: Theme.alpha(Theme.fg, 0.05)
border.width: nameInput.activeFocus ? 2 : 1
border.color: nameInput.activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.12)
TextInput {
id: nameInput
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
activeFocusOnTab: true
verticalAlignment: TextInput.AlignVCenter
maximumLength: 40
color: Theme.fg
selectionColor: Theme.alpha(Theme.accent, 0.35)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
Accessible.name: "Theme profile name"
Text {
anchors.verticalCenter: parent.verticalCenter
visible: nameInput.text === "" && !nameInput.activeFocus
text: "Theme name"
color: Theme.fgMuted
font: nameInput.font
}
onAccepted: saveButton.save()
}
}
SettingsButton {
id: saveButton
text: "Save"
tone: "accent"
activeFocusOnTab: true
border.width: activeFocus ? 2 : 0
border.color: activeFocus ? Theme.fg : "transparent"
function save(): void {
if (ThemeProfiles.saveProfile(nameInput.text))
nameInput.text = "";
}
onClicked: save()
Keys.onReturnPressed: save()
Keys.onSpacePressed: save()
}
}
}
}
@@ -18,6 +18,13 @@ Column {
width: parent ? parent.width : 620 width: parent ? parent.width : 620
spacing: 4 spacing: 4
onOutputsChanged: root.syncSelectedOutput()
function syncSelectedOutput(): void {
if (!root.outputs.includes(root.selectedOutput))
root.selectedOutput = root.outputs.length > 0 ? root.outputs[0] : "";
}
ChoiceGrid { ChoiceGrid {
width: parent.width width: parent.width
label: "Wallpaper mode" label: "Wallpaper mode"
@@ -80,3 +80,5 @@ SearchPicker 1.0 SearchPicker.qml
OnlineAccountsPage 1.0 OnlineAccountsPage.qml OnlineAccountsPage 1.0 OnlineAccountsPage.qml
AccentPicker 1.0 AccentPicker.qml AccentPicker 1.0 AccentPicker.qml
TextFieldRow 1.0 TextFieldRow.qml TextFieldRow 1.0 TextFieldRow.qml
AccentEditor 1.0 AccentEditor.qml
ThemeProfilePicker 1.0 ThemeProfilePicker.qml
+26 -8
View File
@@ -17,8 +17,10 @@ status_file="$state_dir/hyprlock-status.json"
fallback="$config_home/hypr/hyprlock.conf" fallback="$config_home/hypr/hyprlock.conf"
temporary="$generated.tmp.$$" temporary="$generated.tmp.$$"
status_temporary="$status_file.tmp.$$" status_temporary="$status_file.tmp.$$"
shipped_wallpaper="$HOME/Pictures/Wallpapers/faroe_islands.jpg"
settings_valid=false settings_valid=false
wallpaper_warning=""
cleanup() { cleanup() {
rm -f "$temporary" "$status_temporary" 2>/dev/null || true rm -f "$temporary" "$status_temporary" 2>/dev/null || true
@@ -76,7 +78,23 @@ read_object() {
} }
valid_path() { valid_path() {
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* ]] [[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* && -f "$1" && -r "$1" ]]
}
resolve_wallpaper_path() {
local candidate="$1"
if valid_path "$candidate"; then
resolved_wallpaper="$candidate"
return
fi
if [[ -n "$candidate" ]]; then
wallpaper_warning="One or more lock-screen wallpapers were unavailable; a safe fallback is in use."
fi
if valid_path "$shipped_wallpaper"; then
resolved_wallpaper="$shipped_wallpaper"
else
resolved_wallpaper="screenshot"
fi
} }
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by # Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
@@ -134,10 +152,9 @@ load_preferences() {
wallpaper_mode="$(read_string wallpaperMode single)" wallpaper_mode="$(read_string wallpaperMode single)"
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \ [[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|| wallpaper_mode=single || wallpaper_mode=single
wallpaper_path="$(read_string wallpaperPath '')" wallpaper_warning=""
if ! valid_path "$wallpaper_path"; then resolve_wallpaper_path "$(read_string wallpaperPath '')"
wallpaper_path="$HOME/Pictures/Wallpapers/faroe_islands.jpg" wallpaper_path="$resolved_wallpaper"
fi
wallpaper_assignments="$(read_object wallpaperPerMonitor)" wallpaper_assignments="$(read_object wallpaperPerMonitor)"
case "$blur_level" in case "$blur_level" in
@@ -218,8 +235,9 @@ emit_backgrounds() {
candidate="$(jq -r --arg output "$output" \ candidate="$(jq -r --arg output "$output" \
'if has($output) and (.[$output] | type) == "string" then .[$output] else "" end' \ 'if has($output) and (.[$output] | type) == "string" then .[$output] else "" end' \
<<<"$wallpaper_assignments" 2>/dev/null || true)" <<<"$wallpaper_assignments" 2>/dev/null || true)"
if valid_path "$candidate"; then if [[ -n "$candidate" ]]; then
assigned="$candidate" resolve_wallpaper_path "$candidate"
assigned="$resolved_wallpaper"
fi fi
fi fi
emit_background "$output" "$assigned" emit_background "$output" "$assigned"
@@ -344,7 +362,7 @@ generate() {
write_status false "$fallback" true "The lock-screen configuration could not be generated." write_status false "$fallback" true "The lock-screen configuration could not be generated."
return 1 return 1
fi fi
write_status true "$generated" false "" write_status true "$generated" false "$wallpaper_warning"
} }
status() { status() {
@@ -150,12 +150,12 @@ def build() -> dict[Path, str]:
files: dict[Path, str] = {} files: dict[Path, str] = {}
for page, label in pages(): for page, label in pages():
words = keywords_for(page, routing, schema, extras) words = keywords_for(page, routing, schema, extras)
files[OUTPUT_DIR / f"settings-{page}.sh"] = command_for(page, label, words) files[OUTPUT_DIR / f"settings-{page}"] = command_for(page, label, words)
return files return files
def existing() -> set[Path]: def existing() -> set[Path]:
return {path for path in OUTPUT_DIR.glob("settings-*.sh")} return {path for path in OUTPUT_DIR.glob("settings-*")}
def main(arguments: list[str]) -> int: def main(arguments: list[str]) -> int:
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Finds wallpaper candidates from fixed roots passed as separate arguments.
# Keeping paths as argv values avoids shell interpolation for names containing
# quotes, spaces, or other shell syntax.
set -euo pipefail
find "$@" -maxdepth 2 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) \
-printf '%T@ %p\n' 2>/dev/null \
| sort -rn \
| cut -d' ' -f2- \
| awk 'NR <= 60'
@@ -93,6 +93,14 @@ Singleton {
function onRevisionChanged(): void { coalesce.restart(); } function onRevisionChanged(): void { coalesce.restart(); }
} }
// Profile selection and custom edits both update preferences, but binding
// directly to the effective profile also documents the in-shell ownership
// boundary and keeps this propagation reactive if that storage changes.
Connections {
target: ThemeProfiles
function onActiveProfileChanged(): void { coalesce.restart(); }
}
Timer { Timer {
id: coalesce id: coalesce
interval: 250 interval: 250
@@ -29,8 +29,24 @@ Singleton {
// [{ name, description, width, height, refreshRate, scale, transform, // [{ name, description, width, height, refreshRate, scale, transform,
// modes: [{ label, mode, width, height, refresh }] }] // modes: [{ label, mode, width, height, refresh }] }]
property var monitors: [] property var monitors: []
// Quickshell.screens is the topology authority. The override is only the
// isolated harness model; normal sessions always observe Quickshell.
property var screenOverride: null
property string lastError: "" property string lastError: ""
readonly property var screenModel: Array.isArray(root.screenOverride)
? root.screenOverride : Quickshell.screens
readonly property string screenSignature: root.screenModel
.map(screen => typeof screen === "string" ? screen : screen.name)
.filter(name => !!name)
.sort()
.join("|")
readonly property var primaryFirstMonitors: root.monitors.slice().sort((left, right) => {
if (left.primary !== right.primary)
return left.primary ? -1 : 1;
return left.name.localeCompare(right.name);
})
// Set while a change is applied but not yet confirmed. // Set while a change is applied but not yet confirmed.
property var pendingPreviousLayout: null property var pendingPreviousLayout: null
property var pendingRequestedLayout: null property var pendingRequestedLayout: null
@@ -113,6 +129,17 @@ Singleton {
Component.onCompleted: root.refresh() Component.onCompleted: root.refresh()
onScreenSignatureChanged: root.reconcileTopology()
// A hotplug can invalidate the unconfirmed layout while the confirmation
// is visible. Read the current compositor layout first; the normal queued
// rollback then filters out any output that has disappeared.
function reconcileTopology(): void {
root.refresh();
if (root.awaitingConfirmation)
root.revertWithMessage("A display was connected or disconnected, so Panama restored the previous setting.");
}
function refresh(): bool { function refresh(): bool {
if (!query.running) { if (!query.running) {
query.generation = root.operationGeneration; query.generation = root.operationGeneration;
+1 -1
View File
@@ -226,7 +226,7 @@ Singleton {
// A bind with no description cannot be presented usefully -- // A bind with no description cannot be presented usefully --
// the dispatcher is "__lua" and the argument is a bytecode // the dispatcher is "__lua" and the argument is a bytecode
// offset. Showing the chord alone would be worse than omitting // offset. Showing the chord alone would be worse than omitting
// it, and tests/quickshell/keybinds-contract.sh fails the build // it, and tests/quickshell/keybinds-contract fails the build
// if any exist, so this should never be reached in practice. // if any exist, so this should never be reached in practice.
if (description === "") if (description === "")
continue; continue;
@@ -136,7 +136,10 @@ Singleton {
{ label: "Per-display wallpaper", detail: "Assign a different image to each connected display", page: "appearance" }, { label: "Per-display wallpaper", detail: "Assign a different image to each connected display", page: "appearance" },
{ label: "Arrange displays", detail: "Drag connected displays into their physical positions", page: "displays" }, { label: "Arrange displays", detail: "Drag connected displays into their physical positions", page: "displays" },
{ label: "Monitor position", detail: "Set where each display sits in the desktop", page: "displays" }, { label: "Monitor position", detail: "Set where each display sits in the desktop", page: "displays" },
{ label: "Primary display", detail: "Choose the display that anchors the desktop", page: "displays" } { label: "Primary display", detail: "Choose the display that anchors the desktop", page: "displays" },
{ label: "Theme profiles", detail: "Switch between Moon, Moon Rose, Day, and saved themes", page: "appearance" },
{ label: "Advanced accent", detail: "Adjust primary and secondary hue, saturation, and value", page: "appearance" },
{ label: "Pick colour from screen", detail: "Sample an accent colour with hyprpicker", page: "appearance" }
] ]
function pageFor(group: string): string { function pageFor(group: string): string {
@@ -0,0 +1,419 @@
var MAX_NAME_LENGTH = 40;
var SHIPPED = [
{
id: "moon",
name: "Moon",
scheme: "dark",
accent: "#82aaff",
secondary: "#b172b0",
shipped: true
},
{
id: "moon-rose",
name: "Moon Rose",
scheme: "dark",
accent: "#ff757f",
secondary: "#c099ff",
shipped: true
},
{
id: "day",
name: "Day",
scheme: "light",
accent: "#2e7de9",
secondary: "#9854f1",
shipped: true
}
];
// The curated accents. `gnome` is the nearest member of GNOME's own
// accent-color enum, which is a fixed list of nine we do not get to extend. It
// is what libadwaita applications -- Files, Papers, Loupe -- are told to use, so
// choosing an accent here recolors them too instead of leaving them in GNOME
// blue. Nearest by hue, not by name: "rose" maps to red rather than pink
// because it is the red role in this palette. adwaita-accent-contract reads
// this table and fails when a member is missing or is not in GNOME's enum.
var CURATED = {
blue: {
dark: "#82aaff", darkSecondary: "#b172b0",
light: "#2e7de9", lightSecondary: "#9854f1",
label: "Prism blue",
gnome: "blue"
},
orchid: {
dark: "#c099ff", darkSecondary: "#fca7ea",
light: "#7847bd", lightSecondary: "#9854f1",
label: "Orchid",
gnome: "purple"
},
teal: {
dark: "#86e1fc", darkSecondary: "#82aaff",
light: "#007197", lightSecondary: "#2e7de9",
label: "Teal",
gnome: "teal"
},
green: {
dark: "#c3e88d", darkSecondary: "#86e1fc",
light: "#587539", lightSecondary: "#007197",
label: "Green",
gnome: "green"
},
amber: {
dark: "#ffc777", darkSecondary: "#ff966c",
light: "#8c6c3e", lightSecondary: "#b15c00",
label: "Amber",
gnome: "yellow"
},
orange: {
dark: "#ff966c", darkSecondary: "#ff757f",
light: "#b15c00", lightSecondary: "#c64343",
label: "Orange",
gnome: "orange"
},
rose: {
dark: "#ff757f", darkSecondary: "#c099ff",
light: "#f52a65", lightSecondary: "#9854f1",
label: "Rose",
gnome: "red"
},
slate: {
dark: "#828bb8", darkSecondary: "#82aaff",
light: "#6172b0", lightSecondary: "#2e7de9",
label: "Slate",
gnome: "slate"
}
};
function copyProfile(profile) {
return {
id: profile.id,
name: profile.name,
scheme: profile.scheme,
accent: profile.accent,
secondary: profile.secondary,
shipped: profile.shipped === true
};
}
function shippedProfiles() {
return SHIPPED.map(copyProfile);
}
function curatedAccents() {
var result = {};
Object.keys(CURATED).forEach(function(name) {
result[name] = Object.assign({}, CURATED[name]);
});
return result;
}
function isScheme(value) {
return value === "dark" || value === "light";
}
function normalizedColor(value) {
var color = String(value || "").trim().toLowerCase();
return /^#[0-9a-f]{6}$/.test(color) ? color : null;
}
function normalizeStoredProfile(value) {
if (!value || typeof value !== "object" || value.shipped === true)
return null;
var id = String(value.id || "").trim();
var name = String(value.name || "").trim();
var accent = normalizedColor(value.accent);
var secondary = normalizedColor(value.secondary);
if (!/^custom-[a-z0-9][a-z0-9-]{0,56}$/.test(id)
|| name.length === 0 || name.length > MAX_NAME_LENGTH
|| !isScheme(value.scheme) || !accent || !secondary)
return null;
return {
id: id,
name: name,
scheme: value.scheme,
accent: accent,
secondary: secondary,
shipped: false
};
}
function validCustomProfiles(values) {
if (!Array.isArray(values))
return [];
var ids = {};
var names = {};
SHIPPED.forEach(function(profile) {
ids[profile.id] = true;
names[profile.name.toLowerCase()] = true;
});
var result = [];
values.forEach(function(value) {
var profile = normalizeStoredProfile(value);
if (!profile)
return;
var foldedName = profile.name.toLowerCase();
if (ids[profile.id] || names[foldedName])
return;
ids[profile.id] = true;
names[foldedName] = true;
result.push(profile);
});
return result;
}
function profileCatalog(values) {
return shippedProfiles().concat(validCustomProfiles(values));
}
function boundedName(value) {
var name = String(value || "").trim();
if (!name)
name = "Custom theme";
return name.slice(0, MAX_NAME_LENGTH);
}
function uniqueName(value, profiles) {
var requested = boundedName(value);
var names = {};
profiles.forEach(function(profile) {
names[profile.name.toLowerCase()] = true;
});
if (!names[requested.toLowerCase()])
return requested;
for (var suffix = 2; suffix < 10000; suffix++) {
var ending = " " + suffix;
var candidate = requested.slice(0, MAX_NAME_LENGTH - ending.length) + ending;
if (!names[candidate.toLowerCase()])
return candidate;
}
return requested.slice(0, MAX_NAME_LENGTH - 6) + " 10000";
}
function slug(value) {
var result = String(value || "").toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48)
.replace(/-+$/g, "");
return result || "theme";
}
function uniqueId(name, profiles) {
var base = "custom-" + slug(name);
var ids = {};
profiles.forEach(function(profile) { ids[profile.id] = true; });
if (!ids[base])
return base;
for (var suffix = 2; suffix < 10000; suffix++) {
var candidate = base.slice(0, 57 - String(suffix).length) + "-" + suffix;
if (!ids[candidate])
return candidate;
}
return base.slice(0, 52) + "-10000";
}
function createCustomProfile(values, input) {
var customs = validCustomProfiles(values);
var catalog = shippedProfiles().concat(customs);
var scheme = input && input.scheme;
var accent = normalizedColor(input && input.accent);
var secondary = normalizedColor(input && input.secondary);
if (!isScheme(scheme) || !accent || !secondary)
return { profiles: customs, profile: null };
var name = uniqueName(input && input.name, catalog);
var profile = {
id: uniqueId(name, catalog),
name: name,
scheme: scheme,
accent: accent,
secondary: secondary,
shipped: false
};
return { profiles: customs.concat([profile]), profile: profile };
}
function findProfile(values, id) {
var catalog = profileCatalog(values);
for (var index = 0; index < catalog.length; index++) {
if (catalog[index].id === id)
return catalog[index];
}
return null;
}
function editProfile(values, selected, changes) {
var customs = validCustomProfiles(values);
if (!selected || typeof selected !== "object")
return { profiles: customs, profile: null };
var accent = normalizedColor(changes && changes.accent !== undefined
? changes.accent : selected.accent);
var secondary = normalizedColor(changes && changes.secondary !== undefined
? changes.secondary : selected.secondary);
var scheme = changes && changes.scheme !== undefined ? changes.scheme : selected.scheme;
if (!isScheme(scheme) || !accent || !secondary)
return { profiles: customs, profile: null };
if (selected.shipped === true) {
return createCustomProfile(customs, {
name: selected.name + " custom",
scheme: scheme,
accent: accent,
secondary: secondary
});
}
var stored = normalizeStoredProfile(selected);
if (!stored)
return { profiles: customs, profile: null };
var updated = Object.assign({}, stored, {
scheme: scheme,
accent: accent,
secondary: secondary
});
var found = false;
var next = customs.map(function(profile) {
if (profile.id !== updated.id)
return profile;
found = true;
return updated;
});
if (!found)
next.push(updated);
return { profiles: next, profile: updated };
}
function deleteProfile(values, id) {
var customs = validCustomProfiles(values);
for (var shippedIndex = 0; shippedIndex < SHIPPED.length; shippedIndex++) {
if (SHIPPED[shippedIndex].id === id)
return { profiles: customs, removed: false };
}
var next = customs.filter(function(profile) { return profile.id !== id; });
return { profiles: next, removed: next.length !== customs.length };
}
function curatedPair(name, scheme) {
var entry = CURATED[name] || CURATED.blue;
if (scheme === "light")
return { accent: entry.light, secondary: entry.lightSecondary };
return { accent: entry.dark, secondary: entry.darkSecondary };
}
function matchingShippedProfile(scheme, accent, secondary) {
var first = normalizedColor(accent);
var second = normalizedColor(secondary);
for (var index = 0; index < SHIPPED.length; index++) {
var profile = SHIPPED[index];
if (profile.scheme === scheme && profile.accent === first && profile.secondary === second)
return copyProfile(profile);
}
return null;
}
function curatedNameForProfile(profile) {
if (!profile || !isScheme(profile.scheme))
return "";
var accent = normalizedColor(profile.accent);
var secondary = normalizedColor(profile.secondary);
var names = Object.keys(CURATED);
for (var index = 0; index < names.length; index++) {
var name = names[index];
var pair = curatedPair(name, profile.scheme);
if (pair.accent === accent && pair.secondary === secondary)
return name;
}
return "";
}
function clamp(value, minimum, maximum) {
return Math.max(minimum, Math.min(maximum, Number(value)));
}
function channelHex(value) {
var text = Math.round(value).toString(16);
return text.length < 2 ? "0" + text : text;
}
function hsvToHex(hue, saturation, value) {
var h = Number(hue);
var s = Number(saturation);
var v = Number(value);
if (!isFinite(h) || !isFinite(s) || !isFinite(v))
return null;
h = ((h % 360) + 360) % 360;
s = clamp(s, 0, 100) / 100;
v = clamp(v, 0, 100) / 100;
var chroma = v * s;
var section = h / 60;
var x = chroma * (1 - Math.abs(section % 2 - 1));
var red = 0;
var green = 0;
var blue = 0;
if (section < 1) { red = chroma; green = x; }
else if (section < 2) { red = x; green = chroma; }
else if (section < 3) { green = chroma; blue = x; }
else if (section < 4) { green = x; blue = chroma; }
else if (section < 5) { red = x; blue = chroma; }
else { red = chroma; blue = x; }
var match = v - chroma;
return "#" + channelHex((red + match) * 255)
+ channelHex((green + match) * 255)
+ channelHex((blue + match) * 255);
}
function hexToHsv(value) {
var color = normalizedColor(value);
if (!color)
return null;
var red = parseInt(color.slice(1, 3), 16) / 255;
var green = parseInt(color.slice(3, 5), 16) / 255;
var blue = parseInt(color.slice(5, 7), 16) / 255;
var maximum = Math.max(red, green, blue);
var minimum = Math.min(red, green, blue);
var delta = maximum - minimum;
var hue = 0;
if (delta !== 0) {
if (maximum === red)
hue = 60 * (((green - blue) / delta) % 6);
else if (maximum === green)
hue = 60 * ((blue - red) / delta + 2);
else
hue = 60 * ((red - green) / delta + 4);
}
if (hue < 0)
hue += 360;
return {
h: Math.round(hue),
s: Math.round((maximum === 0 ? 0 : delta / maximum) * 100),
v: Math.round(maximum * 100)
};
}
if (typeof module !== "undefined") {
module.exports = {
MAX_NAME_LENGTH: MAX_NAME_LENGTH,
shippedProfiles: shippedProfiles,
curatedAccents: curatedAccents,
validCustomProfiles: validCustomProfiles,
profileCatalog: profileCatalog,
createCustomProfile: createCustomProfile,
findProfile: findProfile,
editProfile: editProfile,
deleteProfile: deleteProfile,
curatedPair: curatedPair,
matchingShippedProfile: matchingShippedProfile,
curatedNameForProfile: curatedNameForProfile,
hsvToHex: hsvToHex,
hexToHsv: hexToHsv
};
}
@@ -0,0 +1,108 @@
pragma Singleton
import Quickshell
import QtQuick
import qs.config
import "ThemeProfileModel.js" as ThemeProfileModel
Singleton {
id: root
readonly property var customProfiles: ThemeProfileModel.validCustomProfiles(
DesktopPreferences.get("themeProfiles"))
readonly property var curatedAccents: ThemeProfileModel.curatedAccents()
readonly property var profiles: ThemeProfileModel.profileCatalog(root.customProfiles)
readonly property string activeId: DesktopPreferences.get("themeProfileId") || "moon"
readonly property var activeProfile: ThemeProfileModel.findProfile(
root.customProfiles, root.activeId) || ThemeProfileModel.shippedProfiles()[0]
readonly property string activeAccentName: ThemeProfileModel.curatedNameForProfile(
root.activeProfile)
property bool reconciling: false
function commitActive(profile: var): bool {
if (!profile)
return false;
// Scheme first: if another control changed it, the reconciliation hook
// may briefly select Day/Moon before this final stable profile id lands.
const schemeAccepted = SystemSettings.commitPreference("colorScheme", profile.scheme);
const profileAccepted = SystemSettings.commitPreference("themeProfileId", profile.id);
return schemeAccepted && profileAccepted;
}
function selectProfile(id: string): bool {
return root.commitActive(ThemeProfileModel.findProfile(root.customProfiles, id));
}
// Editing a shipped profile creates a saved custom copy. Editing a custom
// profile updates only that record. ThemeProfileModel enforces both rules,
// leaving this singleton responsible only for validated preference routing.
function setAccentPair(accent: string, secondary: string): bool {
const result = ThemeProfileModel.editProfile(root.customProfiles, root.activeProfile, {
accent: accent,
secondary: secondary
});
if (!result.profile)
return false;
if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
return false;
return SystemSettings.commitPreference("themeProfileId", result.profile.id);
}
function saveProfile(name: string): bool {
const result = ThemeProfileModel.createCustomProfile(root.customProfiles, {
name: name,
scheme: root.activeProfile.scheme,
accent: root.activeProfile.accent,
secondary: root.activeProfile.secondary
});
if (!result.profile)
return false;
if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
return false;
return SystemSettings.commitPreference("themeProfileId", result.profile.id);
}
function deleteProfile(id: string): bool {
const result = ThemeProfileModel.deleteProfile(root.customProfiles, id);
if (!result.removed)
return false;
if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
return false;
if (root.activeId === id)
return root.selectProfile(DesktopPreferences.get("colorScheme") === "light" ? "day" : "moon");
return true;
}
function useCuratedAccent(name: string): bool {
const scheme = DesktopPreferences.get("colorScheme") === "light" ? "light" : "dark";
const pair = ThemeProfileModel.curatedPair(name, scheme);
const shipped = ThemeProfileModel.matchingShippedProfile(
scheme, pair.accent, pair.secondary);
const accepted = shipped
? root.commitActive(shipped)
: root.setAccentPair(pair.accent, pair.secondary);
if (accepted)
SystemSettings.commitPreference("accentName", name);
return accepted;
}
function reconcileScheme(): void {
if (root.reconciling)
return;
const scheme = DesktopPreferences.get("colorScheme") === "light" ? "light" : "dark";
if (root.activeProfile.scheme === scheme)
return;
root.reconciling = true;
SystemSettings.commitPreference("themeProfileId", scheme === "light" ? "day" : "moon");
root.reconciling = false;
}
Component.onCompleted: root.reconcileScheme()
Connections {
target: DesktopPreferences
function onRevisionChanged(): void { root.reconcileScheme(); }
}
}
+18 -7
View File
@@ -45,9 +45,10 @@ Singleton {
readonly property string outputSignature: root.outputNames().slice().sort().join("|") readonly property string outputSignature: root.outputNames().slice().sort().join("|")
property var outputNames: function() { property var outputNames: function() {
if (Array.isArray(root.outputOverride)) const outputs = Array.isArray(root.outputOverride)
return root.outputOverride.slice(); ? root.outputOverride.slice()
return Quickshell.screens.map(screen => screen.name).filter(name => !!name); : Quickshell.screens.map(screen => screen.name).filter(name => !!name);
return root.primaryFirstOutputs(outputs);
} }
readonly property var searchRoots: [ readonly property var searchRoots: [
@@ -59,10 +60,8 @@ Singleton {
Process { Process {
id: scan id: scan
command: ["bash", "-lc", command: [Quickshell.shellDir + "/scripts/panama-wallpaper-scan"]
"find " + root.searchRoots.map(dir => `'${dir}'`).join(" ") .concat(root.searchRoots)
+ " -maxdepth 2 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\)"
+ " -printf '%T@ %p\\n' 2>/dev/null | sort -rn | cut -d' ' -f2- | head -60"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
root.available = this.text.split("\n") root.available = this.text.split("\n")
@@ -164,6 +163,18 @@ Singleton {
activeQuery.running = true; activeQuery.running = true;
} }
// The saved primary role anchors Panama's per-monitor choices. Keep the
// compositor's remaining order stable so a reconnect does not reshuffle
// the rest of the picker unnecessarily.
function primaryFirstOutputs(outputs: var): var {
const stored = DesktopPreferences.get("displays");
const layouts = stored && typeof stored === "object" ? stored : {};
const primary = outputs.find(output => layouts[output]?.primary === true);
return primary === undefined
? outputs
: [primary].concat(outputs.filter(output => output !== primary));
}
function candidates(extra: var): var { function candidates(extra: var): var {
const result = []; const result = [];
const discovered = Array.isArray(root.candidateOverride) const discovered = Array.isArray(root.candidateOverride)
@@ -0,0 +1,40 @@
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
import qs.services
ShellRoot {
IpcHandler {
target: "theme-profiles-test"
function status(): string {
return JSON.stringify({
active: ThemeProfiles.activeProfile,
profiles: ThemeProfiles.profiles,
stored: DesktopPreferences.get("themeProfiles")
});
}
function select(id: string): bool {
return ThemeProfiles.selectProfile(id);
}
function scheme(value: string): bool {
return SystemSettings.commitPreference("colorScheme", value);
}
function edit(accent: string, secondary: string): bool {
return ThemeProfiles.setAccentPair(accent, secondary);
}
function save(name: string): bool {
return ThemeProfiles.saveProfile(name);
}
function remove(id: string): bool {
return ThemeProfiles.deleteProfile(id);
}
}
}
@@ -7,7 +7,7 @@ import qs.services
ShellRoot { ShellRoot {
Component.onCompleted: { Component.onCompleted: {
Wallpaper.outputOverride = ["DP-2", "HDMI-A-1"]; Wallpaper.outputOverride = ["HDMI-A-1", "DP-2"];
Wallpaper.candidateOverride = ["/images/a.jpg", "/images/b.jpg", "/images/c.jpg"]; Wallpaper.candidateOverride = ["/images/a.jpg", "/images/b.jpg", "/images/c.jpg"];
Wallpaper.startupRestoreEnabled = false; Wallpaper.startupRestoreEnabled = false;
Wallpaper.slideshowIntervalOverrideMs = 60000; Wallpaper.slideshowIntervalOverrideMs = 60000;
@@ -63,7 +63,9 @@ ShellRoot {
assignments: DesktopPreferences.get("wallpaperPerMonitor"), assignments: DesktopPreferences.get("wallpaperPerMonitor"),
slideshowPath: Wallpaper.slideshowPath, slideshowPath: Wallpaper.slideshowPath,
shuffleBag: Wallpaper.shuffleBag, shuffleBag: Wallpaper.shuffleBag,
slideshowTimerRunning: Wallpaper.slideshowTimerRunning slideshowTimerRunning: Wallpaper.slideshowTimerRunning,
scanning: Wallpaper.scanning,
available: Wallpaper.available
}); });
} }
} }
@@ -46,6 +46,16 @@ ShellRoot {
return JSON.stringify(root.calls); return JSON.stringify(root.calls);
} }
function selectOutput(output: string): void {
controls.selectedOutput = output;
}
function updateOutputs(outputs: string): void {
controls.outputs = outputs === "" ? [] : outputs.split("|");
}
function selectedOutput(): string { return controls.selectedOutput; }
function states(): string { function states(): string {
picker.mode = "single"; picker.mode = "single";
const single = { const single = {
+35
View File
@@ -0,0 +1,35 @@
# Firefox chrome
This directory is **not Panama's work**. It is [Edge-Frfox][upstream] — "a
Firefox userChrome.css theme that aims to recreate the look and feel of
Microsoft Edge" — vendored here, plus two local tweaks.
- Upstream: <https://github.com/bmFtZQ/edge-frfox>
- License: MIT
`link-dotfiles` places `user.js` and `chrome/` into the Firefox profile. The
preferences in `user.js` are what the theme needs to work at all —
`toolkit.legacyUserProfileCustomizations.stylesheets` in particular, without
which Firefox ignores `chrome/` entirely.
## Why it is vendored rather than fetched
The same reason the cursor theme is: downloading it during the install would add
a URL that can rot, and this repository is trying to stop installing things that
way. 732K in git once, and then the theme is simply there.
## Two things to know before editing
`userChrome.css` imports `custom.css` last, deliberately — local changes go
there, not into the upstream files, so a future update to the theme is a
directory replacement rather than a merge. The imports of
`tweaks/hide-tabs-bar.css` and `tweaks/shrink-sideberry.css` are personal
choices carried over from sunhat, not upstream defaults.
`userChrome.css` is unsupported by Mozilla. It breaks on major Firefox releases,
and when it does, the fix is upstream's or none. Panama installs Firefox as a
second engine to test pages against; if the theme ever costs more than it is
worth, deleting this directory and the `link-dotfiles` block that reads it
leaves a working browser behind.
[upstream]: https://github.com/bmFtZQ/edge-frfox
+105
View File
@@ -0,0 +1,105 @@
/* Edge-Frfox - content/common.css */
/*
MARK: Default colours
*/
@-moz-document url-prefix(about:) {
:root {
--in-content-page-color: light-dark(#252525, #fff) !important;
--in-content-page-background: light-dark(#f7f7f7, #252525) !important;
--in-content-deemphasized-text: light-dark(#666, #bbb) !important;
--in-content-box-background: light-dark(#fff, #333) !important;
--in-content-box-background-odd: light-dark(rgb(0 0 0 / .05), rgb(255 255 255 / .05)) !important;
--in-content-box-info-background: light-dark(#f7f7f7, rgb(255 255 255 / 0.15)) !important;
--in-content-icon-color: light-dark(#666, #fff) !important;
--in-content-accent-color: light-dark(#006CBE, #75B6E8) !important;
--in-content-accent-color-active: light-dark(#1683D8, #4C98D1) !important;
--in-content-border-hover: light-dark(rgb(0 0 0 / .5), rgb(255 255 255 / .3)) !important;
--in-content-border-invalid: var(--red-50) !important;
--in-content-border-color: light-dark(#bebebe, rgb(255 255 255 / .2)) !important;
--in-content-error-text-color: light-dark(#FF848A, #FF9AA2) !important;
--in-content-link-color: light-dark(#0078D4, #75B6E8) !important;
--in-content-link-color-hover: light-dark(#006CBE, #63ADE5) !important;
--in-content-link-color-active: light-dark(#0749AC, #4C98D1) !important;
--in-content-link-color-visited: light-dark(#0078D4, #75B6E8) !important;
--link-color: var(--in-content-link-color) !important;
--link-color-hover: var(--in-content-link-color-hover) !important;
--link-color-active: var(--in-content-link-color-active) !important;
--link-color-visited: var(--in-content-link-color-visited) !important;
--in-content-button-text-color: var(--in-content-text-color) !important;
--in-content-button-text-color-hover: var(--in-content-text-color) !important;
--in-content-button-text-color-active: var(--in-content-button-text-color-hover) !important;
--in-content-button-background: light-dark(#EDEDED, #3B3B3B) !important;
--in-content-button-background-hover: light-dark(#e5e5e5, #545454) !important;
--in-content-button-background-active: light-dark(#d5d5d5, #606060) !important;
--in-content-primary-button-text-color: light-dark(#fff, #fff) !important;
--in-content-primary-button-background: light-dark(#0061e0, #006CBE) !important;
--in-content-primary-button-background-hover: light-dark(#0250bb, #0078D4) !important;
--in-content-primary-button-background-active: light-dark(#053e94, #005CA3) !important;
--color-accent-primary: light-dark(#0078D4, #006CBE) !important;
--color-accent-primary-hover: light-dark(#006CBE, #0078D4) !important;
--color-accent-primary-active: light-dark(#0749AC, #005CA3) !important;
--in-content-danger-button-background: light-dark(#DF4951, #E12424) !important;
--in-content-danger-button-background-hover: light-dark(#EA656C, #F94343) !important;
--in-content-danger-button-background-active: light-dark(#BB353C, #AA1E1E) !important;
--in-content-focus-outline-color: light-dark(rgb(0 85 215 / .5), #63ADE5) !important;
--focus-outline-color: var(--in-content-focus-outline-color) !important;
--in-content-table-background: light-dark(#f7f7f7, #252525) !important;
--in-content-text-color: var(--in-content-page-color) !important;
--border-interactive-color: light-dark(#929292, #858585) !important;
scrollbar-color:
light-dark(rgb(37 37 37 / .4), rgb(255 255 255 / .4))
light-dark(#f7f7f7, #252525) !important;
}
::selection {
background-color: light-dark(#0078D4, #93B8E7);
color: light-dark(#fff, #000);
}
}
/*
MARK: Preferences page
*/
@-moz-document url-prefix(about:preferences),
url-prefix(about:addons) {
.category[selected] {
position: relative !important;
border-radius: 4px !important;
}
.category[selected]:not(:hover, :active) {
background-color: var(--in-content-button-background) !important;
}
.category[selected]::before {
content: "";
position: absolute;
inset-block: 0;
margin-block: auto;
height: 24px;
width: 3px;
left: 2px;
background-color: var(--in-content-accent-color);
border-radius: 3px;
}
button.tab-button:hover {
border-block-color: transparent var(--in-content-box-border-color) !important;
}
button.tab-button[selected],
button.tab-button[selected]:hover {
border-block-color: transparent currentColor !important;
}
.toggle-button {
--toggle-dot-margin: 4px !important;
--toggle-width: 40px !important;
--toggle-height: 20px !important;
--toggle-background-color: transparent !important;
--toggle-dot-background-color: var(--in-content-page-color) !important;
--toggle-dot-background-color-on-pressed: #fff !important;
--toggle-dot-transform-x: calc(var(--toggle-width) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width) - var(--toggle-dot-width)) !important;
}
}
+415
View File
@@ -0,0 +1,415 @@
/* Edge-Frfox - content/newtab.css */
/*
MARK: Default colours
*/
@-moz-document url-prefix("about:newtab"),
url-prefix("about:home"),
url-prefix("about:firefoxview") {
:root:not([lwt-newtab], [lwt-newtab-brighttext]) {
--newtab-background-color: #F7F7F7 !important;
--newtab-background-color-secondary: #fff !important;
--newtab-text-primary-color: #252525 !important;
--newtab-primary-action-background: #0078D4 !important;
--newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 80%, transparent) !important;
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 95%, #000) !important;
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000) !important;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent) !important;
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent) !important;
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent) !important;
--newtab-primary-element-hover-color: color-mix(in srgb, var(--newtab-primary-action-background) 90%, #000) !important;
--newtab-primary-element-active-color: color-mix(in srgb, var(--newtab-primary-action-background) 80%, #000) !important;
--newtab-primary-element-text-color: #fff !important;
--newtab-primary-action-background-dimmed: color-mix(in srgb, var(--newtab-primary-action-background) 25%, transparent) !important;
--newtab-border-color: color-mix(in srgb, var(--newtab-background-color) 75%, #000) !important;
--newtab-wordmark-color: #737373 !important;
--newtab-status-success: #50B080 !important;
--newtab-status-error: #FF848A !important;
--newtab-inner-box-shadow-color: rgb(0 0 0 / .1) !important;
--newtab-overlay-color: color-mix(in srgb, var(--newtab-background-color) 85%, transparent) !important;
--newtab-text-emphasis-background: #FFED32 !important;
--newtab-text-emphasis-text-color: #000 !important;
--newtab-textbox-focus-color: var(--newtab-primary-action-background) !important;
--newtab-textbox-focus-boxshadow: 0 0 0 1px var(--newtab-primary-action-background), 0 0 0 4px rgba(var(--newtab-primary-action-background), 0.3) !important;
--newtab-button-secondary-color: inherit !important;
}
:root:not([style*="color"])[lwt-newtab-brighttext] {
--newtab-background-color: #252525 !important;
--newtab-background-color-secondary: #4A4A4A !important;
--newtab-text-primary-color: #fff !important;
--newtab-primary-action-background: #75B6E8 !important;
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 95%, #FFF) !important;
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 90%, #FFF) !important;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 10%, transparent) !important;
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 17%, transparent) !important;
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 30%, transparent) !important;
--newtab-border-color: color-mix(in srgb, var(--newtab-background-color) 75%, #FFF) !important;
--newtab-primary-element-text-color: #252525 !important;
--newtab-wordmark-color: #fff !important;
--newtab-status-success: #50B080 !important;
}
/*
MARK: Custom GTK colours
*/
@media (-moz-gtk-csd-available) {
:root:not([lwt-newtab]):not([style*="color"]) {
/* Colour adjusted for readability. */
--uc-accent-adjusted: light-dark(color-mix(in oklch, AccentColor 92%, black), color-mix(in oklch, AccentColor 60%, white));
--newtab-background-color: light-dark(-moz-dialog, color-mix(in srgb, -moz-dialog 82%, black)) !important;
--newtab-background-color-secondary: light-dark(Field, color-mix(in srgb, -moz-dialog 86%, white)) !important;
--newtab-text-primary-color: -moz-dialogtext !important;
--newtab-wordmark-color: light-dark(color-mix(in srgb, -moz-dialogtext 65%, transparent), -moz-dialogtext) !important;
--newtab-primary-action-background: var(--uc-accent-adjusted) !important;
}
}
/* Adjustments for themes. */
:root[style*="--newtab-background-color"] {
/* Make the Firefox logo workmark follow text colour. */
--newtab-wordmark-color: color-mix(in srgb, var(--newtab-text-primary-color) 60%, var(--newtab-background-color)) !important;
/* If there is no secondary background, create one from other colours. */
&:not([style*="--newtab-background-color-secondary"]) {
--newtab-background-color-secondary: color-mix(in srgb, var(--newtab-background-color) 15%, white) !important;
}
/* Dark adjustments for themes. */
&[lwt-newtab-brighttext] {
/* Make the Firefox logo workmark follow text colour. */
--newtab-wordmark-color: var(--newtab-text-primary-color) !important;
/* If there is no secondary background, create one from other colours. */
&:not([style*="--newtab-background-color-secondary"]) {
--newtab-background-color-secondary: color-mix(in srgb, var(--newtab-text-primary-color) 15%, var(--newtab-background-color)) !important;
}
}
}
}
/*
MARK: New tab page
*/
@-moz-document url-prefix("about:newtab"),
url-prefix("about:home") {
/* Enforce system font selection. */
:where(body) {
font-family: system-ui !important;
}
/* Hide pin icons. */
.icon.icon-pin-small {
display: none !important;
}
/* Unpinned sites have lower opacity titles. */
.top-site-button .title:not(.pinned) {
opacity: 0.7 !important;
}
/*
MARK: Search bar
*/
.search-wrapper {
& .search-handoff-button {
border-radius: 99px !important;
background-position: 12px !important;
padding-inline-start: 48px !important;
white-space: nowrap !important;
text-overflow: ellipsis !important;
--elevation: 4;
box-shadow:
0 0 calc((var(--elevation) * 0.225px) + 2px) rgb(0 0 0 / .11),
0 calc(var(--elevation) * 0.4px) calc((var(--elevation) * 0.9px)) rgb(0 0 0 / .13) !important;
&:hover {
--elevation: 6;
}
}
& .search-inner-wrapper {
min-height: 44px !important;
}
&.fake-focus:not(.search.disabled) .search-handoff-button {
border-color: transparent !important;
outline: 2px solid var(--newtab-primary-action-background) !important;
outline-offset: -1px !important;
}
&.search-disabled .search-handoff-button {
opacity: 0 !important;
}
.search-handoff-button .fake-caret {
inset-inline-start: 48px !important;
top: 13px !important;
}
@media (height > 700px) {
.fixed-search & {
padding: 15px 0 !important;
min-height: auto !important;
border-bottom: none !important;
}
}
}
.fake-textbox {
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.outer-wrapper.only-search {
display: flex !important;
padding: 30px !important;
}
/* Sticky search bar header when scrolling. */
@media (height > 700px) {
body:not(.inline-onboarding) .fixed-search.visible-logo main {
padding-top: 246px !important;
@media (-moz-bool-pref: "uc.tweak.hide-newtab-logo") {
padding-top: 82px !important;
}
}
}
.icon-settings {
background-image: url("../icons/settings.svg") !important;
}
/*
MARK: Top sites tiles
*/
.top-site-outer {
& .tile {
border-radius: 12px !important;
height: 44px !important;
width: 44px !important;
box-shadow: rgb(0 0 0 / 0.1) 0px 2px 4px !important;
& .icon-wrapper {
border-radius: 4px !important;
width: 24px !important;
height: 24px !important;
&.letter-fallback::before {
font-size: 32px !important;
}
}
}
& .default-icon,
& .search-topsite {
background-size: 16px !important;
height: 24px !important;
width: 24px !important;
}
}
/*
MARK: Recent activity cards
*/
.card-outer {
--uc-border-radius: 8px;
&,
& .card,
& .ds-card-grid.ds-card-grid-border .ds-card:not(.placeholder) {
border-radius: var(--uc-border-radius) !important;
}
& .card,
& .ds-card-grid.ds-card-grid-border .ds-card:not(.placeholder) {
box-shadow:
rgb(0 0 0 / 0.14) 0px 1px 2px,
rgb(0 0 0 / 0.12) 0px 0px 2px !important;
}
& .card-preview-image-outer,
& .ds-card-grid.ds-card-grid-border .ds-card:not(.placeholder) .img-wrapper .img img {
border-radius: var(--uc-border-radius) var(--uc-border-radius) 0 0 !important
}
}
/* Add background & adjust padding for Recent activity & Pocket section. */
.collapsible-section:not(.top-sites) {
background-color: var(--newtab-background-color) !important;
border-radius: 12px !important;
padding: 10px 20px 20px !important;
& .section-list {
gap: 12px !important;
}
}
@media (-moz-bool-pref: "uc.tweak.hide-newtab-logo") {
.logo-and-wordmark {
display: none !important;
}
.outer-wrapper:not(.fixed-search) .search-wrapper {
padding-top: 0 !important;
}
}
}
/*
MARK: Private browsing
*/
@-moz-document url-prefix(about:privatebrowsing) {
:root {
--uc-wordmark-color: light-dark(#737373, #fff);
--uc-background-color-secondary: light-dark(#fff, #4a4a4a);
@media (-moz-bool-pref: "browser.theme.dark-private-windows") {
color-scheme: dark !important;
}
}
/* Custom GTK colours. */
@media (-moz-gtk-csd-available) {
:root:not([lwt-newtab]):not([style*="color"]) {
/* Colour adjusted for readability. */
--uc-accent-adjusted: light-dark(color-mix(in oklch, AccentColor 92%, black), color-mix(in oklch, AccentColor 60%, white));
--in-content-page-background: light-dark(-moz-dialog, color-mix(in srgb, -moz-dialog 82%, black)) !important;
--uc-background-color-secondary: light-dark(Field, color-mix(in srgb, -moz-dialog 86%, white)) !important;
--in-content-page-color: -moz-dialogtext !important;
--in-content-focus-outline-color: var(--uc-accent-adjusted) !important;
}
}
/*
MARK: Search bar
*/
.search-inner-wrapper {
height: 44px !important;
.search-handoff-button,
.search-handoff-button:active,
.search-handoff-button:enabled:hover:active {
background-color: var(--uc-background-color-secondary) !important;
border: 0 !important;
border-radius: 99px !important;
padding-inline-start: 48px !important;
--elevation: 4;
box-shadow:
0 0 calc((var(--elevation) * 0.225px) + 2px) rgb(0 0 0 / .11),
0 calc(var(--elevation) * 0.4px) calc((var(--elevation) * 0.9px)) rgb(0 0 0 / .13) !important;
&:hover {
--elevation: 6;
}
&.focused:not(.disabled) {
box-shadow: none !important;
outline: 2px solid var(--in-content-focus-outline-color) !important;
outline-offset: -1px !important;
}
&.disabled {
opacity: 0 !important;
}
& .fake-textbox {
color: var(--in-content-page-color) !important;
}
}
}
.wordmark {
fill: var(--uc-wordmark-color) !important;
}
/* Information box in private browsing. */
.info {
background-color: var(--uc-background-color-secondary) !important;
background-image: none !important;
position: relative !important;
/* Private browsing icon. */
&::before {
content: "";
position: absolute;
left: 32px;
top: 20px;
height: 32px;
aspect-ratio: 1 / 1;
background: center / contain url("../icons/account-private.svg");
border-radius: 99px;
}
}
}
/*
MARK: Background images
*/
@media (-moz-bool-pref: "uc.tweak.newtab-background") {
@-moz-document url(about:newtab),
url(about:home),
url(about:privatebrowsing) {
body {
/* Specify both jpg and png file extensions, but jpg images will always
* have a higher priority. */
--bg-0: url(../background-0.jpg), url(../background-0.png);
--bg-1: url(../background-1.jpg), url(../background-1.png);
--newtab-element-hover-color: rgb(239 239 239 / .3) !important;
--newtab-element-active-color: rgb(239 239 239 / .45) !important;
--text-shadow: 0 1px 2px #000;
--icon-shadow: drop-shadow(0 0.3px 0.6px #000);
background-color: #252525 !important;
background-image: var(--bg-0) !important;
background-position: center !important;
background-size: cover !important;
background-attachment: fixed !important;
:root[lwt-newtab-brighttext] &,
:root.private & {
background-image: var(--bg-1), var(--bg-0) !important;
--newtab-element-hover-color: rgb(66 66 66 / .4) !important;
--newtab-element-active-color: rgb(66 66 66 / .55) !important;
}
@media not (-moz-bool-pref: "browser.theme.dark-private-windows") {
:root.private & {
background-image: var(--bg-0) !important;
@media (prefers-color-scheme: dark) {
background-image: var(--bg-1), var(--bg-0) !important;
}
}
}
}
/* Change text colour & add backgrounds to text over the background. */
.top-site-outer .title {
text-shadow: var(--text-shadow) !important;
color: #fff !important;
-moz-osx-font-smoothing: auto !important;
}
.wordmark,
.icon-settings,
.top-site-outer .context-menu-button {
filter: var(--icon-shadow) !important;
color: #fff !important;
fill: currentColor !important
}
.top-site-outer .title .sponsored-label {
color: #eee !important;
}
/* Add transparent sticky search header. */
@media (height > 700px) {
.fixed-search .search-wrapper {
border-bottom: 0 !important;
background: transparent linear-gradient(#0004, transparent) !important;
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
/* Local overrides.
*
* userChrome.css imports this last, so anything here wins over Edge-Frfox
* without editing its files -- which keeps updating the theme a matter of
* replacing the directory rather than merging into it.
*
* Deliberately empty. The import resolving to nothing is not an error in CSS,
* but a file that exists is a seam somebody can find. */
+206
View File
@@ -0,0 +1,206 @@
/* Edge-Frfox - browser.css
* https://github.com/bmFtZQ/edge-frfox */
/* Windows has 1px missing space on top of window, but not when accent colour is
* applied to the title bar. */
@media (-moz-platform: windows) and (not (-moz-windows-accent-color-in-titlebar)) {
:root[sizemode="normal"][tabsintitlebar]:not([inFullscreen]) #navigator-toolbox {
padding-top: 1px !important;
}
}
/* Add a shadow when the toolbars move over content, eg. in macOS hovering over
* the menu bar in fullscreen mode. */
#navigator-toolbox[style*="transform"] {
box-shadow: 0 -3px 5px 3px light-dark(rgb(0 0 0 / .3), rgb(0 0 0 / .6)) !important;
}
/*
MARK: Rounded corners (Rc)
*/
@media (-moz-bool-pref: "uc.tweak.rounded-corners") {
/* Since Firefox 120, when the bookmarks bar is set to only open on new tabs,
* the bookmarks bar overlaps the #browser element. */
/* Change the element that shifts the browser content to the correct position. */
#main-window[BookmarksToolbarOverlapsBrowser] .newTabBrowserPanel,
#main-window[BookmarksToolbarOverlapsBrowser] #sidebar-box {
padding-top: 0 !important;
}
#main-window[BookmarksToolbarOverlapsBrowser] #browser {
margin-top: var(--bookmarks-toolbar-overlapping-browser-height);
}
/* Make sure that the toolbars are not positioned in-front of the browser
* element. */
#main-window[BookmarksToolbarOverlapsBrowser] #navigator-toolbox:not([style*="z-index"]) {
z-index: auto !important;
}
/* Move the browser element in-front of the toolbars when on the new tab page. */
#main-window:not([BookmarksToolbarOverlapsBrowser]) #browser {
z-index: 1 !important;
}
/*
MARK: Rc Variables
*/
#main-window:not([chromehidden~="toolbar"]) {
--uc-tweak-rounded-corners-padding: 4px;
--uc-tweak-rounded-corners-radius: 8px;
--uc-tweak-rounded-corners-shadow:
0 -.8px 0 0 rgb(0 0 0 / 0.02),
0 0.5px 1px 1px rgb(0 0 0 / 0.06),
0 1px 1px rgb(0 0 0 / 0.1);
--browser-frame-bgcolor: var(--toolbar-bgcolor, var(--lwt-accent-color));
/* Different shadow for higher resolution displays. */
@media (resolution > 1.8dppx) {
--uc-tweak-rounded-corners-shadow:
0 0 0.5px 0.5px rgb(0 0 0 / 0.06),
0 0.5px 1.5px rgb(0 0 0 / 0.1);
}
@media (-moz-bool-pref: "uc.tweak.floating-tabs") {
--browser-frame-bgcolor: var(--lwt-accent-color);
&:-moz-window-inactive {
--browser-frame-bgcolor: var(--lwt-accent-color-inactive, var(--lwt-accent-color));
}
}
}
/* Hide border below titlebar on macOS, as its titlebar already has a border. */
@media (-moz-platform: macos) {
:root[chromehidden~="toolbar"][chromehidden~="location"][chromehidden~="directories"] {
border-top: none !important;
}
}
/* Remove separator between toolbars and the browser window. */
#navigator-toolbox {
border-bottom: none !important;
}
/* Disable rounded corners in fullscreen, only if the toolbar is hidden. */
:root[inFullscreen] #navigator-toolbox[style*="margin-top"] + #browser,
:root[inDOMFullscreen] #browser {
--uc-tweak-rounded-corners-padding: 0;
--uc-tweak-rounded-corners-radius: 0;
--uc-tweak-rounded-corners-shadow: unset;
}
/*
MARK: Rc Browser
*/
#appcontent .browserStack {
margin-inline: var(--uc-tweak-rounded-corners-padding) !important;
margin-block-end: var(--uc-tweak-rounded-corners-padding) !important;
border-radius: var(--uc-tweak-rounded-corners-radius) !important;
box-shadow: var(--uc-tweak-rounded-corners-shadow) !important;
background-color: var(--tabpanel-background-color) !important;
position: relative !important;
/* Use pseudo element overlay to simulate rounded corners, similar to the
* tab corners, fixes bug with backdrop-filter. */
&::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
/* Draw toolbar background on the negative corner space, then draw the
* same shadow from the browser window on top. */
box-shadow:
var(--uc-tweak-rounded-corners-shadow, 0 0 transparent),
0 0 0 16px var(--browser-frame-bgcolor) !important;
clip-path: inset(0);
pointer-events: none;
}
}
/* Prevent status panel corners and shadow from appearing outside the browser. */
#statuspanel {
overflow: hidden;
}
/*
MARK: Rc Sidebar
*/
#sidebar-box {
margin-inline-start: var(--uc-tweak-rounded-corners-padding) !important;
margin-block-end: var(--uc-tweak-rounded-corners-padding) !important;
border-radius: var(--uc-tweak-rounded-corners-radius) !important;
overflow: hidden !important;
box-shadow: var(--uc-tweak-rounded-corners-shadow) !important;
&[positionend] {
margin-inline-start: 0 !important;
margin-inline-end: var(--uc-tweak-rounded-corners-padding) !important;
}
}
/*
MARK: Rc Devtools
*/
/* Devtools window when docked to the left or right. */
.devtools-toolbox-side-iframe {
margin-block-end: var(--uc-tweak-rounded-corners-padding) !important;
border-radius: var(--uc-tweak-rounded-corners-radius) !important;
overflow: hidden !important;
box-shadow: var(--uc-tweak-rounded-corners-shadow) !important;
/* Devtools window when docked to the left. */
&:first-child {
margin-inline-start: var(--uc-tweak-rounded-corners-padding) !important;
}
/* Devtools window when docked to the right. */
&:last-child {
margin-inline-end: var(--uc-tweak-rounded-corners-padding) !important;
}
}
/* Devtools window when docked to the bottom. */
.devtools-toolbox-bottom-iframe {
margin: var(--uc-tweak-rounded-corners-padding) !important;
margin-block-start: 0 !important;
border-radius: var(--uc-tweak-rounded-corners-radius) !important;
overflow: hidden !important;
box-shadow: var(--uc-tweak-rounded-corners-shadow) !important;
}
/* Hide devtools splitter. */
.devtools-side-splitter,
.devtools-horizontal-splitter {
background-color: transparent !important;
}
/* Responsive design mode. */
.browserContainer.responsive-mode {
background-color: transparent !important;
& .rdm-toolbar {
margin-inline: var(--uc-tweak-rounded-corners-padding) !important;
border-top-left-radius: var(--uc-tweak-rounded-corners-radius) !important;
border-top-right-radius: var(--uc-tweak-rounded-corners-radius) !important;
width: calc(100% - var(--uc-tweak-rounded-corners-padding) * 2) !important;
}
}
/* Remove sidebar border when not in full screen. */
:not(:root[inFullscreen] #navigator-toolbox[style*="margin-top"]) + #browser #sidebar-splitter {
border-inline-width: 0 !important;
}
/* Apply colour to the frame around the browser window. */
#browser {
background-color: var(--browser-frame-bgcolor);
}
/* Change colour of page when loading */
#tabbrowser-tabpanels {
background-color: transparent !important;
}
}
+366
View File
@@ -0,0 +1,366 @@
/* Edge-Frfox - colors.css
* https://github.com/bmFtZQ/edge-frfox */
/*
MARK: Default colours
*/
:root:not([lwtheme]),
/* `--newtab-background-color-secondary used` to differentiate between a dark
* private window and private window with the 'Dark' theme enabled.
* May not be the most reliable method, but works for now up to FF 123. */
:root[privatebrowsingmode="temporary"]:where([style*="--lwt-accent-color: rgb(28, 27, 34)"]:not([style*="--newtab-background-color-secondary: rgb(66, 65, 77)"])) {
/* Toolbars */
--lwt-accent-color: light-dark(#cecece, #1C1C1C) !important;
--lwt-accent-color-inactive: light-dark(#e8e8e8, #2e2e2e) !important;
--lwt-text-color: light-dark(#000, #FFF) !important;
--lwt-text-color-inactive: light-dark(#3A3A3A, #CBCBCB) !important;
--tab-selected-textcolor: light-dark(#000, #fff) !important;
--tab-selected-textcolor-inactive: light-dark(#3E3E3E, #CBCBCB) !important;
--tab-selected-bgcolor: var(--toolbar-bgcolor) !important;
--tab-attention-icon-color: light-dark(#0078D4, #93d0ff) !important;
--toolbar-bgcolor: light-dark(#f7f7f7, #3B3B3B) !important;
--toolbar-non-lwt-bgcolor: var(--toolbar-bgcolor) !important;
--toolbar-color: light-dark(#000, #fff) !important;
--toolbarbutton-icon-fill: var(--toolbar-color) !important;
--toolbarbutton-icon-fill-attention: light-dark(#0078D4, #4C98D1) !important;
--toolbarseparator-color: color-mix(in srgb, currentColor 25%, transparent) !important;
--chrome-content-separator-color: light-dark(#bfbfbf, #686868) !important;
--newtab-background-color: light-dark(#f7f7f7, #252525) !important;
--tabpanel-background-color: var(--newtab-background-color, var(--toolbar-bgcolor, transparent)) !important;
/* Toolbar button overlays */
--toolbarbutton-hover-background: color-mix(in srgb, currentColor 8%, transparent) !important;
--toolbarbutton-active-background: color-mix(in srgb, currentColor 13.5%, transparent) !important;
/* Tab audio indicator overlay icon */
--tab-icon-overlay-fill: var(--toolbar-color) !important;
--tab-icon-overlay-stroke: var(--toolbar-bgcolor) !important;
/* URL bar */
--toolbar-field-background-color: light-dark(#fff, #2B2B2B) !important;
--toolbar-field-color: light-dark(#000, #fff) !important;
--toolbar-field-border-color: transparent !important;
--toolbar-field-focus-background-color: var(--toolbar-field-background-color) !important;
--toolbar-field-focus-color: var(--toolbar-field-color) !important;
--toolbar-field-focus-border-color: light-dark(#0055D780, #63ADE5) !important;
--urlbar-icon-fill-opacity: 0.58 !important;
--urlbar-box-bgcolor: light-dark(rgb(0 0 0 / .09), rgb(255 255 255 / .11)) !important;
--urlbar-box-hover-bgcolor: light-dark(rgb(0 0 0 / .09), rgb(255 255 255 / .11)) !important;
--urlbar-box-active-bgcolor: light-dark(rgb(0 0 0 / .22), rgb(255 255 255 / .28)) !important;
--urlbar-box-focus-bgcolor: var(--urlbar-box-hover-bgcolor) !important;
/* URL bar suggestions */
--link-color: light-dark(#0078D4, #75B6E8) !important;
--uc-urlbarView-accent-color: light-dark(#1A6397, #75B6E8) !important;
--urlbarView-highlight-background: light-dark(#F2F2F2, #383838) !important;
--urlbarView-highlight-color: light-dark(#000, #fff) !important;
--urlbarView-hover-background: light-dark(#e5e5e5, #444444) !important;
--urlbarView-separator-color: light-dark(#EFEFEF, #5F5F5F) !important;
/* Menus/panels */
--arrowpanel-background: light-dark(#fff, #4A4A4A) !important;
--arrowpanel-color: light-dark(#1c1c1c, #fff) !important;
--arrowpanel-border-color: light-dark(#fff, #636363) !important;
--arrowpanel-dimmed: light-dark(#565656, #565656) !important;
--arrowpanel-dimmed-further: light-dark(#707070, #707070) !important;
--panel-separator-color: light-dark(#dadada, #636363) !important;
--panel-description-color: light-dark(#000a, #fffb) !important;
--panel-disabled-color: light-dark(#9E9E9E, #B9B9B9) !important;
--panel-item-hover-bgcolor: light-dark(#eee, #646464) !important;
--panel-item-active-bgcolor: light-dark(#ddd, #747474) !important;
--panel-banner-item-update-supported-bgcolor: light-dark(#188038, #81C995) !important;
/* Primary button */
--button-primary-bgcolor: light-dark(#0078D4, #006CBE) !important;
--button-primary-color: light-dark(#fff, #fff) !important;
--button-primary-hover-bgcolor: light-dark(#006BBD, #1C7CC5) !important;
--button-primary-active-bgcolor: light-dark(#0060AA, #3289CB) !important;
/* Secondary button */
--button-bgcolor: light-dark(#EFEFEF, #5C5C5C) !important;
--button-color: light-dark(#000, #fff) !important;
--button-hover-bgcolor: light-dark(#D5D5D5, #6E6E6E) !important;
--button-active-bgcolor: light-dark(#C0C0C0, #7C7C7C) !important;
/* Text input field, e.g. bookmarks editor */
--focus-outline-color: light-dark(#0055D780, #63ADE5) !important;
--input-bgcolor: light-dark(#fff, #4A4A4A) !important;
--input-color: light-dark(#2b2b2b, #fff) !important;
--input-border-color: light-dark(#bebebe, #808080) !important;
--error-text-color: light-dark(#FF9AA2, #FF9AA2) !important;
--input-error-border-color: light-dark(#FF848A, #FF848A) !important;
/* Side bar colours. */
--sidebar-background-color: light-dark(#f7f7f7, #3b3b3b) !important;
--sidebar-text-color: light-dark(#000, #fff) !important;
--sidebar-border-color: light-dark(#bfbfbf, #686868) !important;
/* ::::: Dialog theme ::::: */
& #window-modal-dialog::backdrop {
/* Overlay when dialog is shown. */
background-color: rgb(20 20 20 / .2) !important;
}
&[dialogroot] {
/* Dialog */
--in-content-page-background: var(--arrowpanel-background) !important;
--in-content-page-color: var(--arrowpanel-color) !important;
/* Primary button */
--in-content-primary-button-text-color: var(--button-primary-color) !important;
--in-content-primary-button-background: var(--button-primary-bgcolor) !important;
--in-content-primary-button-background-hover: var(--button-primary-hover-bgcolor) !important;
--in-content-primary-button-background-active: var(--button-primary-active-bgcolor) !important;
/* Secondary button */
--in-content-button-text-color: var(--button-color) !important;
--in-content-button-background: var(--button-bgcolor) !important;
--in-content-button-background-hover: var(--button-hover-bgcolor) !important;
--in-content-button-background-active: var(--button-active-bgcolor) !important;
/* Text input */
--in-content-box-background: var(--input-bgcolor) !important;
--in-content-box-border-color: var(--input-border-color) !important;
}
}
/* Ensure that dark mode is enabled completely in private browsing mode. */
@media (-moz-bool-pref: "browser.theme.dark-private-windows") {
:root[privatebrowsingmode="temporary"],
:root[privatebrowsingmode="temporary"] #nav-bar,
:root[privatebrowsingmode="temporary"] #PersonalToolbar {
--toolbar-color-scheme: dark !important;
--tab-selected-color-scheme: dark !important;
color-scheme: dark !important;
}
}
/* Apply colours to the default 'Auto' theme. */
:root:not([lwtheme]) #navigator-toolbox {
background-color: var(--lwt-accent-color) !important;
color: var(--lwt-text-color) !important;
/* Inactive window background colours. */
&:-moz-window-inactive {
background-color: var(--lwt-accent-color-inactive, var(--lwt-accent-color)) !important;
color: var(--lwt-text-color-inactive, var(--lwt-text-color)) !important;
}
}
/* Make sure that the tab toolbar opacity is always 1, inactive window colours
* are already supplied by this theme. */
:root[tabsintitlebar] #titlebar:-moz-window-inactive {
opacity: 1 !important;
}
/* Make the titlebar buttons black/white on default themes */
:root:not([lwtheme])[tabsintitlebar] .titlebar-buttonbox {
color: var(--lwt-text-color) !important;
}
/* Status/link preview panel */
#statuspanel-label {
background-color: var(--toolbar-field-focus-background-color) !important;
color: var(--toolbar-field-focus-color) !important;
border-color: transparent !important;
box-shadow: 0 2px 5px #0002, 0 0 0 1px light-dark(#0001, #0002) !important;
border-radius: 0 4px 0 0 !important;
#statuspanel[mirror] & {
border-radius: 4px 0 0 0 !important;
}
}
/*
MARK: Windows
*/
@media (-moz-platform: windows) {
/* Colour the text selection background and foreground. */
::selection {
background-color: light-dark(#0078D4, #93B8E7) !important;
color: light-dark(#fff, #000) !important;
}
tooltip {
background-color: var(--arrowpanel-background) !important;
color: var(--arrowpanel-color) !important;
border-color: var(--arrowpanel-border-color) !important;
}
/* Use accent colour in titlebar when Windows option is enabled. */
@media (-moz-windows-accent-color-in-titlebar) and (-moz-bool-pref: "browser.theme.windows.accent-color-in-tabs.enabled") {
/* Uses :root:root to beat specificity of floating tabs adjustments. */
:root:root:not([lwtheme]) {
--lwt-accent-color: ActiveCaption !important;
--lwt-text-color: CaptionText !important;
--lwt-accent-color-inactive: InActiveCaption !important;
--lwt-text-color-inactive: InActiveCaptionText !important;
}
}
.titlebar-button:not(.titlebar-close):hover {
background-color: color-mix(in srgb, currentColor 11%, transparent) !important;
&:active {
background-color: color-mix(in srgb, currentColor 20%, transparent) !important;
}
}
.titlebar-close:hover {
stroke: white !important;
background-color: #e81123 !important;
&:active {
background-color: #DC5C66 !important;
}
toolbar[brighttext] &:active {
background-color: #971821 !important;
}
}
/* Library window colour theme. */
window#places {
--organizer-color: light-dark(#000, #fff) !important;
--organizer-deemphasized-color: light-dark(#666, #bbb) !important;
--organizer-toolbar-background: light-dark(#f7f7f7, #3B3B3B) !important;
--organizer-pane-background: light-dark(#f7f7f7, #2B2B2B) !important;
--organizer-content-background: light-dark(#f7f7f7, #1C1C1C) !important;
--organizer-hover-background: light-dark(#00000025, #ffffff25) !important;
--organizer-selected-background: light-dark(#006CBE80, #006CBE80) !important;
--organizer-outline-color: light-dark(#0055D7a0, #63ADE5) !important;
--organizer-toolbar-field-background: light-dark(#fff, #2B2B2B) !important;
--organizer-toolbar-field-background-focused: light-dark(#fff, #2B2B2B) !important;
--organizer-border-color: light-dark(#bfbfbf, #686868) !important;
scrollbar-color:
light-dark(#0006, #fff6)
light-dark(#f7f7f740, #2B2B2B40) !important;
}
#placesViewsBox #downloadsListBox richlistitem[selected="true"] {
background-color: light-dark(#006CBE, #006CBE) !important;
color: light-dark(#fff, #fff) !important;
}
#placeContentColumns :is(treecol, treecolpicker) {
background-color: light-dark(#f7f7f7, #3B3B3B) !important;
color: light-dark(#000, #fff) !important;
border-inline-start: 1px solid light-dark(#bfbfbf, #686868) !important;
box-shadow: inset 0 -1px light-dark(#bfbfbf, #686868) !important;
appearance: none !important;
}
#placeContentColumns :is(treecol, treecolpicker):where(:hover) {
background-color: light-dark(#cecece, #4A4A4A) !important;
color: light-dark(#000, #fff) !important;
}
}
/* Prevent macOS vibrancy appearance, instead using a solid background. */
#sidebar-box {
appearance: none !important;
}
.sidebar-panel:not([lwt-sidebar]) {
color: light-dark(#000, #fff) !important;
scrollbar-color: light-dark(#0006, #fff6) light-dark(#f7f7f740, #3b3b3b40) !important;
/* Sidebar search box (Windows) */
@media(-moz-platform: windows) {
& #sidebar-search-container > #search-box,
& #viewButton {
appearance: none !important;
background-color: var(--toolbar-field-background-color) !important;
color: inherit !important;
border: 1px solid var(--input-border-color) !important;
border-radius: 4px !important;
padding: 6px 8px !important;
&:focus {
outline: 2px solid var(--input-border-color) !important;
outline-offset: -2px !important;
border-color: transparent !important;
}
}
}
}
@-moz-document url(chrome://browser/content/syncedtabs/sidebar.xhtml) {
body {
background: transparent !important;
}
}
/*
MARK: Linux / GTK csd
*/
@media (-moz-gtk-csd-available) {
:root:not([lwtheme]),
/* `--newtab-background-color-secondary used` to differentiate between a dark
* private window and private window with the 'Dark' theme enabled.
* May not be the most reliable method, but works for now up to FF 123. */
:root[privatebrowsingmode="temporary"]:where([style*="--lwt-accent-color: rgb(28, 27, 34)"]:not([style*="--newtab-background-color-secondary: rgb(66, 65, 77)"])) {
/* Used for mixing colours differently in light/dark themes. */
--uc-tint: light-dark(black, white);
/* Colour adjusted for readability. */
--uc-accent-adjusted: light-dark(
color-mix(in oklch, AccentColor 92%, black),
color-mix(in oklch, AccentColor 60%, white)
);
/* Toolbars */
--lwt-accent-color: light-dark(color-mix(in srgb, ActiveCaption 90%, black), ActiveCaption) !important;
--lwt-accent-color-inactive: light-dark(color-mix(in srgb, InactiveCaption 94%, black), InactiveCaption) !important;
--lwt-text-color: CaptionText !important;
--lwt-text-color-inactive: color-mix(in srgb, CaptionText 65%, transparent) !important;
--tab-selected-textcolor: var(--toolbar-color) !important;
--tab-selected-textcolor-inactive: color-mix(in srgb, var(--toolbar-color) 65%, transparent) !important;
--tab-selected-bgcolor: var(--toolbar-bgcolor) !important;
--tab-attention-icon-color: var(--uc-accent-adjusted) !important;
--toolbar-bgcolor: light-dark(-moz-dialog, color-mix(in srgb, -moz-dialog 92%, white)) !important;
--toolbar-color: -moz-dialogtext !important;
--toolbarbutton-icon-fill-attention: var(--uc-accent-adjusted) !important;
--chrome-content-separator-color: color-mix(in srgb, currentColor 17%, var(--toolbar-bgcolor)) !important;
--newtab-background-color: light-dark(-moz-dialog, color-mix(in srgb, -moz-dialog 82%, black)) !important;
/* URL bar */
--toolbar-field-background-color: light-dark(Field, color-mix(in srgb, Field 96%, -moz-dialogtext)) !important;
--toolbar-field-color: FieldText !important;
--toolbar-field-focus-border-color: light-dark(color-mix(in srgb, var(--focus-outline-color) 50%, transparent), var(--focus-outline-color)) !important;
--urlbar-box-bgcolor: var(--button-bgcolor) !important;
--urlbar-box-hover-bgcolor: var(--button-hover-bgcolor) !important;
--urlbar-box-active-bgcolor: var(--button-active-bgcolor) !important;
/* URL bar suggestions. */
--link-color: var(--uc-accent-adjusted) !important;
--uc-urlbarView-accent-color: var(--uc-accent-adjusted) !important;
--urlbarView-highlight-background: var(--panel-item-hover-bgcolor) !important;
--urlbarView-highlight-color: var(--toolbar-field-color) !important;
--urlbarView-hover-background: var(--panel-item-active-bgcolor) !important;
--urlbarView-separator-color: var(--panel-separator-color) !important;
/* Menus/panels */
--arrowpanel-background: Field !important;
--arrowpanel-color: FieldText !important;
--arrowpanel-border-color: light-dark(transparent, var(--panel-separator-color)) !important;
--arrowpanel-dimmed: color-mix(in srgb, currentColor 17%, transparent) !important;
--arrowpanel-dimmed-further: color-mix(in srgb, currentColor 30%, transparent) !important;
--panel-separator-color: color-mix(in srgb, currentColor 17%, var(--arrowpanel-background)) !important;
--panel-description-color: GrayText !important;
--panel-disabled-color: GrayText !important;
--panel-item-hover-bgcolor: color-mix(in srgb, currentColor 8%, transparent) !important;
--panel-item-active-bgcolor: color-mix(in srgb, currentColor 13%, transparent) !important;
/* Primary button */
--button-primary-bgcolor: AccentColor !important;
--button-primary-color: AccentColorText !important;
--button-primary-hover-bgcolor: color-mix(in srgb, var(--uc-tint) 10%, var(--button-primary-bgcolor)) !important;
--button-primary-active-bgcolor: color-mix(in srgb, var(--uc-tint) 20%, var(--button-primary-bgcolor)) !important;
/* Secondary button */
--button-bgcolor: color-mix(in srgb, currentColor 10%, transparent) !important;
--button-color: currentColor !important;
--button-hover-bgcolor: color-mix(in srgb, currentColor 14%, transparent) !important;
--button-active-bgcolor: color-mix(in srgb, currentColor 20%, transparent) !important;
/* Text input field, e.g. bookmarks editor. */
--focus-outline-color: var(--uc-accent-adjusted) !important;
--input-bgcolor: var(--arrowpanel-background) !important;
--input-color: var(--arrowpanel-color) !important;
--input-border-color: color-mix(in srgb, currentColor 35%, var(--arrowpanel-background)) !important;
/* Sidebar */
--sidebar-background-color: var(--toolbar-bgcolor) !important;
--sidebar-text-color: var(--toolbar-color) !important;
--sidebar-border-color: var(--chrome-content-separator-color) !important;
}
/* Use the same sidebar splitter style as other platforms. */
.sidebar-splitter {
border: 0 solid var(--sidebar-border-color) !important;
border-inline-end-width: 1px !important;
width: 4px !important;
background-color: transparent !important;
margin-inline-start: -4px !important;
}
}
+847
View File
@@ -0,0 +1,847 @@
/* Edge-Frfox - popup.css */
/*
MARK: Variables
*/
:root {
--panel-subview-body-padding: 2px 0 !important;
--arrowpanel-border-radius: 8px !important;
--arrowpanel-menuitem-border-radius: 4px !important;
--arrowpanel-menuitem-margin: var(--uc-arrowpanel-menuitem-margin-block) var(--uc-arrowpanel-menuitem-margin-inline) !important;
--arrowpanel-menuitem-padding-block: 6px !important;
--arrowpanel-menuitem-padding-inline: 10px !important;
--uc-arrowpanel-menuicon-margin-inline: 14px;
--uc-arrowpanel-menuitem-margin-inline: 4px;
--uc-arrowpanel-menuitem-margin-block: 2px;
--panel-separator-margin-vertical: 2px !important;
--panel-separator-margin-horizontal: 0 !important;
--uc-panel-zoom-button-padding: 8px;
--uc-panel-zoom-button-inline-padding: 9px;
--uc-panel-zoom-padding-block: calc(var(--panel-separator-margin-vertical) + var(--uc-arrowpanel-menuitem-margin-block));
--uc-autocomplete-panel-menuitem-margin: 4px;
--uc-autocomplete-panel-menuicon-padding-inline: 14px;
--uc-autocomplete-panel-separator-margin-vertical: 4px;
--uc-permission-itemcontainer-padding-block: 8px;
--uc-permission-item-margin-block: 4px;
--uc-permission-item-padding-inline: 16px;
--uc-contextmenu-border-radius: 8px;
--uc-contextmenu-padding: calc(4px - var(--uc-contextmenu-menuitem-border-width)) 0;
--uc-contextmenu-menuitem-border-radius: calc(4px + var(--uc-contextmenu-menuitem-border-width));
--uc-contextmenu-menuitem-padding-block: 6px;
--uc-contextmenu-menuitem-padding-inline: 10px;
--uc-contextmenu-menuitem-border-width: 2px;
--uc-contextmenu-menuicon-margin-inline: 12px;
--uc-contextmenu-menuitem-margin-inline: calc(4px - var(--uc-contextmenu-menuitem-border-width));
--uc-contextmenu-menuitem-margin-block: 0px;
--uc-contextmenu-menuitem-margin: var(--uc-contextmenu-menuitem-margin-block) var(--uc-contextmenu-menuitem-margin-inline);
--uc-contextmenu-separator-vertical: calc(4px - var(--uc-contextmenu-menuitem-border-width));
--uc-contextmenu-separator-horizontal: 0;
}
/*
MARK: App menu
*/
.addon-banner-item,
.panel-banner-item {
margin: 2px 4px 2px !important;
padding-inline: 4px 12px !important;
padding-block: var(--arrowpanel-menuitem-padding-block) !important;
border-radius: var(--arrowpanel-menuitem-border-radius) !important;
}
#appMenu-fxa-label2 label,
#PanelUI-fxa-menu-syncnow-button label {
margin-block: 0 !important;
}
.widget-overflow-list .toolbarbutton-1:not(.toolbarbutton-combined) > .toolbarbutton-text,
.subviewbutton:not(#appMenu-zoom-controls > .subviewbutton) > .toolbarbutton-icon + .toolbarbutton-text,
#appMenu-fxa-label2 > vbox {
padding-inline-start: var(--uc-arrowpanel-menuicon-margin-inline) !important;
}
/* special case menuitems with no icons */
#appMenu-zoom-controls > .toolbarbutton-text,
#fxa-manage-account-button > vbox,
#PanelUI-fxa-menu-syncnow-button > hbox {
padding-inline-start: calc(16px + var(--uc-arrowpanel-menuicon-margin-inline)) !important;
}
.subviewbutton > .toolbarbutton-icon {
width: 16px !important;
}
/* firefox profile avatar in appmenu */
#appMenu-fxa-label2::before {
content: "";
display: flex;
height: 16px;
width: 16px;
background: var(--avatar-image-url) 0/16px;
scale: 1.25;
border-radius: 99px;
}
/* disable proton account separator */
#appMenu-fxa-separator {
border-image: none !important;
}
#appMenu-fxa-status2:not([fxastatus]) {
padding-block: 0 !important;
}
#appMenu-fxa-status2:not([fxastatus]) > #appMenu-fxa-label2 {
margin-inline-end: calc(var(--arrowpanel-menuitem-padding-inline) * -1) !important;
}
/* zoom controls */
#appMenu-zoom-controls {
border-top: 1px solid var(--panel-separator-color) !important;
padding-inline: calc(var(--arrowpanel-menuitem-padding-inline) + var(--uc-arrowpanel-menuitem-margin-inline)) var(--uc-arrowpanel-menuitem-margin-inline) !important;
padding-block: var(--uc-panel-zoom-padding-block) !important;
margin: var(--panel-separator-margin-vertical) 0 calc(var(--panel-separator-margin-vertical) * -1) !important;
}
#appMenu-zoom-controls > .subviewbutton {
padding: var(--uc-panel-zoom-button-padding) var(--uc-panel-zoom-button-inline-padding) !important;
margin: 0 !important;
}
#appMenu-zoom-controls > #appMenu-zoomReset-button2 {
padding: var(--uc-panel-zoom-button-padding) calc(var(--uc-panel-zoom-button-padding) / 2) !important;
}
/* #appMenu-zoomReduce-button2, */
#appMenu-zoom-controls > #appMenu-fullscreen-button2 {
margin-left: calc((var(--panel-separator-margin-vertical) + var(--uc-arrowpanel-menuitem-margin-block)) * 2 + 1px) !important;
}
#appMenu-zoom-controls > #appMenu-fullscreen-button2::before {
content: "";
border-inline-start: 1px solid var(--panel-separator-color);
display: block;
position: relative;
height: 32px;
margin-block: calc(var(--uc-panel-zoom-button-padding) * -1);
transform: translateX(calc(var(--uc-panel-zoom-button-inline-padding) * -1 - (var(--panel-separator-margin-vertical) + var(--uc-arrowpanel-menuitem-margin-block)) - 1px));
}
#appMenu-zoomReset-button2 {
height: calc(16px + var(--uc-panel-zoom-button-padding) * 2) !important;
min-height: calc(16px + var(--uc-panel-zoom-button-padding) * 2) !important;
}
#appMenu-zoomReduce-button2:not([disabled], [open], :active):is(:hover),
#appMenu-zoomEnlarge-button2:not([disabled], [open], :active):is(:hover),
#appMenu-fullscreen-button2:not([disabled], [open], :active):is(:hover),
#appMenu-zoomReset-button2:not([disabled], [open], :active):is(:hover) {
background-color: var(--panel-item-hover-bgcolor) !important;
}
#appMenu-zoomReduce-button2:not([disabled]):is([open], :hover:active),
#appMenu-zoomEnlarge-button2:not([disabled]):is([open], :hover:active),
#appMenu-fullscreen-button2:not([disabled]):is([open], :hover:active),
#appMenu-zoomReset-button2:not([disabled]):is([open], :hover:active) {
background-color: var(--panel-item-active-bgcolor) !important;
}
#appMenu-zoomReduce-button2 > .toolbarbutton-icon,
#appMenu-zoomEnlarge-button2 > .toolbarbutton-icon {
fill: currentColor !important;
stroke: none !important;
}
#appMenu-zoomReset-button2 > .toolbarbutton-text,
#appMenu-fullscreen-button2 > .toolbarbutton-icon {
background-color: transparent !important;
padding: 0 !important;
}
.subviewbutton[shortcut]::after {
opacity: 0.7 !important;
}
#widget-overflow-mainView .panel-subview-body {
padding-bottom: 0 !important;
}
.PanelUI-subView > .panel-header + toolbarseparator {
margin-bottom: 0 !important;
}
.PanelUI-subView > .panel-header + toolbarseparator + .panel-subview-body {
padding-top: var(--panel-separator-margin-vertical) !important;
}
#identity-popup-security-button {
margin-bottom: var(--panel-separator-margin-vertical) !important;
}
#permission-popup-mainView-panel-header,
#identity-popup-mainView-panel-header,
#protections-popup-mainView-panel-header,
.panel-header {
min-height: calc((var(--arrowpanel-menuitem-padding-block) + 4px) * 2 + 16px) !important;
}
/* URL bar popup */
.panel-footer > button {
border-radius: 2px !important;
}
.identity-popup-security-connection > hbox > description {
margin-inline-start: 0 !important;
}
.identity-popup-security-connection.identity-button {
margin-inline-end: calc(-1 * (var(--arrowpanel-menuitem-padding-inline) - 10px)) !important;
}
#identity-popup-mainView-panel-header-span,
#permission-popup-mainView-panel-header-span,
#identity-popup-mainView-panel-header label,
#permission-popup-mainView-panel-header label,
#protections-popup-mainView-panel-header-span {
margin-block: 0 !important;
}
.permission-popup-section {
padding-block: var(--uc-permission-itemcontainer-padding-block) !important;
}
#permission-popup-permissions-content {
padding-inline: var(--uc-permission-item-padding-inline) !important;
}
.permission-popup-permission-item,
#permission-popup-storage-access-permission-list-header {
margin-block: var(--uc-permission-item-margin-block) !important;
}
.permission-popup-permission-label,
.permission-popup-permission-header-label {
margin-inline-start: var(--uc-arrowpanel-menuicon-margin-inline) !important;
}
#editBookmarkPanel > #editBookmarkHeaderSeparator,
#editBookmarkPanel > .panel-subview-body > #editBookmarkSeparator {
margin-inline: 0 !important;
}
/* subview */
.panel-header {
padding: 4px !important;
padding-top: 4px !important;
}
.panel-header + toolbarseparator,
#identity-popup-mainView-panel-header + toolbarseparator,
#permission-popup-mainView-panel-header + toolbarseparator,
#protections-popup-mainView-panel-header-section + toolbarseparator {
margin-block-start: 0 !important;
}
.panel-header > .subviewbutton-back {
margin: 0 !important;
padding: var(--arrowpanel-menuitem-padding-block) !important;
}
.panel-subview-footer-button {
margin-bottom: 4px !important;
}
.webextension-popup-browser,
.webextension-popup-stack {
margin-block-start: calc(var(--panel-separator-margin-vertical) * -.5) !important;
}
/*
MARK: Downloads
*/
.downloadTypeIcon {
height: 16px !important;
width: 16px !important;
}
.downloadButton {
min-width: calc(16px + var(--uc-toolbarbutton-inner-inline-padding) * 2) !important;
height: calc(16px + var(--toolbarbutton-inner-padding) * 2) !important;
border-radius: var(--toolbarbutton-border-radius) !important;
}
.downloadProgress::-moz-progress-bar,
.downloadProgress > div {
background-color: var(--toolbarbutton-icon-fill-attention) !important;
}
.downloadMainArea {
padding-block: 4px !important;
padding-inline: 12px !important;
border-radius: 0 !important;
}
#downloadsListBox > richlistitem {
padding-inline-end: 8px !important;
border-radius: var(--arrowpanel-menuitem-border-radius) !important;
}
#downloadsListBox,
.downloadsPanelFooterButton {
margin: var(--arrowpanel-menuitem-margin) !important;
width: auto !important;
min-width: auto !important;
}
#downloadsPanel-blockedSubview,
#downloadsPanel-mainView {
padding: var(--panel-separator-margin-vertical) 0 !important;
}
#emptyDownloads {
padding: 1.5em 16px 1.7em !important;
}
#downloadsHistory .button-text {
margin-block: 0 !important;
margin-inline: 0 !important;
}
.PanelUI-subView .subviewbutton-nav::after,
#PlacesToolbar menu > .menu-right,
.subviewbutton > .menu-right {
fill-opacity: 1 !important;
}
/*
MARK: Extensions
*/
/* #region */
.toolbaritem-combined-buttons:is(:not([cui-areatype="toolbar"]), [overflowedItem="true"]) > toolbarbutton:not(.toolbarbutton-1)[disabled] {
opacity: 1 !important;
color: var(--panel-disabled-color) !important;
}
/* Removes space above extensions items. */
#unified-extensions-messages-container {
margin-block: 0 !important;
}
:root {
--uei-icon-size: 24px !important;
}
@media (-moz-bool-pref: "uc.tweak.context-menu.compact-extensions-menu") {
:root {
--uei-icon-size: 16px !important;
}
/* Hide site access permissions label. */
.unified-extensions-item-message-deck {
display: none !important;
}
/* Use smaller toolbar extension icon if available. */
toolbaritem:is([overflowedItem="true"], [cui-areatype="panel"]) > .webextension-browser-action {
list-style-image: var(--webextension-toolbar-image, var(--webextension-menupanel-image, inherit)) !important;
}
}
/* Use smaller font-size in extensions menu on Windows. */
@media (-moz-platform: windows) {
#unified-extensions-panel .toolbarbutton-text {
font: menu !important;
font-family: inherit !important;
}
}
/* Prevent extension labels from using >1 line, adding ellipsis if necessary. */
.unified-extensions-item-contents {
flex: 1 !important;
& > .unified-extensions-item-name,
& > .unified-extensions-item-message-deck > .unified-extensions-item-message {
max-width: calc(100% - 4px) !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
}
/* Extension item menu button. */
.unified-extensions-item-menu-button {
padding: 0 !important;
& > .toolbarbutton-icon {
border: none !important;
padding-inline: var(--arrowpanel-menuitem-padding-inline) !important;
padding-block: var(--arrowpanel-menuitem-padding-block) !important;
margin-inline-start: var(--uc-arrowpanel-menuitem-margin-inline) !important;
}
}
/* Add grayscale filter to extensions with no access to current site. */
.unified-extensions-list .unified-extensions-item-action-button:has(.unified-extensions-item-message[data-l10n-id="origin-controls-state-no-access"]) .unified-extensions-item-icon {
filter: grayscale(1) !important;
}
/* Adjust spacing between the extension item's icon and label. */
.unified-extensions-item-name,
.unified-extensions-item-message {
padding-inline-start: var(--uc-arrowpanel-menuicon-margin-inline) !important;
}
.unified-extensions-item-icon,
.unified-extensions-item .webextension-browser-action > .toolbarbutton-badge-stack {
margin-inline-end: 0 !important;
}
/* #endregion */
/*
MARK: Autocomplete
*/
#PopupAutoComplete {
/* workaround for inaccessible shadow-root element
moves the shadow-root element underneath, and away from panel edges */
padding-block: 10px !important;
padding-inline: 2px !important;
& > .autocomplete-richlistbox {
background: var(--arrowpanel-background) !important;
box-shadow: 0 0 0 1px inset var(--arrowpanel-border-color) !important;
height: auto !important;
margin-block: -10px !important;
margin-inline: -2px !important;
padding: var(--uc-contextmenu-padding) !important;
border-radius: var(--uc-contextmenu-border-radius) !important;
color: inherit !important;
}
& .autocomplete-richlistitem[originaltype="loginsFooter"] {
justify-content: start !important;
color: inherit !important;
background-color: transparent !important;
height: 28px !important;
min-height: 28px !important;
padding-inline-start: var(--uc-contextmenu-menuitem-padding-inline) !important;
position: relative !important;
overflow: visible !important;
margin-top: calc(var(--uc-contextmenu-separator-vertical) * 2 + 1px) !important;
}
/* Separator */
& .autocomplete-richlistitem[originaltype="loginsFooter"]::before {
content: "";
width: calc(100% + var(--uc-contextmenu-menuitem-margin-inline) * 2);
border-top: 1px solid var(--panel-separator-color);
position: absolute;
top: calc((var(--uc-contextmenu-separator-vertical) + var(--uc-contextmenu-menuitem-border-width)) * -1 - 1px);
left: calc(var(--uc-contextmenu-menuitem-margin-inline) * -1);
}
& .autocomplete-richlistitem:hover {
background-color: var(--panel-item-hover-bgcolor) !important;
}
& .autocomplete-richlistitem[selected] {
background-color: var(--panel-item-active-bgcolor) !important;
color: inherit !important;
}
& .ac-site-icon {
fill: currentColor !important;
margin-block: auto !important;
margin-inline: 0 var(--uc-autocomplete-panel-menuicon-padding-inline) !important;
}
& .ac-title {
margin-inline: calc(16px + var(--uc-autocomplete-panel-menuicon-padding-inline)) !important;
}
& .two-line-wrapper {
padding: 0 !important;
gap: 0 !important;
}
}
#PlacesToolbar menuitem,
#PlacesToolbar menu,
panelview .toolbarbutton-1,
.subviewbutton,
.widget-overflow-list .toolbarbutton-1 {
min-height: calc(16px + var(--uc-contextmenu-menuitem-padding-block) * 2) !important;
}
/*
MARK: Search bar suggestions
*/
#PopupSearchAutoComplete {
& .searchbar-separator {
margin: 4px 0 !important;
}
/* Search suggestions. */
& .autocomplete-richlistitem,
& .search-panel-header {
padding-block: 2px !important;
padding-inline: var(--uc-contextmenu-menuitem-padding-inline) !important;
margin: 0 var(--uc-arrowpanel-menuitem-margin-inline) !important;
border-radius: var(--arrowpanel-menuitem-border-radius) !important;
& > .ac-type-icon {
margin-inline-start: 0 !important;
}
}
/* Search engine selector. */
& .search-one-offs {
padding-block: 0 5px !important;
& #searchbar-one-offs-header-label {
margin-inline-start: 0 !important;
}
& .search-panel-one-offs-container {
margin-inline: 1px 7px !important;
}
}
}
/*
MARK: Context menu
*/
/* disable context menu when this tweak is applied */
@media not (-moz-bool-pref: "uc.tweak.revert-context-menu") {
/* Context Menu */
menupopup:not(.PanelUI-subView),
menupopup:not(.PanelUI-subView) menuitem,
menupopup:not(.PanelUI-subView) menu,
menupopup:not(.PanelUI-subView) menuseparator,
panel:not(#PopupSearchAutoComplete) {
appearance: none !important;
}
menupopup:not(.PanelUI-subView),
#PopupAutoComplete,
menulist > menupopup:not(.PanelUI-subView) > menucaption {
-moz-window-shadow: none !important;
background: 0 !important;
border: 0 !important;
color: var(--arrowpanel-color) !important;
}
.popup-internal-box,
#PopupAutoComplete > .autocomplete-richlistbox,
.menupopup-arrowscrollbox {
background: var(--arrowpanel-background) !important;
border: 1px solid var(--arrowpanel-border-color) !important;
}
/* make room for menu shadow */
menupopup:not(.PanelUI-subView) {
padding: 4px 12px 20px !important;
margin: -6px -12px -16px !important;
}
/* cut off shadow so it doesn't get in the way of the menubar items */
menubar menupopup {
padding: 1px 8px 20px !important;
margin: -1px -4px -16px !important;
}
menupopup {
--uc-margin: 0 !important;
--uc-outline-color: #0001;
--uc-box-shadow:
0 8px 12px #0003,
0 4px 5px #0002,
0 2px 4px #0002;
}
@media (prefers-color-scheme:dark) {
menupopup {
--uc-outline-color: #0002;
--uc-box-shadow:
0 8px 12px #0003,
0 4px 5px #0003,
0 2px 4px #0002;
}
}
/* use default shadow for subview menupopup (eg. bookmarks) */
menupopup.PanelUI-subView {
--uc-box-shadow: 0 0 0 1px var(--panel-border-color);
--uc-margin: 1px !important;
margin-top: -5px !important;
}
.menupopup-arrowscrollbox {
box-shadow: var(--uc-box-shadow) !important;
outline: .5px solid var(--uc-outline-color) !important;
}
.popup-internal-box {
margin: 0 !important;
}
.popup-internal-box,
.menupopup-arrowscrollbox {
padding: var(--uc-contextmenu-padding) !important;
border-radius: var(--uc-contextmenu-border-radius) !important;
color: inherit !important;
}
menupopup menu[disabled="true"],
menupopup menuitem[disabled="true"] {
color: var(--panel-disabled-color) !important;
background: none !important;
}
menupopup menu:hover:not([disabled=true]),
menupopup menuitem:hover:not([disabled=true]),
menupopup menu[_moz-menuactive]:not([disabled=true]),
menupopup menuitem[_moz-menuactive]:not([disabled=true]) {
background-color: var(--panel-item-hover-bgcolor) !important;
color: inherit !important;
}
menupopup menuitem:hover:active:not([disabled=true]),
menupopup menu:hover:active:not([disabled=true]) {
background-color: var(--panel-item-active-bgcolor) !important;
color: inherit !important;
}
menupopup menuseparator,
panel menuseparator,
#PopupSearchAutoComplete .searchbar-separator {
margin: var(--uc-contextmenu-separator-vertical) 0 !important;
padding: 0 !important;
background: none !important;
border-top: 1px solid var(--panel-separator-color) !important;
&::before {
content: unset !important;
}
}
#PlacesToolbar menupopup[placespopup="true"] menuseparator::before {
display: none !important;
}
menupopup menu,
menupopup menuitem,
menupopup menucaption,
#PopupAutoComplete > richlistbox > richlistitem,
#PopupSearchAutoComplete .autocomplete-richlistitem {
padding: var(--uc-contextmenu-menuitem-padding-block) var(--uc-contextmenu-menuitem-padding-inline) !important;
margin: var(--uc-contextmenu-menuitem-margin) !important;
border-radius: var(--uc-contextmenu-menuitem-border-radius) !important;
border: var(--uc-contextmenu-menuitem-border-width) solid transparent !important;
background-clip: padding-box !important;
min-height: calc(16px + (var(--uc-contextmenu-menuitem-padding-block) + var(--uc-contextmenu-menuitem-border-width)) * 2) !important;
}
:not(:not(menubar)>menu, #ContentSelectDropdown) > menupopup > menuitem:not(.menuitem-iconic, [type="checkbox"], [type="radio"], .in-menulist, .in-menulist menuitem, .unified-nav-current),
:not(:not(menubar)>menu, #ContentSelectDropdown) > menupopup > menu:not(.menu-iconic, [type="checkbox"], [type="radio"], .in-menulist, .in-menulist menu, .unified-nav-current),
:not(:not(menubar)>menu, #ContentSelectDropdown) > menupopup > menucaption {
padding-inline-start: calc(var(--uc-contextmenu-menuitem-padding-inline) + 16px + var(--uc-contextmenu-menuicon-margin-inline)) !important;
}
.menu-iconic-icon {
margin-block: 0 !important;
margin-inline-end: 0 !important;
margin-inline-start: 0 !important;
}
menu > .menu-iconic-text,
menuitem > .menu-iconic-text,
/* Used for vertical context navigation tweak. */
#context-navigation > .menuitem-iconic::after {
padding-inline-start: var(--uc-contextmenu-menuicon-margin-inline) !important;
}
.menu-text,
.menu-iconic-text {
margin-inline-start: 0 !important;
padding-block: 0 !important;
}
.menu-accel,
.menu-iconic-accel {
margin-inline-end: 0 !important;
padding-block: 0 !important;
opacity: 0.7 !important;
}
.menu-iconic > .menu-iconic-left,
.menuitem-iconic > .menu-iconic-left {
padding-inline-end: 0 !important;
padding-block: 0 !important;
}
menuitem:is(.menuitem-iconic:not([data-usercontextid]), [type="checkbox"], [type="radio"], [checked], [selected]) .menu-iconic-icon {
-moz-context-properties: fill !important;
fill: currentColor !important;
}
.menu-right,
.menu-accel-container {
appearance: none !important;
margin-inline-end: 0 !important;
-moz-context-properties: fill;
fill: currentColor !important;
}
menupopup menuitem:is([type="checkbox"], [type="radio"]) .menu-iconic-icon,
menulist > menupopup > menuitem .menu-iconic-icon,
menu .menu-right {
height: 16px !important;
width: 16px !important;
}
menulist > menupopup > menuitem:is([checked="true"], [selected="true"])::before,
menupopup > menuitem::before,
menupopup > menu::before,
menupopup > menuitem::after {
display: none !important;
}
menuitem:is([type="checkbox"], [checked="true"], [type="radio"]) > .menu-iconic-left > .menu-iconic-icon {
display: flex !important;
}
menuitem[checked="true"] > .menu-iconic-left,
menu > .menu-iconic-left,
menuitem > .menu-iconic-left {
margin: 0 !important;
padding: 0 !important
}
#context-navigation:not([hidden]) {
padding: 0 !important;
--menuitem-min-width: calc(16px + var(--uc-contextmenu-menuitem-padding-inline) * 2) !important;
min-width: calc(4 * var(--menuitem-min-width));
}
#context-navigation > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon {
width: 16px !important;
height: 16px !important;
padding: 0 !important;
}
#context-navigation {
margin-inline: var(--uc-contextmenu-menuitem-margin-inline) !important;
}
#context-navigation > .menuitem-iconic {
justify-content: center !important;
}
#context-navigation > .menuitem-iconic {
margin: 0 !important;
}
#context-navigation > .menuitem-iconic {
flex-grow: 1 !important;
width: unset !important;
}
#context-navigation > .menuitem-iconic[_moz-menuactive="true"] > .menu-iconic-left > .menu-iconic-icon {
background-color: transparent !important;
}
#context-bookmarkpage[starred] .menu-iconic-icon {
fill: var(--lwt-toolbarbutton-icon-fill-attention, var(--toolbarbutton-icon-fill-attention, currentColor)) !important;
}
@media (not (-moz-bool-pref: "uc.tweak.smaller-context-menu-text")) and (-moz-platform: windows) {
.menu-text,
.menu-iconic-text,
panel .toolbarbutton-text,
#PopupSearchAutoComplete .ac-title,
/* Used for vertical context navigation tweak. */
#context-navigation > .menuitem-iconic::after {
margin-inline-start: 0 !important;
margin-block: -3px -1px !important;
padding-block: 0 !important;
}
.menu-text,
.menu-iconic-text,
panel .toolbarbutton-text,
#appMenu-fxa-status2,
.panel-header,
#PopupSearchAutoComplete,
/* Used for vertical context navigation tweak. */
#context-navigation > .menuitem-iconic::after {
font-size: 13.6px !important;
}
}
}
/*
MARK: Dialog box
*/
/* Dialog shadow and outline adjustments. */
.dialogBox:not(.spotlightBox) {
border-radius: 8px !important;
outline: 0.5px solid var(--panel-separator-color) !important;
box-shadow:
0 20px 38px rgb(0 0 0 / 0.23),
0 5px 9px rgb(0 0 0 / 0.22) !important;
}
dialog {
/* Dialog title. */
& #titleContainer {
/* Add larger gap between icon and title text. */
& .titleIcon {
padding-inline-end: 8px !important;
}
/* Match font style in Edge. */
& #titleText {
font-size: 1.18em !important;
font-weight: 700 !important;
}
/* Adjust vertical alignment of title text. */
@media (-moz-platform: macOS) {
&:not([noicon]) > #titleCropper {
translate: 0 calc(-1px - max(.55 * var(--icon-size) - .6em, 0px)) !important;
}
}
}
/* Input focus style. */
& input:where([type="email"], [type="tel"], [type="text"], [type="password"], [type="url"], [type="number"]):focus {
outline: 2px solid var(--in-content-box-border-color) !important;
outline-offset: -2px !important;
}
}
/* FF123+ tab preview pop-up.
* CSS reference: https://searchfox.org/mozilla-central/source/browser/components/tabpreview/tabpreview.css
* Requires `browser.tabs.cardPreview.enabled` set to `true` */
.tab-preview-container {
background-color: var(--arrowpanel-background) !important;
color: var(--arrowpanel-color) !important;
border: 0.5px solid var(--panel-separator-color) !important;
border-radius: 8.5px !important;
/* Place thumbnail at the top of the tooltip. */
display: flex !important;
flex-direction: column-reverse !important;
& .tab-preview-uri {
color: var(--panel-description-color) !important;
}
}
+87
View File
@@ -0,0 +1,87 @@
/* Edge-Frfox - tree.css */
/* Change height of tree items and add rounded corners */
treechildren::-moz-tree-row,
treecol:not([hideheader="true"]),
.tree-columnpicker-button {
min-height: max(28px, 1.3em) !important;
border-radius: 2px !important;
}
/* Change the background colour on hover */
@media not (prefers-contrast) {
treechildren::-moz-tree-row(hover) {
background-color: var(--toolbarbutton-hover-background) !important;
}
}
/* Change the background colour when pressed */
treechildren::-moz-tree-row(selected) {
background-color: var(--toolbarbutton-active-background) !important;
}
/* Change the background colour when focused */
treechildren::-moz-tree-row(selected, focus) {
background-color: var(--button-primary-bgcolor) !important;
}
treechildren::-moz-tree-row(current, focus) {
outline: var(--default-focusring);
outline-color: var(--button-primary-bgcolor) !important;
outline-offset: calc(-1 * var(--default-focusring-width));
}
treechildren::-moz-tree-image(selected),
treechildren::-moz-tree-twisty(selected),
treechildren::-moz-tree-cell-text(selected) {
color: inherit !important;
}
treechildren::-moz-tree-image(selected, focus),
treechildren::-moz-tree-twisty(selected, focus),
treechildren::-moz-tree-cell-text(selected, focus) {
color: var(--button-primary-color) !important;
}
/* Spacing between icon and label */
treechildren::-moz-tree-image {
margin-inline-end: 8px !important;
}
/* Separator appearance */
treechildren::-moz-tree-separator {
border-top: 1px solid var(--toolbarseparator-color) !important;
border-bottom: none !important;
}
/* Change the colour of the drop feedback elements */
treechildren::-moz-tree-cell-text(primary, dropOn) {
background-color: var(--button-primary-bgcolor) !important;
color: var(--button-primary-color) !important;
}
treechildren::-moz-tree-drop-feedback {
background-color: var(--toolbarbutton-icon-fill-attention) !important;
border-radius: 2px !important;
}
/* Change the appearance for the expandable items */
treechildren::-moz-tree-twisty {
padding-top: 0 !important;
padding-inline: 4px !important;
width: 16px !important;
list-style-image: url("../icons/arrow-filled-right.svg") !important;
}
treechildren:-moz-locale-dir(rtl)::-moz-tree-twisty(closed) {
list-style-image: url("../icons/arrow-filled-left.svg") !important;
}
treechildren::-moz-tree-twisty(open) {
list-style-image: url("../icons/arrow-filled-down.svg") !important;
}
/* Change the identation of child items */
treechildren::-moz-tree-indentation {
width: 16px !important;
}
+247
View File
@@ -0,0 +1,247 @@
/* Edge-Frfox - tweaks.css */
/*
MARK: Disable drag space
*/
@media (-moz-bool-pref: "uc.tweak.disable-drag-space") {
/* disable drag space above tabs */
:root {
--uc-tab-top-margin: 0px !important;
}
}
/*
MARK: Force tab colour
*/
/* force tabs to toolbar bg (useful for proton themes) (might experience some
bugs with certain themes eg. dark text on dark background.) */
@media (-moz-bool-pref: "uc.tweak.force-tab-colour") {
:root[lwtheme] {
--tab-selected-bgcolor: var(--toolbar-bgcolor) !important;
--tab-selected-textcolor: var(--toolbar-color) !important;
}
}
/*
MARK: Remove tab separators
*/
@media (-moz-bool-pref: "uc.tweak.remove-tab-separators") {
.tabbrowser-tab .tab-stack {
&::before,
&::after {
content: initial !important;
}
}
}
/*
MARK: Floating tabs
*/
@media (-moz-bool-pref: "uc.tweak.floating-tabs") {
:root:not([lwtheme]),
/* `--newtab-background-color-secondary used` to differentiate between a dark
* private window and private window with the 'Dark' theme enabled.
* May not be the most reliable method, but works for now up to FF 123. */
:root[privatebrowsingmode="temporary"]:where([style*="--lwt-accent-color: rgb(28, 27, 34)"]:not([style*="--newtab-background-color-secondary: rgb(66, 65, 77)"])) {
--lwt-accent-color: light-dark(#f3f3f3, #202020) !important;
--lwt-accent-color-inactive: unset !important;
--toolbar-bgcolor: light-dark(#fff, #3F3F3F) !important;
--lwt-tab-text: light-dark(#262626, #fff) !important;
--chrome-content-separator-color: light-dark(#bfbfbf, #535353) !important;
--lwt-tab-line-color: light-dark(transparent, rgb(255 255 255 / 0.06)) !important;
}
/* Uses :root body to avoid affecting other variables by changing
* --toolbar-color. */
:root body {
/* Use tabbar colours for better readability when using custom themes. */
--toolbar-color: var(--lwt-text-color) !important;
--toolbarbutton-icon-fill: var(--lwt-text-color) !important;
&:-moz-window-inactive {
--toolbar-color: var(--lwt-text-color-inactive, var(--lwt-text-color)) !important;
--toolbarbutton-icon-fill: var(--lwt-text-color-inactive, var(--lwt-text-color)) !important;
}
}
@media (-moz-gtk-csd-available) {
:root:not([lwtheme]) {
--toolbar-bgcolor: color-mix(in srgb, -moz-dialog 80%, #fff) !important;
--toolbar-field-background-color: Field !important;
--lwt-tab-text: -moz-dialogtext !important;
--lwt-tab-line-color: rgb(255 255 255 / 0.06) !important;
--lwt-accent-color: var(--toolbox-non-lwt-bgcolor) !important;
&:-moz-window-inactive {
--lwt-accent-color: var(--toolbox-non-lwt-bgcolor-inactive) !important;
}
}
}
/* Shadow for navbar and tabs */
:root,
:root .tabbrowser-tab {
--uc-titlebar-shadow: none;
--uc-tab-shadow-outline: rgb(0 0 0 / .11);
--uc-tab-shadow: 0 2.5px 3px 1px rgb(0 0 0 / .08);
& [brighttext],
& [brighttext] .tabbrowser-tab {
--uc-titlebar-shadow: none;
--uc-tab-shadow-outline: transparent;
--uc-tab-shadow: 0 2px 3px rgb(0 0 0 / .12);
}
& #TabsToolbar {
--tab-border-radius: 8px !important;
}
}
/* remove toolbar bg */
#nav-bar,
#PersonalToolbar {
background-color: transparent !important;
}
/* invert bottom corner radius for tabs */
.tab-background {
border-radius: var(--tab-border-radius) !important;
&::before,
&::after {
display: none !important;
}
}
.tab-background[selected] {
border: 1px solid var(--lwt-tab-line-color, var(--lwt-tabs-border-color, transparent)) !important;
outline: 1px solid var(--uc-tab-shadow-outline) !important;
/* Fractional scaling adjustments (150%, 175%, etc.) */
@media (1dppx < resolution < 2dppx) {
outline-width: 1.5px !important;
}
}
/* adjust spacing of area above tabs */
@media not (-moz-platform: macos) {
:root[sizemode="maximized"] {
--uc-tab-top-margin: 4px !important;
}
}
:root:is([inFullscreen], :not([tabsintitlebar])) {
--uc-tab-top-margin: 4px !important;
}
@media (-moz-bool-pref: "uc.tweak.disable-drag-space") {
:root {
--uc-tab-top-margin: 4px !important;
}
}
/* IMPORTANT: Adds padding and removes margin on top of tabs to allow user to
* select it from the top edge of the window */
#tabbrowser-tabs {
margin-top: calc(var(--uc-tab-top-margin) - 4px) !important;
& .tabbrowser-tab,
& #tabs-newtab-button {
padding-top: 4px !important;
}
}
#scrollbutton-up:not(.menupopup-scrollbutton),
#scrollbutton-down:not(.menupopup-scrollbutton) {
&,
& ~ spacer {
margin-top: 4px !important;
}
}
/* move tab shadow behind urlbar */
.tabbrowser-tab[visuallyselected="true"],
#nav-bar {
z-index: 2 !important;
}
/* tab shadow adjustments */
.tabbrowser-tab {
overflow-clip-margin: 8px !important;
}
#tabbrowser-arrowscrollbox > *,
#scrollbutton-up:not(.menupopup-scrollbutton),
#scrollbutton-up:not(.menupopup-scrollbutton) ~ spacer,
#scrollbutton-down:not(.menupopup-scrollbutton) {
margin-bottom: 8px !important;
}
#tabbrowser-arrowscrollbox {
margin-bottom: -8px !important;
}
}
/*
MARK: Vertical context nav
*/
@media (not (-moz-platform: macos)) and (not (-moz-bool-pref: "uc.tweak.revert-context-menu")) {
@media (-moz-bool-pref: "uc.tweak.vertical-context-navigation") {
#context-navigation {
/* Stack menu items vertically. */
flex-direction: column !important;
/* Align the image and label to the left edge. */
& > .menuitem-iconic {
justify-content: start !important;
&::after {
/* Get the menuitem label from the aria-label attribute. */
content: attr(aria-label);
}
}
}
}
}
/*
MARK: Hide access key
*/
@media (-moz-bool-pref: "uc.tweak.context-menu.hide-access-key") {
:where(menuitem, menu) > :is(.menu-text, .menu-iconic-text)[value]:not([value=""]) {
&::before {
/* The default value is -moz-label-content, which takes the data from the
* value attribute and highlights the character found in the accesskey
* attribute. This skips that and uses the value attribute directly. */
content: attr(value) !important;
}
}
}
/*
MARK: Show tab close button
*/
@media (-moz-bool-pref: "uc.tweak.show-tab-close-button-on-hover") {
#tabbrowser-tabs[closebuttons="activetab"] .tabbrowser-tab:not([pinned], [selected]):hover .tab-close-button {
display: flex !important;
}
}
/*
MARK: Hide forward button
*/
@media (-moz-bool-pref: "uc.tweak.hide-forward-button") {
:root:not([customizing]) #forward-button[disabled] {
display: none !important;
}
}
/*
MARK: Hide Firefox account
*/
@media (-moz-bool-pref: "uc.tweak.context-menu.hide-firefox-account") {
#appMenu-fxa-status2,
#appMenu-fxa-separator {
display: none !important;
}
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 0a2.5 2.5 0 0 0-2.396 3.213l-2.24-.847a1.746 1.746 0 0 0-1.245 3.26L5 6.736v2.398l-1.643 4.524a1.747 1.747 0 0 0 3.284 1.194L8 11.145l1.365 3.702a1.745 1.745 0 0 0 3.278-1.2L11 9.124V6.73l2.867-1.087a1.752 1.752 0 1 0-1.244-3.278l-2.227.843A2.5 2.5 0 0 0 8 0zm0 1a1.5 1.5 0 1 1 0 2.999 1.5 1.5 0 0 1 0-3zM2.729 3.252a.74.74 0 0 1 .28.05l4.104 1.551c.571.216 1.201.216 1.772 0l4.094-1.552a.754.754 0 1 1 .535 1.41l-3.192 1.208a.498.498 0 0 0-.322.467V9.21c0 .058.01.117.03.171l1.673 4.608a.745.745 0 0 1-1.4.511l-1.37-3.719c-.319-.867-1.547-.866-1.865.002L5.703 14.51a.747.747 0 1 1-1.404-.511l1.67-4.606A.497.497 0 0 0 6 9.222v-2.83a.5.5 0 0 0-.32-.467L2.479 4.693a.745.745 0 0 1 .25-1.44z"/>
</svg>

After

Width:  |  Height:  |  Size: 834 B

@@ -0,0 +1,13 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="a" x1="4.22" x2="11.78" y1="7.979" y2="7.979" gradientUnits="userSpaceOnUse">
<stop stop-color="#b2b2b2" offset="0"/>
<stop stop-color="#b2b2b2" stop-opacity="0" offset="1"/>
</linearGradient>
</defs>
<path fill="#303030" d="M0 0h16v16H0z"/>
<g fill="url(#a)">
<circle cx="8" cy="5.767" r="2.433"/>
<path d="M5.244 8.867c-.613 0-1.024.46-1.024 1.107 0 1.949 1.996 2.651 3.78 2.651 1.784 0 3.78-.702 3.78-2.651 0-.648-.41-1.107-1.023-1.107z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 584 B

+7
View File
@@ -0,0 +1,7 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path fill="#fafafa" d="M0 0h16v16H0z"/>
<g fill="#8f8f8f">
<circle cx="8" cy="5.767" r="2.433"/>
<path d="M5.244 8.867c-.613 0-1.024.46-1.024 1.107 0 1.949 1.996 2.651 3.78 2.651 1.784 0 3.78-.702 3.78-2.651 0-.648-.41-1.107-1.023-1.107z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 332 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4.35a5.769 5.769 0 0 1-1.076-1H4.001a1 1 0 0 1-1-1h3.605a5.376 5.376 0 0 1-.406-1H3V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v4.199a5.2 5.2 0 0 1 1 .398V2a2 2 0 0 0-2-2H4zm1 2a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H5zm0 1h6v1H5V3z"/>
<path d="M16 11.5a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V11H9.5a.5.5 0 0 0 0 1H11v1.5a.5.5 0 0 0 1 0V12h1.5a.5.5 0 0 0 0-1H12z"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 11.5a.547.547 0 0 1-.389-.162L2.146 5.854a.5.5 0 1 1 .707-.705L8 10.313l5.146-5.164a.5.5 0 1 1 .71.705l-5.466 5.484A.55.55 0 0 1 8 11.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path transform="rotate(90)" transform-origin="center" d="M6.479 2.729 11.1 7.35a.92.92 0 0 1 0 1.3l-4.621 4.621a.72.72 0 0 1-1.229-.509V3.237a.72.72 0 0 1 1.229-.508z"/>
</svg>

After

Width:  |  Height:  |  Size: 300 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path transform="rotate(180)" transform-origin="center" d="M6.479 2.729 11.1 7.35a.92.92 0 0 1 0 1.3l-4.621 4.621a.72.72 0 0 1-1.229-.509V3.237a.72.72 0 0 1 1.229-.508z"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M6.479 2.729 11.1 7.35a.92.92 0 0 1 0 1.3l-4.621 4.621a.72.72 0 0 1-1.229-.509V3.237a.72.72 0 0 1 1.229-.508z"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path transform="scale(-1 1) translate(-16 0)" d="M5.5 2a.5.5 0 0 0-.354.853L10.313 8l-5.167 5.146a.5.5 0 0 0 .707.71l5.484-5.468a.548.548 0 0 0 0-.777L5.853 2.143A.496.496 0 0 0 5.5 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M5.5 2a.5.5 0 0 0-.354.853L10.313 8l-5.167 5.146a.5.5 0 0 0 .707.71l5.484-5.468a.548.548 0 0 0 0-.777L5.853 2.143A.496.496 0 0 0 5.5 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 4.5a.547.547 0 0 0-.389.162l-5.465 5.484a.5.5 0 1 0 .707.705L8 5.687l5.146 5.164a.5.5 0 1 0 .71-.705L8.388 4.662A.55.55 0 0 0 8 4.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M14.13 7c-.478 0-.957.18-1.322.547l-4.828 4.83a2.2 2.2 0 0 0-.578 1.02l-.375 1.498a.889.889 0 0 0 1.078 1.078l1.498-.375a2.2 2.2 0 0 0 1.02-.578l4.828-4.828A1.872 1.872 0 0 0 14.13 7zm-.002 1.002c.427.002.671.224.8.537.13.314.116.643-.187.947l-4.826 4.826a1.204 1.204 0 0 1-.556.315l-1.316.332.33-1.32H8.37c.053-.21.163-.403.316-.557l4.828-4.83a.85.85 0 0 1 .614-.25zm-6.266 7 .002.002H7.86z"/>
<path d="M12.94.182c-.08.004-.162.02-.242.045l-7 2.188a.998.998 0 0 0-.701.953V11.5a2.5 2.5 0 1 0 1 2V6.368l7-2.188v2.055c.32-.137.66-.213 1-.229V1.18c0-.59-.5-1.032-1.057-.998zm.057.998v1.953l-7 2.188V3.368l7-2.188zM3.497 12a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<defs><clipPath id="a"><path d="M2.1 0 16 13.9V0zM0 .7V16h15.3z"/></clipPath></defs>
<path clip-path="url(#a)" d="M12.94.182c-.08.004-.162.017-.242.043l-7 2.188a1 1 0 0 0-.701.955V11.5a2.5 2.5 0 1 0 1 2V6.368l7-2.188V9.5a2.5 2.5 0 1 0 1 2V1.18c0-.59-.5-1.032-1.057-.998zm.057.998v1.953l-7 2.188V3.368l7-2.188zm-1.5 8.82a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm-8 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z"/>
<path d="M.5 0a.5.5 0 0 0-.354.146.5.5 0 0 0 0 .707l15 15a.5.5 0 0 0 .707 0 .5.5 0 0 0 0-.707l-15-15A.5.5 0 0 0 .499 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 658 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M12.94.182c-.08.004-.162.017-.242.043l-7 2.188a1 1 0 0 0-.701.955V11.5a2.5 2.5 0 1 0 1 2V6.368l7-2.188V9.5a2.5 2.5 0 1 0 1 2V1.18c0-.59-.5-1.032-1.057-.998zm.057.998v1.953l-7 2.188V3.368l7-2.188zm-1.5 8.82a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm-8 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M7.527.5a.5.5 0 0 0-.361.127L.322 6.764a.998.998 0 0 0 0 1.473l6.844 6.137a.5.5 0 0 0 .707-.04.5.5 0 0 0-.04-.706L1.558 8h13.44a.5.5 0 0 0 0-1H1.557l6.277-5.627a.5.5 0 0 0 .039-.707A.5.5 0 0 0 7.527.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8.002.3a.99.99 0 0 0-.898.557L5.174 4.77l-4.316.627a1 1 0 0 0-.555 1.705l3.123 3.045-.736 4.3A1 1 0 0 0 4.14 15.5l3.862-2.029 3.859 2.03a1 1 0 0 0 1.45-1.056l-.736-4.299 3.123-3.045a1 1 0 0 0-.554-1.705l-4.316-.627L8.898.857A.987.987 0 0 0 8 .301zm0 1 1.93 3.91c.146.296.426.5.752.548l4.318.629-3.125 3.043c-.236.23-.343.562-.287.886l.738 4.3-3.86-2.03a1.002 1.002 0 0 0-.932 0l-3.86 2.03.737-4.3a1.003 1.003 0 0 0-.287-.886L1.003 6.387l4.316-.63a.998.998 0 0 0 .752-.546z"/>
</svg>

After

Width:  |  Height:  |  Size: 615 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="round" stroke-width="1.001">
<path d="m8 12.95-4.09 2.151a.501.501 0 0 1-.727-.528l.731-4.266a.561.562 0 0 0-.161-.498L.655 6.79a.501.501 0 0 1 .278-.855l4.28-.623a.561.562 0 0 0 .423-.307L7.55 1.123a.501.501 0 0 1 .9 0l2.031 4.115a.475.475 0 0 0 .426.264H15.5"/>
<path d="M15.5 8.5h-5M10.5 11.5h5"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8.002.3a.99.99 0 0 0-.898.557L5.174 4.77l-4.316.627a1 1 0 0 0-.555 1.705l3.123 3.045-.736 4.3A1 1 0 0 0 4.14 15.5l3.862-2.029 3.859 2.03a1 1 0 0 0 1.45-1.056l-.736-4.299 3.123-3.045a1 1 0 0 0-.554-1.705l-4.316-.627L8.898.857A.987.987 0 0 0 8 .301z"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<defs><clipPath id="a"><path d="M2.1 0 16 13.9V0zM0 .7V16h15.3z"/></clipPath></defs>
<path clip-path="url(#a)" d="M6.119 0a1.5 1.5 0 0 0-1.34.826L4.189 2H2.5A2.5 2.5 0 0 0 0 4.5v8A2.5 2.5 0 0 0 2.5 15h11a2.5 2.5 0 0 0 2.5-2.5v-8A2.5 2.5 0 0 0 13.5 2h-1.689L11.225.83A1.501 1.501 0 0 0 9.883 0H6.12zm0 1h3.764a.5.5 0 0 1 .447.277l.725 1.445c.085.17.256.278.445.278h2A1.5 1.5 0 0 1 15 4.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-8A1.5 1.5 0 0 1 2.5 3h1.998a.5.5 0 0 0 .447-.276l.727-1.449A.5.5 0 0 1 6.12 1zm1.877 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 1a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/>
<path d="M.5 0a.5.5 0 0 0-.354.146.5.5 0 0 0 0 .707l15 15a.5.5 0 0 0 .707 0 .5.5 0 0 0 0-.707l-15-15A.5.5 0 0 0 .499 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M6.119 0a1.5 1.5 0 0 0-1.34.826L4.189 2H2.5A2.5 2.5 0 0 0 0 4.5v8A2.5 2.5 0 0 0 2.5 15h11a2.5 2.5 0 0 0 2.5-2.5v-8A2.5 2.5 0 0 0 13.5 2h-1.689L11.225.83A1.501 1.501 0 0 0 9.883 0H6.12zm0 1h3.764a.5.5 0 0 1 .447.277l.725 1.445c.085.17.256.278.445.278h2A1.5 1.5 0 0 1 15 4.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-8A1.5 1.5 0 0 1 2.5 3h1.998a.5.5 0 0 0 .447-.276l.727-1.449A.5.5 0 0 1 6.12 1zm1.877 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 1a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 613 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<defs><clipPath id="a"><path d="M2.1 0 16 13.9V0zM0 .7V16h15.3z"/></clipPath></defs>
<path clip-path="url(#a)" d="M4 1a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V4a3 3 0 0 0-3-3H4zm0 1h8a2 2 0 0 1 2 2v8c0 .373-.102.72-.28 1.02L9.05 8.432a1.5 1.5 0 0 0-2.101 0l-4.67 4.588a1.988 1.988 0 0 1-.28-1.02V4a2 2 0 0 1 2-2zm6.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm0 1a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1zM8 9.002c.127 0 .252.047.35.143l4.662 4.582A1.99 1.99 0 0 1 12 14H4c-.37 0-.715-.099-1.012-.273L7.65 9.145A.496.496 0 0 1 8 9.002z"/>
<path d="M.5 0a.5.5 0 0 0-.354.146.5.5 0 0 0 0 .707l15 15a.5.5 0 0 0 .707 0 .5.5 0 0 0 0-.707l-15-15A.5.5 0 0 0 .499 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M4 1a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V4a3 3 0 0 0-3-3H4zm0 1h8a2 2 0 0 1 2 2v8c0 .373-.102.72-.28 1.02L9.05 8.432a1.5 1.5 0 0 0-2.101 0l-4.67 4.588a1.988 1.988 0 0 1-.28-1.02V4a2 2 0 0 1 2-2zm6.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm0 1a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1zM8 9.002c.127 0 .252.047.35.143l4.662 4.582A1.99 1.99 0 0 1 12 14H4c-.37 0-.715-.099-1.012-.273L7.65 9.145A.496.496 0 0 1 8 9.002z"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M14.5 2a.5.5 0 0 0-.379.174l-9.129 10.57-4.12-4.578a.5.5 0 0 0-.706-.037.5.5 0 0 0-.037.705l4.5 5a.5.5 0 0 0 .75-.008l9.5-11a.5.5 0 0 0-.053-.705A.5.5 0 0 0 14.5 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M2.5 2a.5.5 0 0 0-.353.146.5.5 0 0 0 0 .708L7.293 8l-5.146 5.145a.5.5 0 0 0 0 .707.5.5 0 0 0 .707 0l5.5-5.5a.5.5 0 0 0 0-.707l-5.5-5.5A.5.5 0 0 0 2.5 2z"/>
<path d="M8.146 2.146a.5.5 0 0 0 0 .707l5.146 5.146-5.146 5.146a.5.5 0 0 0 0 .707.5.5 0 0 0 .707 0l5.5-5.5a.5.5 0 0 0 0-.707l-5.5-5.5a.5.5 0 0 0-.707 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

@@ -0,0 +1,3 @@
<svg width="9" height="9" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M1 .412a.588.588 0 0 0-.416.172.588.588 0 0 0 0 .832L3.668 4.5.584 7.584a.588.588 0 0 0 0 .832.588.588 0 0 0 .832 0L4.5 5.332l3.084 3.084a.588.588 0 0 0 .832 0 .588.588 0 0 0 0-.832L5.332 4.5l3.084-3.084a.588.588 0 0 0 0-.832.588.588 0 0 0-.832 0L4.5 3.668 1.416.584A.588.588 0 0 0 1 .412z"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M2.5 2a.5.5 0 0 0-.353.146.5.5 0 0 0 0 .708L7.293 8l-5.146 5.145a.5.5 0 0 0 0 .707.5.5 0 0 0 .707 0L8 8.706l5.146 5.146a.5.5 0 0 0 .706 0 .5.5 0 0 0 0-.707L8.708 8l5.146-5.146a.5.5 0 0 0 0-.707.5.5 0 0 0-.707 0L8 7.292 2.854 2.146A.5.5 0 0 0 2.5 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M2 0C.901 0 0 .901 0 2v1.5a.5.5 0 0 0 .5.5.5.5 0 0 0 .5-.5V2c0-.558.442-1 1-1h1.5A.5.5 0 0 0 4 .5a.5.5 0 0 0-.5-.5H2zm10.5 0a.5.5 0 0 0-.5.5.5.5 0 0 0 .5.5H14c.558 0 1 .442 1 1v1.5a.5.5 0 0 0 .5.5.5.5 0 0 0 .5-.5V2c0-1.099-.901-2-2-2h-1.5zm-8 2A2.508 2.508 0 0 0 2 4.5v7C2 12.875 3.125 14 4.5 14h7c1.375 0 2.5-1.125 2.5-2.5v-7C14 3.125 12.875 2 11.5 2h-7zm0 1H7v1.5C7 5.323 7.677 6 8.5 6H13v5.5c0 .834-.666 1.5-1.5 1.5h-7c-.834 0-1.5-.666-1.5-1.5v-7C3 3.666 3.666 3 4.5 3zM8 3h3.5c.834 0 1.5.666 1.5 1.5V5H8.5a.493.493 0 0 1-.5-.5V3zM.5 12a.5.5 0 0 0-.5.5V14c0 1.099.901 2 2 2h1.5a.5.5 0 0 0 .5-.5.5.5 0 0 0-.5-.5H2c-.558 0-1-.442-1-1v-1.5a.5.5 0 0 0-.5-.5zm15 0a.5.5 0 0 0-.5.5V14c0 .558-.442 1-1 1h-1.5a.5.5 0 0 0-.5.5.5.5 0 0 0 .5.5H14c1.099 0 2-.901 2-2v-1.5a.5.5 0 0 0-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M3.5 0a.5.5 0 0 0-.5.5v8.504a2 2 0 0 0 2 2h1.004v2.998a2 2 0 1 0 4 0v-2.998H11a2 2 0 0 0 2-2V.5a.5.5 0 0 0-.5-.5h-9zM4 1h4v1.5a.5.5 0 1 0 1 0V1h1v2.502a.5.5 0 0 0 1 0V1h1v6.004H4V1zm0 7.004h8v1a1 1 0 0 1-1 1H9.504a.5.5 0 0 0-.5.5v3.498a1 1 0 1 1-2 0v-3.498a.5.5 0 0 0-.5-.5H5a1 1 0 0 1-1-1v-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<defs><clipPath id="a"><path d="M2.1 0 16 13.9V0zM0 .7V16h15.3z"/></clipPath></defs>
<path clip-path="url(#a)" d="M8 0C4.692 0 2 2.692 2 6v3.4l-.928 2.229c-.265.636.225 1.371.914 1.371H5.5c0 1.375 1.125 2.5 2.5 2.5s2.5-1.125 2.5-2.5h3.514c.69 0 1.18-.735.914-1.371L14.001 9.4V6c0-3.308-2.692-6-6-6zm0 1c2.767 0 5 2.233 5 5v3.5a.5.5 0 0 0 .04.191l.96 2.31H2l.96-2.31A.5.5 0 0 0 3 9.5V6c0-2.767 2.233-5 5-5zM6.5 13h3c0 .834-.666 1.5-1.5 1.5s-1.5-.666-1.5-1.5z" style="-inkscape-stroke:none;color:#000;stroke-linecap:round;stroke-linejoin:round"/>
<path d="M.5 0a.5.5 0 0 0-.354.146.5.5 0 0 0 0 .707l15 15a.5.5 0 0 0 .707 0 .5.5 0 0 0 0-.707l-15-15A.5.5 0 0 0 .499 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 0C4.692 0 2 2.692 2 6v3.4l-.928 2.229c-.265.636.225 1.371.914 1.371H5.5c0 1.375 1.125 2.5 2.5 2.5s2.5-1.125 2.5-2.5h3.514c.69 0 1.18-.735.914-1.371L14.001 9.4V6c0-3.308-2.692-6-6-6zm0 1c2.767 0 5 2.233 5 5v3.5a.5.5 0 0 0 .04.191l.96 2.31H2l.96-2.31A.5.5 0 0 0 3 9.5V6c0-2.767 2.233-5 5-5zM6.5 13h3c0 .834-.666 1.5-1.5 1.5s-1.5-.666-1.5-1.5z" style="-inkscape-stroke:none;color:#000;stroke-linecap:round;stroke-linejoin:round"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M11.5 0a4.5 4.5 0 0 0-4.418 5.36L.656 12.016a2.358 2.358 0 0 0 3.375 3.294l6.365-6.447a4.5 4.5 0 0 0 5.207-6.215.503.503 0 0 0-.81-.149L12.5 4.793 11.207 3.5 13.5 1.207A.5.5 0 0 0 13.353.4 4.49 4.49 0 0 0 11.499 0zm0 1c.247 0 .487.026.719.074l-2.072 2.072a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2.072-2.072a3.5 3.5 0 0 1-4.52 4.045.502.502 0 0 0-.514.123L3.32 14.61a1.358 1.358 0 0 1-1.943-1.896L7.99 5.859a.498.498 0 0 0 .123-.473A3.5 3.5 0 0 1 11.5.999z"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 0a.5.5 0 0 0-.5.5v10.79L3.854 7.644a.5.5 0 1 0-.707.707l4.5 4.5a.5.5 0 0 0 .707 0l4.5-4.5a.5.5 0 0 0-.707-.707L8.501 11.29V.5a.5.5 0 0 0-.5-.5z"/>
<path d="M1.5 12a.5.5 0 0 0-.5.5v1C1 14.875 2.125 16 3.5 16h9c1.375 0 2.5-1.125 2.5-2.5v-1a.5.5 0 0 0-1 0v1c0 .834-.666 1.5-1.5 1.5h-9c-.834 0-1.5-.666-1.5-1.5v-1a.5.5 0 0 0-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M5.5 0A2.5 2.5 0 0 0 3 2.5v8A2.5 2.5 0 0 0 5.5 13h8a2.5 2.5 0 0 0 2.5-2.5v-8A2.5 2.5 0 0 0 13.5 0h-8zm0 1H7v1.5A1.5 1.5 0 0 0 8.5 4H15v6.5a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 4 10.5v-8A1.5 1.5 0 0 1 5.5 1zM8 1h5.5A1.5 1.5 0 0 1 15 2.5V3H8.5a.5.5 0 0 1-.5-.5V1zM2 3.05A2.5 2.5 0 0 0 0 5.5V12a4 4 0 0 0 4 4h6.5a2.5 2.5 0 0 0 2.45-2h-1.036a1.5 1.5 0 0 1-1.414 1H4a3 3 0 0 1-3-3V5.5a1.5 1.5 0 0 1 1-1.414V3.05z"/>
</svg>

After

Width:  |  Height:  |  Size: 551 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M11.72 6.586c-.799.537-1.787 1.167-3.303 1.422a.5.5 0 0 0-.418.492v2.564c0 2.288 1.648 4.08 3.822 4.902a.5.5 0 0 0 .356 0c2.174-.822 3.822-2.614 3.822-4.902V8.5a.5.5 0 0 0-.419-.492c-1.514-.255-2.503-.885-3.302-1.422a.5.5 0 0 0-.56 0zm.28.992c.716.47 1.69.982 3 1.281v2.205c0 1.741-1.23 3.108-3 3.848-1.77-.74-3-2.107-3-3.848V8.86a9.083 9.083 0 0 0 3-1.28z"/>
<path d="M6 0C4.901 0 4 .901 4 2v10c0 1.099.901 2 2 2h1.836a5.373 5.373 0 0 1-.488-1H6c-.558 0-1-.442-1-1V2c0-.558.442-1 1-1h6c.558 0 1 .442 1 1v3.865c.203.139.748.48 1 .592V2c0-1.1-.902-2-2-2H6z"/>
<path d="M3 2.271C2.404 2.619 2 3.265 2 4v8.5C2 14.427 3.573 16 5.5 16h4.252a7.236 7.236 0 0 1-1.154-1H5.5A2.492 2.492 0 0 1 3 12.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 837 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M6 0a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H6zm0 1h6a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM3 2.268C2.402 2.614 2 3.26 2 4v8.5A3.5 3.5 0 0 0 5.5 16H10c.74 0 1.387-.402 1.732-1H5.5A2.5 2.5 0 0 1 3 12.5V2.27z"/>
</svg>

After

Width:  |  Height:  |  Size: 397 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M3.508 0a.5.5 0 0 0-.428.771L7.404 7.43 5.48 10.39a3 3 0 1 0 .78.636L8 8.347l1.74 2.68a3 3 0 1 0 .78-.636L3.92.23a.503.503 0 0 0-.412-.228zm8.984 0a.503.503 0 0 0-.412.229L8.596 5.594l.598.918L12.92.772A.5.5 0 0 0 12.493 0zM4 11a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm8 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 432 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M8 .5A2.5 2.5 0 0 0 5.5 3H1a.5.5 0 0 0-.5.5l.008.09A.5.5 0 0 0 1 4h.553L2.85 14.229A2 2 0 0 0 4.836 16h6.328a2 2 0 0 0 1.986-1.771L14.445 4H15a.5.5 0 0 0 0-1h-4.5A2.5 2.5 0 0 0 8 .5zm0 1A1.5 1.5 0 0 1 9.5 3h-3A1.5 1.5 0 0 1 8 1.5zM2.56 4h10.877l-1.28 10.116a.998.998 0 0 1-.993.884H4.836a.998.998 0 0 1-.992-.884zM6.5 6.5c-.276 0-.5.196-.5.438v5.124l.008.078c.042.204.247.36.492.36.276 0 .5-.196.5-.438V6.938l-.008-.079C6.95 6.655 6.745 6.5 6.5 6.5zm3 0c-.276 0-.5.196-.5.438v5.124l.008.078c.042.204.247.36.492.36.276 0 .5-.196.5-.438V6.938l-.008-.079C9.95 6.655 9.745 6.5 9.5 6.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M6.5 0a1.5 1.5 0 0 0-1.414 1H3.5A1.5 1.5 0 0 0 2 2.5v12A1.5 1.5 0 0 0 3.5 16h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 1-.5-.5v-12a.5.5 0 0 1 .5-.5h1.586A1.5 1.5 0 0 0 6.5 3h3a1.5 1.5 0 0 0 1.414-1H12.5a.5.5 0 0 1 .5.5v1a.5.5 0 0 0 1 0v-1A1.5 1.5 0 0 0 12.5 1h-1.586A1.5 1.5 0 0 0 9.5 0h-3zm0 1h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm3 4A1.5 1.5 0 0 0 8 6.5v8A1.5 1.5 0 0 0 9.5 16h5a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 5h-5zm0 1h5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path transform="scale(-1 1) translate(-16 0)" d="M1.5 0a.5.5 0 0 0-.5.5v4.9a.6.6 0 0 0 .6.6h4.9a.5.5 0 0 0 0-1H2.906L6.38 1.98a4 4 0 1 1 5.248 6.04l-8.172 7.103a.5.5 0 1 0 .656.754l8.172-7.104a5.001 5.001 0 0 0-6.56-7.547L2 4.464V.5a.5.5 0 0 0-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M6 0a2 2 0 0 0-2 2v.5a.5.5 0 0 0 1 0V2a1 1 0 0 1 1-1h.5a.5.5 0 0 0 0-1H6zm2.5 0a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zm5 0a.5.5 0 0 0 0 1h.5a1 1 0 0 1 1 1v.5a.5.5 0 0 0 1 0V2a2 2 0 0 0-2-2h-.5zM2 4a2 2 0 0 0-2 2v6.5A3.5 3.5 0 0 0 3.5 16H10a2 2 0 0 0 2-2v-1h-1v1a1 1 0 0 1-1 1H3.5A2.5 2.5 0 0 1 1 12.5V6a1 1 0 0 1 1-1h1V4H2zm2.5 0a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5zm11 0a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5zm-11 5a.5.5 0 0 0-.5.5v.5a2 2 0 0 0 2 2h.5a.5.5 0 0 0 0-1H6a1 1 0 0 1-1-1v-.5a.5.5 0 0 0-.5-.5zm11 0a.5.5 0 0 0-.5.5v.5a1 1 0 0 1-1 1h-.5a.5.5 0 0 0 0 1h.5a2 2 0 0 0 2-2v-.5a.5.5 0 0 0-.5-.5zm-7 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 813 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M1.5 0a.5.5 0 0 0-.5.5v4.9a.6.6 0 0 0 .6.6h4.9a.5.5 0 0 0 0-1H2.906L6.38 1.98a4 4 0 1 1 5.248 6.04l-8.172 7.103a.5.5 0 1 0 .656.754l8.172-7.104a5.001 5.001 0 0 0-6.56-7.547L2 4.464V.5a.5.5 0 0 0-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M11.25.815a2.783 2.783 0 0 1 4.066 3.796l-.13.14-9.606 9.605a2 2 0 0 1-.723.463l-.165.053-4.055 1.106a.5.5 0 0 1-.63-.535l.016-.08L1.13 11.31a2 2 0 0 1 .398-.76l.117-.128zm-.86 2.275-8.04 8.038a1 1 0 0 0-.215.321l-.042.123-.876 3.211 3.212-.876a1 1 0 0 0 .238-.1l.108-.071.098-.086 8.038-8.04zm4.089-1.568a1.784 1.784 0 0 0-2.402-.11l-.12.11-.86.86 2.52 2.522.861-.86a1.784 1.784 0 0 0 .11-2.402z"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<defs><clipPath id="a"><path d="M2.1 0 16 13.9V0zM0 .7V16h15.3z"/></clipPath></defs>
<path clip-path="url(#a)" d="m8.25 0-.154.006a2.249 2.249 0 0 0-2.074 1.939L6.014 2H3.502l-.145.006A1.5 1.5 0 0 0 2.002 3.5v2.264l-.058.006A2.25 2.25 0 0 0 0 8.001l.006.155a2.249 2.249 0 0 0 1.939 2.074l.056.005v2.266l.006.145A1.5 1.5 0 0 0 3.502 14h2.014l.006.055a2.25 2.25 0 0 0 2.23 1.943l.154-.006a2.25 2.25 0 0 0 2.074-1.94L9.986 14h2.516a1.5 1.5 0 0 0 1.5-1.5L14 9.247l-1.75.002-.123-.006A1.246 1.246 0 0 1 11 8l.006-.123A1.246 1.246 0 0 1 12.25 6.75L14 6.747l.002-3.246-.008-.144a1.499 1.499 0 0 0-1.492-1.355h-2.018l-.004-.057A2.252 2.252 0 0 0 8.25 0zm0 1c.69 0 1.25.56 1.25 1.25V3l3.002.002.09.008a.5.5 0 0 1 .41.492v2.246l-.752.002a2.25 2.25 0 0 0-2.242 2.066L10 7.98c0 1.194.903 2.167 2.066 2.262l.164.008.772-.002v2.254a.5.5 0 0 1-.5.5L9 13l.002.752a1.25 1.25 0 0 1-2.5-.002L6.5 13l-2.998.002a.5.5 0 0 1-.5-.5L3 9.25l-.752.002a1.249 1.249 0 1 1 .002-2.5L3 6.75l.002-3.248a.5.5 0 0 1 .5-.5L7 3v-.75C7 1.56 7.56 1 8.25 1z"/>
<path d="M.5 0a.5.5 0 0 0-.354.146.5.5 0 0 0 0 .707l15 15a.5.5 0 0 0 .707 0 .5.5 0 0 0 0-.707l-15-15A.5.5 0 0 0 .499 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="m8.25 0-.154.006a2.249 2.249 0 0 0-2.074 1.939L6.014 2H3.502l-.145.006a1.5 1.5 0 0 0-1.355 1.494v2.264l-.058.006A2.25 2.25 0 0 0 0 8.001l.006.155a2.249 2.249 0 0 0 1.939 2.074l.056.005v2.266l.006.145A1.5 1.5 0 0 0 3.502 14h2.014l.006.055a2.25 2.25 0 0 0 2.23 1.943l.154-.006a2.25 2.25 0 0 0 2.074-1.94l.006-.052h2.516a1.5 1.5 0 0 0 1.5-1.5L14 9.247l-1.75.002-.123-.006A1.246 1.246 0 0 1 11 8l.006-.123A1.246 1.246 0 0 1 12.25 6.75L14 6.747l.002-3.246-.008-.144a1.499 1.499 0 0 0-1.492-1.355h-2.018l-.004-.057A2.252 2.252 0 0 0 8.25 0zm0 1c.69 0 1.25.56 1.25 1.25V3l3.002.002.09.008a.5.5 0 0 1 .41.492v2.246l-.752.002a2.25 2.25 0 0 0-2.242 2.066L10 7.98c0 1.194.903 2.167 2.066 2.262l.164.008.772-.002v2.254a.5.5 0 0 1-.5.5L9 13l.002.752a1.25 1.25 0 0 1-2.5-.002L6.5 13l-2.998.002a.5.5 0 0 1-.5-.5L3 9.25l-.752.002a1.249 1.249 0 1 1 .002-2.5L3 6.75l.002-3.248a.5.5 0 0 1 .5-.5L7 3v-.75C7 1.56 7.56 1 8.25 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M5.5 7a.5.5 0 0 0-.5.5.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5.5.5 0 0 0-.5-.5zM4 2c-.736 0-1.381.404-1.729 1h11.46c-.347-.596-.993-1-1.729-1zM3 4c-1.099 0-2 .9-2 2v7c0 1.099.9 2 2 2h10c1.099 0 2-.9 2-2V6c0-1.099-.9-2-2-2zm0 1h10c.563 0 1 .437 1 1v7c0 .563-.437 1-1 1H3c-.563 0-1-.437-1-1V6c0-.563.437-1 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

Some files were not shown because too many files have changed in this diff Show More