Compare commits

...
Author SHA1 Message Date
pastilhasandClaude Opus 5 78db9c8c93 2026.08.22.01 — 349 themes, spotlight, prompt history, email multi-account
No schema and no dependency change, verified against the previous tag rather than assumed. The last release
needed a push for anthropic_accounts and this one needs none, so the plan says so explicitly — a host that
runs db:push here has run something the release did not ask for.

Two things the plan states that a reader could not infer from the diff. The email plugin's multi-account
work is in ANOTHER repository and arrives through plugins:update, affecting only hosts where that plugin is
installed. And officer-dev is skipped as always but is NOT behind this time: it was restarted repeatedly
through the night as the work landed, so it is already running the tree this tag names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 04:20:18 +00:00
pastilhasandClaude Fable 5 ff133f88ff todo: settings as portable files, per user and per server
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 04:19:40 +00:00
pastilhasandClaude Opus 5 8e181ddf3c any key after a recall ends it, and Left is the one that proves the point
A recalled prompt you have started moving around in is a prompt you have adopted, so the next Up must not
walk further back over it — the same fault as recalling over a fresh draft, arrived at from the other side.

Left and Right are why this cannot be inferred from the text. They change nothing, so any rule watching for
edits would miss them entirely, and arrowing into the middle of a recalled prompt is the clearest statement
of intent available: you are working on this one now.

So the composer leaves the history on any keystroke that is not Up or Down. Bare modifiers are excluded —
holding Shift to capitalise the next letter is not editing, and treating it as such would end a recall
between two presses of Up. `Dead` is in that set too: a pending accent on a composing layout is not a
finished keystroke.

leave() clears the stashed draft as well as the position. That draft belonged to the composing session being
left, and handing it back after you have started editing something else would paste an older draft over a
newer one.

Six tests, three of them on the interaction between this and the emptiness rule from the commit before —
together they are what stops Up from eating an edited prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 04:15:08 +00:00
pastilhasandClaude Opus 5 02df4ac61c prompt history only takes the arrow key from an empty composer
Up replaced what you were typing. The guard was a caret-position test — Up only recalls from the first line
— which is correct for a multi-line draft and useless for a one-line one: the caret IS on the first line, so
every check passed and a half-written thought was swapped for something from last week. A draft is not
recoverable from anywhere and the history is, so the tie goes to the draft.

"Only when empty" alone would have made the feature useless, though, and that is the part worth writing
down: once a recalled prompt is in the box, the box is not empty, so an emptiness test allows exactly one
step back and then refuses. usePromptHistory already knew the difference — indexRef null means composing —
it just never told anyone. It now exposes isRecalling(), so the rule is "no text OF YOURS" rather than "no
text".

Whitespace counts as empty: a composer holding two spaces is not a draft anybody would miss, and an
invisible character silently blocking Up is worse than losing it. A draft that merely STARTS with
whitespace is still protected.

The line tests stay on top of it — in a recalled prompt spanning three lines, Up from the middle moves the
caret, because there is a line above to go to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 04:03:03 +00:00
pastilhasandClaude Opus 5 a5f387929d spotlight takes focus, and its arrow keys no longer depend on getting it
The arrow keys sometimes did nothing when the launcher was opened from a text field, and the two halves of
that are one bug: navigation lived on the panel input's onKeyDown, so every keystroke was conditional on
focus having actually landed there. When it had not — the composer kept it — ArrowDown went to the textarea
and the launcher never saw the key.

Two fixes, because either alone leaves the other half.

Opening now blurs whatever had focus, explicitly, before the overlay mounts. The effect that focuses the
panel's own input was already there and is enough on paper; "on paper" is what produced an
intermittent bug, so the outgoing element is blurred rather than assumed away.

And navigation moved to the window listener that already handled Escape, so it works regardless of what has
focus — a panel that grabbed it back, a blur that lost a race, an element with its own key handling. The
input's copy is deleted; leaving both would move the cursor twice per press whenever focus did land.

The handler reads `results` and `active` through refs, deliberately. The listener registers once per open,
so closing over them would pin it to their values at registration — arrowing down would read a stale list
and Enter would open whatever was first when the panel appeared. Putting them in the effect's deps instead
re-registers the listener on every keystroke, which is the same bug in a costume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 04:00:45 +00:00
pastilhasandClaude Opus 5 e4117441b0 spotlight had list metrics, and it is not a list
It read as cramped because the first pass reused the app's own list sizing — a 7px tile, text-sm, py-2 —
which is right for a row inside a panel competing with everything around it, and wrong for a modal you look
at for two seconds with nothing else on screen.

So the search input leads at text-base with a 5px glyph, rows are py-2.5 with a 9px tile, and the label is
text-[15px] with the hint and the route kept deliberately subordinate rather than scaled with it.

Two knock-on changes that are arithmetic rather than taste. The panel widened 560 → 640, because a row now
carries a label, a hint and a route on one line and the route is the first thing squeezed out — it is also
what disambiguates two rows with the same name. And the results box grew 52vh → 60vh, because taller rows in
the same box show fewer results, which would have quietly undone removing the eight-item cap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:56:01 +00:00
pastilhasandClaude Opus 5 7671368faf alt-space opens spotlight from a text field too
The focus guard was still there. I moved the trigger to alt-space, wrote that the guard was no longer
load-bearing, and then kept it anyway "as belt-and-braces" — which left the exact bug the move was meant to
fix. Focus is usually in the chat composer, so the launcher did not open from the place it is most often
wanted. Reported one commit later, by the owner, from a text field.

The guard existed because shift-space types a space and eating that keystroke was unacceptable. Alt-space
types nothing, so there is nothing to protect. preventDefault on the handler covers the residual case a
layout might deliver, which is what the guard was really insuring against.

isTyping is deleted rather than left unused: a dead helper carrying a long comment about a decision that has
been reversed is worse than no helper, because the comment still reads as current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:37:20 +00:00
pastilhasandClaude Opus 5 6b07aafc67 spotlight opens on alt-space, and on nothing else
Third chord in a day, and each move was the owner naming a collision on the machine he actually uses:
ctrl-space is Brave's address bar, shift-space types a space, alt-space is the window menu — and he rebound
that rather than give up the chord.

Shift-space was the worst of the three and it is worth saying why, because it read as the safe choice.
It is a real key combination inside a text field: it types a space, and people hold shift through one more
often than they think while typing capitals. So it had to stand down whenever focus was in an input — which
is where focus usually is, the chat composer — and a launcher you have to click out of a field to reach is
not a launcher.

Alt-space means nothing in a text field, so the isTyping guard stops being the thing holding the feature up
and becomes belt-and-braces. Kept anyway: two lines against some layout delivering alt-space as a literal
character, which is cheap to prevent and expensive to discover.

`altKey` with `ev.code`, not `ev.key` — alt-space produces a different `key` on several layouts and nothing
at all on others, while the physical key is `Space` everywhere.

No second chord. Two triggers for one thing means two things to collide with, and this one has moved twice.

The TODO item's id still says ctrl-space. Ids are stable by design, so it stays; its body now records all
three moves and which guard was load-bearing for which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:31:44 +00:00
pastilhasandClaude Opus 5 f9dfd05676 header cluster and breadcrumb colour
Breadcrumb links rested at duck-forest and hovered to duck-teal; they now sit at the
hover colour, so the hover and its transition go with it. One shared component, so the
full browser and the dashboard widget both pick it up. The current folder stays
duck-dark semibold, keeping clickable and current distinct.

Header: the theme control leads the right cluster and names the theme it is wearing
rather than showing a palette glyph — with 227 of them, which one is on is worth reading.
It takes JobsIndicator's pill styling so the cluster stays one shape, and keeps its
colour on --header-fg rather than hardcoding white.

The jobs indicators and the rescan button are Super Admin only now. Cosmetic, and the
comment says so: /jobs stays reachable by URL and rescan is gated server-side, so
nothing here is load-bearing. Uses the same user?.role check UserMenu already makes in
this folder rather than adding a second convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:24:08 +00:00
pastilhasandClaude Opus 5 cbcd8d65df fix accent/secondary foreground derivation; 202 community themes
The nine remaining contrast failures all had the same cause, and it was a real bug in
map.ts rather than a limit of the source palettes: --accent and --secondary are both
s.high, but their foregrounds were taken from fgRaised, which is checked against
s.raised. That is precisely the near-miss the file already documents and fixes for
--muted-foreground, one token over. Deriving them against s.high instead rescues all
nine (every Solarized variant among them) and is provably a no-op for the official 22 —
each already clears 4.5:1 on s.high by 1.3x or more, and ensureContrast returns its
input untouched when it passes. Measured all 22 before the change; generate-themes.ts
--check reports 0 changes after it.

Also: GitLab-hosted repos are now discoverable (one theme lives there, and the raw-URL
and default-branch shapes differ from GitHub's), and sourceFor probes a directory named
after the theme's own slug, for the packs that nest one directory per theme. An exact
slug match is the only prefix tried on purpose — a pack whose variants have names of
their own has no single theme that is "the" one, so guessing would be wrong.

Every community theme is regenerated in this commit, not just the new ones. aura turned
up generated under an older pickAccent rule than its neighbours, because it was ported
before that rule changed — the same drift generate-themes.ts was written to prevent, and
the reason both halves are written from one source. All 202 now come from one mapping
version. Verified afterwards: 227 stylesheets, 227 imports, 227 registry entries, no
orphans in either direction.

Still out: 14 repos that 404 (deleted or renamed since the site indexed them),
grimdark-solarized (no source link at all), cobalt2 (its page links an awesome-list
README with no theme in it), and oasis (a six-variant pack with no variant named oasis —
porting it means inventing six slugs the site does not list, which is a naming decision).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:42:43 +00:00
pastilhasandClaude Sonnet 5 861fc4a58b add colors.toml and 0x-hex support, rescuing 44 more community themes
Poked at the 62 "no parseable source" skips from the first pass and found two things
the pipeline didn't handle: several community repos genuinely ship colors.toml too — some
in Omarchy 4's own semantic schema (best case, no derivation needed at all, same quality
as the official 22), others reusing the filename for an unrelated flat color0..color15
scheme — and a handful of alacritty.toml files use 0xrrggbb instead of #rrggbb.

palette.ts's loadPalette is now a thin wrapper over two exported pieces (parseTomlText,
paletteFromRecord) so the community pipeline can attempt the real schema first without
duplicating the reader; falls through to the color16 or ANSI-derived paths otherwise.
generate-themes.ts --check still reports 0 changes for the official set — the refactor
is behavior-preserving there, the lowercase-hex normalization it picked up along the way
touched 7 official theme files cosmetically (their source colors.toml has uppercase hex),
regenerated for real rather than left inconsistent with their own source.

147 -> 191 community themes ported. Of the original 62 unparseable repos, most turned out
to be simply gone (404 on GitHub — repos deleted or renamed since the site listed them);
the rest needed one of the two format additions above. 20 themes remain stuck: 16 dead
repos, 2 with no repo link at all, and the same handful of genuinely low-contrast source
palettes (mostly Solarized variants) where fixing contrast would mean changing the
author's actual colours rather than porting them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 01:48:21 +00:00
pastilhasandClaude Sonnet 5 a58f4d2b32 port 147 community Omarchy themes from omarchythemes.com
The official 22 themes come from basecamp/omarchy's own colors.toml, contrast-checked
by generate-themes.ts. A community theme has no such file — it ships per-app configs
(alacritty.toml, ghostty.conf) in the older 16-ANSI-slot style, so this adds a sibling
pipeline: from-ansi.ts derives the same 24-key Palette shape by role (accent, three
background depths, three foreground weights) from those 16 slots, then runs through the
same mapPalette + contrast checks the official themes use. Nothing about the official
generator changed — verified generate-themes.ts --check still reports 0 changes.

port-community-themes.ts discovers each theme's GitHub repo from its omarchythemes.com
page, fetches alacritty.toml (preferred) or ghostty.conf, and is idempotent via
community-manifest.json so a re-run only fetches what's missing. Community themes get
their own registry/index.css marker block, separate from the official GENERATED THEMES
block, so the two pipelines can never clobber each other.

121 of 212 attempted made it through cleanly; a second pass tightening accent selection
and nudging --input recovered 20 more borderline cases (147 total). The rest failed for
real reasons: 62 repos had no parseable colour source, 7 had an accent colour whose own
luminance can't clear 4.5:1 against either black or white text, a handful were narrowly
under threshold even after the rescue pass, and 2 pages had no repo link at all. Every
generated file's header states its source repo and whether it's a light or dark theme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 01:37:05 +00:00
pastilhasandClaude Fable 5 a01ae73a1f todo: plugin permissions on by default for every role
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 00:22:25 +00:00
pastilhasandClaude Opus 5 78f9331b89 deploy report for 2026.08.21.04 — six hosts, one skipped on purpose
alpha, fofos, macbookpro, edge, production and deadpool all took 98177c3b, the
anthropic_accounts push and a restart. officer-dev is skipped because the agent
writing this runs on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:14:48 +00:00
pastilhasandClaude Opus 5 98177c3bc9 2026.08.21.04 — /login in the chat, account switching, and a spotlight that reaches the plugins
First release in three with a SCHEMA CHANGE. anthropic_accounts is new, and without the push the account
switcher's route fails on a missing relation the first time the usage panel is opened — so the plan names
db:push as a step rather than leaving it to be inferred from the diff.

alpha already runs this code, deployed by hand as a preview. The plan says its pull will be a no-op and to
run the rest anyway, so it does not read as a skipped host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 23:05:39 +00:00
pastilhasandClaude Opus 5 3cab16ca5b spotlight showed eight items, so every plugin was unreachable
MAX_RESULTS was 8. Core items alone fill eight slots, so on a populated server the empty-query list stopped
at Browser and none of the fourteen plugin tiles below it could be reached — unless you already knew the
name well enough to type it, which is not a thing a launcher gets to require. The owner caught it on alpha
with the dock and the launcher side by side; on this machine, which has few plugins, it looked complete.

The items were there the whole time. This was not a missing source — it was a silent truncation, with
nothing on screen saying "and 20 more", which is exactly how a cap reads as an absent feature.

There was never a layout reason for it either: the results container has scrolled since it was written.
Removed, and the active row now scrolls itself into view with `block: 'nearest'` so arrowing past the fold
works and arrowing through the visible middle does not jump the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:24:01 +00:00
pastilhasandClaude Opus 5 f023ab5da0 spotlight draws the dock's tiles, and lists the settings that actually exist
Two things made it read as a generic list rather than as this app's own, and neither was the item source —
that was already the dock, and plugins were already in it.

TILES. SpotlightItem dropped `tile`, so every row got a `${color}22` tinted square: a third visual language
belonging to neither of the dock's two. Which put a coloured pane behind Calendar, Contacts and every
plugin's logo — the exact thing the dock's own comment calls out as reading like a mistake. Rows now use the
dock's rule: a filled square of the item's colour with a white glyph, or no background at all when the
artwork brings its own frame. A settings row with no colour keeps a muted glyph instead of getting a swatch
invented for it.

SETTINGS. The hand-listed sections had drifted within two days of being written, and the file's own comment
predicted it ("worth revisiting if it drifts"). Nine rows against a settings area that has twenty-one:
Languages and Voice missing entirely, every AI and System section missing, `/settings/users` pointing at a
route that does not exist — it is `/settings/user-management/users` — and four different rows sharing the
User glyph, which is most of what made the panel look generic.

Now twenty rows, copied from the five screens that declare them, with each group naming its source file so
checking is one grep. The comment records that the drift HAPPENED rather than predicting it, and says
deriving them from the screens is the fix if it happens a third time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:17:58 +00:00
pastilhasandClaude Opus 5 b10a49c082 an account with no saved sign-in gets a chip you can read, not one you can use
Three chip states now, and the third is a different question rather than a broken version of the second:

  filled          the account you are
  plain           an account you can become — click switches
  dashed outline  an account you can only look at — click shows its last known usage

"Has my other allowance come back yet" is answerable from a snapshot, and the machinery for it was already
right: a reset time is an absolute instant, and currentLimit rolls a past window forward and zeroes it
instead of showing a stale percentage. Phase one was hiding a correct answer.

What you cannot do is act on it without a /login, so the chip says that before it is clicked (in its title
and in its border) and the panel says it again while you are reading one. Naming the fix rather than the
problem: "No saved sign-in — run /login to use this one".

Viewing is deliberately NOT a general tab selection. Only an account with no credential can be viewed,
because for any other one the useful gesture is switching to it and then reading live numbers — which keeps
phase one's invariant intact: if the panel shows a signed-in account, it is THE signed-in account. Switching
clears the view for the same reason, so the numbers and the chips can never disagree.

Replaces the count line, which was a fact about somewhere else. The staleness line that was already in this
file — "Not signed in · as of 20 minutes ago" — now finally has a case that reaches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:33:58 +00:00
pastilhasandClaude Opus 5 4b2cb3d266 the account chips show the whole address, because the local part is the shared half
Two chips both read `andre.padez`. They are andre.padez@gmail.com and andre.padez@pertento.ai — one
person's personal and work accounts, which is the entire case this switcher exists for.

The chip showed the local part on the reasoning that the domain is what accounts have in common. That is
true of accounts belonging to different people and exactly backwards here: the local part is the half they
share and the domain is the half that tells them apart. So the strip wraps instead of scrolling, and the
address is not truncated.

The "N other accounts not shown" line was also lying. It counted stored usage READINGS minus the one being
displayed, which since the switcher landed includes accounts whose chip is two lines below — it said "2
other accounts not shown" with both of them on screen. It now counts only accounts with a reading and no
saved credential: an old login you can look at and cannot return to, which is the thing actually worth
saying. On this machine that is one, jrg.conceicao@gmail.com.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:30:06 +00:00
pastilhasandClaude Opus 5 7d7a80782e switch Anthropic accounts from the usage pills
The owner keeps a personal account and a company one and moves between them during a day — by what the work
is, and sometimes because one has hit a limit and the work has to carry on. /login handles ARRIVING at an
account. It could not go back to one, because the credential it overwrote was gone.

ai_provider_usage already answered "which of my accounts has room". This answers what immediately follows:
"then use that one". The chips under the numbers stop selecting whose stats you read and start selecting who
you ARE — which turned out to be the useful half of the same gesture, since reading the other account's
percentages was only ever a prelude to using it.

anthropic_accounts holds each login's OAuth object encrypted, keyed (user_id, account_uuid).

One key per ACCOUNT, not one for the table: encryptSecret('anthropic-account:<uuid>', …), so the secret
store mints a separate key each and one leak is one account. Same reasoning that split VAULT_STORE_KEY into
purposes. The uuid comes from Anthropic and survives a re-login; the email is stable too but is editable at
the provider.

What is stored includes a REFRESH token, deliberately — the access token lives hours and the refresh token
is what makes a switch work a week later. So a row plus its key is thirty days of acting as that account.
That was accepted knowingly; the per-account key narrows it, and refreshExpiresAt is a column so a dead
credential gets purged on read rather than sitting here for ever. An expired one cannot be switched to at
all: reveal returns null, because writing an expired credential would sign the owner out with no message.

Capture happens on login AND on every read of the account list. Without that second one this would know
only about logins that went through this dialog, and an account signed into from a terminal would be
invisible to the switcher — half your accounts missing, silently. The switch also captures the OUTGOING
credential first, or switching away from a terminal login loses it.

.credentials.json is replaced wholesale; .claude.json is PATCHED. The second holds the CLI's entire config
and overwriting it would take out every project entry and preference in it.

Owner only, and the isolation is unchanged: getOwnerHomeDir ignores the email it is passed, so every path
here is the owner's. A member's claude reads their own home and none of this touches it. The earlier claim
in this work that the credential was "machine-wide" was wrong — the owner corrected it. What a switch
actually changes is every claude running AS THE OWNER: terminals, task runners, pipelines, the commander.

The spinner is not decoration. A switch rewrites a file and changes nothing visible, so without it the
honest reading of a click is "nothing happened" and the second click is somebody checking. A running turn
keeps the credential it was spawned with, and the response says so rather than leaving the UI to imply
otherwise.

Verified before shipping: capture records the live account with org, plan and expiry; reveal decrypts
byte-identical to what is on disk; /api/claude-auth/accounts is 401 unauthenticated and 403 for a non-owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:24:28 +00:00
pastilhasandClaude Fable 5 bd9d26199f the ephemeral pane is open when it exists, not when an effect says so
Refresh on /files/...?view=x.m4a came back with the query string intact and no
viewer, and then double-clicking the file did nothing at all. Two symptoms, one
cause, and the second is what made it read as a click bug.

WorkspaceView gave the pane defaultSize={0} and opened it from an effect calling
panel.resize(), so open/closed was expressed twice — the markup said closed and
the effect was meant to correct it. A correction step can be missed, and this one
was: on a deep link isEphemeralOpen is true on the first render, but
workspace.isLoaded is false until /dashboards answers and the early return means
nothing is rendered, so the effect ran against a null ref and returned. Its deps
were [isEphemeralOpen, defaultBaseSize] — one already true, the other a constant
— so it never ran again. URL open, pane at zero width.

Then the inert double-click follows for free: ?view= already held that name, so
setSearchParams changed nothing, so there was no transition for the effect to
react to. The URL being already correct is exactly why the click had nothing to
say.

Now the pane is rendered conditionally with id + order — the same pair
WorkspaceRenderer already uses — and both panels take declarative sizes. There is
no moment where markup and intent disagree, and a deep link is right on first
paint rather than corrected after it. The try/catch that guarded resizing an
unlaid-out group goes with it; nothing calls resize now.

ephemeral-pane.test.tsx asserts the SIZE the group computes, not that the element
exists — a pane at zero width satisfies any is-it-in-the-document check, which is
the failure being guarded against. Five of its six tests fail against the old
implementation, including the load-order one. Full suite: 1316 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 21:11:15 +00:00
pastilhasandClaude Opus 5 58615ee5e7 /login can hand you the URL instead of opening it here
Opening the link opens it in THIS browser — already signed into whichever account you are trying to move
away from. Copying it is how you authorise as somebody else: another browser profile, another browser, or
your phone. This dialog exists for a machine with two Anthropic accounts on it, so that is the common case
rather than the awkward one.

Uses helpers/clipboard, which falls back to execCommand when navigator.clipboard is absent, and reports a
failure rather than showing "Copied" over an empty clipboard.

Corrected a claim in that helper's own comment while touching it: it said Officer is reached over the
tailnet on plain http, as though that were the whole story. The owner browses over https and did when it
was written. The http origins are the other ways in, and an install with no certificate has no other way
in at all — which is why the fallback stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:02:57 +00:00
pastilhasandClaude Opus 5 31cdb93691 usage shows the signed-in account only, and says which one it is
Phase one of answering a question the panel could not answer: whose numbers am I looking at.

This machine's owner has two Anthropic accounts, personal and work, and both had stored snapshots — so
both appeared as tabs. After signing in there was no way to tell from a glance which one was now being
spent. The panel was answering "here is usage for two accounts" while the question was "which am I on".

So the hook filters to the account marked `current`, and the absence of one now MEANS something: no current
row renders as "Not signed in" rather than as whichever tab happened to be selected first.

The email also gets its own line instead of sharing one with the plan. Joined as `Team · a@b.c` in a 320px
popover, the email is the half that truncates — and it is the half being read.

The other accounts are said out loud rather than silently dropped: "1 other account not shown". Knowing one
exists and is hidden deliberately is a different thing from wondering where it went.

Temporary and a one-line revert by construction: delete `onlyCurrent` in useClaudeUsage and return `all`.
The server still serves every account, the tab strip is still in the file, and nothing was deleted — the
stored snapshots are a real feature, they were simply drowning the question being asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:56:45 +00:00
pastilhasandClaude Opus 5 8917f36be5 /login works in the chat window
Officer authenticates claude with a Claude SUBSCRIPTION over OAuth, and that is a browser round trip: open
a URL, authorise, paste a code back. It was the one part of running an agent a browser-only user could not
do from the browser — the answer was "open a terminal panel and drive the CLI by hand", which works, and is
a strange thing to ask of somebody whose entire session is a web page.

It needs no pty. `claude auth login` behaves identically through plain pipes, verified by capturing its raw
bytes, so this is an ordinary subprocess with two reads and one write: spawn, read until the URL, wait for
the person, write the code to stdin.

The parsing is the only non-obvious part. The CLI wraps the URL in an OSC-8 hyperlink escape AND prints it
again as visible text, so `https://\S+` returns two URLs concatenated with no separator between them.
Stripping the escape wrappers first leaves exactly one. Written against captured bytes rather than against
what the terminal appeared to show.

Three decisions worth keeping:

NOT under /auth. totality.ts exempts that prefix from the permission gate entirely, and correctly — it is
where a caller goes to BECOME authenticated. Mounted there, an unauthenticated POST could have started a
login as the owner. Verified after moving it: unauthenticated POST /api/claude-auth is 401.

On the `chat` grant rather than its own. The pairing is exact: an account that may use the agent must be
able to authenticate the agent, and an account that may not has nothing to authenticate. `confined` then
carries the rest for free — authorize.ts drops a confined grant for an account with no osUser, which is
precisely the account that cannot hold credentials of its own.

Nothing Anthropic-shaped is inherited by the child. For the owner those variables point at the local proxy,
whose whole job is swapping a shared secret for the owner's bearer token — handing them to a login is how
you get a success that authenticates nothing. A member's login runs through setpriv --reset-env as their own
Linux user, into their own home, because their turns bill their own subscription and that only holds if the
credential is theirs.

Pending logins are in memory on purpose: one is worthless once the process holding its PKCE verifier dies,
so persisting it would store a row that can only ever be stale. Ten-minute reaper, one per account, and the
dialog abandons its own on close so a dismissed dialog does not leave claude parked on a read of stdin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:47:10 +00:00
pastilhasandClaude Opus 5 530e5880e1 todo: what tonight's deploy work left behind, and two items it closed
Closed:

- fleet/an-orchestrator-that-acts-on-every-server-at — built, and as none of the three shapes it
  proposed. Not a script, not a screen, not a chat capability: an agent handed a written procedure and a
  server list it has to read rather than one baked into it. All three verified facts in the item earned
  their place; two things it did not anticipate are now new items.
- fleet/a-way-to-mark-a-commit-this-one-ships-so-a — built as a TAG, which the item explicitly ruled out.
  The owner reversed it, and the argument that changed his mind is recorded: a codeword can be written by
  accident, because every document about deploying contains it, so the trigger needs rules about where in
  a subject line it sits and a rule like that is one somebody eventually gets wrong.

Opened, all from tonight and all with the evidence attached:

- a deploy verifies pm2 and not that the app serves anything — the commander raised this against itself
- the commander cannot finish its own host, so one machine is always a build behind
- the watcher loop is rewritten by an agent every arming, so its correctness is re-rolled each time
- plugins:update restarts every sidecar, so a report naming only officer understates what moved
- an agent spawned by a background job is invisible in the UI while being perfectly readable
- AskUserQuestion can fail to parse its own call
- a commander opened from /chat loses its commander controls, because isCommander comes from the screen
  rather than the session

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:27:00 +00:00
pastilhasandClaude Opus 5 887749722c fofos's capabilities failure was ownership, and one stat proved it
Three deploys reported the same two errors and neither report could explain them:

  dubious ownership in repository at '~/officerdev/capabilities'
  EACCES: permission denied, mkdir '~/officerdev/capabilities/affordances/officerdev'

They read as two problems — git refusing to operate, and Node refusing to create a directory, at different
steps of the deploy — and they were one sentence in two dialects: this path is not yours. Fifty paths under
that checkout were owned by root, .git and affordances among them, all stamped 19 Aug 21:43. Something ran
with sudo and left them.

Mode 755 is what made the shape confusing. Everything was READABLE, so git could see the repo and refused
it on ownership, while the plugin could see affordances and could not write into it. Two tools, two
messages, one inode owner.

The general point, which is why this is in servers.md rather than only in a commit: stat prints the owner
instead of inferring it, so it either proves ownership is the cause or eliminates it — one command, binary,
no theory. Three reports had guessed; none had looked.

The deploy commander's refusal to add a safe.directory entry was right and is recorded as such. It would
have silenced the git message and left the EACCES exactly as it was, spending the obvious explanation on
half the problem.

Fixed by the owner with chown -R; verified after by a mkdir into the directory that used to refuse one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:23:45 +00:00
pastilhas 9ce81547c4 2026.08.21.03 deployed to six hosts — the watcher fired, the claim held 2026-08-21 20:14:36 +00:00
265 changed files with 24526 additions and 296 deletions
+11 -10
View File
@@ -20,30 +20,31 @@ the owner's OS user and can never be granted. Indirection there really is accide
## Themes
**78 open, 20 closed**, across 28 themes.
**87 open, 22 closed**, across 29 themes.
| Theme | Open | Closed |
| ------------------------------------------------------------------------------------------ | ---- | ------ |
| [Affordances](todo/affordances/) | 1 | 0 |
| [Speaking into a session from the backend](todo/agent-delivery/) | 2 | 0 |
| [Backburner](todo/backburner/) | 2 | 0 |
| [Background jobs](todo/background-jobs/) | 1 | 0 |
| [Background jobs](todo/background-jobs/) | 2 | 0 |
| [Chat interface](todo/chat/) | 1 | 0 |
| [Code editor](todo/code-editor/) | 1 | 0 |
| [Components — the shared UI layer](todo/components/) | 1 | 0 |
| [File watchers](todo/file-watchers/) | 1 | 0 |
| [Fleet — one command across every server](todo/fleet/) | 3 | 0 |
| [Fleet — one command across every server](todo/fleet/) | 5 | 2 |
| [Headscale](todo/headscale/) | 0 | 5 |
| [Infra (alpha)](todo/infra/) | 6 | 0 |
| [Known bugs](todo/known-bugs/) | 2 | 1 |
| [Mobile apps](todo/mobile-apps/) | 2 | 0 |
| [Multi-user](todo/multi-user/) | 17 | 3 |
| [Known bugs](todo/known-bugs/) | 4 | 1 |
| [Mobile apps](todo/mobile-apps/) | 3 | 0 |
| [Multi-user](todo/multi-user/) | 18 | 3 |
| [Notifications](todo/notifications/) | 1 | 0 |
| [Plugin ecosystem — before it opens to third parties (2026-08-18)](todo/plugin-ecosystem/) | 3 | 0 |
| [Plugin infrastructure](todo/plugin-infrastructure/) | 2 | 0 |
| [Plugin repos — what writing their READMEs turned up](todo/plugin-repos/) | 10 | 9 |
| [Plugin system — gaps the email extraction found (2026-08-18)](todo/plugin-system/) | 3 | 0 |
| [Prompt library](todo/prompt-library/) | 1 | 0 |
| [Settings — portable, and readable by an agent](todo/settings/) | 2 | 0 |
| [Shell — spotlight and the dock](todo/shell/) | 1 | 1 |
| [Sub-agents — agents spawning agents](todo/sub-agents/) | 1 | 1 |
| [Sudo terminal modal](todo/sudo-terminal-modal/) | 1 | 0 |
@@ -62,13 +63,13 @@ the owner's OS user and can never be granted. Indirection there really is accide
## Newest ten
- `2026-08-22` **A plugin's permission should be on for every role by default, off only on request** — [multi-user/a-plugin-s-permission-should-be-on-for-every](todo/multi-user/a-plugin-s-permission-should-be-on-for-every.md)
- `2026-08-22` **A user's settings should export and import as a plain file** — [settings/a-user-s-settings-should-export-and-import-as](todo/settings/a-user-s-settings-should-export-and-import-as.md)
- `2026-08-22` **The same for system settings, owned by the Super Admin** — [settings/the-same-for-system-settings-owned-by-the](todo/settings/the-same-for-system-settings-owned-by-the.md)
- `2026-08-21` **No way to see which affordances are reaching your agents, or to turn one off** — [affordances/no-way-to-see-which-affordances-are-reaching](todo/affordances/no-way-to-see-which-affordances-are-reaching.md)
- `2026-08-21` **Generalise it: deliver a turn to a named session, from anywhere in the backend** — [agent-delivery/generalise-it-deliver-a-turn-to-a-named](todo/agent-delivery/generalise-it-deliver-a-turn-to-a-named.md)
- `2026-08-21` **The cost of resuming grows with the transcript** — [agent-delivery/the-cost-of-resuming-grows-with-the-transcript](todo/agent-delivery/the-cost-of-resuming-grows-with-the-transcript.md)
- `2026-08-21` **'An agent spawned by a background job is invisible in the interface'** — [background-jobs/a-spawned-agent-is-invisible-in-the-interface](todo/background-jobs/a-spawned-agent-is-invisible-in-the-interface.md)
- `2026-08-21` **Show how long a turn took, beside its cost and tokens** — [chat/show-how-long-a-turn-took-beside-its-cost](todo/chat/show-how-long-a-turn-took-beside-its-cost.md)
- `2026-08-21` **Map every custom component, centralise it, and let a theme override it** — [components/map-every-custom-component-and-centralise-it](todo/components/map-every-custom-component-and-centralise-it.md)
- `2026-08-21` **Watch a directory, and attach actions to what lands in it** — [file-watchers/watch-a-directory-and-attach-actions-to-what](todo/file-watchers/watch-a-directory-and-attach-actions-to-what.md)
- `2026-08-21` **Settings to choose which server each feature uses** — [fleet/settings-to-choose-which-server-each-feature](todo/fleet/settings-to-choose-which-server-each-feature.md)
- `2026-08-21` **A way to mark a commit "this one ships", so a release is not just `master`'s tip** — [fleet/a-way-to-mark-a-commit-this-one-ships-so-a](todo/fleet/a-way-to-mark-a-commit-this-one-ships-so-a.md)
- `2026-08-21` **OPM — Officer's own process manager, or at least its own PM2** — [infra/opm-officer-s-own-process-manager](todo/infra/opm-officer-s-own-process-manager.md)
- `2026-08-21` **iPhone Action Button → run an Officer task from a locked phone** — [mobile-apps/iphone-action-button-run-an-officer-task-from](todo/mobile-apps/iphone-action-button-run-an-officer-task-from.md)
+83
View File
@@ -0,0 +1,83 @@
# 2026.08.21.03 — deployed
**6/6 hosts.** Every platform the plan reaches is on `3343b21a`, `officer` restarted and back `online`,
and no other process on any host was left in a state other than `online`.
`officer-dev` is the seventh platform and was **skipped entirely** — it is the host this deploy ran on.
See below; its skip is now two releases deep.
This was a drill. The release changes `package.json` and adds its own plan file, nothing else, and it
exists to watch the claim rule work. It did: `scripts/deploy-claim.sh claim 2026.08.21.03` was taken
before any host was touched, and `state` reads `done`.
## The release
Tag `deploy/2026.08.21.03` → `3343b21a`; `package.json` at that commit says `2026.08.21.03`. They agree.
Plan: `deploy/2026.08.21.03.md` — pull, `plugins:update`, restart `officer` only. No `bun install`, no
`db:push`, and neither ran anywhere.
| Host | Before | After | plugins | restarted | restart_time | back |
| ---------------------------- | ---------- | ---------- | -------------- | --------- | ------------ | ------- |
| `alpha` | `55ecbf4f` | `3343b21a` | 12/12 | officer | 61 → 62 | online |
| `fofos` | `55ecbf4f` | `3343b21a` | 1/1 | officer | 16 → 17 | online |
| `macbookpro` | `55ecbf4f` | `3343b21a` | none installed | officer | 3 → 4 | online |
| `edge-pertento-bridge` | `55ecbf4f` | `3343b21a` | 2/2 | officer | 5 → 6 | online |
| `production-pertento-bridge` | `55ecbf4f` | `3343b21a` | 1/1 | officer | 7 → 8 | online |
| `deadpool-rezio-bridge` | `55ecbf4f` | `3343b21a` | none installed | officer | 1 → 2 | online |
| `officer-dev` | `3343b21a` | `3343b21a` | not run | **none** | 155 | skipped |
Every host came in at `55ecbf4f` — the previous release, level for the first time in three deploys — and
every pull was a real fast-forward. Checkout paths were taken from each `officer` process's `pm_cwd`, so
`alpha`'s stale second checkout at `~/dockers/officer.dev/platform` was never touched. `macbookpro` was
awake.
**More than `officer` restarted, and the plan did not ask for it.** `bun run plugins:update` bounces each
plugin's sidecar as it goes: twelve on `alpha`, two on `edge`, one each on `fofos` and
`production-pertento-bridge`. Every one came back `online`. Nothing was left down.
## What was NOT done, and why
- **`officer-dev` was skipped completely** — no pull (it is already at the tag, which was cut here), no
`plugins:update`, no restart. This session runs inside `officer-claude-code` on it, and restarting
`officer` there ends the rollout mid-flight.
**Its running process is now two releases old.** `restart_time` is 155, unchanged since before
`2026.08.21.02` — so the same restart the last report left outstanding is still outstanding, and it now
carries both releases. The code for both is on disk. `pm2 restart officer` on `officer-dev` finishes
them, whenever the owner wants it. **Outstanding, and no longer new.**
- **`fofos` still cannot sync its capabilities.** Third deploy running, same two failures, same
unexplained cause:
- `capabilities: failed — fatal: detected dubious ownership in repository at
'/home/pastilhas/officerdev/capabilities'`
- `affordances: NOT published — EACCES: permission denied, mkdir
'/home/pastilhas/officerdev/capabilities/affordances/officerdev'`
Both say that checkout is not owned by `pastilhas`. Still not papered over with a `safe.directory`
entry, because a `safe.directory` line would silence the first message and leave the `EACCES` — they are
one cause, and one `stat` settles it. `plugins:update` exited 0 and the gitea plugin itself updated, so
this costs capability sync on that host and nothing else. **Unexplained.**
- **`fofos` still carries one untracked file**, `scripts/chk.ts`. A note, not a skip — the rule works.
- **No `bun install` and no `db:push` ran anywhere.** The plan says this release needs neither, and
`deploy/2026.08.21.02..deploy/2026.08.21.03` is two files.
- **Nothing was verified beyond `pm2`.** `officer` is `online` with a moved `restart_time` on all six
hosts; no HTTP request was made to any domain. That is what the plan asked to check.
## Notes for the next one
- **The `macbookpro` PATH rule in `docs/servers.md` is now correct and was followed as written.**
`export PATH=$PATH:/opt/homebrew/bin` for the `bun` command, `pm2` as a separate command, and both
worked first time. Nothing to change.
- **`$SHELL -ic "pm2 jlist"` with stderr discarded came back empty once**, on `edge-pertento-bridge`, and
the same command with `2>&1` a second later returned the full list. Transient, and it looked exactly
like a dead pm2. Capture stderr and parse the JSON out of the stream rather than trusting the whole of
stdout to be JSON — an interactive shell puts its own noise (`starship`, aliases) in there.
- **`officer-dev`'s skip needs a home other than a report.** It has been carried forward twice now, and a
report is not a place work waits — the next deploy will write the same paragraph. Either the owner
restarts it, or it belongs in `TODO.md`.
+51
View File
@@ -0,0 +1,51 @@
# 2026.08.21.04
`/login` in the chat window, switching between Anthropic accounts from the usage pills, and a Spotlight
that can actually reach the plugins.
**This release has a SCHEMA CHANGE.** The previous two did not, and said so; this one adds
`anthropic_accounts`. Without the push, `GET /api/claude-auth/accounts` fails on a missing relation the
first time the usage panel is opened — so `bun db:push` is a step here, not an optional tidy.
No dependency change: nothing in `deploy/2026.08.21.03..HEAD` touches `package.json` dependencies or
`bun.lock`. Do not run `bun install`.
## What is in it
- **`/login` works in the chat composer.** `claude auth login` behaves the same through plain pipes as
through a terminal, so it is a subprocess with two reads and one write — no pty. Copy-link beside the
open-link button, because opening it here opens it in the browser already signed into the account you are
leaving.
- **Account switching from the usage pills.** `anthropic_accounts` keeps each login's OAuth credential
encrypted under a per-account key from the secret store, so a switch needs no browser round trip. An
account with no saved credential still gets a chip you can read — cached percentages and a correct reset
time — marked as needing `/login`.
- **Spotlight reached eight items.** Core filled all eight slots, so on a populated server every plugin was
unreachable. The cap is gone, rows draw the dock's own tiles, and the settings list went from nine
hand-written rows to twenty read off the screens that declare them.
- The ephemeral pane opens from the markup rather than an effect.
## Every host
1. `git pull --ff-only`
2. **`bun db:push`** — new this release, see above
3. `bun run plugins:update`
4. `pm2 restart officer`
`officer` only; no sidecar changed. Restarting it rebuilds the SPA, which is what ships the frontend.
**`alpha` is already at this code**, deployed by hand as a preview before the tag. Its pull will be a
no-op and that is expected — run the remaining steps anyway rather than skipping the host, so its
`restart_time` moves with everyone else's.
**On `macbookpro`, append to the PATH for `bun`** — `export PATH=$PATH:/opt/homebrew/bin` — and run `pm2`
as a separate command. `docs/servers.md` has the detail; replacing the PATH is what broke `pm2` on an
earlier deploy.
## Then check
`pm2 jlist`: `officer` is `online` and `restart_time` moved. Nothing else restarted by step 4 — note that
step 3 restarts every plugin's sidecar on its own, which is the script's behaviour and worth recording
rather than omitting.
`scripts/deploy-claim.sh state 2026.08.21.04` says `done`.
+102
View File
@@ -0,0 +1,102 @@
# 2026.08.21.04 — deploy report
**6 of 6 hosts in scope succeeded.** `officer-dev` was skipped entirely — it is the host this agent runs
on. Seven platforms exist; six were deployable.
Target commit `98177c3b`, tag `deploy/2026.08.21.04`, `package.json` version `2026.08.21.04` — tag and
version agree.
Every host ran the same four steps from `deploy/2026.08.21.04.md`: `git pull --ff-only`, `bun db:push`
(the schema change), `bun run plugins:update`, `pm2 restart officer`. No host ran `bun install` — the plan
said not to.
---
## Per host
### `alpha` — succeeded
- HEAD `3cab16ca` → `98177c3b`. Tree clean, no untracked files.
- The pull moved only `deploy/2026.08.21.04.md` and `package.json`: the code was already there from the
hand deploy the plan describes. Expected, and the remaining steps ran anyway.
- `db:push` — changes applied. `anthropic_accounts` verified present by direct query.
- `plugins:update` — 12/12 updated in 44s, exit 0. **Twelve sidecars restarted by that step**: atelier,
bitcoin-wallet, email, gitea, invoices, jellyfin, music, offscale, photos, soulseek, transmission,
vault. That is the script's own behaviour, not something the plan asked for.
- `pm2 restart officer` — `restart_time` 63 → 64, `online`.
### `fofos` — succeeded
- HEAD `3343b21a` → `98177c3b`. No tracked changes.
- **Untracked file present: `scripts/chk.ts`.** Left alone — a fast-forward cannot touch it, and it is
not a reason to skip a host.
- `db:push` — changes applied. `anthropic_accounts` verified by direct query.
- `plugins:update` — 1/1 (gitea), exit 0. One sidecar restarted: `officer-gitea`.
- `pm2 restart officer` — 17 → 18, `online`.
- The capabilities ownership fault recorded in `docs/servers.md` did not recur: `capabilities: pulled`.
### `macbookpro` — succeeded
- HEAD `b10a49c0` → `98177c3b`. Tree clean.
- Awake and reachable; no skip needed.
- `bun` reached with `export PATH=$PATH:/opt/homebrew/bin` appended for the `bun` commands only, `pm2`
run as a separate command, per the plan and `docs/servers.md`.
- `db:push` — changes applied.
- `plugins:update` — "no plugins installed — nothing to update", exit 0. No sidecar restarted.
- `pm2 restart officer` — 4 → 5, `online`.
- **The one thing not verified here:** `psql` is not on this machine's PATH under any prefix tried, so
`anthropic_accounts` was confirmed only by `db:push` reporting "Changes applied", not by querying the
table. Every other host was checked directly.
- Cosmetic noise from the interactive shell: `.zshrc:123: command not found: starship`. Harmless, and
unrelated to the deploy.
### `edge-pertento-bridge` — succeeded
- HEAD `3343b21a` → `98177c3b`. Tree clean.
- `db:push` — changes applied. `anthropic_accounts` verified by direct query.
- `plugins:update` — 2/2 (gitea, offscale), exit 0. Two sidecars restarted.
- `pm2 restart officer` — 6 → 7, `online`.
### `production-pertento-bridge` — succeeded
- HEAD `3343b21a` → `98177c3b`. Tree clean.
- `db:push` — changes applied. `anthropic_accounts` verified by direct query.
- `plugins:update` — 1/1 (gitea), exit 0. One sidecar restarted.
- `pm2 restart officer` — 8 → 9, `online`.
### `deadpool-rezio-bridge` — succeeded
- Logged in as `br`, checkout `/home/br/officerdev/platform`.
- HEAD `3343b21a` → `98177c3b`. Tree clean.
- `db:push` — changes applied. `anthropic_accounts` verified by direct query.
- `plugins:update` — "no plugins installed — nothing to update", exit 0. No sidecar restarted.
- `pm2 restart officer` — 2 → 3, `online`.
### `officer-dev` — SKIPPED, deliberately
Not pulled, no schema push, no plugin update, nothing restarted. This agent is a session inside
`officer-claude-code` on this machine, and restarting `officer` here ends the rollout mid-report. The
host is therefore one release behind the other six until the owner deploys it by hand — checkout
`~/officer-dev/platform`, and its working tree is already at `98177c3b` plus this report.
---
## What was checked, and how
- Every checkout path came from PM2 (`officer`'s `pm_cwd`), not from the table — so `alpha`'s second,
stale checkout was never a candidate.
- `pm2 jlist` after each restart: `officer` `online` with `restart_time` moved, on all six.
- A full sweep for anything not `online` on all six hosts afterwards: **none**. No process came back
`errored`, including the sixteen sidecars that `plugins:update` bounced across four hosts.
- `anthropic_accounts` queried directly with `to_regclass` on five hosts; on `macbookpro` only the
`db:push` output stands behind it.
## What was not done
- `officer-dev` — the whole host, as above.
- No host was skipped for a dirty tree; none had tracked changes.
- No process other than `officer` was restarted by this agent. The sidecars that moved were restarted by
`plugins:update` itself.
- `bun install` was not run anywhere, per the plan.
Local record: `data/deploys/2026.08.21.04.done`, written before this report.
+53
View File
@@ -0,0 +1,53 @@
# 2026.08.22.01
349 community themes, Spotlight made usable, prompt history that stops eating drafts, and the email
plugin's multi-account backend.
**No schema change and no dependency change.** Verified against `deploy/2026.08.21.04..HEAD`: nothing
touches a `databases/*/schema.ts`, `package.json` dependencies or `bun.lock`. So **no `bun db:push` and no
`bun install`** — a host that runs either has run something this release did not ask for. Note that the
previous release DID need a push; that was `anthropic_accounts` and it is already applied everywhere.
## What is in it
- **The theme collection.** 147 ports from omarchythemes.com, then 44 more rescued by supporting
`colors.toml` and `0x`-prefixed hex, then a fix to accent/secondary foreground derivation across 202 of
them. This is the bulk of the diff: 229 files, ~22.5k lines, almost all data.
- **Spotlight.** Opens on **alt-space** and nothing else, from anywhere including a focused text field.
The eight-item cap is gone — core items filled it, so every plugin tile was unreachable on a populated
server. Rows draw the dock's own tiles, and the settings list went from nine hand-written entries to
twenty read off the screens that declare them.
- **Prompt history.** Up/Down only recall from an empty composer, and any other keystroke ends a recall —
so a half-written thought is never swapped for one from last week.
- **The email plugin's multi-account backend**, which arrives via `plugins:update` rather than through this
repository. `?account=<id>` names a mailbox, `?account=all` spans them. **Absent is unchanged**, so
nothing that calls those routes today behaves differently — including the mobile app and the agent's
`email_db` tool. Only hosts with the email plugin installed are affected at all.
## Every host
1. `git pull --ff-only` — larger than usual, ~22.5k lines of theme data
2. `bun run plugins:update`
3. `pm2 restart officer`
`officer` only. Step 2 restarts each plugin's own sidecar, which is that script's behaviour and worth
recording in the report rather than omitting.
**`macbookpro`:** append `/opt/homebrew/bin` to PATH for the `bun` commands and run `pm2` separately. See
`docs/servers.md` — replacing the PATH is what broke `pm2` on an earlier deploy.
## `officer-dev` — skip it, and do NOT report it as behind
The commander runs there and skips its own host, as always. But this time the host is **already on this
code**: `officer` was restarted there many times during the night as the work landed, so it is running the
tree this tag names rather than lagging. Report it as skipped and current, not skipped and stale.
## Then check
`pm2 jlist`: `officer` is `online` and `restart_time` moved. Nothing `errored`, including the sidecars step
2 bounced.
A theme picker still lists themes and one loads — the diff is data, so a bad parse would show as a missing
or unreadable theme rather than as a failed boot.
`scripts/deploy-claim.sh state 2026.08.22.01` says `done`.
+16 -4
View File
@@ -77,10 +77,22 @@ All verified, none cosmetic.
- **`bun run plugins:update` restarts sidecars itself**, printing `sidecar: started` per plugin — on
`alpha` that is twelve processes bouncing during the plugins step, before any restart the plan asked for.
Not a fault, but a deploy report that lists only `officer` under "restarted" is understating what moved.
- **`fofos` cannot sync its capabilities**, and could not publish gitea affordances on 2026-08-21:
`dubious ownership` from git and `EACCES … mkdir '…/capabilities/affordances/officerdev'`. Both point at
that capabilities checkout being owned by somebody other than `pastilhas`. Deliberately not papered over
with a `safe.directory` entry until somebody has run one `stat` on it. **Unexplained.**
- **`fofos` failed its capabilities sync for three deploys, and it was ownership.** Fixed 2026-08-21.
Two errors that read as unrelated problems — `dubious ownership` from git, and
`EACCES … mkdir '…/capabilities/affordances/officerdev'` from the gitea plugin — were one cause in two
dialects: fifty paths under that checkout were owned by `root`, including `.git` and `affordances`, all
stamped `19 Aug 21:43`. Something was run with `sudo` and left them behind. Mode `755` is why the split
looked odd: everything could be READ, so git saw the repo and refused it on ownership while the plugin
saw `affordances` and could not write into it.
One `stat` settled it, and that is the general lesson — `stat` prints the owner rather than inferring
anything, so it either proves ownership is the cause or eliminates it, in one command. Fixed with
`sudo chown -R pastilhas:pastilhas ~/officerdev/capabilities`; verified after by a `mkdir` into the
directory that used to refuse one.
**A `safe.directory` entry was deliberately NOT used**, and that call is worth keeping. It would have
silenced the git message and left the `EACCES` untouched — one symptom of a two-symptom cause hidden,
with the obvious explanation spent.
## Not platforms — ignore for now
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "officer",
"version": "2026.08.21.03",
"version": "2026.08.22.01",
"private": true,
"type": "module",
"workspaces": [
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/env bun
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import { mapPalette } from './themes/map';
import type { Palette } from './themes/palette';
import { paletteFromRecord, parseTomlText } from './themes/palette';
import { deriveOmarchyPalette } from './themes/from-ansi';
import { parseAlacrittyToml, parseColor16Toml, parseGhosttyConf } from './themes/ansi-source';
import { contrast, ensureContrast, TEXT, UI } from './themes/contrast';
// Port community Omarchy themes from omarchythemes.com into the same 84-token shape the official
// pipeline produces — see `generate-themes.ts` for that one. This is its sibling for themes that have no
// `colors.toml`: a single-theme repo instead ships per-app configs, so `from-ansi.ts` reconstructs the
// same `Palette` from `alacritty.toml` or `ghostty.conf`, and everything downstream (mapping, contrast
// checking) is identical. What's NOT identical: these are unofficial, one author's repo can vanish or
// change, and the reconstruction is a heuristic, not Omarchy's own curated palette — every generated file
// says so in its header.
//
// bun scripts/port-community-themes.ts [--limit=N] [--slug=<one-slug>] [--redo]
//
// State lives in `scripts/themes/community-manifest.json` — one entry per theme successfully ported, so a
// re-run only fetches what's missing (or everything, with --redo) and the registry/index blocks are
// always rewritten fresh from the whole manifest, never hand-edited.
const CSS_THEME_DIR = join(import.meta.dir, '..', 'src', 'workspaces', 'theme', 'css', 'themes');
const INDEX_CSS = join(CSS_THEME_DIR, '..', 'index.css');
const REGISTRY = join(import.meta.dir, '..', 'src', 'workspaces', 'theme', 'src', 'themes.ts');
const MANIFEST_PATH = join(import.meta.dir, 'themes', 'community-manifest.json');
const SITE = 'https://omarchythemes.com';
const CONCURRENCY = 8;
const REDO = process.argv.includes('--redo');
const ONLY_SLUG = process.argv.find((a) => a.startsWith('--slug='))?.split('=')[1];
const LIMIT = Number(process.argv.find((a) => a.startsWith('--limit='))?.split('=')[1] ?? Infinity);
type ManifestEntry = {
id: string;
name: string;
repo: string;
/** Omitted means GitHub, which is where all but a couple of these live. */
host?: 'gitlab';
kind: string;
dark: boolean;
};
type Repo = { host: 'github' | 'gitlab'; path: string };
const repoUrl = (r: Repo): string => `https://${r.host === 'gitlab' ? 'gitlab.com' : 'github.com'}/${r.path}`;
// Same pairs `generate-themes.ts` checks, kept in sync by hand since this is the only other place that
// needs them and duplicating a 20-line array beats a shared module for one caller each.
const PAIRS: [string, string, number][] = [
['--foreground', '--background', TEXT],
['--card-foreground', '--card', TEXT],
['--popover-foreground', '--popover', TEXT],
['--muted-foreground', '--muted', TEXT],
['--accent-foreground', '--accent', TEXT],
['--primary-foreground', '--primary', TEXT],
['--secondary-foreground', '--secondary', TEXT],
['--cta-foreground', '--cta', TEXT],
['--success-foreground', '--success', TEXT],
['--warning-foreground', '--warning', TEXT],
['--destructive-foreground', '--destructive', TEXT],
['--info-foreground', '--info', TEXT],
['--busy-foreground', '--busy', TEXT],
['--busy-ink', '--background', TEXT],
['--input', '--background', 1.35],
['--ring', '--background', UI],
];
const title = (id: string): string =>
id
.split('-')
.map((w) => (w.length <= 2 ? w.toUpperCase() : w[0]!.toUpperCase() + w.slice(1)))
.join(' ');
function loadManifest(): ManifestEntry[] {
if (!existsSync(MANIFEST_PATH)) return [];
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
}
function saveManifest(entries: ManifestEntry[]): void {
writeFileSync(MANIFEST_PATH, `${JSON.stringify(entries, null, 2)}\n`);
}
function ghApiDefaultBranch(repo: string): string {
try {
const out = execSync(`gh api repos/${repo} --jq .default_branch`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return out.trim() || 'main';
} catch {
return 'main';
}
}
async function gitlabDefaultBranch(path: string): Promise<string> {
const json = await fetchText(`https://gitlab.com/api/v4/projects/${encodeURIComponent(path)}`);
if (!json) return 'main';
try {
return (JSON.parse(json).default_branch as string) || 'main';
} catch {
return 'main';
}
}
async function fetchText(url: string): Promise<string | undefined> {
try {
const res = await fetch(url);
if (!res.ok) return undefined;
return await res.text();
} catch {
return undefined;
}
}
async function discoverRepo(slug: string): Promise<Repo | undefined> {
const html = await fetchText(`${SITE}/themes/${slug}`);
if (!html) return undefined;
const gh = html.match(/https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)/);
if (gh) return { host: 'github', path: `${gh[1]}/${gh[2]}` };
// A couple of authors host on GitLab instead. Its group paths can nest, hence the greedier match.
const gl = html.match(/https:\/\/gitlab\.com\/([\w.-]+(?:\/[\w.-]+)+?)(?=["'\s<]|$)/);
if (gl) return { host: 'gitlab', path: gl[1]!.replace(/\.git$/, '') };
return undefined;
}
async function sourceFor(
repo: Repo,
slug: string,
): Promise<{ palette: Palette; kind: string; derived: boolean } | undefined> {
const branch = repo.host === 'gitlab' ? await gitlabDefaultBranch(repo.path) : ghApiDefaultBranch(repo.path);
const raw = (p: string) =>
repo.host === 'gitlab'
? `https://gitlab.com/${repo.path}/-/raw/${branch}/${p}`
: `https://raw.githubusercontent.com/${repo.path}/${branch}/${p}`;
// Most repos hold one theme at the root. A few are packs that nest one directory per theme, so also
// look under the directory named after THIS slug — an exact match is the only safe pick, since a pack
// with variant names of its own (`abyss-dark`, `lagoon-light`, …) has no one theme that is "the" one.
const prefixes = ['', `themes/${slug}/`, `omarchy/themes/${slug}/`, `${slug}/`];
for (const prefix of prefixes) {
// `colors.toml` is ambiguous across community repos: it might genuinely be Omarchy 4's own semantic
// schema (in which case no derivation is needed at all — same quality as the official 22), or it might
// reuse the filename for an unrelated flat `color0..color15` ANSI scheme. Try the real schema first;
// only fall through to the ANSI reading if the required keys aren't there.
const tomlText = await fetchText(raw(`${prefix}colors.toml`));
if (tomlText) {
const kv = parseTomlText(tomlText);
try {
return {
palette: paletteFromRecord(kv, `${repo.path}/${prefix}colors.toml`),
kind: 'colors.toml (Omarchy schema)',
derived: false,
};
} catch {
const ansi = parseColor16Toml(kv);
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'colors.toml (16-colour ANSI)', derived: true };
}
}
for (const path of [
'alacritty.toml',
'.config/alacritty.toml',
'alacritty/alacritty.toml',
'config/alacritty.toml',
]) {
const text = await fetchText(raw(`${prefix}${path}`));
if (!text) continue;
const ansi = parseAlacrittyToml(text);
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'alacritty.toml', derived: true };
}
for (const path of ['ghostty.conf', 'ghostty-theme', '.config/ghostty/config']) {
const text = await fetchText(raw(`${prefix}${path}`));
if (!text) continue;
const ansi = parseGhosttyConf(text);
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'ghostty.conf', derived: true };
}
}
return undefined;
}
async function portOne(slug: string): Promise<{ entry: ManifestEntry; css: string } | { skip: string }> {
const repo = await discoverRepo(slug);
if (!repo) return { skip: 'no github/gitlab repo link found on its omarchythemes.com page' };
const source = await sourceFor(repo, slug);
if (!source) return { skip: `no parseable colors.toml/alacritty.toml/ghostty.conf in ${repo.path}` };
const palette = source.palette;
const tokens = mapPalette(palette);
// `--input` carries a real accessibility requirement (a field boundary you can't see) where `--border`
// is decoration and isn't held to one — see the README. `map.ts` derives both from the same mix for
// every theme, official ones included, and it normally clears 1.35:1 on its own; a few derived palettes
// land just under it, so nudge only this one rather than loosening the check.
tokens['--input'] = ensureContrast(tokens['--input']!, tokens['--background']!, 1.35);
const failures: string[] = [];
for (const [fgKey, bgKey, min] of PAIRS) {
const fg = tokens[fgKey]!;
const bg = tokens[bgKey]!;
if (!fg.startsWith('#') || !bg.startsWith('#')) continue;
const ratio = contrast(fg, bg);
if (ratio < min) failures.push(`${fgKey} on ${bgKey} is ${ratio.toFixed(2)}:1, needs ${min}:1`);
}
if (failures.length > 0) return { skip: `contrast failed after derivation: ${failures.join('; ')}` };
const name = title(slug);
const body = Object.entries(tokens)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n');
const modeLine = source.kind.startsWith('colors.toml (Omarchy')
? `${palette.mode === 'dark' ? 'Dark theme' : 'Light theme'} (${palette.mode}) — the source repo's own colors.toml says so directly.`
: `${palette.mode === 'dark' ? 'Dark theme' : 'Light theme'} (${palette.mode}), by luminance of its own background — the source repo carries no mode flag of its own to read.`;
const provenanceLine = source.derived
? `went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4\n colors.toml — some tokens here are reconstructed, not authored.`
: `came straight from the repo's own colors.toml — the same schema and reader the official 22 themes\n use, so nothing here is reconstructed.`;
const css = `/*
${name} — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
${modeLine}
Source: ${repoUrl(repo)} (${source.kind}), read by scripts/port-community-themes.ts. Values
${provenanceLine} Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='${slug}'] {
${body}
}
`;
return {
entry: {
id: slug,
name,
repo: repo.path,
...(repo.host === 'gitlab' ? { host: 'gitlab' as const } : {}),
kind: source.kind,
dark: palette.mode === 'dark',
},
css,
};
}
function rewriteIndexCss(entries: ManifestEntry[]): void {
const index = readFileSync(INDEX_CSS, 'utf8');
const marker = '/* Community themes';
const startIdx = index.indexOf(marker);
const head = startIdx >= 0 ? index.slice(0, startIdx) : index.replace(/\n@import '\.\/chrome\.css';\n$/, '\n');
const imports = entries.map((e) => `@import './themes/${e.id}.css';`).join('\n');
writeFileSync(
INDEX_CSS,
`${head}/* Community themes — ported from omarchythemes.com by scripts/port-community-themes.ts. Do not edit this block. */\n${imports}\n\n@import './chrome.css';\n`,
);
}
function rewriteRegistry(entries: ManifestEntry[]): void {
const registry = readFileSync(REGISTRY, 'utf8');
const START = ' /* COMMUNITY THEMES — start */';
const END = ' /* COMMUNITY THEMES — end */';
const rows = entries
.map((e) => ` { id: '${e.id}', name: '${e.name}', family: '${e.name}', applyDarkClass: ${e.dark} },`)
.join('\n');
const block = `${START}\n${rows}\n${END}`;
if (registry.includes(START) && registry.includes(END)) {
// Snap to the start of the marker's own line — `indexOf` on a string with leading spaces can match
// partway into stray indentation left by an earlier write, which would otherwise accumulate.
const startLineBegin = registry.lastIndexOf('\n', registry.indexOf(START)) + 1;
const before = registry.slice(0, startLineBegin);
const after = registry.slice(registry.indexOf(END) + END.length);
writeFileSync(REGISTRY, `${before}${block}${after}`);
} else {
const anchor = '] as const;';
const idx = registry.indexOf(anchor);
writeFileSync(REGISTRY, `${registry.slice(0, idx)}${block}\n${registry.slice(idx)}`);
}
}
async function main() {
const slugsFile = process.argv.find((a) => a.startsWith('--slugs='))?.split('=')[1];
const allSlugs: string[] = slugsFile ? readFileSync(slugsFile, 'utf8').trim().split('\n').filter(Boolean) : [];
const manifest = loadManifest();
const known = new Set(manifest.map((e) => e.id));
// Never take a slug the OFFICIAL/hand-authored part of the registry already owns — that's everything
// before this script's own block, which it is free to redo.
const registryHead = readFileSync(REGISTRY, 'utf8').split('/* COMMUNITY THEMES — start */')[0]!;
const registryIds = new Set(Array.from(registryHead.matchAll(/id: '([\w-]+)'/g)).map((m) => m[1]!));
let todo = ONLY_SLUG ? [ONLY_SLUG] : allSlugs.filter((s) => !registryIds.has(s) && (REDO || !known.has(s)));
todo = todo.slice(0, LIMIT);
console.log(`community theme port: ${todo.length} to try, ${manifest.length} already in the manifest`);
const results: ManifestEntry[] = [...manifest.filter((e) => !todo.includes(e.id))];
let done = 0;
let ok = 0;
let skipped = 0;
async function worker() {
while (todo.length > 0) {
const slug = todo.shift();
if (!slug) break;
try {
const result = await portOne(slug);
done++;
if ('skip' in result) {
skipped++;
console.log(`[${done}] SKIP ${slug} — ${result.skip}`);
} else {
writeFileSync(join(CSS_THEME_DIR, `${slug}.css`), result.css);
results.push(result.entry);
ok++;
console.log(`[${done}] OK ${slug} — from ${result.entry.repo} (${result.entry.kind})`);
}
} catch (err) {
done++;
skipped++;
console.log(`[${done}] ERROR ${slug} — ${(err as Error).message}`);
}
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
results.sort((a, b) => a.id.localeCompare(b.id));
saveManifest(results);
rewriteIndexCss(results);
rewriteRegistry(results);
console.log(`\ndone: ${ok} ported, ${skipped} skipped, ${results.length} total in manifest`);
}
main();
+205
View File
@@ -0,0 +1,205 @@
// Reading a community theme's actual shipped format.
//
// Omarchy's own 22 themes carry a `colors.toml` in the semantic shape `palette.ts` expects — three
// background depths, four foreground weights, an accent, a selection, a muted. A community theme on
// omarchythemes.com ships none of that: it's per-app config files in the classic 16-ANSI-slot style,
// because that's what predates Omarchy 4's own format and what most single-theme repos still use.
//
// This file reads the two shapes that cover almost every repo: `alacritty.toml` (structured, typed,
// preferred) and `ghostty.conf`/`ghostty-theme` (flat `key = value` lines, fallback). Either becomes an
// `AnsiSource` — the 16 ANSI slots plus primary background/foreground — which `from-ansi.ts` turns into
// the same `Palette` shape the official pipeline consumes, so every downstream guarantee (contrast
// checking, the 84-token mapping) applies unchanged.
export type AnsiSource = {
background: string;
foreground: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
};
// `#rrggbb` is standard; `0xrrggbb` shows up in a handful of community alacritty.toml files (their
// authors apparently ported from a scheme that used C-style hex literals).
const HEX = /#[0-9a-fA-F]{3,8}|0x[0-9a-fA-F]{3,8}/;
function normalizeHex(v: string): string | undefined {
const m = v.match(HEX);
if (!m) return undefined;
let h = m[0].replace(/^(#|0x)/, '').toLowerCase();
if (h.length === 3)
h = h
.split('')
.map((c) => c + c)
.join('');
if (h.length >= 6) h = h.slice(0, 6);
return `#${h}`;
}
/** A deliberately small TOML reader: flat `key = "value"` lines plus `[section]` headers. */
export function parseAlacrittyToml(text: string): AnsiSource | undefined {
let section = '';
const rows: Record<string, Record<string, string>> = {};
for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
const sectionMatch = line.match(/^\[([\w.]+)]$/);
if (sectionMatch) {
section = sectionMatch[1]!;
rows[section] ??= {};
continue;
}
// Quoted specifically, not "anything but a comment" — a hex value starts with `#`, the same
// character that opens a trailing comment, so the two can only be told apart by the quotes.
const kv = line.match(/^([\w-]+)\s*=\s*(['"])(.*?)\2/);
if (kv && section) {
const hex = normalizeHex(kv[3]!);
if (hex) rows[section]![kv[1]!] = hex;
}
}
const primary = rows['colors.primary'];
const normal = rows['colors.normal'];
const bright = rows['colors.bright'];
if (!primary?.background || !primary?.foreground || !normal || !bright) return undefined;
const need = (r: Record<string, string>, k: string): string | undefined => r[k];
const n = (k: string) => need(normal, k);
const b = (k: string) => need(bright, k);
if (
!n('black') ||
!n('red') ||
!n('green') ||
!n('yellow') ||
!n('blue') ||
!n('magenta') ||
!n('cyan') ||
!n('white')
)
return undefined;
if (
!b('black') ||
!b('red') ||
!b('green') ||
!b('yellow') ||
!b('blue') ||
!b('magenta') ||
!b('cyan') ||
!b('white')
)
return undefined;
return {
background: primary.background,
foreground: primary.foreground,
black: n('black')!,
red: n('red')!,
green: n('green')!,
yellow: n('yellow')!,
blue: n('blue')!,
magenta: n('magenta')!,
cyan: n('cyan')!,
white: n('white')!,
brightBlack: b('black')!,
brightRed: b('red')!,
brightGreen: b('green')!,
brightYellow: b('yellow')!,
brightBlue: b('blue')!,
brightMagenta: b('magenta')!,
brightCyan: b('cyan')!,
brightWhite: b('white')!,
};
}
/** Ghostty's config: flat `key = value`, ANSI palette as sixteen `palette = N=#hex` lines. */
export function parseGhosttyConf(text: string): AnsiSource | undefined {
let background: string | undefined;
let foreground: string | undefined;
const palette: Record<number, string> = {};
for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
if (line.startsWith('#')) continue;
const bg = line.match(/^background\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (bg) background = normalizeHex(bg[1]!);
const fg = line.match(/^foreground\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (fg) foreground = normalizeHex(fg[1]!);
const pal = line.match(/^palette\s*=\s*(\d+)\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (pal) {
const hex = normalizeHex(pal[2]!);
if (hex) palette[Number(pal[1])] = hex;
}
}
if (!background || !foreground) return undefined;
for (let i = 0; i <= 15; i++) if (!palette[i]) return undefined;
return {
background,
foreground,
black: palette[0]!,
red: palette[1]!,
green: palette[2]!,
yellow: palette[3]!,
blue: palette[4]!,
magenta: palette[5]!,
cyan: palette[6]!,
white: palette[7]!,
brightBlack: palette[8]!,
brightRed: palette[9]!,
brightGreen: palette[10]!,
brightYellow: palette[11]!,
brightBlue: palette[12]!,
brightMagenta: palette[13]!,
brightCyan: palette[14]!,
brightWhite: palette[15]!,
};
}
/**
* A third shape some community repos name `colors.toml` too, unrelated to Omarchy 4's own — flat
* `color0`..`color15` in standard ANSI order, plus `background`/`foreground`. Try this only after
* `paletteFromRecord` (the real Omarchy schema) has already failed on the same file: the two are
* distinguished only by which keys are present, and a file can't be both.
*/
export function parseColor16Toml(t: Record<string, string>): AnsiSource | undefined {
const at = (i: number): string | undefined => (t[`color${i}`] ? normalizeHex(t[`color${i}`]!) : undefined);
const background = t.background ? normalizeHex(t.background) : undefined;
const foreground = t.foreground ? normalizeHex(t.foreground) : undefined;
if (!background || !foreground) return undefined;
const slots = Array.from({ length: 16 }, (_, i) => at(i));
if (slots.some((s) => !s)) return undefined;
return {
background,
foreground,
black: slots[0]!,
red: slots[1]!,
green: slots[2]!,
yellow: slots[3]!,
blue: slots[4]!,
magenta: slots[5]!,
cyan: slots[6]!,
white: slots[7]!,
brightBlack: slots[8]!,
brightRed: slots[9]!,
brightGreen: slots[10]!,
brightYellow: slots[11]!,
brightBlue: slots[12]!,
brightMagenta: slots[13]!,
brightCyan: slots[14]!,
brightWhite: slots[15]!,
};
}
File diff suppressed because it is too large Load Diff
+76
View File
@@ -0,0 +1,76 @@
import type { Palette } from './palette';
import type { AnsiSource } from './ansi-source';
import { UI, contrast, ensureContrast, luminance, mix, parseHex } from './contrast';
// An `AnsiSource` (16 ANSI slots + primary bg/fg) has no notion of "three background depths" or "an
// accent" — Omarchy 4's `colors.toml` is a richer, hand-curated shape that a sixteen-slot terminal scheme
// was never designed to fill. This derives one anyway, by role rather than by guessing a fixed value:
//
// - accent is whichever blue-ish slot actually stands out from the background (every official palette
// picks blue too — this just guards against a scheme that reused the background for it).
// - the three depths are the background pushed a fixed distance toward black or white, in the direction
// each mode's surfaces already move (dark themes rise lighter, light themes recede darker — see the
// ratios comment below, calibrated against Nord and Rosé Pine's real colors.toml).
// - the three foreground weights are the foreground pulled toward the background by a fixed fraction,
// standing in for "faint", "muted" and "strong" text.
//
// None of this has to be exact. `mapPalette`'s `ensureContrast` re-checks every text/surface pair
// afterward and nudges anything that fails — the approximation only has to be plausible, not correct.
function pickAccent(a: AnsiSource): string {
const candidates = [a.blue, a.brightBlue, a.cyan, a.brightCyan];
const best = candidates.reduce((hi, c) => (contrast(c, a.background) > contrast(hi, a.background) ? c : hi));
// `--ring` is this colour laid directly on `--background` (see `map.ts`), and it's in the contract's
// PAIRS check same as the official themes — so guarantee it the way `ensureContrast` guarantees text,
// rather than only guarding against a background-coloured accent.
return ensureContrast(best, a.background, UI);
}
export function deriveOmarchyPalette(a: AnsiSource): Palette {
const dark = luminance(parseHex(a.background)) < 0.5;
const mode: Palette['mode'] = dark ? 'dark' : 'light';
const darkerBackground = dark ? mix(a.background, '#000000', 0.45) : mix(a.background, '#000000', 0.1);
const darkBackground = dark ? mix(a.background, '#000000', 0.22) : mix(a.background, '#000000', 0.06);
const lighterBackground = dark ? mix(a.background, '#ffffff', 0.07) : mix(a.background, '#000000', 0.035);
// Faintest → strongest, all pulled toward the background rather than toward a fixed grey, so a warm
// palette's dim text stays warm instead of turning neutral.
const darkForeground = mix(a.foreground, a.background, 0.55);
const lightForeground = mix(a.foreground, a.background, 0.3);
const accent = pickAccent(a);
const muted = contrast(a.brightBlack, a.background) >= 1.15 ? a.brightBlack : mix(a.foreground, a.background, 0.6);
return {
mode,
accent,
// Unused by `mapPalette` today (grep confirms `p.selection` has no reader) — kept only because the
// `Palette` type requires it.
selection: mix(a.background, a.foreground, 0.2),
muted,
background: a.background,
darkBackground,
darkerBackground,
lighterBackground,
foreground: a.foreground,
darkForeground,
lightForeground,
brightForeground: a.foreground,
red: a.red,
yellow: a.yellow,
green: a.green,
cyan: a.cyan,
blue: a.blue,
magenta: a.magenta,
// Standard ANSI has no orange slot. Mixing red and yellow reads better as "busy" than falling back to
// plain yellow the way `loadPalette` does for the 3 official themes that omit it.
orange: mix(a.red, a.yellow, 0.5),
brightRed: a.brightRed,
brightYellow: a.brightYellow,
brightGreen: a.brightGreen,
brightCyan: a.brightCyan,
brightBlue: a.brightBlue,
brightMagenta: a.brightMagenta,
};
}
+9 -2
View File
@@ -49,6 +49,13 @@ export function mapPalette(p: Palette): Tokens {
// miasma at 4.00 — close enough to look fine and still fail, which is the kind of near-miss that a
// person eyeballing a screenshot would have shipped.
const fgMuted = ensureContrast(p.lightForeground, s.high);
// Same reasoning as `fgMuted`, for the same surface: `--accent` and `--secondary` are BOTH `s.high`,
// so their text has to be checked against `s.high` too. It was deriving from `fgRaised` — the weight
// for `s.raised` — which is the identical near-miss the comment above describes, just one token over.
// A no-op for all 22 official palettes (each already clears 4.5:1 on `s.high` by 1.4x or more, and
// `ensureContrast` returns its input untouched when it passes); what it fixes is the ported palettes
// whose foreground sits between the two surfaces' thresholds.
const fgHigh = ensureContrast(p.foreground, s.high);
const fgFaint = ensureContrast(p.darkForeground, s.raised, UI);
/**
@@ -88,13 +95,13 @@ export function mapPalette(p: Palette): Tokens {
'--muted': s.high,
'--muted-foreground': fgMuted,
'--accent': s.high,
'--accent-foreground': fgRaised,
'--accent-foreground': fgHigh,
// ── Actions ──
'--primary': accent,
'--primary-foreground': on(accent),
'--secondary': s.high,
'--secondary-foreground': fgRaised,
'--secondary-foreground': fgHigh,
'--cta': accent,
'--cta-foreground': on(accent),
'--ctahover': mix(accent, s.dark ? '#ffffff' : '#000000', 0.12),
+18 -7
View File
@@ -38,21 +38,28 @@ export type Palette = {
brightMagenta: string;
};
/** A deliberately small TOML reader: this file is flat `key = "value"` and nothing else. */
function readToml(path: string): Record<string, string> {
/**
* A deliberately small TOML reader: this file is flat `key = "value"` and nothing else.
*
* Exported (not just used by `loadPalette` below) because a community theme can ship this exact shape
* too — see `port-community-themes.ts`, which reads it from a fetched string rather than a file path.
*/
export function parseTomlText(text: string): Record<string, string> {
const out: Record<string, string> = {};
for (const line of readFileSync(path, 'utf8').split('\n')) {
for (const line of text.split('\n')) {
const m = line.match(/^\s*([a-z_0-9]+)\s*=\s*"([^"]*)"/i);
if (m) out[m[1]!] = m[2]!;
// basecamp/omarchy's own files are already lowercase hex; a community `colors.toml` isn't guaranteed
// to be, so normalize here rather than leaving the two sources visibly inconsistent.
if (m) out[m[1]!] = m[2]!.toLowerCase();
}
return out;
}
export function loadPalette(tomlPath: string): Palette {
const t = readToml(tomlPath);
/** The 24-key contract, given an already-parsed record rather than a path — see `parseTomlText`. */
export function paletteFromRecord(t: Record<string, string>, label: string): Palette {
const need = (key: string): string => {
const v = t[key];
if (!v) throw new Error(`${tomlPath}: missing "${key}"`);
if (!v) throw new Error(`${label}: missing "${key}"`);
return v;
};
@@ -84,3 +91,7 @@ export function loadPalette(tomlPath: string): Palette {
brightMagenta: need('bright_magenta'),
};
}
export function loadPalette(tomlPath: string): Palette {
return paletteFromRecord(parseTomlText(readFileSync(tomlPath, 'utf8')), tomlPath);
}
@@ -11,6 +11,7 @@ import { JobsIndicator } from './JobsIndicator';
import { ThemeSwitcher } from './ThemeSwitcher';
import { RescanButton } from '../Rescan/RescanButton';
import { usePageTitle } from '@/state/usePageTitle';
import { useAuth } from 'hooks/useAuth';
import { useThemeAsset } from '@/components/ui/ThemeProvider';
// import { BugReportButton } from '../BugReport/BugReportButton';
@@ -46,6 +47,7 @@ type HeaderProps = {
export function Header({ dockItems, hidden }: HeaderProps) {
const [open, setOpen] = useState(false);
const isTouch = useIsTouch();
const { user } = useAuth();
// The mark comes from the theme — a white-labelled build replaces it without touching this file.
const logo = useThemeAsset('--logo-src');
@@ -100,9 +102,15 @@ export function Header({ dockItems, hidden }: HeaderProps) {
*/}
{/* Bug Report — hidden */}
{/* <BugReportButton /> */}
<JobsIndicator />
<RescanButton />
<ThemeSwitcher />
{/* Owner only, and tidiness rather than access control — /jobs stays reachable and the rescan
endpoint is gated server-side, because a hidden button is not a permission. */}
{user?.role === 'Super Admin' && (
<>
<JobsIndicator />
<RescanButton />
</>
)}
<UserMenu />
</div>
</div>
@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom';
import { Palette, Check } from 'lucide-react';
import { Check } from 'lucide-react';
import * as Dropdown from '@/components/ui/dropdown-menu';
import type { Theme } from '@/components/ui/ThemeProvider';
import { useTheme, useThemeKeys } from '@/components/ui/ThemeProvider';
@@ -45,11 +45,14 @@ export const ThemeSwitcher = () => {
<DropdownMenuTrigger asChild>
<button
title={`Theme: ${theme.name} · ctrl+shift+←/→ to cycle · ctrl+shift+space for the list`}
aria-label="Theme"
className="flex h-8 w-8 items-center justify-center rounded-full transition-transform hover:scale-110 cursor-pointer"
aria-label={`Theme: ${theme.name}`}
// Named rather than a palette glyph: with 227 themes the one you are wearing is worth
// reading at a glance, and an icon cannot say which that is. Same pill as JobsIndicator so
// the cluster stays one shape.
className="flex h-8 cursor-pointer items-center rounded-full bg-black/25 px-2.5 text-xs font-semibold transition-colors hover:bg-black/35"
style={{ color: 'var(--header-fg)' }}
>
<Palette className="h-4 w-4" />
{theme.name}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-50 max-h-[70vh] w-56 overflow-y-auto">
@@ -11,34 +11,55 @@ import { useSpotlightItems, type SpotlightItem } from './useSpotlightItems';
//
// ── Keyboard first, and the trigger ──
//
// `shift-space`. The two obvious chords were both taken: `ctrl-space` focuses the address bar in Brave,
// and `alt-space` is load-bearing for the owner elsewhere. `cmd-k` belongs to the browser on some
// platforms and to text fields on others.
// `alt-space`, and ONLY that. It went ctrl-space → shift-space → alt-space, and each move was the owner
// telling us what the chord actually collides with on the machine he uses:
//
// **Shift-space is a real key combination inside a text field** — it types a space, and people hold shift
// through one more often than they think while typing capitals. So the trigger is IGNORED when focus is
// in an input, a textarea or anything contenteditable, and the space is typed as normal.
// ctrl-space focuses the address bar in Brave
// shift-space types a space — see below, this was the real problem
// alt-space taken by the window menu, and the owner rebound it rather than give up the chord
//
// The cost of that is worth stating: focus usually IS in the chat composer, so opening Spotlight from
// there means clicking out first. If that turns out to be the common case, a double-tap of shift is the
// escape hatch — it conflicts with nothing anywhere.
// **Shift-space was never a good choice**, and that is why it did not last a day. It is a real key
// combination inside a text field: it types a space, and people hold shift through one more often than
// they think while typing capitals. Guarding it meant IGNORING the trigger whenever focus was in an input
// — which is where focus usually is, the chat composer — so opening Spotlight meant clicking out first.
// A launcher you have to prepare for is not a launcher.
//
// Alt-space has no meaning inside a text field, so there is NO focus check at all. The guard shift-space
// needed was kept for one commit as "belt-and-braces" and was simply the old bug wearing a new hat: focus
// is usually in the chat composer, so a launcher that stands down there is a launcher that does not open
// when you want it. `preventDefault` on the handler covers the only real risk the guard addressed.
//
// Nothing else opens it. There is deliberately no second chord — two triggers for one thing means two
// things to collide with, and this one moved twice already.
//
// Escape closes, arrows move, enter goes. The mouse works, but nothing here is designed around it.
//
// ── Sizing ──
//
// Loosened 2026-08-22: the first pass reused the app's list metrics (7px tile, text-sm, py-2) and read as
// cramped, because this is not a list inside a panel — it is a modal you look at for two seconds with
// nothing else competing for the eye. So the search input leads at `text-base`, rows are `py-2.5` with a
// 9px tile, and the label is `text-[15px]` with the hint and path kept deliberately subordinate.
//
// The panel widened with them (560 → 640). Rows now carry a label, a hint and a route on one line, and the
// route is the first thing to be squeezed out — it is also the thing that disambiguates two rows with the
// same name.
//
// The results box grew 52vh → 60vh for the arithmetic reason: taller rows in the same box show fewer
// results, and the cap on how many are listed was removed for a reason.
const MAX_RESULTS = 8;
/*
No cap, deliberately, and it used to be 8.
/**
* Is the user typing into something?
*
* `shift-space` types a space in any text field, so the trigger has to stand down there or it eats a
* keystroke people make by accident all day. `isContentEditable` covers the editor and any rich input a
* panel brings with it.
*/
function isTyping(target: EventTarget | null): boolean {
const el = target as HTMLElement | null;
if (!el || !el.tagName) return false;
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable === true;
}
Eight was fine on the machine it was written on, which has few plugins. On a populated server the dock
carries fourteen plugin tiles and the core items alone fill all eight slots — so with an empty query the
list stopped at Browser and every plugin was unreachable unless you already knew its name well enough to
type it. A launcher you can only use for things you can already name is not doing the job.
The container has scrolled since it was written (`max-h-[52vh] overflow-y-auto`), so there was never a
layout reason for the limit, and the truncation was silent — nothing said "and 20 more", which is how a
cap becomes indistinguishable from a missing feature.
*/
/** Label with the matched characters picked out, so it is obvious WHY a result is here. */
const Highlighted = ({ label, hits }: { label: string; hits: number[] }) => {
@@ -71,8 +92,15 @@ const Row = ({
onPick: () => void;
}) => {
const Icon = item.icon;
// Keyboard navigation has to survive a list taller than the box. `block: 'nearest'` scrolls only when the
// row is actually out of sight, so arrowing through the visible middle of the list does not jump it.
const ref = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (active) ref.current?.scrollIntoView({ block: 'nearest' });
}, [active]);
return (
<button
ref={ref}
type="button"
// `onMouseDown`, not `onClick`: the input holds focus and a click would blur it first, closing the
// overlay on the blur handler before the navigation ever ran.
@@ -80,29 +108,52 @@ const Row = ({
ev.preventDefault();
onPick();
}}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors ${
className={`flex w-full items-center gap-3.5 rounded-lg px-3.5 py-2.5 text-left transition-colors ${
active ? 'bg-accent' : 'hover:bg-accent/50'
}`}
>
{/*
The dock's tile, at row size. Same two cases and the same rule for choosing between them, because
these are the SAME things and a second visual language for them is what made this list read as
somebody else's menu:
badge a filled square of the item's colour with a white glyph — a white glyph on nothing is
invisible, which is why the fill is not decoration
bare no background, artwork fills the box — a logo framed in an arbitrary swatch reads as a
mistake, and every plugin shipping its own art is this case
The tinted `${color}22` square this used for everything was a third style belonging to neither, and
it put a coloured pane behind Calendar, Contacts and every plugin logo.
*/}
<span
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md"
style={item.color ? { backgroundColor: `${item.color}22`, color: item.color } : undefined}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md"
style={{
background: item.tile === 'bare' ? 'transparent' : (item.color ?? 'transparent'),
// No colour at all is a settings row: keep the muted glyph rather than inventing a swatch.
color: item.color ? '#fff' : undefined,
}}
>
{item.image ? (
<img src={item.image} alt="" className="h-5 w-5 object-contain" />
<img
src={item.image}
alt=""
className={item.tile === 'bare' ? 'h-full w-full object-contain' : 'h-5 w-5 object-contain'}
/>
) : Icon ? (
<Icon className="h-4 w-4" />
<Icon className={`h-[18px] w-[18px] ${item.color ? '' : 'text-muted-foreground'}`} />
) : (
<Search className="h-4 w-4" />
<Search className="h-[18px] w-[18px] text-muted-foreground" />
)}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm text-foreground">
<span className="block truncate text-[15px] leading-snug text-foreground">
<Highlighted label={item.label} hits={hits} />
</span>
{item.hint ? <span className="block truncate text-xs text-muted-foreground">{item.hint}</span> : null}
{item.hint ? (
<span className="block truncate text-[13px] leading-snug text-muted-foreground">{item.hint}</span>
) : null}
</span>
<span className="shrink-0 font-mono text-[10px] text-muted-foreground/60">{item.to}</span>
<span className="shrink-0 font-mono text-[11px] text-muted-foreground/60">{item.to}</span>
</button>
);
};
@@ -115,7 +166,20 @@ export const Spotlight = () => {
const navigate = useNavigate();
const items = useSpotlightItems();
const results = useMemo(() => rank(items, query).slice(0, MAX_RESULTS), [items, query]);
const results = useMemo(() => rank(items, query), [items, query]);
/*
The window handler reads these through refs.
It has to: the listener is registered once per open, and closing over `results` and `active` would pin
it to their values at registration — so arrowing down would read a stale list and Enter would open
whatever was first when the panel appeared. Adding them to the effect's deps instead would tear down and
re-register the listener on every keystroke, which is the same bug wearing a costume.
*/
const resultsRef = useRef(results);
resultsRef.current = results;
const activeRef = useRef(active);
activeRef.current = active;
const close = useCallback(() => {
setOpen(false);
@@ -136,16 +200,48 @@ export const Spotlight = () => {
// anywhere — including inside a panel that has its own key handling.
useEffect(() => {
const onKey = (ev: KeyboardEvent) => {
if (ev.shiftKey && ev.code === 'Space' && !isTyping(ev.target)) {
// `altKey` and `code`, not `key`: alt-space produces a different `key` on several layouts (and
// nothing at all on some), while the physical key is `Space` everywhere.
//
// No focus check. There was one, inherited from shift-space where it was mandatory, and keeping it
// "as belt-and-braces" was wrong in the plainest way: focus is usually IN the chat composer, so the
// guard meant the launcher did not open from the place it is most often wanted. `preventDefault`
// below covers the only thing the guard was protecting against — a layout that would otherwise type
// a character.
if (ev.altKey && ev.code === 'Space') {
ev.preventDefault();
// Take focus off whatever had it, explicitly, before the overlay mounts. The effect below focuses
// this panel's own input, and on paper that is enough — but "on paper" is what produced arrow keys
// that sometimes did nothing: focus stayed in the composer, so ArrowDown went to the textarea and
// the launcher never saw it. Blurring the outgoing element is one line and removes the race rather
// than narrowing it.
(document.activeElement as HTMLElement | null)?.blur?.();
setOpen((wasOpen) => !wasOpen);
return;
}
if (ev.key === 'Escape' && open) close();
if (!open) return;
if (ev.key === 'Escape') {
close();
return;
}
// Navigation is handled HERE rather than on the input, so it works no matter what has focus — a
// panel that grabbed it back, a blur that lost the race, an element with its own key handling. The
// input's own onKeyDown used to own this, which made every keystroke conditional on focus having
// landed where we expected.
if (ev.key === 'ArrowDown') {
ev.preventDefault();
setActive((i) => Math.min(i + 1, resultsRef.current.length - 1));
} else if (ev.key === 'ArrowUp') {
ev.preventDefault();
setActive((i) => Math.max(i - 1, 0));
} else if (ev.key === 'Enter') {
ev.preventDefault();
pick(resultsRef.current[activeRef.current]?.item);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, close]);
}, [open, close, pick]);
useEffect(() => {
if (open) inputRef.current?.focus();
@@ -162,34 +258,22 @@ export const Spotlight = () => {
onMouseDown={close}
>
<div
className="w-[min(92vw,560px)] overflow-hidden rounded-2xl border border-border bg-background/95 shadow-2xl"
className="w-[min(92vw,640px)] overflow-hidden rounded-2xl border border-border bg-background/95 shadow-2xl"
// The backdrop closes on mousedown; this stops a click INSIDE the panel counting as one.
onMouseDown={(ev) => ev.stopPropagation()}
>
<div className="flex items-center gap-2 border-b border-border px-4">
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
<Search className="h-5 w-5 shrink-0 text-muted-foreground" />
<input
ref={inputRef}
value={query}
onChange={(ev) => setQuery(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'ArrowDown') {
ev.preventDefault();
setActive((i) => Math.min(i + 1, results.length - 1));
} else if (ev.key === 'ArrowUp') {
ev.preventDefault();
setActive((i) => Math.max(i - 1, 0));
} else if (ev.key === 'Enter') {
ev.preventDefault();
pick(results[active]?.item);
}
}}
placeholder="Go to…"
className="w-full bg-transparent py-3.5 text-sm text-foreground outline-none placeholder:text-muted-foreground"
className="w-full bg-transparent py-4 text-base text-foreground outline-none placeholder:text-muted-foreground"
/>
</div>
<div className="max-h-[52vh] overflow-y-auto p-2">
<div className="max-h-[60vh] overflow-y-auto p-2">
{results.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">Nothing matches “{query}”.</p>
) : (
@@ -1,6 +1,23 @@
import { useMemo } from 'react';
import type { LucideIcon } from 'lucide-react';
import { Settings, User, Puzzle, Server, Cpu, Users } from 'lucide-react';
import {
Bot,
CalendarDays,
Cpu,
Globe,
KeyRound,
LayoutGrid,
Lock,
Mail,
Mic,
ScanText,
ShieldCheck,
Sparkles,
User,
Users,
Volume2,
Wrench,
} from 'lucide-react';
import { usePermissions } from 'hooks/usePermissions';
import { CORE_DOCK_ITEMS, dockItemsFromPlugins } from '../Dock';
@@ -32,6 +49,15 @@ export type SpotlightItem = {
/** Plugin artwork, when the tile has some. */
image?: string;
color?: string;
/**
* Copied from the dock tile, and it has to be.
*
* `bare` means the artwork brings its own frame — calendar, contacts, and every plugin shipping a logo.
* Dropping it here painted a coloured swatch behind those logos, which the dock's own comment calls out
* as reading like a mistake. It was dropped, so Spotlight looked like a different app's list of the same
* things.
*/
tile?: 'badge' | 'bare';
};
/**
@@ -41,19 +67,67 @@ export type SpotlightItem = {
* will not appear here until someone adds it. The alternative is exporting every settings screen's
* section array up into the shell, which couples five screens to this one to save five lines.
*
* Worth revisiting if it drifts. `hint` is what makes them findable — typing "keys" should reach the API
* keys screen, and the label alone does not say where it lives.
* IT DRIFTED, and within two days. The first version listed nine rows against a settings area that has
* twenty-one, missed Languages and Voice entirely, pointed `/settings/users` at a route that does not
* exist (it is `/settings/user-management/users`), and gave four different rows the same User glyph —
* which is most of why the whole panel read as a generic list rather than as the app's own.
*
* So the cost is now measured rather than predicted, and deriving these from the screens is the fix if it
* happens a third time. Each group below names the file it is copied from, so checking is one grep.
*
* `hint` is what makes them findable — typing "keys" should reach the API keys screen, and the label alone
* does not say where it lives.
*/
const SETTINGS_ITEMS: SpotlightItem[] = [
// Profile — ProfileSettings/index.tsx
{ id: 'set:profile', label: 'Profile', to: '/settings/profile', hint: 'avatar, name, email', icon: User },
{ id: 'set:password', label: 'Change password', to: '/settings/profile/change-password', icon: User },
{ id: 'set:api-keys', label: 'API keys', to: '/settings/profile/api-keys', hint: 'per-app keys', icon: User },
{ id: 'set:dock', label: 'Dock', to: '/settings/profile/dock', hint: 'choose and reorder', icon: User },
{ id: 'set:integrations', label: 'Integrations', to: '/settings/integrations', icon: Puzzle },
{ id: 'set:server', label: 'Server settings', to: '/settings/server', icon: Server },
{ id: 'set:system', label: 'System', to: '/settings/system', icon: Cpu },
{ id: 'set:users', label: 'Users', to: '/settings/users', hint: 'accounts and permissions', icon: Users },
{ id: 'set:ai', label: 'AI', to: '/settings/ai', hint: 'models and harnesses', icon: Settings },
{ id: 'set:password', label: 'Change password', to: '/settings/profile/change-password', icon: Lock },
{
id: 'set:api-keys',
label: 'API keys',
to: '/settings/profile/api-keys',
hint: 'keys that sign in as you',
icon: KeyRound,
},
{
id: 'set:languages',
label: 'Languages',
to: '/settings/profile/languages',
hint: 'spoken and translation',
icon: Globe,
},
{ id: 'set:voice', label: 'Voice', to: '/settings/profile/voice', hint: 'text-to-speech', icon: Volume2 },
{ id: 'set:dock', label: 'Dock', to: '/settings/profile/dock', hint: 'choose and reorder', icon: LayoutGrid },
// AI — AISettings.tsx
{ id: 'set:ai-my-models', label: 'My Models', to: '/settings/ai/my-models', icon: Sparkles },
{ id: 'set:ai-channel-models', label: 'Channel Models', to: '/settings/ai/member-models', icon: Sparkles },
{ id: 'set:ai-default-models', label: 'Default Models', to: '/settings/ai/default-models', icon: Sparkles },
{ id: 'set:ai-harnesses', label: 'Providers', to: '/settings/ai/ai-harnesses', hint: 'harnesses', icon: Bot },
// System — SystemSettings.tsx
{ id: 'set:tts', label: 'Text to Speech', to: '/settings/system/tts', icon: Volume2 },
{ id: 'set:stt', label: 'Speech to Text', to: '/settings/system/stt', icon: Mic },
{ id: 'set:ocr', label: 'OCR', to: '/settings/system/ocr', icon: ScanText },
{
id: 'set:maintenance',
label: 'Rebuild & restart',
to: '/settings/system/maintenance',
hint: 'maintenance',
icon: Cpu,
},
{ id: 'set:smtp', label: 'SMTP', to: '/settings/system/smtp', hint: 'outgoing mail', icon: Mail },
// Integrations — IntegrationsSettings
{ id: 'set:google-oauth', label: 'Google OAuth', to: '/settings/integrations/google-oauth', icon: KeyRound },
{ id: 'set:apify', label: 'Apify', to: '/settings/integrations/apify', icon: Wrench },
{
id: 'set:dav',
label: 'Calendar & Contacts sync',
to: '/settings/integrations/dav-app-passwords',
hint: 'app passwords',
icon: CalendarDays,
},
// Users — UserManagement
{ id: 'set:users', label: 'Accounts', to: '/settings/user-management/users', hint: 'users', icon: Users },
{ id: 'set:permissions', label: 'Permissions', to: '/settings/user-management/permissions', icon: ShieldCheck },
];
/** Everything Spotlight can offer this account, in a stable order. */
@@ -70,6 +144,7 @@ export function useSpotlightItems(): SpotlightItem[] {
icon: item.icon,
image: item.image,
color: item.color,
tile: item.tile,
}));
// Settings go through `canVisit` too. `usePermissions` fails OPEN by design — hiding a door is a
@@ -0,0 +1,2 @@
export * from './schema';
export * from './queries';
@@ -0,0 +1,147 @@
import { and, desc, eq, lt } from 'drizzle-orm';
import { db } from '../db';
import { decryptSecret, encryptSecret } from '../crypto';
import { anthropicAccounts } from './schema';
/*
A stored Anthropic login, and the two shapes it comes in.
`StoredAccount` is what an API may return: who it is, how stale, whether it can still be used. It carries
no secret. `revealAccount` is the only way to the credential itself and exists for exactly one caller —
the switch, which writes it back to disk.
*/
/** The encryption purpose for one account. Per-account by design; see the schema note. */
const purposeFor = (accountUuid: string) => `anthropic-account:${accountUuid}`;
export type StoredAccount = {
accountUuid: string;
emailAccount: string;
orgName: string | null;
plan: string | null;
capturedAt: string;
lastUsedAt: string | null;
refreshExpiresAt: string | null;
/** False once the refresh token is past its expiry: switching would fail, so the UI must offer /login. */
usable: boolean;
};
type SaveParams = {
userId: number;
accountUuid: string;
emailAccount: string;
orgName: string | null;
plan: string | null;
/** The `claudeAiOauth` object, verbatim, as JSON. */
credentialsJson: string;
/** The `oauthAccount` object, verbatim, as JSON. */
oauthAccountJson: string;
refreshExpiresAt: Date | null;
};
const publish = (row: typeof anthropicAccounts.$inferSelect): StoredAccount => ({
accountUuid: row.accountUuid,
emailAccount: row.emailAccount,
orgName: row.orgName,
plan: row.plan,
capturedAt: row.capturedAt.toISOString(),
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
refreshExpiresAt: row.refreshExpiresAt?.toISOString() ?? null,
usable: !row.refreshExpiresAt || row.refreshExpiresAt.getTime() > Date.now(),
});
/**
* Record (or re-record) one account's credential.
*
* Upsert on `(userId, accountUuid)` rather than insert, because capturing happens every time the current
* account is noticed to have changed — so the common case is refreshing a row that already exists with a
* newer token. `capturedAt` moves; `lastUsedAt` deliberately does not, since capturing is not using.
*/
export async function saveAnthropicAccount(p: SaveParams): Promise<void> {
const purpose = purposeFor(p.accountUuid);
const values = {
userId: p.userId,
accountUuid: p.accountUuid,
emailAccount: p.emailAccount,
orgName: p.orgName,
plan: p.plan,
credentials: encryptSecret(purpose, p.credentialsJson),
oauthAccount: encryptSecret(purpose, p.oauthAccountJson),
refreshExpiresAt: p.refreshExpiresAt,
capturedAt: new Date(),
};
await db
.insert(anthropicAccounts)
.values(values)
.onConflictDoUpdate({
target: [anthropicAccounts.userId, anthropicAccounts.accountUuid],
set: {
emailAccount: values.emailAccount,
orgName: values.orgName,
plan: values.plan,
credentials: values.credentials,
oauthAccount: values.oauthAccount,
refreshExpiresAt: values.refreshExpiresAt,
capturedAt: values.capturedAt,
},
});
}
/** Every account this Officer user can switch to, most recently used first. Carries no secret. */
export async function listAnthropicAccounts(userId: number): Promise<StoredAccount[]> {
const rows = await db
.select()
.from(anthropicAccounts)
.where(eq(anthropicAccounts.userId, userId))
.orderBy(desc(anthropicAccounts.lastUsedAt), desc(anthropicAccounts.capturedAt));
return rows.map(publish);
}
/**
* The credential itself, for the switch. Decrypts, so every call is a secret leaving the database.
*
* Returns null for an unknown account AND for one whose refresh token has expired: a caller that wrote an
* expired credential to disk would have signed the owner out with no message, which is the worst of the
* available failures.
*/
export async function revealAnthropicAccount(
userId: number,
accountUuid: string,
): Promise<{ credentialsJson: string; oauthAccountJson: string; emailAccount: string } | null> {
const [row] = await db
.select()
.from(anthropicAccounts)
.where(and(eq(anthropicAccounts.userId, userId), eq(anthropicAccounts.accountUuid, accountUuid)))
.limit(1);
if (!row) return null;
if (row.refreshExpiresAt && row.refreshExpiresAt.getTime() <= Date.now()) return null;
const purpose = purposeFor(row.accountUuid);
return {
credentialsJson: decryptSecret(purpose, row.credentials),
oauthAccountJson: decryptSecret(purpose, row.oauthAccount),
emailAccount: row.emailAccount,
};
}
/** Marks the switch. Separate from reveal so a failed write does not claim the account is in use. */
export async function markAnthropicAccountUsed(userId: number, accountUuid: string): Promise<void> {
await db
.update(anthropicAccounts)
.set({ lastUsedAt: new Date() })
.where(and(eq(anthropicAccounts.userId, userId), eq(anthropicAccounts.accountUuid, accountUuid)));
}
/**
* Drop credentials that can no longer be used.
*
* A dead refresh token is not a credential, it is a liability with a thirty-day head start already spent —
* so it goes rather than sitting in the table for ever. Called opportunistically on list, not scheduled: a
* cron for one DELETE is a moving part that can silently stop.
*/
export async function purgeExpiredAnthropicAccounts(userId: number): Promise<number> {
const gone = await db
.delete(anthropicAccounts)
.where(and(eq(anthropicAccounts.userId, userId), lt(anthropicAccounts.refreshExpiresAt, new Date())))
.returning({ id: anthropicAccounts.id });
return gone.length;
}
@@ -0,0 +1,72 @@
import { integer, pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from '../auth/schema';
/**
* The OAuth credential for each Anthropic account the owner signs into, so they can be switched between
* without a browser round trip.
*
* ── Why keep them at all ──
*
* The owner has a personal account and a company one and moves between them during a day — by what they are
* working on, and sometimes because one has hit a limit and the work has to continue. `claude auth login`
* handles the first switch and every switch after it, at the cost of a browser round trip each time, and it
* cannot switch BACK to an account whose credential the CLI has already overwritten.
*
* `ai_provider_usage` already answers "which of my accounts has room". This answers the question that
* immediately follows it: "then use that one".
*
* ── One key per account, not one key for the table ──
*
* `credentials` and `oauthAccount` are encrypted with `encryptSecret('anthropic-account:<uuid>', …)`, so the
* secret store mints a separate key per Anthropic account. One leaked key is one account rather than all of
* them, which is the same reasoning that split `VAULT_STORE_KEY` into purposes in the first place. The uuid
* comes from Anthropic (`oauthAccount.accountUuid`) so it is stable across a re-login, where the email is
* also stable but is user-editable at the provider.
*
* ── What is stored, and what that is worth to somebody who steals it ──
*
* The whole `claudeAiOauth` object, which includes a REFRESH token — deliberately, because the access token
* lives hours and the refresh token is what makes a switch work a week later. So a row plus the key is
* thirty days of acting as that account. That is a real escalation over holding one credential on disk, it
* was accepted knowingly, and two things reduce it: the per-account key above, and `refreshExpiresAt` being
* a column so a dead credential can be purged rather than sitting here for ever.
*
* ── Owner only, and per Officer user regardless ──
*
* Only the owner's `~/.claude` is switched today; a member's `claude` reads their own home and is untouched.
* The rows are keyed by Officer user anyway, because the same mechanism per home is the obvious extension
* and a table that assumed one user would have to be migrated to get there.
*/
export const anthropicAccounts = pgTable(
'anthropic_accounts',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
/** Anthropic's own id for the account. The encryption purpose is derived from this. */
accountUuid: text('account_uuid').notNull(),
/** What the pill reads, and what `ai_provider_usage.email_account` joins on in practice. */
emailAccount: text('email_account').notNull(),
/** `Pertento`, or null on a personal account with no organisation. Display only. */
orgName: text('org_name'),
/** `Team`, `Max 20x`, `Pro`. Display only. */
plan: text('plan'),
/** `claudeAiOauth` from `.credentials.json`, encrypted. Never logged, never returned by an API. */
credentials: text('credentials').notNull(),
/** `oauthAccount` from `.claude.json`, encrypted. Identity rather than credential, but it travels with it. */
oauthAccount: text('oauth_account').notNull(),
/**
* When the refresh token dies. Past this, a switch cannot succeed and the UI must say so instead of
* failing at spawn time — which would look like the agent being broken rather than a sign-in expiring.
*/
refreshExpiresAt: timestamp('refresh_expires_at', { withTimezone: true }),
/** When this credential was captured — how stale the thing you are switching to is. */
capturedAt: timestamp('captured_at', { withTimezone: true }).notNull().defaultNow(),
/** Last time it was switched TO. Orders the pills by what the owner actually uses. */
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
},
// uniqueIndex, never unique().on() — drizzle-kit mis-diffs named composite unique CONSTRAINTS and
// re-creates them on every push. See src/databases/CLAUDE.md → "Composite keys".
(table) => [uniqueIndex('uq_anthropic_accounts_account').on(table.userId, table.accountUuid)],
);
+1
View File
@@ -37,6 +37,7 @@ export * from './chat-session-models';
export * from './chat-notification-prefs';
export * from './nudges';
export * from './ai-provider-usage';
export * from './anthropic-accounts';
export * from './command-hq';
export * from './dashboards';
export * from './integrations';
+1
View File
@@ -34,6 +34,7 @@ export * from './chat-events/schema'; // chat_session_events
export * from './chat-session-models/schema'; // chat_session_models
export * from './chat-notification-prefs/schema'; // chat_notification_prefs
export * from './ai-provider-usage/schema'; // ai_provider_usage — last usage reading per AI account
export * from './anthropic-accounts/schema'; // anthropic_accounts — switchable OAuth credentials
export * from './command-hq/schema'; // command_hq_categories, command_hq_sessions, command_hq_tags, command_hq_session_tags
export * from './agent-panels/schema'; // agent_panels
+384
View File
@@ -0,0 +1,384 @@
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
import { spawn } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import {
listAnthropicAccounts,
markAnthropicAccountUsed,
purgeExpiredAnthropicAccounts,
revealAnthropicAccount,
saveAnthropicAccount,
} from 'officerdb';
import { createRouter } from '../../create-router';
import { getHomeDir, getOwnerHomeDir } from '../../data-path';
import { claudeBinIn } from '../../os-user-claude';
import { runAsArgv } from '../../os-user';
/*
`/login` in the chat composer, doing what it does in a terminal.
Officer's members and its owner both authenticate `claude` with a Claude SUBSCRIPTION over OAuth, not with
an Anthropic API key — see `sidecar/claude/state.ts`. That flow is a browser round trip: the CLI prints a
URL, you authorise in claude.com, and you paste a code back. It is the one part of running an agent that
a browser-only user could not do from the browser: they had to open a terminal panel and drive the CLI by
hand, which works and is exactly the thing this removes.
It needs no pty. `claude auth login` behaves identically through plain pipes — verified 2026-08-21 by
capturing its raw bytes — so this is an ordinary subprocess with two reads and one write:
spawn ─► read stdout until the URL ─► [person authorises] ─► write the code to stdin ─► exit
The one non-obvious piece is the parsing. The CLI wraps the URL in an OSC-8 hyperlink escape AND prints it
again as visible text, so a naive `https://\S+` match returns two URLs concatenated with no separator.
Stripping the escape wrappers first leaves exactly one.
*/
/** OSC-8 hyperlink: `ESC ] 8 ; ; <uri> BEL` … `ESC ] 8 ; ; BEL`. Both halves go. */
const OSC8 = /\x1b\]8;;[^\x07\x1b]*(?:\x07|\x1b\\)/g;
const SGR = /\x1b\[[0-9;]*m/g;
const visibleText = (raw: string): string => raw.replace(OSC8, '').replace(SGR, '');
/** The URL the person has to open, or null while the CLI has not printed it yet. */
export function extractLoginUrl(raw: string): string | null {
const m = visibleText(raw).match(/https:\/\/\S+/);
return m ? m[0] : null;
}
/** The CLI is waiting on stdin. Distinct from "printed a URL" — it prints the URL first. */
export const awaitingCode = (raw: string): boolean => /Paste code here/i.test(visibleText(raw));
type Pending = {
userId: number;
child: ChildProcessWithoutNullStreams;
url: string;
out: string;
reaper: ReturnType<typeof setTimeout>;
};
/*
In memory, and deliberately not in Postgres.
A pending login is worth nothing after the process holding its PKCE verifier dies, so persisting it would
store a row that can only ever be stale. A restart losing them is correct: the code in the person's
clipboard is already useless, and the honest recovery is to type `/login` again.
*/
const pending = new Map<string, Pending>();
/*
The authorisation code expires and so does the person's patience. Ten minutes is long enough to find a
password manager and short enough that a forgotten tab does not leave a `claude` process parked on a
read of stdin for the rest of the week.
*/
const TTL_MS = 10 * 60 * 1000;
function abandon(id: string): void {
const p = pending.get(id);
if (!p) return;
clearTimeout(p.reaper);
pending.delete(id);
try {
p.child.kill('SIGTERM');
} catch {
// Already gone. Nothing to do and nothing worth logging.
}
}
type Who = { id: number; isOwner: boolean; osUser: string | null; email: string };
/**
* The login process for one account.
*
* The owner runs it as themselves, in their real home, so it refreshes the very credentials
* `officer-anthropic-proxy` reads. A member runs through `runAsArgv` — `setpriv --reset-env` — as their own
* Linux user, writing into their own home, which is the whole point: their turns bill their subscription
* rather than the owner's, and that only holds if the credential is theirs.
*
* Nothing Anthropic-shaped is inherited in either case. For the owner those variables point at the local
* proxy, and the proxy exists to swap a shared secret for the owner's bearer token — handing them to a
* login is how you get a "success" that authenticates nothing.
*/
function spawnLogin(who: Who): ChildProcessWithoutNullStreams {
const stripped = { ...process.env };
for (const name of [
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_FOUNDRY_API_KEY',
'_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
'CLAUDE_CODE_OAUTH_TOKEN',
])
delete stripped[name];
if (who.isOwner) {
return spawn('claude', ['auth', 'login', '--claudeai'], {
env: stripped,
stdio: ['pipe', 'pipe', 'pipe'],
}) as ChildProcessWithoutNullStreams;
}
if (!who.osUser) throw new Error('no Linux account: an agent for this member cannot hold its own credentials');
const home = getHomeDir(who.email);
// `env` inside the argv, because `--reset-env` discards anything handed to `setpriv` itself. Only PATH,
// and only so `claude` can find `node`; nothing here carries a credential.
const [launcher, ...args] = runAsArgv(who.osUser, [claudeBinIn(home), 'auth', 'login', '--claudeai']);
if (!launcher) throw new Error('runAsArgv returned an empty argv');
return spawn(launcher, args, { cwd: home, stdio: ['pipe', 'pipe', 'pipe'] }) as ChildProcessWithoutNullStreams;
}
/** Reads until the URL appears, or the process dies, or we run out of patience waiting for it. */
function urlFrom(child: ChildProcessWithoutNullStreams): Promise<{ url: string; out: string }> {
return new Promise((resolve, reject) => {
let out = '';
// Separate from TTL_MS: this is the CLI failing to start, which shows up as silence rather than an error.
const giveUp = setTimeout(
() => reject(new Error(`no login URL after 25s. Output so far: ${visibleText(out)}`)),
25_000,
);
const settle = (fn: () => void) => {
clearTimeout(giveUp);
fn();
};
const onChunk = (b: Buffer) => {
out += b.toString();
const url = extractLoginUrl(out);
if (url) settle(() => resolve({ url, out }));
};
child.stdout.on('data', onChunk);
// The CLI prints the URL to stdout, but its warnings go to stderr and one of them explains a failure.
child.stderr.on('data', (b: Buffer) => {
out += b.toString();
});
child.on('error', (err) => settle(() => reject(err)));
child.on('exit', (code) =>
settle(() => reject(new Error(`claude auth login exited ${code} before printing a URL: ${visibleText(out)}`))),
);
});
}
export const claudeLoginRouter = createRouter();
// POST /auth/claude-login — start one, and hand back the URL to open.
claudeLoginRouter.post('/', async (ctx) => {
const user = ctx.get('user');
const who: Who = { id: user.id, isOwner: user.id === 1, osUser: user.osUser ?? null, email: user.email };
// One at a time per account. A second `/login` while one is open is a person retrying, not a person
// wanting two, and two processes racing to write one credentials file is a corrupt file.
for (const [id, p] of pending) if (p.userId === who.id) abandon(id);
let child: ChildProcessWithoutNullStreams;
try {
child = spawnLogin(who);
} catch (err) {
return ctx.json({ error: err instanceof Error ? err.message : 'could not start the login' }, 400);
}
try {
const { url, out } = await urlFrom(child);
const id = crypto.randomUUID();
pending.set(id, { userId: who.id, child, url, out, reaper: setTimeout(() => abandon(id), TTL_MS) });
// Keep reading, or the pipe fills and the child blocks — and we want its later output for the result.
child.stdout.on('data', (b: Buffer) => {
const p = pending.get(id);
if (p) p.out += b.toString();
});
return ctx.json({ id, url, expiresInMs: TTL_MS });
} catch (err) {
try {
child.kill('SIGTERM');
} catch {
// Nothing to clean up.
}
return ctx.json({ error: err instanceof Error ? err.message : 'the login did not start' }, 502);
}
});
// POST /auth/claude-login/:id/code — hand over what claude.com gave them.
claudeLoginRouter.post('/:id/code', async (ctx) => {
const user = ctx.get('user');
const id = ctx.req.param('id');
const p = pending.get(id);
// Same answer for "expired", "never existed" and "somebody else's": all three are equally not-yours.
if (!p || p.userId !== user.id) return ctx.json({ error: 'that login is no longer open — run /login again' }, 404);
const body = await ctx.req.json().catch(() => ({}) as Record<string, unknown>);
const code = typeof body.code === 'string' ? body.code.trim() : '';
if (!code) return ctx.json({ error: 'no code' }, 400);
const finished = new Promise<number | null>((resolve) => p.child.once('exit', (c) => resolve(c)));
p.child.stdin.write(`${code}\n`);
// The exchange is a network round trip on the CLI's side.
const exit = await Promise.race([finished, new Promise<'timeout'>((r) => setTimeout(() => r('timeout'), 45_000))]);
const tail = visibleText(p.out).slice(-600);
abandon(id);
if (exit === 'timeout') return ctx.json({ ok: false, error: 'the CLI did not finish in 45s', detail: tail }, 504);
if (exit !== 0) return ctx.json({ ok: false, error: `claude auth login exited ${exit}`, detail: tail }, 400);
// Record what was just signed into, so it can be switched BACK to later without another round trip. Only
// for the owner: a member's credential is in their own home and this module only ever reads the owner's.
if (user.id === 1) await captureCurrentAnthropicAccount(user.id);
const now = user.id === 1 ? await readCurrentAccount() : null;
// The account is the answer the person opened this for. Two accounts on one machine means "signed in" on
// its own is not enough to act on.
return ctx.json({ ok: true, detail: tail, email: now?.emailAccount ?? null, orgName: now?.orgName ?? null });
});
// DELETE /auth/claude-login/:id — they closed the dialog.
claudeLoginRouter.delete('/:id', (ctx) => {
const user = ctx.get('user');
const id = ctx.req.param('id');
const p = pending.get(id);
if (p && p.userId === user.id) abandon(id);
return ctx.json({ ok: true });
});
/* ────────────────────────────────────────────────────────────────────────────
Switching between accounts that have already been signed into.
The owner keeps a personal account and a company one and moves between them during a day. `/login` above
handles arriving at an account; this handles going back to one, without a browser round trip, using the
credential kept from last time.
Owner only, and structurally: `getOwnerHomeDir` ignores the email it is passed, so every path below is
the owner's whatever the caller claims. A member's `claude` reads their own home and is untouched by any
of this — the isolation is the same as it was, and this feature does not widen it.
──────────────────────────────────────────────────────────────────────────── */
/** `.credentials.json` holds one key. `.claude.json` holds a whole config and must be PATCHED, not written. */
const credentialsPath = () => join(getOwnerHomeDir(''), '.claude', '.credentials.json');
const configPath = () => join(getOwnerHomeDir(''), '.claude.json');
async function readJson(path: string): Promise<Record<string, unknown> | null> {
try {
return JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>;
} catch {
return null;
}
}
/**
* What the CLI is signed in as right now, read from its own two files.
*
* Both are needed and neither is sufficient: the credential lives in `.claude/.credentials.json` and the
* identity — email, org, account uuid — lives in `.claude.json` beside it.
*/
async function readCurrentAccount(): Promise<{
accountUuid: string;
emailAccount: string;
orgName: string | null;
plan: string | null;
credentialsJson: string;
oauthAccountJson: string;
refreshExpiresAt: Date | null;
} | null> {
const creds = await readJson(credentialsPath());
const config = await readJson(configPath());
const oauth = creds?.claudeAiOauth as Record<string, unknown> | undefined;
const account = config?.oauthAccount as Record<string, unknown> | undefined;
if (!oauth || !account) return null;
const accountUuid = typeof account.accountUuid === 'string' ? account.accountUuid : '';
const emailAccount = typeof account.emailAddress === 'string' ? account.emailAddress : '';
if (!accountUuid || !emailAccount) return null;
const sub = typeof oauth.subscriptionType === 'string' ? oauth.subscriptionType : '';
const refreshMs = typeof oauth.refreshTokenExpiresAt === 'number' ? oauth.refreshTokenExpiresAt : null;
return {
accountUuid,
emailAccount,
orgName: typeof account.organizationName === 'string' ? account.organizationName : null,
plan: sub ? sub[0]!.toUpperCase() + sub.slice(1) : null,
credentialsJson: JSON.stringify(oauth),
oauthAccountJson: JSON.stringify(account),
refreshExpiresAt: refreshMs ? new Date(refreshMs) : null,
};
}
/**
* Keep whatever the CLI is currently signed in as.
*
* Called after a `/login` here AND whenever the account list is read, which is what makes an account signed
* into from a TERMINAL switchable too. Without that second call this would only know about logins that
* happened to go through this dialog — a switcher that silently omits half your accounts.
*
* Never throws into a caller: failing to record a credential must not fail the request that noticed it.
*/
export async function captureCurrentAnthropicAccount(userId: number): Promise<void> {
try {
const now = await readCurrentAccount();
if (!now) return;
await saveAnthropicAccount({ userId, ...now });
} catch (err) {
console.warn('[claude-auth] could not record the current account:', err instanceof Error ? err.message : err);
}
}
// GET /claude-auth/accounts — who can be switched to, and who is current. No secrets.
claudeLoginRouter.get('/accounts', async (ctx) => {
const user = ctx.get('user');
if (user.id !== 1) return ctx.json({ error: 'Anthropic accounts belong to the owner of this machine' }, 403);
// Record before listing, so an account signed into elsewhere appears the first time the panel is opened
// rather than only after the next login through this dialog.
await captureCurrentAnthropicAccount(user.id);
await purgeExpiredAnthropicAccounts(user.id).catch(() => 0);
const current = await readCurrentAccount();
const accounts = (await listAnthropicAccounts(user.id)).map((a) => ({
...a,
current: a.accountUuid === current?.accountUuid,
}));
return ctx.json({ accounts });
});
// POST /claude-auth/switch — become one of them.
claudeLoginRouter.post('/switch', async (ctx) => {
const user = ctx.get('user');
if (user.id !== 1) return ctx.json({ error: 'Anthropic accounts belong to the owner of this machine' }, 403);
const body = await ctx.req.json().catch(() => ({}) as Record<string, unknown>);
const accountUuid = typeof body.accountUuid === 'string' ? body.accountUuid : '';
if (!accountUuid) return ctx.json({ error: 'no account' }, 400);
// Keep the outgoing credential before overwriting it, or switching away from an account signed into
// outside this UI loses it — the one-way trip this feature exists to remove.
await captureCurrentAnthropicAccount(user.id);
const target = await revealAnthropicAccount(user.id, accountUuid);
if (!target) return ctx.json({ error: 'that sign-in has expired — run /login for it again' }, 409);
try {
// `.credentials.json` is replaced wholesale: it holds this one key and nothing else of ours.
// `.claude.json` is PATCHED — it is the CLI's whole config, and overwriting it would take out every
// project entry, tip counter and preference in it.
const existingCreds = (await readJson(credentialsPath())) ?? {};
await writeFile(
credentialsPath(),
JSON.stringify({ ...existingCreds, claudeAiOauth: JSON.parse(target.credentialsJson) }),
{
mode: 0o600,
},
);
const config = (await readJson(configPath())) ?? {};
await writeFile(
configPath(),
JSON.stringify({ ...config, oauthAccount: JSON.parse(target.oauthAccountJson) }, null, 2),
{
mode: 0o600,
},
);
} catch (err) {
return ctx.json({ error: `could not write the credential: ${err instanceof Error ? err.message : err}` }, 500);
}
await markAnthropicAccountUsed(user.id, accountUuid);
// A turn already running keeps the credential it was spawned with. Said here rather than guessed at in
// the UI, because "it did not take effect" and "it takes effect next turn" look identical from outside.
return ctx.json({ ok: true, email: target.emailAccount, appliesTo: 'the next turn, and every new claude' });
});
+2
View File
@@ -41,6 +41,7 @@ import { queueRouter } from './api/queue/queue';
// registry's claim on '/browser' in the same commit, or assertPermissionTotality refuses to boot.
import { bugReportRouter } from './api/bug-report/bug-report';
import { agentStatusRouter } from './api/agent-status/router';
import { claudeLoginRouter } from './api/auth/claude-login';
import { chatRouter } from './api/chat/chat';
import { commandHqRouter } from './api/command-hq/router';
import { pipelineJobsRouter } from './api/tasks/pipeline-jobs-routes';
@@ -149,6 +150,7 @@ const PROTECTED_MOUNTS: [prefix: string, router: ReturnType<typeof createRouter>
['/queue', queueRouter],
['/bug-report', bugReportRouter],
['/agent-status', agentStatusRouter],
['/claude-auth', claudeLoginRouter],
['/chat', chatRouter],
['/command-hq', commandHqRouter],
['/pipeline-jobs', pipelineJobsRouter],
+11 -1
View File
@@ -278,7 +278,17 @@ const CORE_REGISTRY: Permission[] = [
// non-owners wholesale — so an endpoint there could not be read by the accounts that need it. It reports
// two booleans about the caller's own home and nothing about anyone else, which is what lets a member be
// told "run `claude` once to sign in" instead of being shown a tile that 403s.
api: ['/chat', '/agent-status'],
// `/claude-auth` is the `/login` flow — it spawns `claude auth login` AS THE CALLER and writes the
// credential into the caller's own home. It rides this permission rather than earning its own for the
// reason that makes the pairing exact: an account that may use the agent must be able to authenticate
// the agent, and an account that may not has nothing to authenticate. `confined` then carries the rest
// for free — `authorize.ts` drops a confined grant for an account with no `osUser`, which is precisely
// the account that cannot hold credentials of its own.
//
// NOT under `/auth`, which `totality.ts` exempts from the gate entirely because it is where a caller
// goes to BECOME authenticated. Mounted there, an unauthenticated request could start a login as the
// owner.
api: ['/chat', '/agent-status', '/claude-auth'],
// 'chat-notify' is a session-independent socket (the bell feature): it carries no sessionId and no
// transcript, just a ping that a chat this account can already reach has finished a turn or needs
// input. Riding this permission rather than a new one, because it is meaningless without `chat`
+6 -3
View File
@@ -7,9 +7,12 @@
* is not localhost the whole object is undefined, so `navigator.clipboard.writeText`
* throws `TypeError: Cannot read properties of undefined`.
*
* Officer is reached over the tailnet — `http://officer-dev:9000` — which is
* neither https nor localhost. Twenty call sites were affected, in three states
* that all looked fine in review:
* Officer is reachable over plain http — `http://officer-dev:9000` on the tailnet —
* which is neither https nor localhost. The owner's own browsing is over https
* (`platform.officer.dev`) and was already https when this was written; the http
* origins are the other ways in, and an install with no certificate has no other
* way in at all. Twenty call sites were affected, in three states that all looked
* fine in review:
*
* bare `navigator.clipboard.writeText(x)` threw, killing the handler
* `navigator.clipboard?.writeText(x)` did nothing, silently
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'bun:test';
/*
The rule, as a pure function, because the interesting part is not the DOM.
History must never clobber text the person typed, and must still be walkable once a recalled prompt is
sitting in the box. Those two pull in opposite directions and the second is what a naive "only when empty"
check breaks.
*/
const mayRecall = (value: string, recalling: boolean): boolean => value.trim() === '' || recalling;
describe('when history may take the arrow key', () => {
it('takes it on an empty composer', () => {
expect(mayRecall('', false)).toBe(true);
});
// The bug this rule exists for: the caret is on the first line of a one-line draft, so every position
// test passes, and Up used to replace what you were writing.
it('leaves a draft alone', () => {
expect(mayRecall('half a thought', false)).toBe(false);
});
// Without this, you could step back exactly once and then be refused, because the recalled prompt is
// itself text in the box.
it('keeps walking once it is in the history', () => {
expect(mayRecall('a prompt from earlier', true)).toBe(true);
});
it('treats whitespace as empty', () => {
expect(mayRecall(' ', false)).toBe(true);
expect(mayRecall('\n\t ', false)).toBe(true);
});
it('protects a draft that merely starts with whitespace', () => {
expect(mayRecall(' still typing', false)).toBe(false);
});
});
@@ -51,6 +51,15 @@ type UseEmbeddableChatParams = {
contextId?: string;
};
/**
* Keys that are held rather than typed.
*
* A modifier press is not editing, and ending a recall on one would break the ordinary act of holding Shift
* between two presses of Up. `Dead` is a pending accent on layouts that compose — also not a keystroke you
* have finished making.
*/
const MODIFIER_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock', 'Dead']);
export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComplete?: () => void) {
const { initialMessage, defaultInput = '', promptPrefix, cwd, autoSend = false, chat: externalChat } = params;
@@ -389,15 +398,26 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
/**
* Recall a previous prompt, the way a shell does.
*
* Up only reaches the history from the FIRST line and Down only from the last, so in a multi-line draft
* the arrows still move the caret — there is a line above to go to, and taking the key would strand the
* cursor. On the first line there is nowhere up to go, which is exactly when history is what you meant.
* `step` returning null (empty history, or already at the oldest) leaves the key alone too.
* **Only from an empty composer.** The caret-on-the-first-line test below is not enough on its own, and
* that was the bug: with a one-line draft you were typing, the caret IS on the first line, so Up replaced
* what you had written with something from last week. A draft is not recoverable from anywhere, and
* history is — so the tie goes to the draft, always.
*
* `isRecalling` is what keeps the rule from making the feature useless. Once a recalled prompt is in the
* box the box is not empty, so a plain emptiness test would allow one step back and then refuse; walking
* further is still allowed because that text came from the history rather than from you.
*
* Whitespace counts as empty. A composer holding two spaces is not a draft anybody would miss, and an
* invisible character silently blocking Up is worse than losing it.
*
* The line tests stay on top of that: in a recalled prompt spanning three lines, Up from the middle moves
* the caret, because there is a line above to go to and taking the key would strand the cursor.
*/
const recallPrompt = (ev: KeyboardEvent<HTMLTextAreaElement>, direction: -1 | 1): void => {
const field = ev.currentTarget;
const { selectionStart, selectionEnd, value } = field;
if (selectionStart !== selectionEnd) return; // a selection means the arrows are extending/collapsing it
if (value.trim() !== '' && !history.isRecalling()) return;
const onEdge =
direction === -1 ? !value.slice(0, selectionStart).includes('\n') : !value.slice(selectionEnd).includes('\n');
if (!onEdge) return;
@@ -429,6 +449,18 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
}
if (ev.key === 'ArrowUp') return recallPrompt(ev, -1);
if (ev.key === 'ArrowDown') return recallPrompt(ev, 1);
/*
Anything else ends the recall, and Left/Right are the ones that matter.
A recalled prompt you have started moving around in is a prompt you have adopted — so the next Up must
not walk further back over it, for the same reason Up does not overwrite a fresh draft. Arrowing left
into the middle of it is the clearest signal of intent there is, and it types nothing, so a rule based
on the text changing would miss it entirely.
Bare modifiers are excluded: holding Shift to capitalise the next letter is not editing, and treating
it as such would end the recall between two presses of Up.
*/
if (!MODIFIER_KEYS.has(ev.key)) history.leave();
if (ev.key === 'Enter' && !ev.shiftKey) {
ev.preventDefault();
// Enter on an EMPTY composer with something queued means "don't wait — say it now". The keystroke
@@ -103,3 +103,44 @@ describe('what gets remembered', () => {
expect(current.step(-1, '')).toBe('three');
});
});
describe('leaving the history', () => {
// The rule this exists for: a recalled prompt you have started editing — or merely arrowed into — is
// yours now, and the next Up must not walk further back over it.
test('a keystroke after a recall ends it, so Up no longer walks', () => {
const { current } = mount();
current.record('one');
current.record('two');
expect(current.step(-1, '')).toBe('two');
expect(current.isRecalling()).toBe(true);
current.leave();
expect(current.isRecalling()).toBe(false);
});
// The composer's guard is "empty, or recalling". Once we have left, a non-empty box is a draft — so the
// two halves together are what stop Up from eating an edited prompt.
test('after leaving, the recalled text is treated as a draft', () => {
const { current } = mount();
current.record('one');
current.step(-1, '');
current.leave();
const mayRecall = (value: string) => value.trim() === '' || current.isRecalling();
expect(mayRecall('one, edited')).toBe(false);
expect(mayRecall('')).toBe(true);
});
// The stashed draft goes with it. Handing an older draft back after you have started editing something
// else would paste the wrong text over the right one.
test('the stashed draft does not come back later', () => {
const { current } = mount();
current.record('sent');
expect(current.step(-1, 'a draft')).toBe('sent');
current.leave();
// Entering again stashes what is there NOW, and Down returns that rather than the abandoned draft.
expect(current.step(-1, 'something else')).toBe('sent');
expect(current.step(1, 'sent')).toBe('something else');
});
});
@@ -25,6 +25,26 @@ export type PromptHistory = {
* whatever it normally does.
*/
step: (direction: -1 | 1, current: string) => string | null;
/**
* Are we currently walking the history, rather than composing?
*
* The caller needs this to answer "may Up take this key" without also making the history unusable. The
* rule is that history must never clobber text the PERSON typed — but once a recalled prompt is in the
* box, the box is not empty any more, and a plain emptiness test would allow exactly one step back and
* then refuse. This is the difference between "there is text" and "there is text of yours".
*/
isRecalling: () => boolean;
/**
* Stop being in the history — the recalled text is now the person's own draft.
*
* Called on any keystroke that is not Up or Down. The moment you edit a recalled prompt, or even just
* move the caret through it with Left, you have adopted it: pressing Up after that would throw away
* something you were working on, which is the same fault as recalling over a fresh draft.
*
* The stashed draft goes too. It belonged to the composing session this leaves, and handing it back after
* you have started editing something else would paste an older draft over a newer one.
*/
leave: () => void;
};
export function usePromptHistory(): PromptHistory {
@@ -87,5 +107,12 @@ export function usePromptHistory(): PromptHistory {
return list[at - 1] ?? null;
}, []);
return { record, step };
const isRecalling = useCallback(() => indexRef.current !== null, []);
const leave = useCallback(() => {
indexRef.current = null;
draftRef.current = '';
}, []);
return { record, step, isRecalling, leave };
}
@@ -0,0 +1,182 @@
import { useEffect, useRef, useState } from 'react';
import { Check, Copy, ExternalLink, Loader2 } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { useClient } from 'hooks/useClient';
import { errorText } from 'helpers/error-text';
import { copyToClipboard } from 'helpers/clipboard';
/*
`/login` — the same flow as the terminal, without the terminal.
Officer authenticates `claude` with a Claude SUBSCRIPTION over OAuth, which is a browser round trip:
open a URL, authorise, paste a code back. Until now the only way to do that here was to open a terminal
panel and drive the CLI by hand — which works, and is a strange thing to ask of somebody whose whole
session is a browser.
Three states, and the middle one is the person leaving to do something else:
starting → waiting for the code → done
*/
type Started = { id: string; url: string; expiresInMs: number };
type Props = { open: boolean; onOpenChange: (open: boolean) => void };
export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
const client = useClient();
const [started, setStarted] = useState<Started | null>(null);
const [code, setCode] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
const [copied, setCopied] = useState(false);
const codeRef = useRef<HTMLInputElement>(null);
// Start on open, and abandon on close. The server holds a live `claude` process per pending login, so a
// dialog dismissed without telling it leaves that process parked on a read of stdin.
useEffect(() => {
if (!open) return;
let cancelled = false;
setStarted(null);
setCode('');
setError(null);
setDone(false);
void (async () => {
try {
const res = await client.post<Started>('/claude-auth', {});
if (!cancelled) setStarted(res);
} catch (err) {
if (!cancelled) setError(errorText(err, 'could not start the login'));
}
})();
return () => {
cancelled = true;
};
}, [open]);
useEffect(() => {
if (!open && started) void client.delete(`/claude-auth/${started.id}`).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
const submit = async () => {
if (!started || !code.trim() || busy) return;
setBusy(true);
setError(null);
try {
await client.post(`/claude-auth/${started.id}/code`, { code: code.trim() });
setDone(true);
} catch (err) {
setError(errorText(err, 'that code was not accepted'));
} finally {
setBusy(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Sign in to Claude</DialogTitle>
</DialogHeader>
{done ? (
<div className="flex items-center gap-2 py-4 text-sm">
<Check className="h-4 w-4 text-success" />
Signed in. New turns use these credentials — no restart needed.
</div>
) : !started ? (
<div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
{error ? (
<span className="text-destructive">{error}</span>
) : (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Asking the CLI for a sign-in link…
</>
)}
</div>
) : (
<div className="space-y-4 py-2">
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
Open this, authorise, and paste back the code it gives you.
</p>
<div className="flex items-center gap-2">
{/* A real anchor. Every link here opens a new tab, which is what you want for this one —
losing the dialog would lose the process holding the other half of the exchange. */}
<a
href={started.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
>
Authorise in claude.com
<ExternalLink className="h-3.5 w-3.5" />
</a>
{/* Copy, because opening here opens in THIS browser — already signed into whichever account
you are trying to move away from. Pasting into another profile or another device is how
you sign in as somebody else, and this dialog exists precisely for the machine with two
Anthropic accounts on it. */}
<button
type="button"
onClick={() => {
void copyToClipboard(started.url).then((ok) => {
setCopied(ok);
if (ok) setTimeout(() => setCopied(false), 2_000);
else setError('could not reach the clipboard — select the link and copy it by hand');
});
}}
title="Copy the link, to open it in another browser or on your phone"
className="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium hover:bg-foreground/5"
>
{copied ? <Check className="h-3.5 w-3.5 text-success" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? 'Copied' : 'Copy link'}
</button>
</div>
</div>
<div className="space-y-1">
<Input
ref={codeRef}
autoFocus
value={code}
onChange={(ev) => setCode(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') void submit();
}}
placeholder="paste the code"
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
Expires in {Math.round(started.expiresInMs / 60_000)} minutes. Run <code>/login</code> again if it does.
</p>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => onOpenChange(false)}
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
>
Cancel
</button>
<button
type="button"
disabled={!code.trim() || busy}
onClick={() => void submit()}
className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground disabled:opacity-40"
>
{busy && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
Sign in
</button>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
};
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
import type { QueuedPrompt, UseEmbeddableChatType } from '../EmbeddableChat/useEmbeddableChat';
import { ModelSelector } from './ModelSelector';
import { BackgroundJobDialog } from './BackgroundJobDialog';
import { ClaudeLoginDialog } from './ClaudeLoginDialog';
import { armBackgroundJob } from '../../../background-job-origin';
import { useWorkspace } from '../../../components/Workspace';
import { AttachmentList } from './AttachmentList';
@@ -17,6 +18,7 @@ type InputAreaProps = {
};
export const InputArea = ({ manager }: InputAreaProps) => {
const [loginOpen, setLoginOpen] = useState(false);
const {
input,
setInput,
@@ -105,7 +107,17 @@ export const InputArea = ({ manager }: InputAreaProps) => {
ref={textareaRef}
value={input}
onChange={(ev) => setInput(ev.target.value)}
onKeyDown={handleKeyDown}
onKeyDown={(ev) => {
// Enter is how anybody actually sends, so the interception has to be here as well as on the
// button. Shift-Enter is a newline and stays one.
if (ev.key === 'Enter' && !ev.shiftKey && input.trim() === '/login') {
ev.preventDefault();
setInput('');
setLoginOpen(true);
return;
}
handleKeyDown(ev);
}}
onPaste={(ev) => {
// Let an image paste fall through to the browser's default (nothing) rather than attaching
// something this model will discard.
@@ -141,7 +153,16 @@ export const InputArea = ({ manager }: InputAreaProps) => {
</Button>
)}
<Button
onClick={handleSend}
onClick={() => {
// `/login` never reaches the agent. It is not a prompt — it is the browser-only user doing the
// one thing that used to need a terminal panel, so the composer answers it locally.
if (input.trim() === '/login') {
setInput('');
setLoginOpen(true);
return;
}
handleSend();
}}
disabled={!input.trim() || !isConnected}
size="icon"
title={isGenerating ? 'Queue — sends when the current turn finishes' : 'Send'}
@@ -169,6 +190,7 @@ export const InputArea = ({ manager }: InputAreaProps) => {
/>
<WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
<ClaudeLoginDialog open={loginOpen} onOpenChange={setLoginOpen} />
<BackgroundJobDialog
open={jobDialogOpen}
onOpenChange={setJobDialogOpen}
@@ -1,8 +1,11 @@
import { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { usePermissions } from 'hooks/usePermissions';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import type { UsageAccount } from '../useClaudeUsage';
import { currentLimit, formatAge, formatReset, formatTokens, useClaudeUsage, weekdayLabel } from '../useClaudeUsage';
import type { SwitchableAccount } from '../useAnthropicAccounts';
import { useAnthropicAccounts } from '../useAnthropicAccounts';
/**
* Claude Code's own usage display, on the harness pill — for every account, not just the one signed in.
@@ -129,38 +132,70 @@ const Models = ({ account }: { account: UsageAccount }) => {
};
/**
* One chip per account, and only when there is more than one.
* A chip per Anthropic account this machine has seen, and clicking one does one of two things.
*
* A tab strip over a single account is chrome that never does anything — the same reasoning that collapses
* the provider strip to a label when there is one harness on offer.
* switch — a stored credential exists, so the click BECOMES that account
* view — only a stored reading exists, so the click shows its cached numbers and nothing changes
*
* The second kind is not a degraded version of the first, it is a different question. An account whose
* sign-in is gone still has percentages and a reset time worth reading — "has my other allowance come back
* yet" is answerable from a snapshot, because a reset time is an absolute instant and `currentLimit` rolls a
* past window forward rather than showing a stale one. What you cannot do is act on the answer without a
* `/login`, and that is what the chip has to make obvious BEFORE it is clicked.
*
* So: filled for the account you are, plain for one you can become, and dashed-outline for one you can only
* look at.
*/
const AccountTabs = ({
accounts,
selected,
onSelect,
type Chip = {
key: string;
email: string;
title: string;
kind: 'current' | 'switch' | 'view';
busy: boolean;
};
const AccountChips = ({
chips,
isSwitching,
viewing,
onClick,
}: {
accounts: UsageAccount[];
selected: string;
onSelect: (email: string) => void;
chips: Chip[];
isSwitching: boolean;
viewing: string | null;
onClick: (chip: Chip) => void;
}) => (
<div className="flex items-center gap-1 overflow-x-auto border-t border-border px-2 py-2">
{accounts.map((account) => (
<button
key={account.email}
type="button"
onClick={() => onSelect(account.email)}
// The local part alone: the domain is what the accounts have in common and the part before it is
// what tells them apart, and the panel is 320px wide.
title={account.email}
className={`shrink-0 cursor-pointer rounded-md px-2 py-1 text-xs font-medium transition-colors ${
account.email === selected
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-foreground/10 hover:text-foreground'
}`}
>
{account.email.split('@')[0]}
</button>
))}
<div className="flex flex-wrap items-center gap-1 border-t border-border px-2 py-2">
{chips.map((chip) => {
const looking = chip.kind === 'view' && viewing === chip.email;
return (
<button
key={chip.key}
type="button"
disabled={isSwitching || (chip.kind === 'current' && !viewing)}
onClick={() => onClick(chip)}
title={chip.title}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
chip.kind === 'current' && !viewing
? 'bg-primary text-primary-foreground'
: chip.kind === 'view'
? // Dashed and unfilled: the border says "not a real destination" without greying it out to
// the point of looking broken, because it IS clickable — just not for switching.
`cursor-pointer border border-dashed border-border ${
looking ? 'bg-foreground/10 text-foreground' : 'text-muted-foreground/70 hover:text-foreground'
}`
: 'cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground'
} ${isSwitching && !chip.busy ? 'opacity-50' : ''}`}
>
{chip.busy && <Loader2 className="h-3 w-3 animate-spin" />}
{/* The WHOLE address. This showed the local part alone, on the reasoning that the domain is what
accounts have in common — which is wrong for the case that matters: a personal and a work
account belonging to one person share the local part, so the domain is the half that tells
them apart. Two chips read as the same account. */}
{chip.email}
</button>
);
})}
</div>
);
@@ -193,7 +228,8 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
const enabled = isOwner && harness === 'claude-code';
const [open, setOpen] = useState(false);
const { accounts, isLoading, isRefreshing, isError } = useClaudeUsage(enabled && open);
const { accounts: switchable, isSwitching, switchingTo, switchTo, switchError } = useAnthropicAccounts(open);
const { accounts, allAccounts, isLoading, isRefreshing, isError } = useClaudeUsage(enabled && open);
/*
Which tab is showing.
@@ -204,8 +240,57 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
Null means "whichever is current", resolved at render — so the panel opens on the signed-in account
without needing an effect to select it once the data arrives.
*/
const [selectedEmail, setSelectedEmail] = useState<string | null>(null);
const selected = accounts.find((a) => a.email === selectedEmail) ?? accounts[0] ?? null;
/*
No selection state any more, deliberately.
The panel shows the account you are SIGNED INTO, full stop, and `useClaudeUsage` already filters to it.
A selection would let the numbers and the chips disagree — reading one account's percentages with
another's chip lit — and that disagreement is the exact confusion this whole change set out to remove.
*/
/*
`viewing` is null for the normal case — the account you are signed into — and an email when you have
clicked an archived chip to read its cached numbers.
It is deliberately NOT a general tab selection. Only accounts with no stored credential can be viewed,
because for any other one the useful gesture is switching to it and then reading live numbers. That keeps
the invariant that made phase one worth doing: if the panel shows a signed-in account, it is THE
signed-in account.
*/
const [viewing, setViewing] = useState<string | null>(null);
useEffect(() => {
if (!open) setViewing(null);
}, [open]);
const selected = (viewing ? allAccounts.find((a) => a.email === viewing) : accounts[0]) ?? accounts[0] ?? null;
/*
Accounts with a stored READING but no stored credential — an old login that can be looked at and not
returned to. That is what is worth saying out loud.
This counted `allAccounts - accounts` until the switcher existed, which then double-counted: it called
an account "not shown" while its chip sat two lines below.
*/
const archived = allAccounts.filter((a) => !switchable.some((s) => s.emailAccount === a.email));
const chips: Chip[] = [
...switchable.map((a) => ({
key: a.accountUuid,
email: a.emailAccount,
kind: (a.current ? 'current' : 'switch') as Chip['kind'],
busy: isSwitching && switchingTo === a.accountUuid,
title: a.current
? `${a.emailAccount} — signed in`
: a.usable
? `Switch to ${a.emailAccount}${a.orgName ? ` (${a.orgName})` : ''}`
: `${a.emailAccount} — sign-in expired, run /login`,
})),
...archived.map((a) => ({
key: `archived:${a.email}`,
email: a.email,
kind: 'view' as Chip['kind'],
busy: false,
title: `${a.email} — no saved sign-in. Click to read its last known usage; run /login to use it.`,
})),
];
// The countdowns tick. Only while the panel is open — an interval running behind a closed popover is a
// re-render a minute for something nobody is looking at.
@@ -234,13 +319,32 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
{/* Plan and account in the accent rather than muted: this line is the ANSWER to "whose numbers am
I looking at", which matters more the moment there is a second tab, and it was reading as
chrome under the title. */}
<div className="truncate text-xs text-primary">
{selected
? [selected.plan, selected.email].filter(Boolean).join(' · ')
: isLoading
? 'Reading usage…'
: 'Not signed in'}
</div>
{/* The email on its own line, unshared. It is the answer to the only question this panel is
being opened to settle — which account am I spending — and joined with the plan it was the
half that got truncated first in a 320px popover. */}
{selected ? (
<>
<div className="truncate text-xs font-medium text-primary" title={selected.email}>
{selected.email}
</div>
{selected.plan && <div className="text-[11px] text-muted-foreground">{selected.plan}</div>}
</>
) : (
<div className="text-xs text-primary">{isLoading ? 'Reading usage…' : 'Not signed in'}</div>
)}
{/* Said out loud rather than silently dropped. Phase one shows one account; knowing another
exists and is hidden on purpose is different from wondering where it went. */}
{switchError && (
<div className="mt-1 text-[11px] text-destructive">
{switchError instanceof Error ? switchError.message : 'could not switch account'}
</div>
)}
{/* Said on the account being LOOKED AT rather than as a count somewhere else. The numbers below are
real and the reset times are still correct; the only thing missing is the ability to act on
them, and naming the fix beats naming the problem. */}
{viewing && (
<div className="mt-0.5 text-[10px] text-warning">No saved sign-in — run /login to use this one</div>
)}
{/*
How old this reading is, and it earns its line only when it is not live.
@@ -258,8 +362,24 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
)}
</div>
{accounts.length > 1 && (
<AccountTabs accounts={accounts} selected={selected?.email ?? ''} onSelect={setSelectedEmail} />
{chips.length > 1 && (
<AccountChips
chips={chips}
isSwitching={isSwitching}
viewing={viewing}
onClick={(chip) => {
if (chip.kind === 'view') {
setViewing(chip.email);
return;
}
// Switching returns the panel to live numbers: the account you just became is the one whose
// percentages you want, and leaving an archived snapshot on screen after a switch is how the
// numbers and the chips end up disagreeing.
setViewing(null);
const target = switchable.find((a) => a.emailAccount === chip.email);
if (target && !target.current) void switchTo(target.accountUuid).catch(() => {});
}}
/>
)}
{/* A failure never blanks the panel: the two halves are independent server-side, so a limits problem
@@ -0,0 +1,65 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
/*
The Anthropic accounts this machine's owner can BE, and the act of becoming one.
Separate from `useClaudeUsage`, which answers a different question. Usage is a measurement — how much room
is left — and is only readable for the account currently signed in. This is identity: who could I be, and
make me them. They meet in the popover, where the numbers belong to whichever pill is lit.
*/
export type SwitchableAccount = {
accountUuid: string;
emailAccount: string;
orgName: string | null;
plan: string | null;
capturedAt: string;
lastUsedAt: string | null;
refreshExpiresAt: string | null;
/** False once the refresh token has expired: the pill has to offer `/login` rather than a switch. */
usable: boolean;
current: boolean;
};
const KEY = ['anthropic', 'accounts'];
export function useAnthropicAccounts(enabled: boolean) {
const client = useClient();
const queryClient = useQueryClient();
const query = useQuery<{ accounts: SwitchableAccount[] }>({
queryKey: KEY,
queryFn: () => client.get<{ accounts: SwitchableAccount[] }>('/claude-auth/accounts'),
enabled,
// Cheap — two small file reads and one SELECT — and it must not be stale, because a switch made from a
// terminal is exactly the case this is here to notice.
staleTime: 0,
refetchOnWindowFocus: false,
// A member gets a 403 here by design. Retrying it just spends three round trips arriving at the same no.
retry: false,
});
const switchTo = useMutation({
mutationFn: (accountUuid: string) =>
client.post<{ ok: boolean; email: string; appliesTo: string }>('/claude-auth/switch', { accountUuid }),
onSuccess: async () => {
// Both, and in this order. The account list decides which pill is lit; the usage numbers belong to
// whichever account that is, and showing the old account's percentages under the new name is the one
// outcome that would make somebody distrust the whole panel.
await queryClient.invalidateQueries({ queryKey: KEY });
await queryClient.invalidateQueries({ queryKey: ['chat', 'usage'] });
await queryClient.invalidateQueries({ queryKey: ['chat', 'usage', 'fresh'] });
},
});
return {
accounts: query.data?.accounts ?? [],
isLoading: query.isLoading,
/** True while a switch is in flight — the panel must say so, or nobody believes it happened. */
isSwitching: switchTo.isPending,
switchingTo: switchTo.variables ?? null,
switchError: switchTo.error,
switchTo: switchTo.mutateAsync,
};
}
@@ -76,8 +76,27 @@ export function useClaudeUsage(enabled: boolean) {
if (fresh.data) queryClient.setQueryData(KEY, fresh.data);
}, [fresh.data, queryClient]);
const all = fresh.data?.accounts ?? cached.data?.accounts ?? [];
/*
PHASE ONE — the signed-in account only. Temporary, and a one-line revert: delete `onlyCurrent` and
return `all`.
The stored snapshots for other accounts are a real feature and the server still serves them. The problem
is what they do to the question being asked. This machine's owner has a personal account and a
work one, both appear as tabs, and after signing in you cannot tell from a glance WHICH tab you are
now spending — the panel answers "here is usage for two accounts" when the question is "whose am I on".
So while that is the question, show one account and let its absence mean something: no row marked
`current` renders as "Not signed in", which is a true and useful answer rather than a tab that happens
to be selected.
*/
const onlyCurrent = all.filter((a) => a.current);
return {
accounts: fresh.data?.accounts ?? cached.data?.accounts ?? [],
accounts: onlyCurrent,
/** Every account the server knows about — kept so a caller can say how many are being hidden. */
allAccounts: all,
/** Only true when there is nothing at all to show. A stale list is not a loading state. */
isLoading: cached.isLoading && fresh.isLoading,
/** A refresh in flight over an already-painted panel. Worth a hint, never a spinner over the numbers. */
@@ -33,10 +33,7 @@ export const Breadcrumb = ({ path, onNavigate, basePath = '/', hrefForPath }: Br
return (
<div className="shrink-0 border-b border-duck-dark/10 px-4 py-2">
<nav className="flex items-center gap-1 text-sm flex-wrap">
<Crumb
to={basePath}
className="flex items-center gap-1 text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
<Crumb to={basePath} className="flex items-center gap-1 text-duck-teal cursor-pointer font-medium">
<Home className="h-4 w-4" />
<span>{basePath === '/' ? 'home' : basePath.split('/').pop()}</span>
</Crumb>
@@ -52,10 +49,7 @@ export const Breadcrumb = ({ path, onNavigate, basePath = '/', hrefForPath }: Br
{isLast ? (
<span className="text-duck-dark font-semibold">{segment}</span>
) : (
<Crumb
to={segmentPath}
className="text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
<Crumb to={segmentPath} className="text-duck-teal cursor-pointer font-medium">
{segment}
</Crumb>
)}
+204
View File
@@ -41,4 +41,208 @@
@import './themes/vantablack.css';
@import './themes/white.css';
/* Community themes — ported from omarchythemes.com by scripts/port-community-themes.ts. Do not edit this block. */
@import './themes/aamis.css';
@import './themes/ado.css';
@import './themes/adrift.css';
@import './themes/aetheria.css';
@import './themes/akaito.css';
@import './themes/all-hallows-eve.css';
@import './themes/amberbyte.css';
@import './themes/amekoji.css';
@import './themes/andromeda.css';
@import './themes/anonymous.css';
@import './themes/apocalypse.css';
@import './themes/arc-blueberry.css';
@import './themes/archriot.css';
@import './themes/archwave.css';
@import './themes/artzen.css';
@import './themes/ash.css';
@import './themes/astrodark.css';
@import './themes/atari.css';
@import './themes/atelier.css';
@import './themes/aura.css';
@import './themes/ayu-dark.css';
@import './themes/ayu-light.css';
@import './themes/ayu-mirage.css';
@import './themes/azure.css';
@import './themes/azure-glow.css';
@import './themes/bad-hand.css';
@import './themes/batman.css';
@import './themes/batou.css';
@import './themes/bauhaus.css';
@import './themes/beachvan.css';
@import './themes/beta.css';
@import './themes/biscuit-de-mar-dark.css';
@import './themes/black-arch.css';
@import './themes/black-gold.css';
@import './themes/black-money.css';
@import './themes/blackturq.css';
@import './themes/blackwall.css';
@import './themes/blueridge.css';
@import './themes/brutalism.css';
@import './themes/caroline-skyline.css';
@import './themes/catppuccin-mocha.css';
@import './themes/cattpuccin-glass.css';
@import './themes/city-783.css';
@import './themes/copper-night.css';
@import './themes/corporate.css';
@import './themes/cpunk.css';
@import './themes/crimson.css';
@import './themes/crimson-gold.css';
@import './themes/cyberpunk-2.css';
@import './themes/dark-xp.css';
@import './themes/dayfox.css';
@import './themes/dead-space.css';
@import './themes/deckard.css';
@import './themes/delete.css';
@import './themes/delorean.css';
@import './themes/demon.css';
@import './themes/desert-gleam.css';
@import './themes/desert-twilight.css';
@import './themes/dotrb.css';
@import './themes/drac.css';
@import './themes/dracula.css';
@import './themes/dragon.css';
@import './themes/dreamwave.css';
@import './themes/elysian.css';
@import './themes/ember-n-ash.css';
@import './themes/eva-unit-01.css';
@import './themes/event-horizon.css';
@import './themes/everblush.css';
@import './themes/evergarden.css';
@import './themes/farline.css';
@import './themes/felix.css';
@import './themes/fiery-ocean.css';
@import './themes/fireside.css';
@import './themes/firesky.css';
@import './themes/flat-dracula.css';
@import './themes/flexoki-dark.css';
@import './themes/florida-man.css';
@import './themes/fluid-glass.css';
@import './themes/forest-green.css';
@import './themes/frost.css';
@import './themes/futurism.css';
@import './themes/ghost-pastel.css';
@import './themes/glory-antic.css';
@import './themes/gold-rush.css';
@import './themes/gornergrat-station.css';
@import './themes/gotham-city.css';
@import './themes/greek-noir.css';
@import './themes/green-city.css';
@import './themes/green-garden.css';
@import './themes/gruber-tsoding.css';
@import './themes/grudark.css';
@import './themes/harbor.css';
@import './themes/harbor-dark.css';
@import './themes/hex.css';
@import './themes/himalaya.css';
@import './themes/hinterlands.css';
@import './themes/hollow-knight.css';
@import './themes/infernium.css';
@import './themes/infernium-dark.css';
@import './themes/inkypinky.css';
@import './themes/kanso.css';
@import './themes/killer-klowns.css';
@import './themes/kimiko.css';
@import './themes/koyanagi.css';
@import './themes/kurayami.css';
@import './themes/lain.css';
@import './themes/lairetam.css';
@import './themes/latch.css';
@import './themes/latch-dark.css';
@import './themes/lilly.css';
@import './themes/lowlight.css';
@import './themes/lunar.css';
@import './themes/manga.css';
@import './themes/mapquest.css';
@import './themes/mars.css';
@import './themes/mecha-no-onna.css';
@import './themes/memento-mori.css';
@import './themes/midnight.css';
@import './themes/miles-morales.css';
@import './themes/milkmatcha-light.css';
@import './themes/mocha.css';
@import './themes/monochrome.css';
@import './themes/monokai.css';
@import './themes/monokai-dark.css';
@import './themes/monolith.css';
@import './themes/monolith-2.css';
@import './themes/moodpeak.css';
@import './themes/moon-orbit.css';
@import './themes/nagai-poolside.css';
@import './themes/nagai-twilight.css';
@import './themes/nebulite.css';
@import './themes/neo-sploosh.css';
@import './themes/neon-street.css';
@import './themes/neovoid.css';
@import './themes/nes.css';
@import './themes/night-owl.css';
@import './themes/nyc.css';
@import './themes/one-dark-pro.css';
@import './themes/oxford.css';
@import './themes/oxocarbon-dark.css';
@import './themes/pandora.css';
@import './themes/phosphor-os.css';
@import './themes/pina.css';
@import './themes/pink-blood.css';
@import './themes/pmndrs.css';
@import './themes/pulsar.css';
@import './themes/purple-moon.css';
@import './themes/purplewave.css';
@import './themes/rainy-night.css';
@import './themes/rdddcs.css';
@import './themes/red-monarch.css';
@import './themes/retropc.css';
@import './themes/reverie.css';
@import './themes/robco.css';
@import './themes/robco-mojave.css';
@import './themes/rose-pine-dark.css';
@import './themes/roseofdune.css';
@import './themes/royal.css';
@import './themes/rustleaf.css';
@import './themes/saga.css';
@import './themes/sakura.css';
@import './themes/sapphire.css';
@import './themes/seashells.css';
@import './themes/serenity.css';
@import './themes/shadesofjade.css';
@import './themes/snow.css';
@import './themes/snow-black.css';
@import './themes/solarized.css';
@import './themes/solarized-light.css';
@import './themes/solarized-osaka.css';
@import './themes/space-bound.css';
@import './themes/space-monkey.css';
@import './themes/spark.css';
@import './themes/spectra.css';
@import './themes/stellar.css';
@import './themes/stillmoon.css';
@import './themes/sunkissed.css';
@import './themes/sunset.css';
@import './themes/synthetica.css';
@import './themes/synthwave-84.css';
@import './themes/taikami.css';
@import './themes/tarot.css';
@import './themes/temerald.css';
@import './themes/terramour.css';
@import './themes/thegreek.css';
@import './themes/turbonite.css';
@import './themes/tycho.css';
@import './themes/type-17.css';
@import './themes/unicorn-dream.css';
@import './themes/van-gogh.css';
@import './themes/velocity.css';
@import './themes/velvet-night.css';
@import './themes/vesper.css';
@import './themes/vhs80.css';
@import './themes/vice-city.css';
@import './themes/void.css';
@import './themes/vurple.css';
@import './themes/wasteland.css';
@import './themes/waveform-dark.css';
@import './themes/white-gold.css';
@import './themes/x-1632.css';
@import './themes/yuugure.css';
@import './chrome.css';
+97
View File
@@ -0,0 +1,97 @@
/*
Aamis — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/vyrx-dev/omarchy-aamis-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='aamis'] {
--background: #080808;
--foreground: #eadccc;
--card: #0f0f0f;
--card-foreground: #eadccc;
--popover: #0f0f0f;
--popover-foreground: #eadccc;
--muted: #202020;
--muted-foreground: #a89f93;
--accent: #202020;
--accent-foreground: #eadccc;
--primary: #eacea7;
--primary-foreground: #080808;
--secondary: #202020;
--secondary-foreground: #eadccc;
--cta: #eacea7;
--cta-foreground: #080808;
--ctahover: #edd4b2;
--success: #cea37f;
--success-foreground: #080808;
--warning: #f4bb54;
--warning-foreground: #080808;
--destructive: #e25d6c;
--destructive-foreground: #080808;
--info: #e2be8a;
--info-foreground: #080808;
--busy: #eb8c60;
--busy-foreground: #080808;
--busy-ink: #eb8c60;
--border: #3a3734;
--input: #3a3734;
--ring: #eacea7;
--radius: 0.625rem;
--chart-primary: #eacea7;
--chart-secondary: #ede4c8;
--status-success: #cea37f;
--status-warning: #f4bb54;
--status-in-progress: #e2be8a;
--page-bg: #080808;
--page-bg-src: '';
--panel-bg: rgb(15 15 15 / 0.72);
--panel-border: rgb(58 55 52 / 0.9);
--panel-border-bare: rgb(58 55 52 / 0.55);
--panel-raised-bg: rgb(15 15 15 / 0.95);
--panel-raised-border: rgb(58 55 52 / 1);
--panel-accent: #eacea7;
--panel-header-bg: rgb(32 32 32 / 0.6);
--panel-header-fg: #eadccc;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0f0f0f;
--glass-bg: rgb(15 15 15 / 0.55);
--glass-border: rgb(58 55 52 / 0.7);
--dock-bg: rgb(15 15 15 / 0.7);
--dock-border: rgb(58 55 52 / 0.8);
--dock-tooltip-bg: #202020;
--header-bg: rgb(15 15 15 / 0.8);
--header-border: rgb(58 55 52 / 0.9);
--header-fg: #eadccc;
--header-blur: 12px;
--pixel-grid-color: rgb(114 107 100 / 0.07);
--card-grid-color: rgb(114 107 100 / 0.05);
--panel-grid-color: rgb(114 107 100 / 0.06);
--scrollbar-thumb: rgb(112 106 106 / 0.6);
--scrollbar-thumb-hover: rgb(112 106 106 / 0.85);
--scrollbar-thin-thumb: rgb(112 106 106 / 0.45);
--scrollbar-thin-thumb-hover: rgb(112 106 106 / 0.7);
--brand: #eacea7;
--brand-foreground: #080808;
--brand-primary: #eacea7;
--brand-accent: #e8ab3b;
--brand-warm: #eb8c60;
--brand-deep: #080808;
--brand-muted: #706a6a;
--ink: #eadccc;
--surface-warm: #0f0f0f;
--naturegreen: #cea37f;
--naturegreen-foreground: #080808;
--duck-dark: #eadccc;
--duck-teal: #e8ab3b;
--duck-yellow: #f4bb54;
--duck-orange: #eb8c60;
--duck-forest: #cea37f;
--duck-beige: #252422;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Ado — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/errantProgrammer/omarchy-ado-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='ado'] {
--background: #070708;
--foreground: #dfdee6;
--card: #0d0d0f;
--card-foreground: #dfdee6;
--popover: #0d0d0f;
--popover-foreground: #dfdee6;
--muted: #1e1e20;
--muted-foreground: #a09fa6;
--accent: #1e1e20;
--accent-foreground: #dfdee6;
--primary: #a2e0ee;
--primary-foreground: #070708;
--secondary: #1e1e20;
--secondary-foreground: #dfdee6;
--cta: #a2e0ee;
--cta-foreground: #070708;
--ctahover: #ade4f0;
--success: #014761;
--success-foreground: #dfdee6;
--warning: #8bb0c6;
--warning-foreground: #070708;
--destructive: #903034;
--destructive-foreground: #dfdee6;
--info: #4545f5;
--info-foreground: #dfdee6;
--busy: #8e707d;
--busy-foreground: #070708;
--busy-ink: #8e707d;
--border: #363639;
--input: #363639;
--ring: #a2e0ee;
--radius: 0.625rem;
--chart-primary: #a2e0ee;
--chart-secondary: #25516a;
--status-success: #014761;
--status-warning: #8bb0c6;
--status-in-progress: #4545f5;
--page-bg: #070708;
--page-bg-src: '';
--panel-bg: rgb(13 13 15 / 0.72);
--panel-border: rgb(54 54 57 / 0.9);
--panel-border-bare: rgb(54 54 57 / 0.55);
--panel-raised-bg: rgb(13 13 15 / 0.95);
--panel-raised-border: rgb(54 54 57 / 1);
--panel-accent: #a2e0ee;
--panel-header-bg: rgb(30 30 32 / 0.6);
--panel-header-fg: #dfdee6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0d0d0f;
--glass-bg: rgb(13 13 15 / 0.55);
--glass-border: rgb(54 54 57 / 0.7);
--dock-bg: rgb(13 13 15 / 0.7);
--dock-border: rgb(54 54 57 / 0.8);
--dock-tooltip-bg: #1e1e20;
--header-bg: rgb(13 13 15 / 0.8);
--header-border: rgb(54 54 57 / 0.9);
--header-fg: #dfdee6;
--header-blur: 12px;
--pixel-grid-color: rgb(107 107 112 / 0.07);
--card-grid-color: rgb(107 107 112 / 0.05);
--panel-grid-color: rgb(107 107 112 / 0.06);
--scrollbar-thumb: rgb(144 142 170 / 0.6);
--scrollbar-thumb-hover: rgb(144 142 170 / 0.85);
--scrollbar-thin-thumb: rgb(144 142 170 / 0.45);
--scrollbar-thin-thumb-hover: rgb(144 142 170 / 0.7);
--brand: #a2e0ee;
--brand-foreground: #070708;
--brand-primary: #a2e0ee;
--brand-accent: #45c1dd;
--brand-warm: #8e707d;
--brand-deep: #070708;
--brand-muted: #908eaa;
--ink: #dfdee6;
--surface-warm: #0d0d0f;
--naturegreen: #014761;
--naturegreen-foreground: #dfdee6;
--duck-dark: #dfdee6;
--duck-teal: #45c1dd;
--duck-yellow: #8bb0c6;
--duck-orange: #8e707d;
--duck-forest: #014761;
--duck-beige: #222225;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Adrift — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/jaredb1011/omarchy-adrift-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='adrift'] {
--background: #0e1715;
--foreground: #ddd6c2;
--card: #1a2927;
--card-foreground: #ddd6c2;
--popover: #1a2927;
--popover-foreground: #ddd6c2;
--muted: #2a3836;
--muted-foreground: #a3a294;
--accent: #2a3836;
--accent-foreground: #ddd6c2;
--primary: #94d0d0;
--primary-foreground: #0e1715;
--secondary: #2a3836;
--secondary-foreground: #ddd6c2;
--cta: #94d0d0;
--cta-foreground: #0e1715;
--ctahover: #a1d6d6;
--success: #66987b;
--success-foreground: #0e1715;
--warning: #bfa26b;
--warning-foreground: #0e1715;
--destructive: #cb645d;
--destructive-foreground: #0e1715;
--info: #a9bfbf;
--info-foreground: #0e1715;
--busy: #c58364;
--busy-foreground: #0e1715;
--busy-ink: #c58364;
--border: #404b46;
--input: #404b46;
--ring: #94d0d0;
--radius: 0.625rem;
--chart-primary: #94d0d0;
--chart-secondary: #9a5840;
--status-success: #66987b;
--status-warning: #bfa26b;
--status-in-progress: #a9bfbf;
--page-bg: #0e1715;
--page-bg-src: '';
--panel-bg: rgb(26 41 39 / 0.72);
--panel-border: rgb(64 75 70 / 0.9);
--panel-border-bare: rgb(64 75 70 / 0.55);
--panel-raised-bg: rgb(26 41 39 / 0.95);
--panel-raised-border: rgb(64 75 70 / 1);
--panel-accent: #94d0d0;
--panel-header-bg: rgb(42 56 54 / 0.6);
--panel-header-fg: #ddd6c2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a2927;
--glass-bg: rgb(26 41 39 / 0.55);
--glass-border: rgb(64 75 70 / 0.7);
--dock-bg: rgb(26 41 39 / 0.7);
--dock-border: rgb(64 75 70 / 0.8);
--dock-tooltip-bg: #2a3836;
--header-bg: rgb(26 41 39 / 0.8);
--header-border: rgb(64 75 70 / 0.9);
--header-fg: #ddd6c2;
--header-blur: 12px;
--pixel-grid-color: rgb(114 119 109 / 0.07);
--card-grid-color: rgb(114 119 109 / 0.05);
--panel-grid-color: rgb(114 119 109 / 0.06);
--scrollbar-thumb: rgb(181 181 181 / 0.6);
--scrollbar-thumb-hover: rgb(181 181 181 / 0.85);
--scrollbar-thin-thumb: rgb(181 181 181 / 0.45);
--scrollbar-thin-thumb-hover: rgb(181 181 181 / 0.7);
--brand: #94d0d0;
--brand-foreground: #0e1715;
--brand-primary: #94d0d0;
--brand-accent: #a9bfbf;
--brand-warm: #c58364;
--brand-deep: #0e1715;
--brand-muted: #b5b5b5;
--ink: #ddd6c2;
--surface-warm: #1a2927;
--naturegreen: #66987b;
--naturegreen-foreground: #0e1715;
--duck-dark: #ddd6c2;
--duck-teal: #a9bfbf;
--duck-yellow: #bfa26b;
--duck-orange: #c58364;
--duck-forest: #66987b;
--duck-beige: #2e3a37;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Aetheria — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/JJDizz1L/aetheria (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='aetheria'] {
--background: #080510;
--foreground: #14b9b5;
--card: #0e091d;
--card-foreground: #14b9b5;
--popover: #0e091d;
--popover-foreground: #14b9b5;
--muted: #1f1a2d;
--muted-foreground: #309396;
--accent: #1f1a2d;
--accent-foreground: #14b9b5;
--primary: #ffbe74;
--primary-foreground: #080510;
--secondary: #1f1a2d;
--secondary-foreground: #14b9b5;
--cta: #ffbe74;
--cta-foreground: #080510;
--ctahover: #ffc685;
--success: #e20342;
--success-foreground: #f9f9f9;
--warning: #7cd699;
--warning-foreground: #080510;
--destructive: #c8e967;
--destructive-foreground: #080510;
--info: #be3f50;
--info-foreground: #f3f3f3;
--busy: #a2e080;
--busy-foreground: #080510;
--busy-ink: #a2e080;
--border: #0f2b3b;
--input: #0f2b3b;
--ring: #ffbe74;
--radius: 0.625rem;
--chart-primary: #ffbe74;
--chart-secondary: #9147a8;
--status-success: #e20342;
--status-warning: #7cd699;
--status-in-progress: #be3f50;
--page-bg: #080510;
--page-bg-src: '';
--panel-bg: rgb(14 9 29 / 0.72);
--panel-border: rgb(15 43 59 / 0.9);
--panel-border-bare: rgb(15 43 59 / 0.55);
--panel-raised-bg: rgb(14 9 29 / 0.95);
--panel-raised-border: rgb(15 43 59 / 1);
--panel-accent: #ffbe74;
--panel-header-bg: rgb(31 26 45 / 0.6);
--panel-header-fg: #14b9b5;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0e091d;
--glass-bg: rgb(14 9 29 / 0.55);
--glass-border: rgb(15 43 59 / 0.7);
--dock-bg: rgb(14 9 29 / 0.7);
--dock-border: rgb(15 43 59 / 0.8);
--dock-tooltip-bg: #1f1a2d;
--header-bg: rgb(14 9 29 / 0.8);
--header-border: rgb(15 43 59 / 0.9);
--header-fg: #14b9b5;
--header-blur: 12px;
--pixel-grid-color: rgb(41 105 113 / 0.07);
--card-grid-color: rgb(41 105 113 / 0.05);
--panel-grid-color: rgb(41 105 113 / 0.06);
--scrollbar-thumb: rgb(197 50 83 / 0.6);
--scrollbar-thumb-hover: rgb(197 50 83 / 0.85);
--scrollbar-thin-thumb: rgb(197 50 83 / 0.45);
--scrollbar-thin-thumb-hover: rgb(197 50 83 / 0.7);
--brand: #ffbe74;
--brand-foreground: #080510;
--brand-primary: #ffbe74;
--brand-accent: #ff7f41;
--brand-warm: #a2e080;
--brand-deep: #080510;
--brand-muted: #c53253;
--ink: #14b9b5;
--surface-warm: #0e091d;
--naturegreen: #e20342;
--naturegreen-foreground: #f9f9f9;
--duck-dark: #14b9b5;
--duck-teal: #ff7f41;
--duck-yellow: #7cd699;
--duck-orange: #a2e080;
--duck-forest: #e20342;
--duck-beige: #0f1b2c;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Akaito — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Light theme (light), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/stannorbvb-cmd/akaito (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='akaito'] {
--background: #f3e4cb;
--foreground: #4d2e1a;
--card: #eadcc4;
--card-foreground: #4d2e1a;
--popover: #eadcc4;
--popover-foreground: #4d2e1a;
--muted: #e4d6bf;
--muted-foreground: #6f5845;
--accent: #e4d6bf;
--accent-foreground: #4d2e1a;
--primary: #a32f1a;
--primary-foreground: #dbcdb7;
--secondary: #e4d6bf;
--secondary-foreground: #4d2e1a;
--cta: #a32f1a;
--cta-foreground: #dbcdb7;
--ctahover: #8f2917;
--success: #a46d2d;
--success-foreground: #0b0a09;
--warning: #a8611f;
--warning-foreground: #faf8f4;
--destructive: #a4373c;
--destructive-foreground: #e0d5c2;
--info: #a32f1a;
--info-foreground: #dbcdb7;
--busy: #a64c2e;
--busy-foreground: #ece5d9;
--busy-ink: #a64c2e;
--border: #d8c7b0;
--input: #d3c2ac;
--ring: #a32f1a;
--radius: 0.625rem;
--chart-primary: #a32f1a;
--chart-secondary: #9c3521;
--status-success: #a46d2d;
--status-warning: #a8611f;
--status-in-progress: #a32f1a;
--page-bg: #f3e4cb;
--page-bg-src: '';
--panel-bg: rgb(234 220 196 / 0.72);
--panel-border: rgb(216 199 176 / 0.9);
--panel-border-bare: rgb(216 199 176 / 0.55);
--panel-raised-bg: rgb(234 220 196 / 0.95);
--panel-raised-border: rgb(216 199 176 / 1);
--panel-accent: #a32f1a;
--panel-header-bg: rgb(228 214 191 / 0.6);
--panel-header-fg: #4d2e1a;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #eadcc4;
--glass-bg: rgb(234 220 196 / 0.55);
--glass-border: rgb(216 199 176 / 0.7);
--dock-bg: rgb(234 220 196 / 0.7);
--dock-border: rgb(216 199 176 / 0.8);
--dock-tooltip-bg: #e4d6bf;
--header-bg: rgb(234 220 196 / 0.8);
--header-border: rgb(216 199 176 / 0.9);
--header-fg: #4d2e1a;
--header-blur: 12px;
--pixel-grid-color: rgb(139 120 101 / 0.07);
--card-grid-color: rgb(139 120 101 / 0.05);
--panel-grid-color: rgb(139 120 101 / 0.06);
--scrollbar-thumb: rgb(159 130 83 / 0.6);
--scrollbar-thumb-hover: rgb(159 130 83 / 0.85);
--scrollbar-thin-thumb: rgb(159 130 83 / 0.45);
--scrollbar-thin-thumb-hover: rgb(159 130 83 / 0.7);
--brand: #a32f1a;
--brand-foreground: #dbcdb7;
--brand-primary: #a32f1a;
--brand-accent: #755833;
--brand-warm: #a64c2e;
--brand-deep: #e4d6bf;
--brand-muted: #9f8253;
--ink: #4d2e1a;
--surface-warm: #eadcc4;
--naturegreen: #a46d2d;
--naturegreen-foreground: #0b0a09;
--duck-dark: #4d2e1a;
--duck-teal: #755833;
--duck-yellow: #a8611f;
--duck-orange: #a64c2e;
--duck-forest: #a46d2d;
--duck-beige: #dacbb3;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
All Hallows Eve — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/guilhermetk/omarchy-all-hallows-eve-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='all-hallows-eve'] {
--background: #000000;
--foreground: #ffffff;
--card: #000000;
--card-foreground: #ffffff;
--popover: #000000;
--popover-foreground: #ffffff;
--muted: #121212;
--muted-foreground: #b3b3b3;
--accent: #121212;
--accent-foreground: #ffffff;
--primary: #cccc33;
--primary-foreground: #000000;
--secondary: #121212;
--secondary-foreground: #ffffff;
--cta: #cccc33;
--cta-foreground: #000000;
--ctahover: #d2d24b;
--success: #66cc33;
--success-foreground: #000000;
--warning: #cc7833;
--warning-foreground: #000000;
--destructive: #c83730;
--destructive-foreground: #ffffff;
--info: #3387cc;
--info-foreground: #000000;
--busy: #ca5832;
--busy-foreground: #000000;
--busy-ink: #ca5832;
--border: #323232;
--input: #323232;
--ring: #cccc33;
--radius: 0.625rem;
--chart-primary: #cccc33;
--chart-secondary: #9933cc;
--status-success: #66cc33;
--status-warning: #cc7833;
--status-in-progress: #3387cc;
--page-bg: #000000;
--page-bg-src: '';
--panel-bg: rgb(0 0 0 / 0.72);
--panel-border: rgb(50 50 50 / 0.9);
--panel-border-bare: rgb(50 50 50 / 0.55);
--panel-raised-bg: rgb(0 0 0 / 0.95);
--panel-raised-border: rgb(50 50 50 / 1);
--panel-accent: #cccc33;
--panel-header-bg: rgb(18 18 18 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #000000;
--glass-bg: rgb(0 0 0 / 0.55);
--glass-border: rgb(50 50 50 / 0.7);
--dock-bg: rgb(0 0 0 / 0.7);
--dock-border: rgb(50 50 50 / 0.8);
--dock-tooltip-bg: #121212;
--header-bg: rgb(0 0 0 / 0.8);
--header-border: rgb(50 50 50 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(115 115 115 / 0.07);
--card-grid-color: rgb(115 115 115 / 0.05);
--panel-grid-color: rgb(115 115 115 / 0.06);
--scrollbar-thumb: rgb(64 64 64 / 0.6);
--scrollbar-thumb-hover: rgb(64 64 64 / 0.85);
--scrollbar-thin-thumb: rgb(64 64 64 / 0.45);
--scrollbar-thin-thumb-hover: rgb(64 64 64 / 0.7);
--brand: #cccc33;
--brand-foreground: #000000;
--brand-primary: #cccc33;
--brand-accent: #cccc33;
--brand-warm: #ca5832;
--brand-deep: #000000;
--brand-muted: #404040;
--ink: #ffffff;
--surface-warm: #000000;
--naturegreen: #66cc33;
--naturegreen-foreground: #000000;
--duck-dark: #ffffff;
--duck-teal: #cccc33;
--duck-yellow: #cc7833;
--duck-orange: #ca5832;
--duck-forest: #66cc33;
--duck-beige: #1a1a1a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Amberbyte — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/tahfizhabib/omarchy-amberbyte-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='amberbyte'] {
--background: #0f090a;
--foreground: #f2e8e8;
--card: #1b1112;
--card-foreground: #f2e8e8;
--popover: #1b1112;
--popover-foreground: #f2e8e8;
--muted: #2b2223;
--muted-foreground: #b2a8a8;
--accent: #2b2223;
--accent-foreground: #f2e8e8;
--primary: #f7b3b3;
--primary-foreground: #0f090a;
--secondary: #2b2223;
--secondary-foreground: #f2e8e8;
--cta: #f7b3b3;
--cta-foreground: #0f090a;
--ctahover: #f8bcbc;
--success: #c05a5a;
--success-foreground: #0f090a;
--warning: #d66b6b;
--warning-foreground: #0f090a;
--destructive: #b44a4a;
--destructive-foreground: #f5eded;
--info: #d66b6b;
--info-foreground: #0f090a;
--busy: #c55b5b;
--busy-foreground: #0f090a;
--busy-ink: #c55b5b;
--border: #453b3c;
--input: #453b3c;
--ring: #f7b3b3;
--radius: 0.625rem;
--chart-primary: #f7b3b3;
--chart-secondary: #d66b6b;
--status-success: #c05a5a;
--status-warning: #d66b6b;
--status-in-progress: #d66b6b;
--page-bg: #0f090a;
--page-bg-src: '';
--panel-bg: rgb(27 17 18 / 0.72);
--panel-border: rgb(69 59 60 / 0.9);
--panel-border-bare: rgb(69 59 60 / 0.55);
--panel-raised-bg: rgb(27 17 18 / 0.95);
--panel-raised-border: rgb(69 59 60 / 1);
--panel-accent: #f7b3b3;
--panel-header-bg: rgb(43 34 35 / 0.6);
--panel-header-fg: #f2e8e8;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1b1112;
--glass-bg: rgb(27 17 18 / 0.55);
--glass-border: rgb(69 59 60 / 0.7);
--dock-bg: rgb(27 17 18 / 0.7);
--dock-border: rgb(69 59 60 / 0.8);
--dock-tooltip-bg: #2b2223;
--header-bg: rgb(27 17 18 / 0.8);
--header-border: rgb(69 59 60 / 0.9);
--header-fg: #f2e8e8;
--header-blur: 12px;
--pixel-grid-color: rgb(124 114 114 / 0.07);
--card-grid-color: rgb(124 114 114 / 0.05);
--panel-grid-color: rgb(124 114 114 / 0.06);
--scrollbar-thumb: rgb(113 103 104 / 0.6);
--scrollbar-thumb-hover: rgb(113 103 104 / 0.85);
--scrollbar-thin-thumb: rgb(113 103 104 / 0.45);
--scrollbar-thin-thumb-hover: rgb(113 103 104 / 0.7);
--brand: #f7b3b3;
--brand-foreground: #0f090a;
--brand-primary: #f7b3b3;
--brand-accent: #f48a8a;
--brand-warm: #c55b5b;
--brand-deep: #0f090a;
--brand-muted: #716768;
--ink: #f2e8e8;
--surface-warm: #1b1112;
--naturegreen: #c05a5a;
--naturegreen-foreground: #0f090a;
--duck-dark: #f2e8e8;
--duck-teal: #f48a8a;
--duck-yellow: #d66b6b;
--duck-orange: #c55b5b;
--duck-forest: #c05a5a;
--duck-beige: #312727;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Amekoji — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/atif-1402/omarchy-amekoji-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='amekoji'] {
--background: #131617;
--foreground: #dcd6d6;
--card: #232829;
--card-foreground: #dcd6d6;
--popover: #232829;
--popover-foreground: #dcd6d6;
--muted: #323738;
--muted-foreground: #a5a2a2;
--accent: #323738;
--accent-foreground: #dcd6d6;
--primary: #98b3b3;
--primary-foreground: #131617;
--secondary: #323738;
--secondary-foreground: #dcd6d6;
--cta: #98b3b3;
--cta-foreground: #131617;
--ctahover: #a4bcbc;
--success: #98b3a3;
--success-foreground: #131617;
--warning: #93b3a6;
--warning-foreground: #131617;
--destructive: #b39a98;
--destructive-foreground: #131617;
--info: #85abbc;
--info-foreground: #131617;
--busy: #a3a79f;
--busy-foreground: #131617;
--busy-ink: #a3a79f;
--border: #474a4b;
--input: #474a4b;
--ring: #98b3b3;
--radius: 0.625rem;
--chart-primary: #98b3b3;
--chart-secondary: #bea7b0;
--status-success: #98b3a3;
--status-warning: #93b3a6;
--status-in-progress: #85abbc;
--page-bg: #131617;
--page-bg-src: '';
--panel-bg: rgb(35 40 41 / 0.72);
--panel-border: rgb(71 74 75 / 0.9);
--panel-border-bare: rgb(71 74 75 / 0.55);
--panel-raised-bg: rgb(35 40 41 / 0.95);
--panel-raised-border: rgb(71 74 75 / 1);
--panel-accent: #98b3b3;
--panel-header-bg: rgb(50 55 56 / 0.6);
--panel-header-fg: #dcd6d6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #232829;
--glass-bg: rgb(35 40 41 / 0.55);
--glass-border: rgb(71 74 75 / 0.7);
--dock-bg: rgb(35 40 41 / 0.7);
--dock-border: rgb(71 74 75 / 0.8);
--dock-tooltip-bg: #323738;
--header-bg: rgb(35 40 41 / 0.8);
--header-border: rgb(71 74 75 / 0.9);
--header-fg: #dcd6d6;
--header-blur: 12px;
--pixel-grid-color: rgb(118 118 119 / 0.07);
--card-grid-color: rgb(118 118 119 / 0.05);
--panel-grid-color: rgb(118 118 119 / 0.06);
--scrollbar-thumb: rgb(120 133 135 / 0.6);
--scrollbar-thumb-hover: rgb(120 133 135 / 0.85);
--scrollbar-thin-thumb: rgb(120 133 135 / 0.45);
--scrollbar-thin-thumb-hover: rgb(120 133 135 / 0.7);
--brand: #98b3b3;
--brand-foreground: #131617;
--brand-primary: #98b3b3;
--brand-accent: #98b3b3;
--brand-warm: #a3a79f;
--brand-deep: #131617;
--brand-muted: #788587;
--ink: #dcd6d6;
--surface-warm: #232829;
--naturegreen: #98b3a3;
--naturegreen-foreground: #131617;
--duck-dark: #dcd6d6;
--duck-teal: #98b3b3;
--duck-yellow: #93b3a6;
--duck-orange: #a3a79f;
--duck-forest: #98b3a3;
--duck-beige: #36393a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Andromeda — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/signaldirective/andromeda (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='andromeda'] {
--background: #131519;
--foreground: #e5e5e5;
--card: #23262e;
--card-foreground: #e5e5e5;
--popover: #23262e;
--popover-foreground: #e5e5e5;
--muted: #32353d;
--muted-foreground: #abacae;
--accent: #32353d;
--accent-foreground: #e5e5e5;
--primary: #00e8c6;
--primary-foreground: #131519;
--secondary: #32353d;
--secondary-foreground: #e5e5e5;
--cta: #00e8c6;
--cta-foreground: #131519;
--ctahover: #1febcd;
--success: #00e8c6;
--success-foreground: #131519;
--warning: #ffe66d;
--warning-foreground: #131519;
--destructive: #f92672;
--destructive-foreground: #131519;
--info: #7cb7ff;
--info-foreground: #131519;
--busy: #fc8670;
--busy-foreground: #131519;
--busy-ink: #fc8670;
--border: #494c52;
--input: #494c52;
--ring: #00e8c6;
--radius: 0.625rem;
--chart-primary: #00e8c6;
--chart-secondary: #c74ded;
--status-success: #00e8c6;
--status-warning: #ffe66d;
--status-in-progress: #7cb7ff;
--page-bg: #131519;
--page-bg-src: '';
--panel-bg: rgb(35 38 46 / 0.72);
--panel-border: rgb(73 76 82 / 0.9);
--panel-border-bare: rgb(73 76 82 / 0.55);
--panel-raised-bg: rgb(35 38 46 / 0.95);
--panel-raised-border: rgb(73 76 82 / 1);
--panel-accent: #00e8c6;
--panel-header-bg: rgb(50 53 61 / 0.6);
--panel-header-fg: #e5e5e5;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #23262e;
--glass-bg: rgb(35 38 46 / 0.55);
--glass-border: rgb(73 76 82 / 0.7);
--dock-bg: rgb(35 38 46 / 0.7);
--dock-border: rgb(73 76 82 / 0.8);
--dock-tooltip-bg: #32353d;
--header-bg: rgb(35 38 46 / 0.8);
--header-border: rgb(73 76 82 / 0.9);
--header-fg: #e5e5e5;
--header-blur: 12px;
--pixel-grid-color: rgb(122 124 128 / 0.07);
--card-grid-color: rgb(122 124 128 / 0.05);
--panel-grid-color: rgb(122 124 128 / 0.06);
--scrollbar-thumb: rgb(102 102 102 / 0.6);
--scrollbar-thumb-hover: rgb(102 102 102 / 0.85);
--scrollbar-thin-thumb: rgb(102 102 102 / 0.45);
--scrollbar-thin-thumb-hover: rgb(102 102 102 / 0.7);
--brand: #00e8c6;
--brand-foreground: #131519;
--brand-primary: #00e8c6;
--brand-accent: #96e072;
--brand-warm: #fc8670;
--brand-deep: #131519;
--brand-muted: #666666;
--ink: #e5e5e5;
--surface-warm: #23262e;
--naturegreen: #00e8c6;
--naturegreen-foreground: #131519;
--duck-dark: #e5e5e5;
--duck-teal: #96e072;
--duck-yellow: #ffe66d;
--duck-orange: #fc8670;
--duck-forest: #00e8c6;
--duck-beige: #363940;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Anonymous — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/j4v3l/omarchy-anonymous-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='anonymous'] {
--background: #000000;
--foreground: #fefefe;
--card: #000000;
--card-foreground: #fefefe;
--popover: #000000;
--popover-foreground: #fefefe;
--muted: #121212;
--muted-foreground: #b2b2b2;
--accent: #121212;
--accent-foreground: #fefefe;
--primary: #bdbdbd;
--primary-foreground: #000000;
--secondary: #121212;
--secondary-foreground: #fefefe;
--cta: #bdbdbd;
--cta-foreground: #000000;
--ctahover: #c5c5c5;
--success: #bdbdbd;
--success-foreground: #000000;
--warning: #cfcfcf;
--warning-foreground: #000000;
--destructive: #fefefe;
--destructive-foreground: #000000;
--info: #bdbdbd;
--info-foreground: #000000;
--busy: #e7e7e7;
--busy-foreground: #000000;
--busy-ink: #e7e7e7;
--border: #323232;
--input: #323232;
--ring: #bdbdbd;
--radius: 0.625rem;
--chart-primary: #bdbdbd;
--chart-secondary: #bdbdbd;
--status-success: #bdbdbd;
--status-warning: #cfcfcf;
--status-in-progress: #bdbdbd;
--page-bg: #000000;
--page-bg-src: '';
--panel-bg: rgb(0 0 0 / 0.72);
--panel-border: rgb(50 50 50 / 0.9);
--panel-border-bare: rgb(50 50 50 / 0.55);
--panel-raised-bg: rgb(0 0 0 / 0.95);
--panel-raised-border: rgb(50 50 50 / 1);
--panel-accent: #bdbdbd;
--panel-header-bg: rgb(18 18 18 / 0.6);
--panel-header-fg: #fefefe;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #000000;
--glass-bg: rgb(0 0 0 / 0.55);
--glass-border: rgb(50 50 50 / 0.7);
--dock-bg: rgb(0 0 0 / 0.7);
--dock-border: rgb(50 50 50 / 0.8);
--dock-tooltip-bg: #121212;
--header-bg: rgb(0 0 0 / 0.8);
--header-border: rgb(50 50 50 / 0.9);
--header-fg: #fefefe;
--header-blur: 12px;
--pixel-grid-color: rgb(114 114 114 / 0.07);
--card-grid-color: rgb(114 114 114 / 0.05);
--panel-grid-color: rgb(114 114 114 / 0.06);
--scrollbar-thumb: rgb(26 26 26 / 0.6);
--scrollbar-thumb-hover: rgb(26 26 26 / 0.85);
--scrollbar-thin-thumb: rgb(26 26 26 / 0.45);
--scrollbar-thin-thumb-hover: rgb(26 26 26 / 0.7);
--brand: #bdbdbd;
--brand-foreground: #000000;
--brand-primary: #bdbdbd;
--brand-accent: #bdbdbd;
--brand-warm: #e7e7e7;
--brand-deep: #000000;
--brand-muted: #1a1a1a;
--ink: #fefefe;
--surface-warm: #000000;
--naturegreen: #bdbdbd;
--naturegreen-foreground: #000000;
--duck-dark: #fefefe;
--duck-teal: #bdbdbd;
--duck-yellow: #cfcfcf;
--duck-orange: #e7e7e7;
--duck-forest: #bdbdbd;
--duck-beige: #191919;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Apocalypse — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/atif-1402/omarchy-apocalypse-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='apocalypse'] {
--background: #090707;
--foreground: #ffffff;
--card: #110d0c;
--card-foreground: #ffffff;
--popover: #110d0c;
--popover-foreground: #ffffff;
--muted: #221e1d;
--muted-foreground: #b8b6b6;
--accent: #221e1d;
--accent-foreground: #ffffff;
--primary: #e7e9e9;
--primary-foreground: #090707;
--secondary: #221e1d;
--secondary-foreground: #ffffff;
--cta: #e7e9e9;
--cta-foreground: #090707;
--ctahover: #eaecec;
--success: #98a09b;
--success-foreground: #090707;
--warning: #d3c4b8;
--warning-foreground: #090707;
--destructive: #ab7c7c;
--destructive-foreground: #090707;
--info: #a8a8ae;
--info-foreground: #090707;
--busy: #bfa09a;
--busy-foreground: #090707;
--busy-ink: #bfa09a;
--border: #403c3c;
--input: #403c3c;
--ring: #e7e9e9;
--radius: 0.625rem;
--chart-primary: #e7e9e9;
--chart-secondary: #b6afb5;
--status-success: #98a09b;
--status-warning: #d3c4b8;
--status-in-progress: #a8a8ae;
--page-bg: #090707;
--page-bg-src: '';
--panel-bg: rgb(17 13 12 / 0.72);
--panel-border: rgb(64 60 60 / 0.9);
--panel-border-bare: rgb(64 60 60 / 0.55);
--panel-raised-bg: rgb(17 13 12 / 0.95);
--panel-raised-border: rgb(64 60 60 / 1);
--panel-accent: #e7e9e9;
--panel-header-bg: rgb(34 30 29 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #110d0c;
--glass-bg: rgb(17 13 12 / 0.55);
--glass-border: rgb(64 60 60 / 0.7);
--dock-bg: rgb(17 13 12 / 0.7);
--dock-border: rgb(64 60 60 / 0.8);
--dock-tooltip-bg: #221e1d;
--header-bg: rgb(17 13 12 / 0.8);
--header-border: rgb(64 60 60 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(124 122 121 / 0.07);
--card-grid-color: rgb(124 122 121 / 0.05);
--panel-grid-color: rgb(124 122 121 / 0.06);
--scrollbar-thumb: rgb(140 123 118 / 0.6);
--scrollbar-thumb-hover: rgb(140 123 118 / 0.85);
--scrollbar-thin-thumb: rgb(140 123 118 / 0.45);
--scrollbar-thin-thumb-hover: rgb(140 123 118 / 0.7);
--brand: #e7e9e9;
--brand-foreground: #090707;
--brand-primary: #e7e9e9;
--brand-accent: #b8bdbc;
--brand-warm: #bfa09a;
--brand-deep: #090707;
--brand-muted: #8c7b76;
--ink: #ffffff;
--surface-warm: #110d0c;
--naturegreen: #98a09b;
--naturegreen-foreground: #090707;
--duck-dark: #ffffff;
--duck-teal: #b8bdbc;
--duck-yellow: #d3c4b8;
--duck-orange: #bfa09a;
--duck-forest: #98a09b;
--duck-beige: #292524;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Arc Blueberry — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/vale-c/omarchy-arc-blueberry (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='arc-blueberry'] {
--background: #090b13;
--foreground: #bcc1dc;
--card: #111422;
--card-foreground: #bcc1dc;
--popover: #111422;
--popover-foreground: #bcc1dc;
--muted: #222431;
--muted-foreground: #898da4;
--accent: #222431;
--accent-foreground: #bcc1dc;
--primary: #0fffeb;
--primary-foreground: #090b13;
--secondary: #222431;
--secondary-foreground: #bcc1dc;
--cta: #0fffeb;
--cta-foreground: #090b13;
--ctahover: #2cffed;
--success: #3cec85;
--success-foreground: #090b13;
--warning: #eacd61;
--warning-foreground: #090b13;
--destructive: #e35535;
--destructive-foreground: #090b13;
--info: #69c3ff;
--info-foreground: #090b13;
--busy: #e7914b;
--busy-foreground: #090b13;
--busy-ink: #e7914b;
--border: #333646;
--input: #333646;
--ring: #0fffeb;
--radius: 0.625rem;
--chart-primary: #0fffeb;
--chart-secondary: #f38cec;
--status-success: #3cec85;
--status-warning: #eacd61;
--status-in-progress: #69c3ff;
--page-bg: #090b13;
--page-bg-src: '';
--panel-bg: rgb(17 20 34 / 0.72);
--panel-border: rgb(51 54 70 / 0.9);
--panel-border-bare: rgb(51 54 70 / 0.55);
--panel-raised-bg: rgb(17 20 34 / 0.95);
--panel-raised-border: rgb(51 54 70 / 1);
--panel-accent: #0fffeb;
--panel-header-bg: rgb(34 36 49 / 0.6);
--panel-header-fg: #bcc1dc;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #111422;
--glass-bg: rgb(17 20 34 / 0.55);
--glass-border: rgb(51 54 70 / 0.7);
--dock-bg: rgb(17 20 34 / 0.7);
--dock-border: rgb(51 54 70 / 0.8);
--dock-tooltip-bg: #222431;
--header-bg: rgb(17 20 34 / 0.8);
--header-border: rgb(51 54 70 / 0.9);
--header-fg: #bcc1dc;
--header-blur: 12px;
--pixel-grid-color: rgb(94 98 118 / 0.07);
--card-grid-color: rgb(94 98 118 / 0.05);
--panel-grid-color: rgb(94 98 118 / 0.06);
--scrollbar-thumb: rgb(49 59 98 / 0.6);
--scrollbar-thumb-hover: rgb(49 59 98 / 0.85);
--scrollbar-thin-thumb: rgb(49 59 98 / 0.45);
--scrollbar-thin-thumb-hover: rgb(49 59 98 / 0.7);
--brand: #0fffeb;
--brand-foreground: #090b13;
--brand-primary: #0fffeb;
--brand-accent: #22ecdb;
--brand-warm: #e7914b;
--brand-deep: #090b13;
--brand-muted: #313b62;
--ink: #bcc1dc;
--surface-warm: #111422;
--naturegreen: #3cec85;
--naturegreen-foreground: #090b13;
--duck-dark: #bcc1dc;
--duck-teal: #22ecdb;
--duck-yellow: #eacd61;
--duck-orange: #e7914b;
--duck-forest: #3cec85;
--duck-beige: #222535;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Archriot — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/CyphrRiot/omarchy-archriot-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='archriot'] {
--background: #0e0f15;
--foreground: #ffffff;
--card: #1a1b26;
--card-foreground: #ffffff;
--popover: #1a1b26;
--popover-foreground: #ffffff;
--muted: #2a2b35;
--muted-foreground: #babbbe;
--accent: #2a2b35;
--accent-foreground: #ffffff;
--primary: #73daca;
--primary-foreground: #0e0f15;
--secondary: #2a2b35;
--secondary-foreground: #ffffff;
--cta: #73daca;
--cta-foreground: #0e0f15;
--ctahover: #84ded0;
--success: #9ece6a;
--success-foreground: #0e0f15;
--warning: #e0af68;
--warning-foreground: #0e0f15;
--destructive: #ff7a93;
--destructive-foreground: #0e0f15;
--info: #7da6ff;
--info-foreground: #0e0f15;
--busy: #f0957e;
--busy-foreground: #0e0f15;
--busy-ink: #f0957e;
--border: #474851;
--input: #474851;
--ring: #73daca;
--radius: 0.625rem;
--chart-primary: #73daca;
--chart-secondary: #bb9af7;
--status-success: #9ece6a;
--status-warning: #e0af68;
--status-in-progress: #7da6ff;
--page-bg: #0e0f15;
--page-bg-src: '';
--panel-bg: rgb(26 27 38 / 0.72);
--panel-border: rgb(71 72 81 / 0.9);
--panel-border-bare: rgb(71 72 81 / 0.55);
--panel-raised-bg: rgb(26 27 38 / 0.95);
--panel-raised-border: rgb(71 72 81 / 1);
--panel-accent: #73daca;
--panel-header-bg: rgb(42 43 53 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1b26;
--glass-bg: rgb(26 27 38 / 0.55);
--glass-border: rgb(71 72 81 / 0.7);
--dock-bg: rgb(26 27 38 / 0.7);
--dock-border: rgb(71 72 81 / 0.8);
--dock-tooltip-bg: #2a2b35;
--header-bg: rgb(26 27 38 / 0.8);
--header-border: rgb(71 72 81 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(129 130 136 / 0.07);
--card-grid-color: rgb(129 130 136 / 0.05);
--panel-grid-color: rgb(129 130 136 / 0.06);
--scrollbar-thumb: rgb(86 95 137 / 0.6);
--scrollbar-thumb-hover: rgb(86 95 137 / 0.85);
--scrollbar-thin-thumb: rgb(86 95 137 / 0.45);
--scrollbar-thin-thumb-hover: rgb(86 95 137 / 0.7);
--brand: #73daca;
--brand-foreground: #0e0f15;
--brand-primary: #73daca;
--brand-accent: #0db9d7;
--brand-warm: #f0957e;
--brand-deep: #0e0f15;
--brand-muted: #565f89;
--ink: #ffffff;
--surface-warm: #1a1b26;
--naturegreen: #9ece6a;
--naturegreen-foreground: #0e0f15;
--duck-dark: #ffffff;
--duck-teal: #0db9d7;
--duck-yellow: #e0af68;
--duck-orange: #f0957e;
--duck-forest: #9ece6a;
--duck-beige: #31323c;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Archwave — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/davidguttman/archwave (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='archwave'] {
--background: #0e0719;
--foreground: #d4a5ff;
--card: #1a0d2e;
--card-foreground: #d4a5ff;
--popover: #1a0d2e;
--popover-foreground: #d4a5ff;
--muted: #2a1e3d;
--muted-foreground: #a17ec3;
--accent: #2a1e3d;
--accent-foreground: #d4a5ff;
--primary: #8ffef4;
--primary-foreground: #0e0719;
--secondary: #2a1e3d;
--secondary-foreground: #d4a5ff;
--cta: #8ffef4;
--cta-foreground: #0e0719;
--ctahover: #9cfef5;
--success: #5ffbf1;
--success-foreground: #0e0719;
--warning: #f9f871;
--warning-foreground: #0e0719;
--destructive: #ff6ec7;
--destructive-foreground: #0e0719;
--info: #8b9aff;
--info-foreground: #0e0719;
--busy: #fcb39c;
--busy-foreground: #0e0719;
--busy-ink: #fcb39c;
--border: #3e2b57;
--input: #3e2b57;
--ring: #8ffef4;
--radius: 0.625rem;
--chart-primary: #8ffef4;
--chart-secondary: #f4a5ff;
--status-success: #5ffbf1;
--status-warning: #f9f871;
--status-in-progress: #8b9aff;
--page-bg: #0e0719;
--page-bg-src: '';
--panel-bg: rgb(26 13 46 / 0.72);
--panel-border: rgb(62 43 87 / 0.9);
--panel-border-bare: rgb(62 43 87 / 0.55);
--panel-raised-bg: rgb(26 13 46 / 0.95);
--panel-raised-border: rgb(62 43 87 / 1);
--panel-accent: #8ffef4;
--panel-header-bg: rgb(42 30 61 / 0.6);
--panel-header-fg: #d4a5ff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a0d2e;
--glass-bg: rgb(26 13 46 / 0.55);
--glass-border: rgb(62 43 87 / 0.7);
--dock-bg: rgb(26 13 46 / 0.7);
--dock-border: rgb(62 43 87 / 0.8);
--dock-tooltip-bg: #2a1e3d;
--header-bg: rgb(26 13 46 / 0.8);
--header-border: rgb(62 43 87 / 0.9);
--header-fg: #d4a5ff;
--header-blur: 12px;
--pixel-grid-color: rgb(117 90 146 / 0.07);
--card-grid-color: rgb(117 90 146 / 0.05);
--panel-grid-color: rgb(117 90 146 / 0.06);
--scrollbar-thumb: rgb(84 58 110 / 0.6);
--scrollbar-thumb-hover: rgb(84 58 110 / 0.85);
--scrollbar-thin-thumb: rgb(84 58 110 / 0.45);
--scrollbar-thin-thumb-hover: rgb(84 58 110 / 0.7);
--brand: #8ffef4;
--brand-foreground: #0e0719;
--brand-primary: #8ffef4;
--brand-accent: #5ffbf1;
--brand-warm: #fcb39c;
--brand-deep: #0e0719;
--brand-muted: #543a6e;
--ink: #d4a5ff;
--surface-warm: #1a0d2e;
--naturegreen: #5ffbf1;
--naturegreen-foreground: #0e0719;
--duck-dark: #d4a5ff;
--duck-teal: #5ffbf1;
--duck-yellow: #f9f871;
--duck-orange: #fcb39c;
--duck-forest: #5ffbf1;
--duck-beige: #2d1c43;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Artzen — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/tahfizhabib/omarchy-artzen-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='artzen'] {
--background: #0d0f11;
--foreground: #fdf9f8;
--card: #181c1f;
--card-foreground: #fdf9f8;
--popover: #181c1f;
--popover-foreground: #fdf9f8;
--muted: #282c2f;
--muted-foreground: #b8b7b7;
--accent: #282c2f;
--accent-foreground: #fdf9f8;
--primary: #ecbeb9;
--primary-foreground: #0d0f11;
--secondary: #282c2f;
--secondary-foreground: #fdf9f8;
--cta: #ecbeb9;
--cta-foreground: #0d0f11;
--ctahover: #eec6c1;
--success: #9f6769;
--success-foreground: #000000;
--warning: #bb6d6c;
--warning-foreground: #0d0f11;
--destructive: #9b584d;
--destructive-foreground: #fdf9f8;
--info: #b97670;
--info-foreground: #0d0f11;
--busy: #ab635d;
--busy-foreground: #060606;
--busy-ink: #af6b65;
--border: #45474a;
--input: #45474a;
--ring: #ecbeb9;
--radius: 0.625rem;
--chart-primary: #ecbeb9;
--chart-secondary: #9a8c8a;
--status-success: #9f6769;
--status-warning: #bb6d6c;
--status-in-progress: #b97670;
--page-bg: #0d0f11;
--page-bg-src: '';
--panel-bg: rgb(24 28 31 / 0.72);
--panel-border: rgb(69 71 74 / 0.9);
--panel-border-bare: rgb(69 71 74 / 0.55);
--panel-raised-bg: rgb(24 28 31 / 0.95);
--panel-raised-border: rgb(69 71 74 / 1);
--panel-accent: #ecbeb9;
--panel-header-bg: rgb(40 44 47 / 0.6);
--panel-header-fg: #fdf9f8;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #181c1f;
--glass-bg: rgb(24 28 31 / 0.55);
--glass-border: rgb(69 71 74 / 0.7);
--dock-bg: rgb(24 28 31 / 0.7);
--dock-border: rgb(69 71 74 / 0.8);
--dock-tooltip-bg: #282c2f;
--header-bg: rgb(24 28 31 / 0.8);
--header-border: rgb(69 71 74 / 0.9);
--header-fg: #fdf9f8;
--header-blur: 12px;
--pixel-grid-color: rgb(127 127 129 / 0.07);
--card-grid-color: rgb(127 127 129 / 0.05);
--panel-grid-color: rgb(127 127 129 / 0.06);
--scrollbar-thumb: rgb(177 141 133 / 0.6);
--scrollbar-thumb-hover: rgb(177 141 133 / 0.85);
--scrollbar-thin-thumb: rgb(177 141 133 / 0.45);
--scrollbar-thin-thumb-hover: rgb(177 141 133 / 0.7);
--brand: #ecbeb9;
--brand-foreground: #0d0f11;
--brand-primary: #ecbeb9;
--brand-accent: #da7a6f;
--brand-warm: #ab635d;
--brand-deep: #0d0f11;
--brand-muted: #b18d85;
--ink: #fdf9f8;
--surface-warm: #181c1f;
--naturegreen: #9f6769;
--naturegreen-foreground: #000000;
--duck-dark: #fdf9f8;
--duck-teal: #da7a6f;
--duck-yellow: #bb6d6c;
--duck-orange: #ab635d;
--duck-forest: #9f6769;
--duck-beige: #2f3235;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Ash — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/bjarneo/omarchy-ash-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='ash'] {
--background: #0a0a0a;
--foreground: #e0e0e0;
--card: #121212;
--card-foreground: #e0e0e0;
--popover: #121212;
--popover-foreground: #e0e0e0;
--muted: #232323;
--muted-foreground: #a2a2a2;
--accent: #232323;
--accent-foreground: #e0e0e0;
--primary: #9e9e9e;
--primary-foreground: #0a0a0a;
--secondary: #232323;
--secondary-foreground: #e0e0e0;
--cta: #9e9e9e;
--cta-foreground: #0a0a0a;
--ctahover: #aaaaaa;
--success: #767676;
--success-foreground: #040404;
--warning: #9e9e9e;
--warning-foreground: #0a0a0a;
--destructive: #8a8a8a;
--destructive-foreground: #0a0a0a;
--info: #626262;
--info-foreground: #e0e0e0;
--busy: #949494;
--busy-foreground: #0a0a0a;
--busy-ink: #949494;
--border: #3a3a3a;
--input: #3a3a3a;
--ring: #9e9e9e;
--radius: 0.625rem;
--chart-primary: #9e9e9e;
--chart-secondary: #8a8a8a;
--status-success: #767676;
--status-warning: #9e9e9e;
--status-in-progress: #626262;
--page-bg: #0a0a0a;
--page-bg-src: '';
--panel-bg: rgb(18 18 18 / 0.72);
--panel-border: rgb(58 58 58 / 0.9);
--panel-border-bare: rgb(58 58 58 / 0.55);
--panel-raised-bg: rgb(18 18 18 / 0.95);
--panel-raised-border: rgb(58 58 58 / 1);
--panel-accent: #9e9e9e;
--panel-header-bg: rgb(35 35 35 / 0.6);
--panel-header-fg: #e0e0e0;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #121212;
--glass-bg: rgb(18 18 18 / 0.55);
--glass-border: rgb(58 58 58 / 0.7);
--dock-bg: rgb(18 18 18 / 0.7);
--dock-border: rgb(58 58 58 / 0.8);
--dock-tooltip-bg: #232323;
--header-bg: rgb(18 18 18 / 0.8);
--header-border: rgb(58 58 58 / 0.9);
--header-fg: #e0e0e0;
--header-blur: 12px;
--pixel-grid-color: rgb(111 111 111 / 0.07);
--card-grid-color: rgb(111 111 111 / 0.05);
--panel-grid-color: rgb(111 111 111 / 0.06);
--scrollbar-thumb: rgb(138 138 138 / 0.6);
--scrollbar-thumb-hover: rgb(138 138 138 / 0.85);
--scrollbar-thin-thumb: rgb(138 138 138 / 0.45);
--scrollbar-thin-thumb-hover: rgb(138 138 138 / 0.7);
--brand: #9e9e9e;
--brand-foreground: #0a0a0a;
--brand-primary: #9e9e9e;
--brand-accent: #767676;
--brand-warm: #949494;
--brand-deep: #0a0a0a;
--brand-muted: #8a8a8a;
--ink: #e0e0e0;
--surface-warm: #121212;
--naturegreen: #767676;
--naturegreen-foreground: #040404;
--duck-dark: #e0e0e0;
--duck-teal: #767676;
--duck-yellow: #9e9e9e;
--duck-orange: #949494;
--duck-forest: #767676;
--duck-beige: #272727;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Astrodark — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark) — the source repo's own colors.toml says so directly.
Source: https://github.com/JamsMendez/omarchy-astrodark-theme (colors.toml (Omarchy schema)), read by scripts/port-community-themes.ts. Values
came straight from the repo's own colors.toml — the same schema and reader the official 22 themes
use, so nothing here is reconstructed. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='astrodark'] {
--background: #0b0c0f;
--foreground: #9b9fa9;
--card: #1a1d23;
--card-foreground: #9b9fa9;
--popover: #1a1d23;
--popover-foreground: #9b9fa9;
--muted: #26343f;
--muted-foreground: #b8bcc6;
--accent: #26343f;
--accent-foreground: #9b9fa9;
--primary: #5eb7ff;
--primary-foreground: #0b0c0f;
--secondary: #26343f;
--secondary-foreground: #9b9fa9;
--cta: #5eb7ff;
--cta-foreground: #0b0c0f;
--ctahover: #71c0ff;
--success: #87c05f;
--success-foreground: #0b0c0f;
--warning: #dfab25;
--warning-foreground: #0b0c0f;
--destructive: #ff838b;
--destructive-foreground: #0b0c0f;
--info: #5eb7ff;
--info-foreground: #0b0c0f;
--busy: #e8925a;
--busy-foreground: #0b0c0f;
--busy-ink: #e8925a;
--border: #464a51;
--input: #464a51;
--ring: #5eb7ff;
--radius: 0.625rem;
--chart-primary: #5eb7ff;
--chart-secondary: #dd97f1;
--status-success: #87c05f;
--status-warning: #dfab25;
--status-in-progress: #5eb7ff;
--page-bg: #0b0c0f;
--page-bg-src: '';
--panel-bg: rgb(26 29 35 / 0.72);
--panel-border: rgb(70 74 81 / 0.9);
--panel-border-bare: rgb(70 74 81 / 0.55);
--panel-raised-bg: rgb(26 29 35 / 0.95);
--panel-raised-border: rgb(70 74 81 / 1);
--panel-accent: #5eb7ff;
--panel-header-bg: rgb(38 52 63 / 0.6);
--panel-header-fg: #9b9fa9;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1d23;
--glass-bg: rgb(26 29 35 / 0.55);
--glass-border: rgb(70 74 81 / 0.7);
--dock-bg: rgb(26 29 35 / 0.7);
--dock-border: rgb(70 74 81 / 0.8);
--dock-tooltip-bg: #26343f;
--header-bg: rgb(26 29 35 / 0.8);
--header-border: rgb(70 74 81 / 0.9);
--header-fg: #9b9fa9;
--header-blur: 12px;
--pixel-grid-color: rgb(107 111 121 / 0.07);
--card-grid-color: rgb(107 111 121 / 0.05);
--panel-grid-color: rgb(107 111 121 / 0.06);
--scrollbar-thumb: rgb(52 54 58 / 0.6);
--scrollbar-thumb-hover: rgb(52 54 58 / 0.85);
--scrollbar-thin-thumb: rgb(52 54 58 / 0.45);
--scrollbar-thin-thumb-hover: rgb(52 54 58 / 0.7);
--brand: #5eb7ff;
--brand-foreground: #0b0c0f;
--brand-primary: #5eb7ff;
--brand-accent: #4ac2b8;
--brand-warm: #e8925a;
--brand-deep: #0b0c0f;
--brand-muted: #34363a;
--ink: #9b9fa9;
--surface-warm: #1a1d23;
--naturegreen: #87c05f;
--naturegreen-foreground: #0b0c0f;
--duck-dark: #9b9fa9;
--duck-teal: #4ac2b8;
--duck-yellow: #dfab25;
--duck-orange: #e8925a;
--duck-forest: #87c05f;
--duck-beige: #272a30;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Atari — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/atif-1402/omarchy-atari-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='atari'] {
--background: #171317;
--foreground: #dcd6d6;
--card: #292329;
--card-foreground: #dcd6d6;
--popover: #292329;
--popover-foreground: #dcd6d6;
--muted: #383238;
--muted-foreground: #a6a0a2;
--accent: #383238;
--accent-foreground: #dcd6d6;
--primary: #dff1e8;
--primary-foreground: #171317;
--secondary: #383238;
--secondary-foreground: #dcd6d6;
--cta: #dff1e8;
--cta-foreground: #171317;
--ctahover: #e3f3eb;
--success: #79be9a;
--success-foreground: #171317;
--warning: #c3b283;
--warning-foreground: #171317;
--destructive: #b9746f;
--destructive-foreground: #171317;
--info: #9db9b2;
--info-foreground: #171317;
--busy: #be9379;
--busy-foreground: #171317;
--busy-ink: #be9379;
--border: #4c464b;
--input: #4c464b;
--ring: #dff1e8;
--radius: 0.625rem;
--chart-primary: #dff1e8;
--chart-secondary: #cd9b98;
--status-success: #79be9a;
--status-warning: #c3b283;
--status-in-progress: #9db9b2;
--page-bg: #171317;
--page-bg-src: '';
--panel-bg: rgb(41 35 41 / 0.72);
--panel-border: rgb(76 70 75 / 0.9);
--panel-border-bare: rgb(76 70 75 / 0.55);
--panel-raised-bg: rgb(41 35 41 / 0.95);
--panel-raised-border: rgb(76 70 75 / 1);
--panel-accent: #dff1e8;
--panel-header-bg: rgb(56 50 56 / 0.6);
--panel-header-fg: #dcd6d6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #292329;
--glass-bg: rgb(41 35 41 / 0.55);
--glass-border: rgb(76 70 75 / 0.7);
--dock-bg: rgb(41 35 41 / 0.7);
--dock-border: rgb(76 70 75 / 0.8);
--dock-tooltip-bg: #383238;
--header-bg: rgb(41 35 41 / 0.8);
--header-border: rgb(76 70 75 / 0.9);
--header-fg: #dcd6d6;
--header-blur: 12px;
--pixel-grid-color: rgb(122 116 119 / 0.07);
--card-grid-color: rgb(122 116 119 / 0.05);
--panel-grid-color: rgb(122 116 119 / 0.06);
--scrollbar-thumb: rgb(135 120 135 / 0.6);
--scrollbar-thumb-hover: rgb(135 120 135 / 0.85);
--scrollbar-thin-thumb: rgb(135 120 135 / 0.45);
--scrollbar-thin-thumb-hover: rgb(135 120 135 / 0.7);
--brand: #dff1e8;
--brand-foreground: #171317;
--brand-primary: #dff1e8;
--brand-accent: #a2d2b9;
--brand-warm: #be9379;
--brand-deep: #171317;
--brand-muted: #877887;
--ink: #dcd6d6;
--surface-warm: #292329;
--naturegreen: #79be9a;
--naturegreen-foreground: #171317;
--duck-dark: #dcd6d6;
--duck-teal: #a2d2b9;
--duck-yellow: #c3b283;
--duck-orange: #be9379;
--duck-forest: #79be9a;
--duck-beige: #3b353a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Atelier — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/atif-1402/omarchy-atelier-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='atelier'] {
--background: #13130f;
--foreground: #dcd9d6;
--card: #22221b;
--card-foreground: #dcd9d6;
--popover: #22221b;
--popover-foreground: #dcd9d6;
--muted: #31312b;
--muted-foreground: #a4a29e;
--accent: #31312b;
--accent-foreground: #dcd9d6;
--primary: #7bbf7a;
--primary-foreground: #13130f;
--secondary: #31312b;
--secondary-foreground: #dcd9d6;
--cta: #7bbf7a;
--cta-foreground: #13130f;
--ctahover: #8bc78a;
--success: #7d9726;
--success-foreground: #13130f;
--warning: #a5980d;
--warning-foreground: #13130f;
--destructive: #ba6236;
--destructive-foreground: #0d0d0b;
--info: #36a166;
--info-foreground: #13130f;
--busy: #b07d22;
--busy-foreground: #13130f;
--busy-ink: #b07d22;
--border: #464640;
--input: #464640;
--ring: #7bbf7a;
--radius: 0.625rem;
--chart-primary: #7bbf7a;
--chart-secondary: #9d6c7c;
--status-success: #7d9726;
--status-warning: #a5980d;
--status-in-progress: #36a166;
--page-bg: #13130f;
--page-bg-src: '';
--panel-bg: rgb(34 34 27 / 0.72);
--panel-border: rgb(70 70 64 / 0.9);
--panel-border-bare: rgb(70 70 64 / 0.55);
--panel-raised-bg: rgb(34 34 27 / 0.95);
--panel-raised-border: rgb(70 70 64 / 1);
--panel-accent: #7bbf7a;
--panel-header-bg: rgb(49 49 43 / 0.6);
--panel-header-fg: #dcd9d6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #22221b;
--glass-bg: rgb(34 34 27 / 0.55);
--glass-border: rgb(70 70 64 / 0.7);
--dock-bg: rgb(34 34 27 / 0.7);
--dock-border: rgb(70 70 64 / 0.8);
--dock-tooltip-bg: #31312b;
--header-bg: rgb(34 34 27 / 0.8);
--header-border: rgb(70 70 64 / 0.9);
--header-fg: #dcd9d6;
--header-blur: 12px;
--pixel-grid-color: rgb(118 116 111 / 0.07);
--card-grid-color: rgb(118 116 111 / 0.05);
--panel-grid-color: rgb(118 116 111 / 0.06);
--scrollbar-thumb: rgb(108 107 90 / 0.6);
--scrollbar-thumb-hover: rgb(108 107 90 / 0.85);
--scrollbar-thin-thumb: rgb(108 107 90 / 0.45);
--scrollbar-thin-thumb-hover: rgb(108 107 90 / 0.7);
--brand: #7bbf7a;
--brand-foreground: #13130f;
--brand-primary: #7bbf7a;
--brand-accent: #5b9d48;
--brand-warm: #b07d22;
--brand-deep: #13130f;
--brand-muted: #6c6b5a;
--ink: #dcd9d6;
--surface-warm: #22221b;
--naturegreen: #7d9726;
--naturegreen-foreground: #13130f;
--duck-dark: #dcd9d6;
--duck-teal: #5b9d48;
--duck-yellow: #a5980d;
--duck-orange: #b07d22;
--duck-forest: #7d9726;
--duck-beige: #35342e;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Aura — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/bjarneo/omarchy-aura-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='aura'] {
--background: #16171e;
--foreground: #f8f8f2;
--card: #282a36;
--card-foreground: #f8f8f2;
--popover: #282a36;
--popover-foreground: #f8f8f2;
--muted: #373944;
--muted-foreground: #bababa;
--accent: #373944;
--accent-foreground: #f8f8f2;
--primary: #bde0fe;
--primary-foreground: #16171e;
--secondary: #373944;
--secondary-foreground: #f8f8f2;
--cta: #bde0fe;
--cta-foreground: #16171e;
--ctahover: #c5e4fe;
--success: #8be9fd;
--success-foreground: #16171e;
--warning: #cbc3e3;
--warning-foreground: #16171e;
--destructive: #ffafcc;
--destructive-foreground: #16171e;
--info: #8be9fd;
--info-foreground: #16171e;
--busy: #e5b9d8;
--busy-foreground: #16171e;
--busy-ink: #e5b9d8;
--border: #51525b;
--input: #51525b;
--ring: #bde0fe;
--radius: 0.625rem;
--chart-primary: #bde0fe;
--chart-secondary: #ffc8dd;
--status-success: #8be9fd;
--status-warning: #cbc3e3;
--status-in-progress: #8be9fd;
--page-bg: #16171e;
--page-bg-src: '';
--panel-bg: rgb(40 42 54 / 0.72);
--panel-border: rgb(81 82 91 / 0.9);
--panel-border-bare: rgb(81 82 91 / 0.55);
--panel-raised-bg: rgb(40 42 54 / 0.95);
--panel-raised-border: rgb(81 82 91 / 1);
--panel-accent: #bde0fe;
--panel-header-bg: rgb(55 57 68 / 0.6);
--panel-header-fg: #f8f8f2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #282a36;
--glass-bg: rgb(40 42 54 / 0.55);
--glass-border: rgb(81 82 91 / 0.7);
--dock-bg: rgb(40 42 54 / 0.7);
--dock-border: rgb(81 82 91 / 0.8);
--dock-tooltip-bg: #373944;
--header-bg: rgb(40 42 54 / 0.8);
--header-border: rgb(81 82 91 / 0.9);
--header-fg: #f8f8f2;
--header-blur: 12px;
--pixel-grid-color: rgb(134 135 139 / 0.07);
--card-grid-color: rgb(134 135 139 / 0.05);
--panel-grid-color: rgb(134 135 139 / 0.06);
--scrollbar-thumb: rgb(68 71 90 / 0.6);
--scrollbar-thumb-hover: rgb(68 71 90 / 0.85);
--scrollbar-thin-thumb: rgb(68 71 90 / 0.45);
--scrollbar-thin-thumb-hover: rgb(68 71 90 / 0.7);
--brand: #bde0fe;
--brand-foreground: #16171e;
--brand-primary: #bde0fe;
--brand-accent: #8be9fd;
--brand-warm: #e5b9d8;
--brand-deep: #16171e;
--brand-muted: #44475a;
--ink: #f8f8f2;
--surface-warm: #282a36;
--naturegreen: #8be9fd;
--naturegreen-foreground: #16171e;
--duck-dark: #f8f8f2;
--duck-teal: #8be9fd;
--duck-yellow: #cbc3e3;
--duck-orange: #e5b9d8;
--duck-forest: #8be9fd;
--duck-beige: #3d3f49;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Ayu Dark — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/fdidron/omarchy-ayu-dark-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='ayu-dark'] {
--background: #06080b;
--foreground: #bfbdb6;
--card: #0b0e14;
--card-foreground: #bfbdb6;
--popover: #0b0e14;
--popover-foreground: #bfbdb6;
--muted: #1c1f24;
--muted-foreground: #898985;
--accent: #1c1f24;
--accent-foreground: #bfbdb6;
--primary: #95e6cb;
--primary-foreground: #06080b;
--secondary: #1c1f24;
--secondary-foreground: #bfbdb6;
--cta: #95e6cb;
--cta-foreground: #06080b;
--ctahover: #a2e9d1;
--success: #7fd962;
--success-foreground: #06080b;
--warning: #f9af4f;
--warning-foreground: #06080b;
--destructive: #ea6c73;
--destructive-foreground: #06080b;
--info: #53bdfa;
--info-foreground: #06080b;
--busy: #f28e61;
--busy-foreground: #06080b;
--busy-ink: #f28e61;
--border: #2e3034;
--input: #2e3034;
--ring: #95e6cb;
--radius: 0.625rem;
--chart-primary: #95e6cb;
--chart-secondary: #cda1fa;
--status-success: #7fd962;
--status-warning: #f9af4f;
--status-in-progress: #53bdfa;
--page-bg: #06080b;
--page-bg-src: '';
--panel-bg: rgb(11 14 20 / 0.72);
--panel-border: rgb(46 48 52 / 0.9);
--panel-border-bare: rgb(46 48 52 / 0.55);
--panel-raised-bg: rgb(11 14 20 / 0.95);
--panel-raised-border: rgb(46 48 52 / 1);
--panel-accent: #95e6cb;
--panel-header-bg: rgb(28 31 36 / 0.6);
--panel-header-fg: #bfbdb6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0b0e14;
--glass-bg: rgb(11 14 20 / 0.55);
--glass-border: rgb(46 48 52 / 0.7);
--dock-bg: rgb(11 14 20 / 0.7);
--dock-border: rgb(46 48 52 / 0.8);
--dock-tooltip-bg: #1c1f24;
--header-bg: rgb(11 14 20 / 0.8);
--header-border: rgb(46 48 52 / 0.9);
--header-fg: #bfbdb6;
--header-blur: 12px;
--pixel-grid-color: rgb(96 97 97 / 0.07);
--card-grid-color: rgb(96 97 97 / 0.05);
--panel-grid-color: rgb(96 97 97 / 0.06);
--scrollbar-thumb: rgb(104 104 104 / 0.6);
--scrollbar-thumb-hover: rgb(104 104 104 / 0.85);
--scrollbar-thin-thumb: rgb(104 104 104 / 0.45);
--scrollbar-thin-thumb-hover: rgb(104 104 104 / 0.7);
--brand: #95e6cb;
--brand-foreground: #06080b;
--brand-primary: #95e6cb;
--brand-accent: #90e1c6;
--brand-warm: #f28e61;
--brand-deep: #06080b;
--brand-muted: #686868;
--ink: #bfbdb6;
--surface-warm: #0b0e14;
--naturegreen: #7fd962;
--naturegreen-foreground: #06080b;
--duck-dark: #bfbdb6;
--duck-teal: #90e1c6;
--duck-yellow: #f9af4f;
--duck-orange: #f28e61;
--duck-forest: #7fd962;
--duck-beige: #1d2024;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Ayu Light — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Light theme (light), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/fdidron/omarchy-ayu-light-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='ayu-light'] {
--background: #f8f9fa;
--foreground: #5c6166;
--card: #eff0f1;
--card-foreground: #5c6166;
--popover: #eff0f1;
--popover-foreground: #5c6166;
--muted: #e9eaeb;
--muted-foreground: #65686a;
--accent: #e9eaeb;
--accent-foreground: #5c6166;
--primary: #3095db;
--primary-foreground: #272727;
--secondary: #e9eaeb;
--secondary-foreground: #5c6166;
--cta: #3095db;
--cta-foreground: #272727;
--ctahover: #2a83c1;
--success: #6cbf43;
--success-foreground: #3c3f42;
--warning: #eca944;
--warning-foreground: #43464a;
--destructive: #ea6c6d;
--destructive-foreground: #2d2d2d;
--info: #3199e1;
--info-foreground: #272727;
--busy: #eb8b59;
--busy-foreground: #373a3d;
--busy-ink: #a5613e;
--border: #dedfe1;
--input: #d3d4d6;
--ring: #3095db;
--radius: 0.625rem;
--chart-primary: #3095db;
--chart-secondary: #9e75c7;
--status-success: #6cbf43;
--status-warning: #eca944;
--status-in-progress: #3199e1;
--page-bg: #f8f9fa;
--page-bg-src: '';
--panel-bg: rgb(239 240 241 / 0.72);
--panel-border: rgb(222 223 225 / 0.9);
--panel-border-bare: rgb(222 223 225 / 0.55);
--panel-raised-bg: rgb(239 240 241 / 0.95);
--panel-raised-border: rgb(222 223 225 / 1);
--panel-accent: #3095db;
--panel-header-bg: rgb(233 234 235 / 0.6);
--panel-header-fg: #5c6166;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #eff0f1;
--glass-bg: rgb(239 240 241 / 0.55);
--glass-border: rgb(222 223 225 / 0.7);
--dock-bg: rgb(239 240 241 / 0.7);
--dock-border: rgb(222 223 225 / 0.8);
--dock-tooltip-bg: #e9eaeb;
--header-bg: rgb(239 240 241 / 0.8);
--header-border: rgb(222 223 225 / 0.9);
--header-fg: #5c6166;
--header-blur: 12px;
--pixel-grid-color: rgb(134 136 137 / 0.07);
--card-grid-color: rgb(134 136 137 / 0.05);
--panel-grid-color: rgb(134 136 137 / 0.06);
--scrollbar-thumb: rgb(104 104 104 / 0.6);
--scrollbar-thumb-hover: rgb(104 104 104 / 0.85);
--scrollbar-thin-thumb: rgb(104 104 104 / 0.45);
--scrollbar-thin-thumb-hover: rgb(104 104 104 / 0.7);
--brand: #3095db;
--brand-foreground: #272727;
--brand-primary: #3095db;
--brand-accent: #46ba94;
--brand-warm: #eb8b59;
--brand-deep: #e9eaeb;
--brand-muted: #686868;
--ink: #5c6166;
--surface-warm: #eff0f1;
--naturegreen: #6cbf43;
--naturegreen-foreground: #3c3f42;
--duck-dark: #5c6166;
--duck-teal: #46ba94;
--duck-yellow: #eca944;
--duck-orange: #eb8b59;
--duck-forest: #6cbf43;
--duck-beige: #e0e2e3;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Ayu Mirage — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/fdidron/omarchy-ayu-mirage-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='ayu-mirage'] {
--background: #11141a;
--foreground: #cccac2;
--card: #1f2430;
--card-foreground: #cccac2;
--popover: #1f2430;
--popover-foreground: #cccac2;
--muted: #2f333e;
--muted-foreground: #9b9b99;
--accent: #2f333e;
--accent-foreground: #cccac2;
--primary: #95e6cb;
--primary-foreground: #11141a;
--secondary: #2f333e;
--secondary-foreground: #cccac2;
--cta: #95e6cb;
--cta-foreground: #11141a;
--ctahover: #a2e9d1;
--success: #87d96c;
--success-foreground: #11141a;
--warning: #facc6e;
--warning-foreground: #11141a;
--destructive: #ed8274;
--destructive-foreground: #11141a;
--info: #6dcbfa;
--info-foreground: #11141a;
--busy: #f4a771;
--busy-foreground: #11141a;
--busy-ink: #f4a771;
--border: #41444d;
--input: #41444d;
--ring: #95e6cb;
--radius: 0.625rem;
--chart-primary: #95e6cb;
--chart-secondary: #dabafa;
--status-success: #87d96c;
--status-warning: #facc6e;
--status-in-progress: #6dcbfa;
--page-bg: #11141a;
--page-bg-src: '';
--panel-bg: rgb(31 36 48 / 0.72);
--panel-border: rgb(65 68 77 / 0.9);
--panel-border-bare: rgb(65 68 77 / 0.55);
--panel-raised-bg: rgb(31 36 48 / 0.95);
--panel-raised-border: rgb(65 68 77 / 1);
--panel-accent: #95e6cb;
--panel-header-bg: rgb(47 51 62 / 0.6);
--panel-header-fg: #cccac2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1f2430;
--glass-bg: rgb(31 36 48 / 0.55);
--glass-border: rgb(65 68 77 / 0.7);
--dock-bg: rgb(31 36 48 / 0.7);
--dock-border: rgb(65 68 77 / 0.8);
--dock-tooltip-bg: #2f333e;
--header-bg: rgb(31 36 48 / 0.8);
--header-border: rgb(65 68 77 / 0.9);
--header-fg: #cccac2;
--header-blur: 12px;
--pixel-grid-color: rgb(109 111 114 / 0.07);
--card-grid-color: rgb(109 111 114 / 0.05);
--panel-grid-color: rgb(109 111 114 / 0.06);
--scrollbar-thumb: rgb(104 104 104 / 0.6);
--scrollbar-thumb-hover: rgb(104 104 104 / 0.85);
--scrollbar-thin-thumb: rgb(104 104 104 / 0.45);
--scrollbar-thin-thumb-hover: rgb(104 104 104 / 0.7);
--brand: #95e6cb;
--brand-foreground: #11141a;
--brand-primary: #95e6cb;
--brand-accent: #90e1c6;
--brand-warm: #f4a771;
--brand-deep: #11141a;
--brand-muted: #686868;
--ink: #cccac2;
--surface-warm: #1f2430;
--naturegreen: #87d96c;
--naturegreen-foreground: #11141a;
--duck-dark: #cccac2;
--duck-teal: #90e1c6;
--duck-yellow: #facc6e;
--duck-orange: #f4a771;
--duck-forest: #87d96c;
--duck-beige: #30353f;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Azure Glow — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/Hydradevx/omarchy-azure-glow-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='azure-glow'] {
--background: #06080e;
--foreground: #a8dfff;
--card: #0a0f1a;
--card-foreground: #a8dfff;
--popover: #0a0f1a;
--popover-foreground: #a8dfff;
--muted: #1b202a;
--muted-foreground: #79a1ba;
--accent: #1b202a;
--accent-foreground: #a8dfff;
--primary: #99eeff;
--primary-foreground: #06080e;
--secondary: #1b202a;
--secondary-foreground: #a8dfff;
--cta: #99eeff;
--cta-foreground: #06080e;
--ctahover: #a5f0ff;
--success: #00e0b8;
--success-foreground: #06080e;
--warning: #33ccff;
--warning-foreground: #06080e;
--destructive: #0099cc;
--destructive-foreground: #06080e;
--info: #00aaff;
--info-foreground: #06080e;
--busy: #1ab3e6;
--busy-foreground: #06080e;
--busy-ink: #1ab3e6;
--border: #293847;
--input: #293847;
--ring: #99eeff;
--radius: 0.625rem;
--chart-primary: #99eeff;
--chart-secondary: #3399ff;
--status-success: #00e0b8;
--status-warning: #33ccff;
--status-in-progress: #00aaff;
--page-bg: #06080e;
--page-bg-src: '';
--panel-bg: rgb(10 15 26 / 0.72);
--panel-border: rgb(41 56 71 / 0.9);
--panel-border-bare: rgb(41 56 71 / 0.55);
--panel-raised-bg: rgb(10 15 26 / 0.95);
--panel-raised-border: rgb(41 56 71 / 1);
--panel-accent: #99eeff;
--panel-header-bg: rgb(27 32 42 / 0.6);
--panel-header-fg: #a8dfff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0a0f1a;
--glass-bg: rgb(10 15 26 / 0.55);
--glass-border: rgb(41 56 71 / 0.7);
--dock-bg: rgb(10 15 26 / 0.7);
--dock-border: rgb(41 56 71 / 0.8);
--dock-tooltip-bg: #1b202a;
--header-bg: rgb(10 15 26 / 0.8);
--header-border: rgb(41 56 71 / 0.9);
--header-fg: #a8dfff;
--header-blur: 12px;
--pixel-grid-color: rgb(81 109 129 / 0.07);
--card-grid-color: rgb(81 109 129 / 0.05);
--panel-grid-color: rgb(81 109 129 / 0.06);
--scrollbar-thumb: rgb(18 50 71 / 0.6);
--scrollbar-thumb-hover: rgb(18 50 71 / 0.85);
--scrollbar-thin-thumb: rgb(18 50 71 / 0.45);
--scrollbar-thin-thumb-hover: rgb(18 50 71 / 0.7);
--brand: #99eeff;
--brand-foreground: #06080e;
--brand-primary: #99eeff;
--brand-accent: #66e0ff;
--brand-warm: #1ab3e6;
--brand-deep: #06080e;
--brand-muted: #123247;
--ink: #a8dfff;
--surface-warm: #0a0f1a;
--naturegreen: #00e0b8;
--naturegreen-foreground: #06080e;
--duck-dark: #a8dfff;
--duck-teal: #66e0ff;
--duck-yellow: #33ccff;
--duck-orange: #1ab3e6;
--duck-forest: #00e0b8;
--duck-beige: #1a2431;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Azure — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/tahfizhabib/omarchy-azure-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='azure'] {
--background: #0d141f;
--foreground: #eff3f8;
--card: #182538;
--card-foreground: #eff3f8;
--popover: #182538;
--popover-foreground: #eff3f8;
--muted: #283446;
--muted-foreground: #afb5be;
--accent: #283446;
--accent-foreground: #eff3f8;
--primary: #cad4e7;
--primary-foreground: #0d141f;
--secondary: #283446;
--secondary-foreground: #eff3f8;
--cta: #cad4e7;
--cta-foreground: #0d141f;
--ctahover: #d0d9ea;
--success: #61a7d6;
--success-foreground: #0d141f;
--warning: #6cabda;
--warning-foreground: #0d141f;
--destructive: #969293;
--destructive-foreground: #0d141f;
--info: #8da1c8;
--info-foreground: #0d141f;
--busy: #819fb7;
--busy-foreground: #0d141f;
--busy-ink: #819fb7;
--border: #424d5e;
--input: #424d5e;
--ring: #cad4e7;
--radius: 0.625rem;
--chart-primary: #cad4e7;
--chart-secondary: #a0adc5;
--status-success: #61a7d6;
--status-warning: #6cabda;
--status-in-progress: #8da1c8;
--page-bg: #0d141f;
--page-bg-src: '';
--panel-bg: rgb(24 37 56 / 0.72);
--panel-border: rgb(66 77 94 / 0.9);
--panel-border-bare: rgb(66 77 94 / 0.55);
--panel-raised-bg: rgb(24 37 56 / 0.95);
--panel-raised-border: rgb(66 77 94 / 1);
--panel-accent: #cad4e7;
--panel-header-bg: rgb(40 52 70 / 0.6);
--panel-header-fg: #eff3f8;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #182538;
--glass-bg: rgb(24 37 56 / 0.55);
--glass-border: rgb(66 77 94 / 0.7);
--dock-bg: rgb(24 37 56 / 0.7);
--dock-border: rgb(66 77 94 / 0.8);
--dock-tooltip-bg: #283446;
--header-bg: rgb(24 37 56 / 0.8);
--header-border: rgb(66 77 94 / 0.9);
--header-fg: #eff3f8;
--header-blur: 12px;
--pixel-grid-color: rgb(121 130 142 / 0.07);
--card-grid-color: rgb(121 130 142 / 0.05);
--panel-grid-color: rgb(121 130 142 / 0.06);
--scrollbar-thumb: rgb(73 88 109 / 0.6);
--scrollbar-thumb-hover: rgb(73 88 109 / 0.85);
--scrollbar-thin-thumb: rgb(73 88 109 / 0.45);
--scrollbar-thin-thumb-hover: rgb(73 88 109 / 0.7);
--brand: #cad4e7;
--brand-foreground: #0d141f;
--brand-primary: #cad4e7;
--brand-accent: #5693c4;
--brand-warm: #819fb7;
--brand-deep: #0d141f;
--brand-muted: #49586d;
--ink: #eff3f8;
--surface-warm: #182538;
--naturegreen: #61a7d6;
--naturegreen-foreground: #0d141f;
--duck-dark: #eff3f8;
--duck-teal: #5693c4;
--duck-yellow: #6cabda;
--duck-orange: #819fb7;
--duck-forest: #61a7d6;
--duck-beige: #2e3a4b;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Bad Hand — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/bjornramberg/omarchy-bad-hand-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='bad-hand'] {
--background: #110b09;
--foreground: #bdb09d;
--card: #1e1411;
--card-foreground: #bdb09d;
--popover: #1e1411;
--popover-foreground: #bdb09d;
--muted: #2e2422;
--muted-foreground: #988e81;
--accent: #2e2422;
--accent-foreground: #bdb09d;
--primary: #f5efe1;
--primary-foreground: #110b09;
--secondary: #2e2422;
--secondary-foreground: #bdb09d;
--cta: #f5efe1;
--cta-foreground: #110b09;
--ctahover: #f6f1e5;
--success: #921416;
--success-foreground: #c2b6a4;
--warning: #856839;
--warning-foreground: #f3f3f3;
--destructive: #5a0a0a;
--destructive-foreground: #bdb09d;
--info: #4d3a2b;
--info-foreground: #bdb09d;
--busy: #703922;
--busy-foreground: #c2b6a4;
--busy-ink: #9b7464;
--border: #3d332c;
--input: #3d332c;
--ring: #f5efe1;
--radius: 0.625rem;
--chart-primary: #f5efe1;
--chart-secondary: #c2b28c;
--status-success: #921416;
--status-warning: #856839;
--status-in-progress: #4d3a2b;
--page-bg: #110b09;
--page-bg-src: '';
--panel-bg: rgb(30 20 17 / 0.72);
--panel-border: rgb(61 51 44 / 0.9);
--panel-border-bare: rgb(61 51 44 / 0.55);
--panel-raised-bg: rgb(30 20 17 / 0.95);
--panel-raised-border: rgb(61 51 44 / 1);
--panel-accent: #f5efe1;
--panel-header-bg: rgb(46 36 34 / 0.6);
--panel-header-fg: #bdb09d;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1e1411;
--glass-bg: rgb(30 20 17 / 0.55);
--glass-border: rgb(61 51 44 / 0.7);
--dock-bg: rgb(30 20 17 / 0.7);
--dock-border: rgb(61 51 44 / 0.8);
--dock-tooltip-bg: #2e2422;
--header-bg: rgb(30 20 17 / 0.8);
--header-border: rgb(61 51 44 / 0.9);
--header-fg: #bdb09d;
--header-blur: 12px;
--pixel-grid-color: rgb(110 98 89 / 0.07);
--card-grid-color: rgb(110 98 89 / 0.05);
--panel-grid-color: rgb(110 98 89 / 0.06);
--scrollbar-thumb: rgb(53 41 36 / 0.6);
--scrollbar-thumb-hover: rgb(53 41 36 / 0.85);
--scrollbar-thin-thumb: rgb(53 41 36 / 0.45);
--scrollbar-thin-thumb-hover: rgb(53 41 36 / 0.7);
--brand: #f5efe1;
--brand-foreground: #110b09;
--brand-primary: #f5efe1;
--brand-accent: #e4d8c1;
--brand-warm: #703922;
--brand-deep: #110b09;
--brand-muted: #352924;
--ink: #bdb09d;
--surface-warm: #1e1411;
--naturegreen: #921416;
--naturegreen-foreground: #c2b6a4;
--duck-dark: #bdb09d;
--duck-teal: #e4d8c1;
--duck-yellow: #856839;
--duck-orange: #703922;
--duck-forest: #921416;
--duck-beige: #2e241f;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Batman — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/OldJobobo/omarchy-batman-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='batman'] {
--background: #0f1011;
--foreground: #a7a8a3;
--card: #1b1d1e;
--card-foreground: #a7a8a3;
--popover: #1b1d1e;
--popover-foreground: #a7a8a3;
--muted: #2b2d2e;
--muted-foreground: #949592;
--accent: #2b2d2e;
--accent-foreground: #a7a8a3;
--primary: #a2a2a5;
--primary-foreground: #0f1011;
--secondary: #2b2d2e;
--secondary-foreground: #a7a8a3;
--cta: #a2a2a5;
--cta-foreground: #0f1011;
--ctahover: #adadb0;
--success: #c8be46;
--success-foreground: #0f1011;
--warning: #f3fd21;
--warning-foreground: #0f1011;
--destructive: #e6db43;
--destructive-foreground: #0f1011;
--info: #737074;
--info-foreground: #f9f9f9;
--busy: #edec32;
--busy-foreground: #0f1011;
--busy-ink: #edec32;
--border: #363838;
--input: #363838;
--ring: #a2a2a5;
--radius: 0.625rem;
--chart-primary: #a2a2a5;
--chart-secondary: #737271;
--status-success: #c8be46;
--status-warning: #f3fd21;
--status-in-progress: #737074;
--page-bg: #0f1011;
--page-bg-src: '';
--panel-bg: rgb(27 29 30 / 0.72);
--panel-border: rgb(54 56 56 / 0.9);
--panel-border-bare: rgb(54 56 56 / 0.55);
--panel-raised-bg: rgb(27 29 30 / 0.95);
--panel-raised-border: rgb(54 56 56 / 1);
--panel-accent: #a2a2a5;
--panel-header-bg: rgb(43 45 46 / 0.6);
--panel-header-fg: #a7a8a3;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1b1d1e;
--glass-bg: rgb(27 29 30 / 0.55);
--glass-border: rgb(54 56 56 / 0.7);
--dock-bg: rgb(27 29 30 / 0.7);
--dock-border: rgb(54 56 56 / 0.8);
--dock-tooltip-bg: #2b2d2e;
--header-bg: rgb(27 29 30 / 0.8);
--header-border: rgb(54 56 56 / 0.9);
--header-fg: #a7a8a3;
--header-blur: 12px;
--pixel-grid-color: rgb(102 104 102 / 0.07);
--card-grid-color: rgb(102 104 102 / 0.05);
--panel-grid-color: rgb(102 104 102 / 0.06);
--scrollbar-thumb: rgb(109 111 110 / 0.6);
--scrollbar-thumb-hover: rgb(109 111 110 / 0.85);
--scrollbar-thin-thumb: rgb(109 111 110 / 0.45);
--scrollbar-thin-thumb-hover: rgb(109 111 110 / 0.7);
--brand: #a2a2a5;
--brand-foreground: #0f1011;
--brand-primary: #a2a2a5;
--brand-accent: #615f5e;
--brand-warm: #edec32;
--brand-deep: #0f1011;
--brand-muted: #6d6f6e;
--ink: #a7a8a3;
--surface-warm: #1b1d1e;
--naturegreen: #c8be46;
--naturegreen-foreground: #0f1011;
--duck-dark: #a7a8a3;
--duck-teal: #615f5e;
--duck-yellow: #f3fd21;
--duck-orange: #edec32;
--duck-forest: #c8be46;
--duck-beige: #292b2b;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Batou — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/HANCORE-linux/omarchy-batou-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='batou'] {
--background: #0a0a0a;
--foreground: #a4a4a4;
--card: #121212;
--card-foreground: #a4a4a4;
--popover: #121212;
--popover-foreground: #a4a4a4;
--muted: #232323;
--muted-foreground: #8c8c8c;
--accent: #232323;
--accent-foreground: #a4a4a4;
--primary: #c2bbb0;
--primary-foreground: #0a0a0a;
--secondary: #232323;
--secondary-foreground: #a4a4a4;
--cta: #c2bbb0;
--cta-foreground: #0a0a0a;
--ctahover: #c9c3b9;
--success: #a5a297;
--success-foreground: #0a0a0a;
--warning: #c6c4b0;
--warning-foreground: #0a0a0a;
--destructive: #e19e74;
--destructive-foreground: #0a0a0a;
--info: #605d5b;
--info-foreground: #dadada;
--busy: #d4b192;
--busy-foreground: #0a0a0a;
--busy-ink: #d4b192;
--border: #2f2f2f;
--input: #2f2f2f;
--ring: #c2bbb0;
--radius: 0.625rem;
--chart-primary: #c2bbb0;
--chart-secondary: #8a8575;
--status-success: #a5a297;
--status-warning: #c6c4b0;
--status-in-progress: #605d5b;
--page-bg: #0a0a0a;
--page-bg-src: '';
--panel-bg: rgb(18 18 18 / 0.72);
--panel-border: rgb(47 47 47 / 0.9);
--panel-border-bare: rgb(47 47 47 / 0.55);
--panel-raised-bg: rgb(18 18 18 / 0.95);
--panel-raised-border: rgb(47 47 47 / 1);
--panel-accent: #c2bbb0;
--panel-header-bg: rgb(35 35 35 / 0.6);
--panel-header-fg: #a4a4a4;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #121212;
--glass-bg: rgb(18 18 18 / 0.55);
--glass-border: rgb(47 47 47 / 0.7);
--dock-bg: rgb(18 18 18 / 0.7);
--dock-border: rgb(47 47 47 / 0.8);
--dock-tooltip-bg: #232323;
--header-bg: rgb(18 18 18 / 0.8);
--header-border: rgb(47 47 47 / 0.9);
--header-fg: #a4a4a4;
--header-blur: 12px;
--pixel-grid-color: rgb(97 97 97 / 0.07);
--card-grid-color: rgb(97 97 97 / 0.05);
--panel-grid-color: rgb(97 97 97 / 0.06);
--scrollbar-thumb: rgb(56 55 53 / 0.6);
--scrollbar-thumb-hover: rgb(56 55 53 / 0.85);
--scrollbar-thin-thumb: rgb(56 55 53 / 0.45);
--scrollbar-thin-thumb-hover: rgb(56 55 53 / 0.7);
--brand: #c2bbb0;
--brand-foreground: #0a0a0a;
--brand-primary: #c2bbb0;
--brand-accent: #c2bbb0;
--brand-warm: #d4b192;
--brand-deep: #0a0a0a;
--brand-muted: #383735;
--ink: #a4a4a4;
--surface-warm: #121212;
--naturegreen: #a5a297;
--naturegreen-foreground: #0a0a0a;
--duck-dark: #a4a4a4;
--duck-teal: #c2bbb0;
--duck-yellow: #c6c4b0;
--duck-orange: #d4b192;
--duck-forest: #a5a297;
--duck-beige: #212121;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Bauhaus — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/somerocketeer/omarchy-bauhaus-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='bauhaus'] {
--background: #090a0d;
--foreground: #eaeff5;
--card: #101318;
--card-foreground: #eaeff5;
--popover: #101318;
--popover-foreground: #eaeff5;
--muted: #212428;
--muted-foreground: #a9adb3;
--accent: #212428;
--accent-foreground: #eaeff5;
--primary: #809d9e;
--primary-foreground: #090a0d;
--secondary: #212428;
--secondary-foreground: #eaeff5;
--cta: #809d9e;
--cta-foreground: #090a0d;
--ctahover: #8fa9aa;
--success: #789fa2;
--success-foreground: #090a0d;
--warning: #e7a46f;
--warning-foreground: #090a0d;
--destructive: #cb886d;
--destructive-foreground: #090a0d;
--info: #8999aa;
--info-foreground: #090a0d;
--busy: #d9966e;
--busy-foreground: #090a0d;
--busy-ink: #d9966e;
--border: #3b3e43;
--input: #3b3e43;
--ring: #809d9e;
--radius: 0.625rem;
--chart-primary: #809d9e;
--chart-secondary: #8d758f;
--status-success: #789fa2;
--status-warning: #e7a46f;
--status-in-progress: #8999aa;
--page-bg: #090a0d;
--page-bg-src: '';
--panel-bg: rgb(16 19 24 / 0.72);
--panel-border: rgb(59 62 67 / 0.9);
--panel-border-bare: rgb(59 62 67 / 0.55);
--panel-raised-bg: rgb(16 19 24 / 0.95);
--panel-raised-border: rgb(59 62 67 / 1);
--panel-accent: #809d9e;
--panel-header-bg: rgb(33 36 40 / 0.6);
--panel-header-fg: #eaeff5;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #101318;
--glass-bg: rgb(16 19 24 / 0.55);
--glass-border: rgb(59 62 67 / 0.7);
--dock-bg: rgb(16 19 24 / 0.7);
--dock-border: rgb(59 62 67 / 0.8);
--dock-tooltip-bg: #212428;
--header-bg: rgb(16 19 24 / 0.8);
--header-border: rgb(59 62 67 / 0.9);
--header-fg: #eaeff5;
--header-blur: 12px;
--pixel-grid-color: rgb(114 118 123 / 0.07);
--card-grid-color: rgb(114 118 123 / 0.05);
--panel-grid-color: rgb(114 118 123 / 0.06);
--scrollbar-thumb: rgb(58 63 76 / 0.6);
--scrollbar-thumb-hover: rgb(58 63 76 / 0.85);
--scrollbar-thin-thumb: rgb(58 63 76 / 0.45);
--scrollbar-thin-thumb-hover: rgb(58 63 76 / 0.7);
--brand: #809d9e;
--brand-foreground: #090a0d;
--brand-primary: #809d9e;
--brand-accent: #809d9e;
--brand-warm: #d9966e;
--brand-deep: #090a0d;
--brand-muted: #3a3f4c;
--ink: #eaeff5;
--surface-warm: #101318;
--naturegreen: #789fa2;
--naturegreen-foreground: #090a0d;
--duck-dark: #eaeff5;
--duck-teal: #809d9e;
--duck-yellow: #e7a46f;
--duck-orange: #d9966e;
--duck-forest: #789fa2;
--duck-beige: #26292e;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Beachvan — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/haripako/omarchy-BeachVan-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='beachvan'] {
--background: #0f181d;
--foreground: #dee3eb;
--card: #1b2b34;
--card-foreground: #dee3eb;
--popover: #1b2b34;
--popover-foreground: #dee3eb;
--muted: #2b3a42;
--muted-foreground: #a4acb4;
--accent: #2b3a42;
--accent-foreground: #dee3eb;
--primary: #66b0af;
--primary-foreground: #0f181d;
--secondary: #2b3a42;
--secondary-foreground: #dee3eb;
--cta: #66b0af;
--cta-foreground: #0f181d;
--ctahover: #78b9b9;
--success: #9fc69a;
--success-foreground: #0f181d;
--warning: #f8c76b;
--warning-foreground: #0f181d;
--destructive: #ea6770;
--destructive-foreground: #0f181d;
--info: #6d9dca;
--info-foreground: #0f181d;
--busy: #f1976e;
--busy-foreground: #0f181d;
--busy-ink: #f1976e;
--border: #414f58;
--input: #414f58;
--ring: #66b0af;
--radius: 0.625rem;
--chart-primary: #66b0af;
--chart-secondary: #c49ac4;
--status-success: #9fc69a;
--status-warning: #f8c76b;
--status-in-progress: #6d9dca;
--page-bg: #0f181d;
--page-bg-src: '';
--panel-bg: rgb(27 43 52 / 0.72);
--panel-border: rgb(65 79 88 / 0.9);
--panel-border-bare: rgb(65 79 88 / 0.55);
--panel-raised-bg: rgb(27 43 52 / 0.95);
--panel-raised-border: rgb(65 79 88 / 1);
--panel-accent: #66b0af;
--panel-header-bg: rgb(43 58 66 / 0.6);
--panel-header-fg: #dee3eb;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1b2b34;
--glass-bg: rgb(27 43 52 / 0.55);
--glass-border: rgb(65 79 88 / 0.7);
--dock-bg: rgb(27 43 52 / 0.7);
--dock-border: rgb(65 79 88 / 0.8);
--dock-tooltip-bg: #2b3a42;
--header-bg: rgb(27 43 52 / 0.8);
--header-border: rgb(65 79 88 / 0.9);
--header-fg: #dee3eb;
--header-blur: 12px;
--pixel-grid-color: rgb(115 126 134 / 0.07);
--card-grid-color: rgb(115 126 134 / 0.05);
--panel-grid-color: rgb(115 126 134 / 0.06);
--scrollbar-thumb: rgb(106 116 124 / 0.6);
--scrollbar-thumb-hover: rgb(106 116 124 / 0.85);
--scrollbar-thin-thumb: rgb(106 116 124 / 0.45);
--scrollbar-thin-thumb-hover: rgb(106 116 124 / 0.7);
--brand: #66b0af;
--brand-foreground: #0f181d;
--brand-primary: #66b0af;
--brand-accent: #66b0af;
--brand-warm: #f1976e;
--brand-deep: #0f181d;
--brand-muted: #6a747c;
--ink: #dee3eb;
--surface-warm: #1b2b34;
--naturegreen: #9fc69a;
--naturegreen-foreground: #0f181d;
--duck-dark: #dee3eb;
--duck-teal: #66b0af;
--duck-yellow: #f8c76b;
--duck-orange: #f1976e;
--duck-forest: #9fc69a;
--duck-beige: #2f3d46;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Beta — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/jjdizz1l/beta (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='beta'] {
--background: #120212;
--foreground: #ffeca0;
--card: #210321;
--card-foreground: #ffeca0;
--popover: #210321;
--popover-foreground: #ffeca0;
--muted: #311531;
--muted-foreground: #bca67a;
--accent: #311531;
--accent-foreground: #ffeca0;
--primary: #dce9fe;
--primary-foreground: #120212;
--secondary: #311531;
--secondary-foreground: #ffeca0;
--cta: #dce9fe;
--cta-foreground: #120212;
--ctahover: #e0ecfe;
--success: #7ad573;
--success-foreground: #120212;
--warning: #f2c256;
--warning-foreground: #120212;
--destructive: #e71852;
--destructive-foreground: #0d010d;
--info: #5090f8;
--info-foreground: #120212;
--busy: #ed6d54;
--busy-foreground: #120212;
--busy-ink: #ed6d54;
--border: #4c313a;
--input: #4c313a;
--ring: #dce9fe;
--radius: 0.625rem;
--chart-primary: #dce9fe;
--chart-secondary: #9d58f0;
--status-success: #7ad573;
--status-warning: #f2c256;
--status-in-progress: #5090f8;
--page-bg: #120212;
--page-bg-src: '';
--panel-bg: rgb(33 3 33 / 0.72);
--panel-border: rgb(76 49 58 / 0.9);
--panel-border-bare: rgb(76 49 58 / 0.55);
--panel-raised-bg: rgb(33 3 33 / 0.95);
--panel-raised-border: rgb(76 49 58 / 1);
--panel-accent: #dce9fe;
--panel-header-bg: rgb(49 21 49 / 0.6);
--panel-header-fg: #ffeca0;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #210321;
--glass-bg: rgb(33 3 33 / 0.55);
--glass-border: rgb(76 49 58 / 0.7);
--dock-bg: rgb(33 3 33 / 0.7);
--dock-border: rgb(76 49 58 / 0.8);
--dock-tooltip-bg: #311531;
--header-bg: rgb(33 3 33 / 0.8);
--header-border: rgb(76 49 58 / 0.9);
--header-fg: #ffeca0;
--header-blur: 12px;
--pixel-grid-color: rgb(133 108 90 / 0.07);
--card-grid-color: rgb(133 108 90 / 0.05);
--panel-grid-color: rgb(133 108 90 / 0.06);
--scrollbar-thumb: rgb(171 84 136 / 0.6);
--scrollbar-thumb-hover: rgb(171 84 136 / 0.85);
--scrollbar-thin-thumb: rgb(171 84 136 / 0.45);
--scrollbar-thin-thumb-hover: rgb(171 84 136 / 0.7);
--brand: #dce9fe;
--brand-foreground: #120212;
--brand-primary: #dce9fe;
--brand-accent: #53d0bc;
--brand-warm: #ed6d54;
--brand-deep: #120212;
--brand-muted: #ab5488;
--ink: #ffeca0;
--surface-warm: #210321;
--naturegreen: #7ad573;
--naturegreen-foreground: #120212;
--duck-dark: #ffeca0;
--duck-teal: #53d0bc;
--duck-yellow: #f2c256;
--duck-orange: #ed6d54;
--duck-forest: #7ad573;
--duck-beige: #371a2e;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Biscuit DE Mar Dark — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark) — the source repo's own colors.toml says so directly.
Source: https://github.com/OldJobobo/omarchy-biscuit-de-mar-dark-theme (colors.toml (Omarchy schema)), read by scripts/port-community-themes.ts. Values
came straight from the repo's own colors.toml — the same schema and reader the official 22 themes
use, so nothing here is reconstructed. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='biscuit-de-mar-dark'] {
--background: #1a1515;
--foreground: #dcc9bc;
--card: #1a1515;
--card-foreground: #dcc9bc;
--popover: #1a1515;
--popover-foreground: #dcc9bc;
--muted: #2d2424;
--muted-foreground: #ffe9c7;
--accent: #2d2424;
--accent-foreground: #dcc9bc;
--primary: #ae3f82;
--primary-foreground: #ffe9c7;
--secondary: #2d2424;
--secondary-foreground: #dcc9bc;
--cta: #ae3f82;
--cta-foreground: #ffe9c7;
--ctahover: #b85691;
--success: #768f80;
--success-foreground: #1a1515;
--warning: #959a6b;
--warning-foreground: #1a1515;
--destructive: #f07342;
--destructive-foreground: #1a1515;
--info: #614f76;
--info-foreground: #ffe9c7;
--busy: #e39c45;
--busy-foreground: #1a1515;
--busy-ink: #e39c45;
--border: #5a5047;
--input: #5a5047;
--ring: #ae3f82;
--radius: 0.625rem;
--chart-primary: #ae3f82;
--chart-secondary: #7b3d79;
--status-success: #768f80;
--status-warning: #959a6b;
--status-in-progress: #614f76;
--page-bg: #1a1515;
--page-bg-src: '';
--panel-bg: rgb(26 21 21 / 0.72);
--panel-border: rgb(90 80 71 / 0.9);
--panel-border-bare: rgb(90 80 71 / 0.55);
--panel-raised-bg: rgb(26 21 21 / 0.95);
--panel-raised-border: rgb(90 80 71 / 1);
--panel-accent: #ae3f82;
--panel-header-bg: rgb(45 36 36 / 0.6);
--panel-header-fg: #dcc9bc;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1515;
--glass-bg: rgb(26 21 21 / 0.55);
--glass-border: rgb(90 80 71 / 0.7);
--dock-bg: rgb(26 21 21 / 0.7);
--dock-border: rgb(90 80 71 / 0.8);
--dock-tooltip-bg: #2d2424;
--header-bg: rgb(26 21 21 / 0.8);
--header-border: rgb(90 80 71 / 0.9);
--header-fg: #dcc9bc;
--header-blur: 12px;
--pixel-grid-color: rgb(156 129 129 / 0.07);
--card-grid-color: rgb(156 129 129 / 0.05);
--panel-grid-color: rgb(156 129 129 / 0.06);
--scrollbar-thumb: rgb(114 90 90 / 0.6);
--scrollbar-thumb-hover: rgb(114 90 90 / 0.85);
--scrollbar-thin-thumb: rgb(114 90 90 / 0.45);
--scrollbar-thin-thumb-hover: rgb(114 90 90 / 0.7);
--brand: #ae3f82;
--brand-foreground: #ffe9c7;
--brand-primary: #ae3f82;
--brand-accent: #756d94;
--brand-warm: #e39c45;
--brand-deep: #1a1515;
--brand-muted: #725a5a;
--ink: #dcc9bc;
--surface-warm: #1a1515;
--naturegreen: #768f80;
--naturegreen-foreground: #1a1515;
--duck-dark: #dcc9bc;
--duck-teal: #756d94;
--duck-yellow: #959a6b;
--duck-orange: #e39c45;
--duck-forest: #768f80;
--duck-beige: #2d2726;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Black Arch — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/ankur311sudo/black_arch (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='black-arch'] {
--background: #000000;
--foreground: #ffffff;
--card: #000000;
--card-foreground: #ffffff;
--popover: #000000;
--popover-foreground: #ffffff;
--muted: #121212;
--muted-foreground: #b3b3b3;
--accent: #121212;
--accent-foreground: #ffffff;
--primary: #989898;
--primary-foreground: #000000;
--secondary: #121212;
--secondary-foreground: #ffffff;
--cta: #989898;
--cta-foreground: #000000;
--ctahover: #a4a4a4;
--success: #8e8e8e;
--success-foreground: #000000;
--warning: #b9b9b9;
--warning-foreground: #000000;
--destructive: #646464;
--destructive-foreground: #ffffff;
--info: #989898;
--info-foreground: #000000;
--busy: #8f8f8f;
--busy-foreground: #000000;
--busy-ink: #8f8f8f;
--border: #323232;
--input: #323232;
--ring: #989898;
--radius: 0.625rem;
--chart-primary: #989898;
--chart-secondary: #747474;
--status-success: #8e8e8e;
--status-warning: #b9b9b9;
--status-in-progress: #989898;
--page-bg: #000000;
--page-bg-src: '';
--panel-bg: rgb(0 0 0 / 0.72);
--panel-border: rgb(50 50 50 / 0.9);
--panel-border-bare: rgb(50 50 50 / 0.55);
--panel-raised-bg: rgb(0 0 0 / 0.95);
--panel-raised-border: rgb(50 50 50 / 1);
--panel-accent: #989898;
--panel-header-bg: rgb(18 18 18 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #000000;
--glass-bg: rgb(0 0 0 / 0.55);
--glass-border: rgb(50 50 50 / 0.7);
--dock-bg: rgb(0 0 0 / 0.7);
--dock-border: rgb(50 50 50 / 0.8);
--dock-tooltip-bg: #121212;
--header-bg: rgb(0 0 0 / 0.8);
--header-border: rgb(50 50 50 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(115 115 115 / 0.07);
--card-grid-color: rgb(115 115 115 / 0.05);
--panel-grid-color: rgb(115 115 115 / 0.06);
--scrollbar-thumb: rgb(38 38 38 / 0.6);
--scrollbar-thumb-hover: rgb(38 38 38 / 0.85);
--scrollbar-thin-thumb: rgb(38 38 38 / 0.45);
--scrollbar-thin-thumb-hover: rgb(38 38 38 / 0.7);
--brand: #989898;
--brand-foreground: #000000;
--brand-primary: #989898;
--brand-accent: #585858;
--brand-warm: #8f8f8f;
--brand-deep: #000000;
--brand-muted: #262626;
--ink: #ffffff;
--surface-warm: #000000;
--naturegreen: #8e8e8e;
--naturegreen-foreground: #000000;
--duck-dark: #ffffff;
--duck-teal: #585858;
--duck-yellow: #b9b9b9;
--duck-orange: #8f8f8f;
--duck-forest: #8e8e8e;
--duck-beige: #1a1a1a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Black Gold — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/HANCORE-linux/omarchy-blackgold-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='black-gold'] {
--background: #070707;
--foreground: #ebdbb2;
--card: #0d0d0d;
--card-foreground: #ebdbb2;
--popover: #0d0d0d;
--popover-foreground: #ebdbb2;
--muted: #1e1e1e;
--muted-foreground: #a89d81;
--accent: #1e1e1e;
--accent-foreground: #ebdbb2;
--primary: #7a6a2c;
--primary-foreground: #f4ebd5;
--secondary: #1e1e1e;
--secondary-foreground: #ebdbb2;
--cta: #7a6a2c;
--cta-foreground: #f4ebd5;
--ctahover: #8a7c45;
--success: #a3850e;
--success-foreground: #070707;
--warning: #4d574e;
--warning-foreground: #ebdbb2;
--destructive: #d35f5f;
--destructive-foreground: #070707;
--info: #6e6a58;
--info-foreground: #f4ead3;
--busy: #905b57;
--busy-foreground: #f2e8cd;
--busy-ink: #9b6b68;
--border: #38352d;
--input: #38352d;
--ring: #7a6a2c;
--radius: 0.625rem;
--chart-primary: #7a6a2c;
--chart-secondary: #bfa75d;
--status-success: #a3850e;
--status-warning: #4d574e;
--status-in-progress: #6e6a58;
--page-bg: #070707;
--page-bg-src: '';
--panel-bg: rgb(13 13 13 / 0.72);
--panel-border: rgb(56 53 45 / 0.9);
--panel-border-bare: rgb(56 53 45 / 0.55);
--panel-raised-bg: rgb(13 13 13 / 0.95);
--panel-raised-border: rgb(56 53 45 / 1);
--panel-accent: #7a6a2c;
--panel-header-bg: rgb(30 30 30 / 0.6);
--panel-header-fg: #ebdbb2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0d0d0d;
--glass-bg: rgb(13 13 13 / 0.55);
--glass-border: rgb(56 53 45 / 0.7);
--dock-bg: rgb(13 13 13 / 0.7);
--dock-border: rgb(56 53 45 / 0.8);
--dock-tooltip-bg: #1e1e1e;
--header-bg: rgb(13 13 13 / 0.8);
--header-border: rgb(56 53 45 / 0.9);
--header-fg: #ebdbb2;
--header-blur: 12px;
--pixel-grid-color: rgb(113 106 87 / 0.07);
--card-grid-color: rgb(113 106 87 / 0.05);
--panel-grid-color: rgb(113 106 87 / 0.06);
--scrollbar-thumb: rgb(48 53 49 / 0.6);
--scrollbar-thumb-hover: rgb(48 53 49 / 0.85);
--scrollbar-thin-thumb: rgb(48 53 49 / 0.45);
--scrollbar-thin-thumb-hover: rgb(48 53 49 / 0.7);
--brand: #7a6a2c;
--brand-foreground: #f4ebd5;
--brand-primary: #7a6a2c;
--brand-accent: #7a6a2c;
--brand-warm: #905b57;
--brand-deep: #070707;
--brand-muted: #303531;
--ink: #ebdbb2;
--surface-warm: #0d0d0d;
--naturegreen: #a3850e;
--naturegreen-foreground: #070707;
--duck-dark: #ebdbb2;
--duck-teal: #7a6a2c;
--duck-yellow: #4d574e;
--duck-orange: #905b57;
--duck-forest: #a3850e;
--duck-beige: #23221e;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Black Money — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/HANCORE-linux/omarchy-blackmoney-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='black-money'] {
--background: #070707;
--foreground: #d8d4b4;
--card: #0c0c0c;
--card-foreground: #d8d4b4;
--popover: #0c0c0c;
--popover-foreground: #d8d4b4;
--muted: #1d1d1d;
--muted-foreground: #9b9882;
--accent: #1d1d1d;
--accent-foreground: #d8d4b4;
--primary: #d8d4b4;
--primary-foreground: #070707;
--secondary: #1d1d1d;
--secondary-foreground: #d8d4b4;
--cta: #d8d4b4;
--cta-foreground: #070707;
--ctahover: #ddd9bd;
--success: #2a593f;
--success-foreground: #d8d4b4;
--warning: #525b54;
--warning-foreground: #d8d4b4;
--destructive: #dd5670;
--destructive-foreground: #070707;
--info: #d8d4b4;
--info-foreground: #070707;
--busy: #985962;
--busy-foreground: #ececec;
--busy-ink: #a26a72;
--border: #34332d;
--input: #34332d;
--ring: #d8d4b4;
--radius: 0.625rem;
--chart-primary: #d8d4b4;
--chart-secondary: #689e80;
--status-success: #2a593f;
--status-warning: #525b54;
--status-in-progress: #d8d4b4;
--page-bg: #070707;
--page-bg-src: '';
--panel-bg: rgb(12 12 12 / 0.72);
--panel-border: rgb(52 51 45 / 0.9);
--panel-border-bare: rgb(52 51 45 / 0.55);
--panel-raised-bg: rgb(12 12 12 / 0.95);
--panel-raised-border: rgb(52 51 45 / 1);
--panel-accent: #d8d4b4;
--panel-header-bg: rgb(29 29 29 / 0.6);
--panel-header-fg: #d8d4b4;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0c0c0c;
--glass-bg: rgb(12 12 12 / 0.55);
--glass-border: rgb(52 51 45 / 0.7);
--dock-bg: rgb(12 12 12 / 0.7);
--dock-border: rgb(52 51 45 / 0.8);
--dock-tooltip-bg: #1d1d1d;
--header-bg: rgb(12 12 12 / 0.8);
--header-border: rgb(52 51 45 / 0.9);
--header-fg: #d8d4b4;
--header-blur: 12px;
--pixel-grid-color: rgb(104 102 88 / 0.07);
--card-grid-color: rgb(104 102 88 / 0.05);
--panel-grid-color: rgb(104 102 88 / 0.06);
--scrollbar-thumb: rgb(78 89 74 / 0.6);
--scrollbar-thumb-hover: rgb(78 89 74 / 0.85);
--scrollbar-thin-thumb: rgb(78 89 74 / 0.45);
--scrollbar-thin-thumb-hover: rgb(78 89 74 / 0.7);
--brand: #d8d4b4;
--brand-foreground: #070707;
--brand-primary: #d8d4b4;
--brand-accent: #88c6a3;
--brand-warm: #985962;
--brand-deep: #070707;
--brand-muted: #4e594a;
--ink: #d8d4b4;
--surface-warm: #0c0c0c;
--naturegreen: #2a593f;
--naturegreen-foreground: #d8d4b4;
--duck-dark: #d8d4b4;
--duck-teal: #88c6a3;
--duck-yellow: #525b54;
--duck-orange: #985962;
--duck-forest: #2a593f;
--duck-beige: #20201d;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Blackturq — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/HANCORE-linux/omarchy-blackturq-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='blackturq'] {
--background: #060606;
--foreground: #c8dcdc;
--card: #0a0a0a;
--card-foreground: #c8dcdc;
--popover: #0a0a0a;
--popover-foreground: #c8dcdc;
--muted: #1b1b1b;
--muted-foreground: #8f9d9d;
--accent: #1b1b1b;
--accent-foreground: #c8dcdc;
--primary: #adf0e9;
--primary-foreground: #060606;
--secondary: #1b1b1b;
--secondary-foreground: #c8dcdc;
--cta: #adf0e9;
--cta-foreground: #060606;
--ctahover: #b7f2ec;
--success: #8fecd5;
--success-foreground: #060606;
--warning: #a9d1d7;
--warning-foreground: #060606;
--destructive: #d35f5f;
--destructive-foreground: #060606;
--info: #adf0e9;
--info-foreground: #060606;
--busy: #be989b;
--busy-foreground: #060606;
--busy-ink: #be989b;
--border: #2f3333;
--input: #2f3333;
--ring: #adf0e9;
--radius: 0.625rem;
--chart-primary: #adf0e9;
--chart-secondary: #485362;
--status-success: #8fecd5;
--status-warning: #a9d1d7;
--status-in-progress: #adf0e9;
--page-bg: #060606;
--page-bg-src: '';
--panel-bg: rgb(10 10 10 / 0.72);
--panel-border: rgb(47 51 51 / 0.9);
--panel-border-bare: rgb(47 51 51 / 0.55);
--panel-raised-bg: rgb(10 10 10 / 0.95);
--panel-raised-border: rgb(47 51 51 / 1);
--panel-accent: #adf0e9;
--panel-header-bg: rgb(27 27 27 / 0.6);
--panel-header-fg: #c8dcdc;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0a0a0a;
--glass-bg: rgb(10 10 10 / 0.55);
--glass-border: rgb(47 51 51 / 0.7);
--dock-bg: rgb(10 10 10 / 0.7);
--dock-border: rgb(47 51 51 / 0.8);
--dock-tooltip-bg: #1b1b1b;
--header-bg: rgb(10 10 10 / 0.8);
--header-border: rgb(47 51 51 / 0.9);
--header-fg: #c8dcdc;
--header-blur: 12px;
--pixel-grid-color: rgb(95 104 104 / 0.07);
--card-grid-color: rgb(95 104 104 / 0.05);
--panel-grid-color: rgb(95 104 104 / 0.06);
--scrollbar-thumb: rgb(50 47 59 / 0.6);
--scrollbar-thumb-hover: rgb(50 47 59 / 0.85);
--scrollbar-thin-thumb: rgb(50 47 59 / 0.45);
--scrollbar-thin-thumb-hover: rgb(50 47 59 / 0.7);
--brand: #adf0e9;
--brand-foreground: #060606;
--brand-primary: #adf0e9;
--brand-accent: #5a676b;
--brand-warm: #be989b;
--brand-deep: #060606;
--brand-muted: #322f3b;
--ink: #c8dcdc;
--surface-warm: #0a0a0a;
--naturegreen: #8fecd5;
--naturegreen-foreground: #060606;
--duck-dark: #c8dcdc;
--duck-teal: #5a676b;
--duck-yellow: #a9d1d7;
--duck-orange: #be989b;
--duck-forest: #8fecd5;
--duck-beige: #1d1f1f;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Blackwall — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/rlind3r/omarchy-blackwall-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='blackwall'] {
--background: #0d0d0d;
--foreground: #ff3535;
--card: #171717;
--card-foreground: #ff3535;
--popover: #171717;
--popover-foreground: #ff3535;
--muted: #272727;
--muted-foreground: #d27676;
--accent: #272727;
--accent-foreground: #ff4e4e;
--primary: #ff3535;
--primary-foreground: #0d0d0d;
--secondary: #272727;
--secondary-foreground: #ff4e4e;
--cta: #ff3535;
--cta-foreground: #0d0d0d;
--ctahover: #ff4d4d;
--success: #ff3535;
--success-foreground: #0d0d0d;
--warning: #451d1d;
--warning-foreground: #ff5353;
--destructive: #ff0000;
--destructive-foreground: #0d0d0d;
--info: #ff3535;
--info-foreground: #0d0d0d;
--busy: #a20f0f;
--busy-foreground: #c3c3c3;
--busy-ink: #c05d5d;
--border: #441d1d;
--input: #492323;
--ring: #ff3535;
--radius: 0.625rem;
--chart-primary: #ff3535;
--chart-secondary: #ff3535;
--status-success: #ff3535;
--status-warning: #451d1d;
--status-in-progress: #ff3535;
--page-bg: #0d0d0d;
--page-bg-src: '';
--panel-bg: rgb(23 23 23 / 0.72);
--panel-border: rgb(68 29 29 / 0.9);
--panel-border-bare: rgb(68 29 29 / 0.55);
--panel-raised-bg: rgb(23 23 23 / 0.95);
--panel-raised-border: rgb(68 29 29 / 1);
--panel-accent: #ff3535;
--panel-header-bg: rgb(39 39 39 / 0.6);
--panel-header-fg: #ff3535;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #171717;
--glass-bg: rgb(23 23 23 / 0.55);
--glass-border: rgb(68 29 29 / 0.7);
--dock-bg: rgb(23 23 23 / 0.7);
--dock-border: rgb(68 29 29 / 0.8);
--dock-tooltip-bg: #272727;
--header-bg: rgb(23 23 23 / 0.8);
--header-border: rgb(68 29 29 / 0.9);
--header-fg: #ff3535;
--header-blur: 12px;
--pixel-grid-color: rgb(153 81 81 / 0.07);
--card-grid-color: rgb(153 81 81 / 0.05);
--panel-grid-color: rgb(153 81 81 / 0.06);
--scrollbar-thumb: rgb(147 39 39 / 0.6);
--scrollbar-thumb-hover: rgb(147 39 39 / 0.85);
--scrollbar-thin-thumb: rgb(147 39 39 / 0.45);
--scrollbar-thin-thumb-hover: rgb(147 39 39 / 0.7);
--brand: #ff3535;
--brand-foreground: #0d0d0d;
--brand-primary: #ff3535;
--brand-accent: #742323;
--brand-warm: #a20f0f;
--brand-deep: #0d0d0d;
--brand-muted: #932727;
--ink: #ff3535;
--surface-warm: #171717;
--naturegreen: #ff3535;
--naturegreen-foreground: #0d0d0d;
--duck-dark: #ff3535;
--duck-teal: #742323;
--duck-yellow: #451d1d;
--duck-orange: #a20f0f;
--duck-forest: #ff3535;
--duck-beige: #2e1a1a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Blueridge — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/hipsterusername/omarchy-blueridge-dark-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='blueridge'] {
--background: #0f1216;
--foreground: #e8e6e3;
--card: #1c2128;
--card-foreground: #e8e6e3;
--popover: #1c2128;
--popover-foreground: #e8e6e3;
--muted: #2c3137;
--muted-foreground: #ababab;
--accent: #2c3137;
--accent-foreground: #e8e6e3;
--primary: #9bdede;
--primary-foreground: #0f1216;
--secondary: #2c3137;
--secondary-foreground: #e8e6e3;
--cta: #9bdede;
--cta-foreground: #0f1216;
--ctahover: #a7e2e2;
--success: #7ea67c;
--success-foreground: #0f1216;
--warning: #d4af37;
--warning-foreground: #0f1216;
--destructive: #cd9575;
--destructive-foreground: #0f1216;
--info: #6b8cae;
--info-foreground: #0f1216;
--busy: #d1a256;
--busy-foreground: #0f1216;
--busy-ink: #d1a256;
--border: #44484d;
--input: #44484d;
--ring: #9bdede;
--radius: 0.625rem;
--chart-primary: #9bdede;
--chart-secondary: #9b8aa0;
--status-success: #7ea67c;
--status-warning: #d4af37;
--status-in-progress: #6b8cae;
--page-bg: #0f1216;
--page-bg-src: '';
--panel-bg: rgb(28 33 40 / 0.72);
--panel-border: rgb(68 72 77 / 0.9);
--panel-border-bare: rgb(68 72 77 / 0.55);
--panel-raised-bg: rgb(28 33 40 / 0.95);
--panel-raised-border: rgb(68 72 77 / 1);
--panel-accent: #9bdede;
--panel-header-bg: rgb(44 49 55 / 0.6);
--panel-header-fg: #e8e6e3;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1c2128;
--glass-bg: rgb(28 33 40 / 0.55);
--glass-border: rgb(68 72 77 / 0.7);
--dock-bg: rgb(28 33 40 / 0.7);
--dock-border: rgb(68 72 77 / 0.8);
--dock-tooltip-bg: #2c3137;
--header-bg: rgb(28 33 40 / 0.8);
--header-border: rgb(68 72 77 / 0.9);
--header-fg: #e8e6e3;
--header-blur: 12px;
--pixel-grid-color: rgb(120 122 124 / 0.07);
--card-grid-color: rgb(120 122 124 / 0.05);
--panel-grid-color: rgb(120 122 124 / 0.06);
--scrollbar-thumb: rgb(74 85 104 / 0.6);
--scrollbar-thumb-hover: rgb(74 85 104 / 0.85);
--scrollbar-thin-thumb: rgb(74 85 104 / 0.45);
--scrollbar-thin-thumb-hover: rgb(74 85 104 / 0.7);
--brand: #9bdede;
--brand-foreground: #0f1216;
--brand-primary: #9bdede;
--brand-accent: #7ec8c8;
--brand-warm: #d1a256;
--brand-deep: #0f1216;
--brand-muted: #4a5568;
--ink: #e8e6e3;
--surface-warm: #1c2128;
--naturegreen: #7ea67c;
--naturegreen-foreground: #0f1216;
--duck-dark: #e8e6e3;
--duck-teal: #7ec8c8;
--duck-yellow: #d4af37;
--duck-orange: #d1a256;
--duck-forest: #7ea67c;
--duck-beige: #30353b;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Brutalism — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/bjornramberg/omarchy-brutalism-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='brutalism'] {
--background: #0a0a0a;
--foreground: #ded3d3;
--card: #121212;
--card-foreground: #ded3d3;
--popover: #121212;
--popover-foreground: #ded3d3;
--muted: #232323;
--muted-foreground: #a19999;
--accent: #232323;
--accent-foreground: #ded3d3;
--primary: #eb9696;
--primary-foreground: #0a0a0a;
--secondary: #232323;
--secondary-foreground: #ded3d3;
--cta: #eb9696;
--cta-foreground: #0a0a0a;
--ctahover: #eda3a3;
--success: #630a0a;
--success-foreground: #ded3d3;
--warning: #cc7a7a;
--warning-foreground: #0a0a0a;
--destructive: #450404;
--destructive-foreground: #ded3d3;
--info: #ce5757;
--info-foreground: #0a0a0a;
--busy: #893f3f;
--busy-foreground: #ded3d3;
--busy-ink: #a46a6a;
--border: #3a3838;
--input: #3a3838;
--ring: #eb9696;
--radius: 0.625rem;
--chart-primary: #eb9696;
--chart-secondary: #c87777;
--status-success: #630a0a;
--status-warning: #cc7a7a;
--status-in-progress: #ce5757;
--page-bg: #0a0a0a;
--page-bg-src: '';
--panel-bg: rgb(18 18 18 / 0.72);
--panel-border: rgb(58 56 56 / 0.9);
--panel-border-bare: rgb(58 56 56 / 0.55);
--panel-raised-bg: rgb(18 18 18 / 0.95);
--panel-raised-border: rgb(58 56 56 / 1);
--panel-accent: #eb9696;
--panel-header-bg: rgb(35 35 35 / 0.6);
--panel-header-fg: #ded3d3;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #121212;
--glass-bg: rgb(18 18 18 / 0.55);
--glass-border: rgb(58 56 56 / 0.7);
--dock-bg: rgb(18 18 18 / 0.7);
--dock-border: rgb(58 56 56 / 0.8);
--dock-tooltip-bg: #232323;
--header-bg: rgb(18 18 18 / 0.8);
--header-border: rgb(58 56 56 / 0.9);
--header-fg: #ded3d3;
--header-blur: 12px;
--pixel-grid-color: rgb(110 105 105 / 0.07);
--card-grid-color: rgb(110 105 105 / 0.05);
--panel-grid-color: rgb(110 105 105 / 0.06);
--scrollbar-thumb: rgb(184 148 148 / 0.6);
--scrollbar-thumb-hover: rgb(184 148 148 / 0.85);
--scrollbar-thin-thumb: rgb(184 148 148 / 0.45);
--scrollbar-thin-thumb-hover: rgb(184 148 148 / 0.7);
--brand: #eb9696;
--brand-foreground: #0a0a0a;
--brand-primary: #eb9696;
--brand-accent: #d48484;
--brand-warm: #893f3f;
--brand-deep: #0a0a0a;
--brand-muted: #b89494;
--ink: #ded3d3;
--surface-warm: #121212;
--naturegreen: #630a0a;
--naturegreen-foreground: #ded3d3;
--duck-dark: #ded3d3;
--duck-teal: #d48484;
--duck-yellow: #cc7a7a;
--duck-orange: #893f3f;
--duck-forest: #630a0a;
--duck-beige: #262525;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Caroline Skyline — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/OldJobobo/omarchy-caroline-skyline-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='caroline-skyline'] {
--background: #0f0a0a;
--foreground: #e3a68c;
--card: #1c1213;
--card-foreground: #e3a68c;
--popover: #1c1213;
--popover-foreground: #e3a68c;
--muted: #2c2324;
--muted-foreground: #ae8473;
--accent: #2c2324;
--accent-foreground: #e3a68c;
--primary: #6b6566;
--primary-foreground: #e7e7e7;
--secondary: #2c2324;
--secondary-foreground: #e3a68c;
--cta: #6b6566;
--cta-foreground: #e7e7e7;
--ctahover: #7d7778;
--success: #806c61;
--success-foreground: #f9f9f9;
--warning: #f28171;
--warning-foreground: #0f0a0a;
--destructive: #c24f57;
--destructive-foreground: #ffffff;
--info: #684c59;
--info-foreground: #ebbeac;
--busy: #da6864;
--busy-foreground: #0f0a0a;
--busy-ink: #da6864;
--border: #432f2b;
--input: #432f2b;
--ring: #6b6566;
--radius: 0.625rem;
--chart-primary: #6b6566;
--chart-secondary: #a63650;
--status-success: #806c61;
--status-warning: #f28171;
--status-in-progress: #684c59;
--page-bg: #0f0a0a;
--page-bg-src: '';
--panel-bg: rgb(28 18 19 / 0.72);
--panel-border: rgb(67 47 43 / 0.9);
--panel-border-bare: rgb(67 47 43 / 0.55);
--panel-raised-bg: rgb(28 18 19 / 0.95);
--panel-raised-border: rgb(67 47 43 / 1);
--panel-accent: #6b6566;
--panel-header-bg: rgb(44 35 36 / 0.6);
--panel-header-fg: #e3a68c;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1c1213;
--glass-bg: rgb(28 18 19 / 0.55);
--glass-border: rgb(67 47 43 / 0.7);
--dock-bg: rgb(28 18 19 / 0.7);
--dock-border: rgb(67 47 43 / 0.8);
--dock-tooltip-bg: #2c2324;
--header-bg: rgb(28 18 19 / 0.8);
--header-border: rgb(67 47 43 / 0.9);
--header-fg: #e3a68c;
--header-blur: 12px;
--pixel-grid-color: rgb(125 94 82 / 0.07);
--card-grid-color: rgb(125 94 82 / 0.05);
--panel-grid-color: rgb(125 94 82 / 0.06);
--scrollbar-thumb: rgb(109 71 69 / 0.6);
--scrollbar-thumb-hover: rgb(109 71 69 / 0.85);
--scrollbar-thin-thumb: rgb(109 71 69 / 0.45);
--scrollbar-thin-thumb-hover: rgb(109 71 69 / 0.7);
--brand: #6b6566;
--brand-foreground: #e7e7e7;
--brand-primary: #6b6566;
--brand-accent: #6b6566;
--brand-warm: #da6864;
--brand-deep: #0f0a0a;
--brand-muted: #6d4745;
--ink: #e3a68c;
--surface-warm: #1c1213;
--naturegreen: #806c61;
--naturegreen-foreground: #f9f9f9;
--duck-dark: #e3a68c;
--duck-teal: #6b6566;
--duck-yellow: #f28171;
--duck-orange: #da6864;
--duck-forest: #806c61;
--duck-beige: #30211f;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Catppuccin Mocha — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/KidDogDad/omarchy-catppuccin-mocha-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='catppuccin-mocha'] {
--background: #111119;
--foreground: #cdd6f4;
--card: #1e1e2e;
--card-foreground: #cdd6f4;
--popover: #1e1e2e;
--popover-foreground: #cdd6f4;
--muted: #2e2e3d;
--muted-foreground: #999fb9;
--accent: #2e2e3d;
--accent-foreground: #cdd6f4;
--primary: #94e2d5;
--primary-foreground: #111119;
--secondary: #2e2e3d;
--secondary-foreground: #cdd6f4;
--cta: #94e2d5;
--cta-foreground: #111119;
--ctahover: #a1e5da;
--success: #a6e3a1;
--success-foreground: #111119;
--warning: #f9e2af;
--warning-foreground: #111119;
--destructive: #f38ba8;
--destructive-foreground: #111119;
--info: #89b4fa;
--info-foreground: #111119;
--busy: #f6b7ac;
--busy-foreground: #111119;
--busy-ink: #f6b7ac;
--border: #404255;
--input: #404255;
--ring: #94e2d5;
--radius: 0.625rem;
--chart-primary: #94e2d5;
--chart-secondary: #f5c2e7;
--status-success: #a6e3a1;
--status-warning: #f9e2af;
--status-in-progress: #89b4fa;
--page-bg: #111119;
--page-bg-src: '';
--panel-bg: rgb(30 30 46 / 0.72);
--panel-border: rgb(64 66 85 / 0.9);
--panel-border-bare: rgb(64 66 85 / 0.55);
--panel-raised-bg: rgb(30 30 46 / 0.95);
--panel-raised-border: rgb(64 66 85 / 1);
--panel-accent: #94e2d5;
--panel-header-bg: rgb(46 46 61 / 0.6);
--panel-header-fg: #cdd6f4;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1e1e2e;
--glass-bg: rgb(30 30 46 / 0.55);
--glass-border: rgb(64 66 85 / 0.7);
--dock-bg: rgb(30 30 46 / 0.7);
--dock-border: rgb(64 66 85 / 0.8);
--dock-tooltip-bg: #2e2e3d;
--header-bg: rgb(30 30 46 / 0.8);
--header-border: rgb(64 66 85 / 0.9);
--header-fg: #cdd6f4;
--header-blur: 12px;
--pixel-grid-color: rgb(109 113 135 / 0.07);
--card-grid-color: rgb(109 113 135 / 0.05);
--panel-grid-color: rgb(109 113 135 / 0.06);
--scrollbar-thumb: rgb(69 71 90 / 0.6);
--scrollbar-thumb-hover: rgb(69 71 90 / 0.85);
--scrollbar-thin-thumb: rgb(69 71 90 / 0.45);
--scrollbar-thin-thumb-hover: rgb(69 71 90 / 0.7);
--brand: #94e2d5;
--brand-foreground: #111119;
--brand-primary: #94e2d5;
--brand-accent: #94e2d5;
--brand-warm: #f6b7ac;
--brand-deep: #111119;
--brand-muted: #45475a;
--ink: #cdd6f4;
--surface-warm: #1e1e2e;
--naturegreen: #a6e3a1;
--naturegreen-foreground: #111119;
--duck-dark: #cdd6f4;
--duck-teal: #94e2d5;
--duck-yellow: #f9e2af;
--duck-orange: #f6b7ac;
--duck-forest: #a6e3a1;
--duck-beige: #303042;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Cattpuccin Glass — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/Luquatic/omarchy-catppuccin-glass (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='cattpuccin-glass'] {
--background: #0e0f11;
--foreground: #cdd6f4;
--card: #1a1b1e;
--card-foreground: #cdd6f4;
--popover: #1a1b1e;
--popover-foreground: #cdd6f4;
--muted: #2a2b2e;
--muted-foreground: #979eb4;
--accent: #2a2b2e;
--accent-foreground: #cdd6f4;
--primary: #94e2d5;
--primary-foreground: #0e0f11;
--secondary: #2a2b2e;
--secondary-foreground: #cdd6f4;
--cta: #94e2d5;
--cta-foreground: #0e0f11;
--ctahover: #a1e5da;
--success: #a6e3a1;
--success-foreground: #0e0f11;
--warning: #f9e2af;
--warning-foreground: #0e0f11;
--destructive: #f38ba8;
--destructive-foreground: #0e0f11;
--info: #89b4fa;
--info-foreground: #0e0f11;
--busy: #f6b7ac;
--busy-foreground: #0e0f11;
--busy-ink: #f6b7ac;
--border: #3d4048;
--input: #3d4048;
--ring: #94e2d5;
--radius: 0.625rem;
--chart-primary: #94e2d5;
--chart-secondary: #f5c2e7;
--status-success: #a6e3a1;
--status-warning: #f9e2af;
--status-in-progress: #89b4fa;
--page-bg: #0e0f11;
--page-bg-src: '';
--panel-bg: rgb(26 27 30 / 0.72);
--panel-border: rgb(61 64 72 / 0.9);
--panel-border-bare: rgb(61 64 72 / 0.55);
--panel-raised-bg: rgb(26 27 30 / 0.95);
--panel-raised-border: rgb(61 64 72 / 1);
--panel-accent: #94e2d5;
--panel-header-bg: rgb(42 43 46 / 0.6);
--panel-header-fg: #cdd6f4;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1b1e;
--glass-bg: rgb(26 27 30 / 0.55);
--glass-border: rgb(61 64 72 / 0.7);
--dock-bg: rgb(26 27 30 / 0.7);
--dock-border: rgb(61 64 72 / 0.8);
--dock-tooltip-bg: #2a2b2e;
--header-bg: rgb(26 27 30 / 0.8);
--header-border: rgb(61 64 72 / 0.9);
--header-fg: #cdd6f4;
--header-blur: 12px;
--pixel-grid-color: rgb(107 111 126 / 0.07);
--card-grid-color: rgb(107 111 126 / 0.05);
--panel-grid-color: rgb(107 111 126 / 0.06);
--scrollbar-thumb: rgb(88 91 112 / 0.6);
--scrollbar-thumb-hover: rgb(88 91 112 / 0.85);
--scrollbar-thin-thumb: rgb(88 91 112 / 0.45);
--scrollbar-thin-thumb-hover: rgb(88 91 112 / 0.7);
--brand: #94e2d5;
--brand-foreground: #0e0f11;
--brand-primary: #94e2d5;
--brand-accent: #94e2d5;
--brand-warm: #f6b7ac;
--brand-deep: #0e0f11;
--brand-muted: #585b70;
--ink: #cdd6f4;
--surface-warm: #1a1b1e;
--naturegreen: #a6e3a1;
--naturegreen-foreground: #0e0f11;
--duck-dark: #cdd6f4;
--duck-teal: #94e2d5;
--duck-yellow: #f9e2af;
--duck-orange: #f6b7ac;
--duck-forest: #a6e3a1;
--duck-beige: #2c2e33;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
City 783 — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/OldJobobo/omarchy-city-783-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='city-783'] {
--background: #0d0e11;
--foreground: #b9bec6;
--card: #181a1f;
--card-foreground: #b9bec6;
--popover: #181a1f;
--popover-foreground: #b9bec6;
--muted: #282a2f;
--muted-foreground: #8f9399;
--accent: #282a2f;
--accent-foreground: #b9bec6;
--primary: #dce0e6;
--primary-foreground: #0d0e11;
--secondary: #282a2f;
--secondary-foreground: #b9bec6;
--cta: #dce0e6;
--cta-foreground: #0d0e11;
--ctahover: #e0e4e9;
--success: #dce0e6;
--success-foreground: #0d0e11;
--warning: #9e1a1a;
--warning-foreground: #bec3ca;
--destructive: #e53939;
--destructive-foreground: #0d0e11;
--info: #ad2222;
--info-foreground: #ced2d7;
--busy: #c22a2a;
--busy-foreground: #e7e7e7;
--busy-ink: #ce5555;
--border: #383a40;
--input: #383a40;
--ring: #dce0e6;
--radius: 0.625rem;
--chart-primary: #dce0e6;
--chart-secondary: #c3c8d0;
--status-success: #dce0e6;
--status-warning: #9e1a1a;
--status-in-progress: #ad2222;
--page-bg: #0d0e11;
--page-bg-src: '';
--panel-bg: rgb(24 26 31 / 0.72);
--panel-border: rgb(56 58 64 / 0.9);
--panel-border-bare: rgb(56 58 64 / 0.55);
--panel-raised-bg: rgb(24 26 31 / 0.95);
--panel-raised-border: rgb(56 58 64 / 1);
--panel-accent: #dce0e6;
--panel-header-bg: rgb(40 42 47 / 0.6);
--panel-header-fg: #b9bec6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #181a1f;
--glass-bg: rgb(24 26 31 / 0.55);
--glass-border: rgb(56 58 64 / 0.7);
--dock-bg: rgb(24 26 31 / 0.7);
--dock-border: rgb(56 58 64 / 0.8);
--dock-tooltip-bg: #282a2f;
--header-bg: rgb(24 26 31 / 0.8);
--header-border: rgb(56 58 64 / 0.9);
--header-fg: #b9bec6;
--header-blur: 12px;
--pixel-grid-color: rgb(100 104 110 / 0.07);
--card-grid-color: rgb(100 104 110 / 0.05);
--panel-grid-color: rgb(100 104 110 / 0.06);
--scrollbar-thumb: rgb(75 81 91 / 0.6);
--scrollbar-thumb-hover: rgb(75 81 91 / 0.85);
--scrollbar-thin-thumb: rgb(75 81 91 / 0.45);
--scrollbar-thin-thumb-hover: rgb(75 81 91 / 0.7);
--brand: #dce0e6;
--brand-foreground: #0d0e11;
--brand-primary: #dce0e6;
--brand-accent: #8f949c;
--brand-warm: #c22a2a;
--brand-deep: #0d0e11;
--brand-muted: #4b515b;
--ink: #b9bec6;
--surface-warm: #181a1f;
--naturegreen: #dce0e6;
--naturegreen-foreground: #0d0e11;
--duck-dark: #b9bec6;
--duck-teal: #8f949c;
--duck-yellow: #9e1a1a;
--duck-orange: #c22a2a;
--duck-forest: #dce0e6;
--duck-beige: #282a30;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Copper Night — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/hembramnishant50-glitch/omarchy-coppernight-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='copper-night'] {
--background: #09090f;
--foreground: #cdd6f4;
--card: #11111b;
--card-foreground: #cdd6f4;
--popover: #11111b;
--popover-foreground: #cdd6f4;
--muted: #22222b;
--muted-foreground: #959bb3;
--accent: #22222b;
--accent-foreground: #cdd6f4;
--primary: #94e2d5;
--primary-foreground: #09090f;
--secondary: #22222b;
--secondary-foreground: #cdd6f4;
--cta: #94e2d5;
--cta-foreground: #09090f;
--ctahover: #a1e5da;
--success: #a6e3a1;
--success-foreground: #09090f;
--warning: #f9e2af;
--warning-foreground: #09090f;
--destructive: #f38ba8;
--destructive-foreground: #09090f;
--info: #89b4fa;
--info-foreground: #09090f;
--busy: #f6b7ac;
--busy-foreground: #09090f;
--busy-ink: #f6b7ac;
--border: #363846;
--input: #363846;
--ring: #94e2d5;
--radius: 0.625rem;
--chart-primary: #94e2d5;
--chart-secondary: #cba6f7;
--status-success: #a6e3a1;
--status-warning: #f9e2af;
--status-in-progress: #89b4fa;
--page-bg: #09090f;
--page-bg-src: '';
--panel-bg: rgb(17 17 27 / 0.72);
--panel-border: rgb(54 56 70 / 0.9);
--panel-border-bare: rgb(54 56 70 / 0.55);
--panel-raised-bg: rgb(17 17 27 / 0.95);
--panel-raised-border: rgb(54 56 70 / 1);
--panel-accent: #94e2d5;
--panel-header-bg: rgb(34 34 43 / 0.6);
--panel-header-fg: #cdd6f4;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #11111b;
--glass-bg: rgb(17 17 27 / 0.55);
--glass-border: rgb(54 56 70 / 0.7);
--dock-bg: rgb(17 17 27 / 0.7);
--dock-border: rgb(54 56 70 / 0.8);
--dock-tooltip-bg: #22222b;
--header-bg: rgb(17 17 27 / 0.8);
--header-border: rgb(54 56 70 / 0.9);
--header-fg: #cdd6f4;
--header-blur: 12px;
--pixel-grid-color: rgb(102 106 125 / 0.07);
--card-grid-color: rgb(102 106 125 / 0.05);
--panel-grid-color: rgb(102 106 125 / 0.06);
--scrollbar-thumb: rgb(88 91 112 / 0.6);
--scrollbar-thumb-hover: rgb(88 91 112 / 0.85);
--scrollbar-thin-thumb: rgb(88 91 112 / 0.45);
--scrollbar-thin-thumb-hover: rgb(88 91 112 / 0.7);
--brand: #94e2d5;
--brand-foreground: #09090f;
--brand-primary: #94e2d5;
--brand-accent: #94e2d5;
--brand-warm: #f6b7ac;
--brand-deep: #09090f;
--brand-muted: #585b70;
--ink: #cdd6f4;
--surface-warm: #11111b;
--naturegreen: #a6e3a1;
--naturegreen-foreground: #09090f;
--duck-dark: #cdd6f4;
--duck-teal: #94e2d5;
--duck-yellow: #f9e2af;
--duck-orange: #f6b7ac;
--duck-forest: #a6e3a1;
--duck-beige: #242531;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Corporate — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Light theme (light), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/defer/omarchy-corporate-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='corporate'] {
--background: #eeeeee;
--foreground: #444444;
--card: #e6e6e6;
--card-foreground: #444444;
--popover: #e6e6e6;
--popover-foreground: #444444;
--muted: #e0e0e0;
--muted-foreground: #626262;
--accent: #e0e0e0;
--accent-foreground: #444444;
--primary: #005f87;
--primary-foreground: #d6d6d6;
--secondary: #e0e0e0;
--secondary-foreground: #444444;
--cta: #005f87;
--cta-foreground: #d6d6d6;
--ctahover: #005477;
--success: #008700;
--success-foreground: #fafafa;
--warning: #5f8700;
--warning-foreground: #0b0b0b;
--destructive: #af0000;
--destructive-foreground: #d6d6d6;
--info: #0087af;
--info-foreground: #101010;
--busy: #874400;
--busy-foreground: #d6d6d6;
--busy-ink: #874400;
--border: #d3d3d3;
--input: #cecece;
--ring: #005f87;
--radius: 0.625rem;
--chart-primary: #005f87;
--chart-secondary: #878787;
--status-success: #008700;
--status-warning: #5f8700;
--status-in-progress: #0087af;
--page-bg: #eeeeee;
--page-bg-src: '';
--panel-bg: rgb(230 230 230 / 0.72);
--panel-border: rgb(211 211 211 / 0.9);
--panel-border-bare: rgb(211 211 211 / 0.55);
--panel-raised-bg: rgb(230 230 230 / 0.95);
--panel-raised-border: rgb(211 211 211 / 1);
--panel-accent: #005f87;
--panel-header-bg: rgb(224 224 224 / 0.6);
--panel-header-fg: #444444;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #e6e6e6;
--glass-bg: rgb(230 230 230 / 0.55);
--glass-border: rgb(211 211 211 / 0.7);
--dock-bg: rgb(230 230 230 / 0.7);
--dock-border: rgb(211 211 211 / 0.8);
--dock-tooltip-bg: #e0e0e0;
--header-bg: rgb(230 230 230 / 0.8);
--header-border: rgb(211 211 211 / 0.9);
--header-fg: #444444;
--header-blur: 12px;
--pixel-grid-color: rgb(130 130 130 / 0.07);
--card-grid-color: rgb(130 130 130 / 0.05);
--panel-grid-color: rgb(130 130 130 / 0.06);
--scrollbar-thumb: rgb(188 188 188 / 0.6);
--scrollbar-thumb-hover: rgb(188 188 188 / 0.85);
--scrollbar-thin-thumb: rgb(188 188 188 / 0.45);
--scrollbar-thin-thumb-hover: rgb(188 188 188 / 0.7);
--brand: #005f87;
--brand-foreground: #d6d6d6;
--brand-primary: #005f87;
--brand-accent: #005f87;
--brand-warm: #874400;
--brand-deep: #e0e0e0;
--brand-muted: #bcbcbc;
--ink: #444444;
--surface-warm: #e6e6e6;
--naturegreen: #008700;
--naturegreen-foreground: #fafafa;
--duck-dark: #444444;
--duck-teal: #005f87;
--duck-yellow: #5f8700;
--duck-orange: #874400;
--duck-forest: #008700;
--duck-beige: #d6d6d6;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Cpunk — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/stannorbvb-cmd/cpunk (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='cpunk'] {
--background: #020202;
--foreground: #ffffff;
--card: #040303;
--card-foreground: #ffffff;
--popover: #040303;
--popover-foreground: #ffffff;
--muted: #161515;
--muted-foreground: #b4b3b3;
--accent: #161515;
--accent-foreground: #ffffff;
--primary: #e8e9e9;
--primary-foreground: #020202;
--secondary: #161515;
--secondary-foreground: #ffffff;
--cta: #e8e9e9;
--cta-foreground: #020202;
--ctahover: #ebecec;
--success: #d1d2d1;
--success-foreground: #020202;
--warning: #fef348;
--warning-foreground: #020202;
--destructive: #f82a2a;
--destructive-foreground: #020202;
--info: #aaacac;
--info-foreground: #020202;
--busy: #fb8f39;
--busy-foreground: #020202;
--busy-ink: #fb8f39;
--border: #353434;
--input: #353434;
--ring: #e8e9e9;
--radius: 0.625rem;
--chart-primary: #e8e9e9;
--chart-secondary: #f76e78;
--status-success: #d1d2d1;
--status-warning: #fef348;
--status-in-progress: #aaacac;
--page-bg: #020202;
--page-bg-src: '';
--panel-bg: rgb(4 3 3 / 0.72);
--panel-border: rgb(53 52 52 / 0.9);
--panel-border-bare: rgb(53 52 52 / 0.55);
--panel-raised-bg: rgb(4 3 3 / 0.95);
--panel-raised-border: rgb(53 52 52 / 1);
--panel-accent: #e8e9e9;
--panel-header-bg: rgb(22 21 21 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #040303;
--glass-bg: rgb(4 3 3 / 0.55);
--glass-border: rgb(53 52 52 / 0.7);
--dock-bg: rgb(4 3 3 / 0.7);
--dock-border: rgb(53 52 52 / 0.8);
--dock-tooltip-bg: #161515;
--header-bg: rgb(4 3 3 / 0.8);
--header-border: rgb(53 52 52 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(117 116 116 / 0.07);
--card-grid-color: rgb(117 116 116 / 0.05);
--panel-grid-color: rgb(117 116 116 / 0.06);
--scrollbar-thumb: rgb(127 110 110 / 0.6);
--scrollbar-thumb-hover: rgb(127 110 110 / 0.85);
--scrollbar-thin-thumb: rgb(127 110 110 / 0.45);
--scrollbar-thin-thumb-hover: rgb(127 110 110 / 0.7);
--brand: #e8e9e9;
--brand-foreground: #020202;
--brand-primary: #e8e9e9;
--brand-accent: #babbbb;
--brand-warm: #fb8f39;
--brand-deep: #020202;
--brand-muted: #7f6e6e;
--ink: #ffffff;
--surface-warm: #040303;
--naturegreen: #d1d2d1;
--naturegreen-foreground: #020202;
--duck-dark: #ffffff;
--duck-teal: #babbbb;
--duck-yellow: #fef348;
--duck-orange: #fb8f39;
--duck-forest: #d1d2d1;
--duck-beige: #1d1c1c;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Crimson Gold — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark) — the source repo's own colors.toml says so directly.
Source: https://github.com/knappkevin/omarchy-crimson-gold-theme (colors.toml (Omarchy schema)), read by scripts/port-community-themes.ts. Values
came straight from the repo's own colors.toml — the same schema and reader the official 22 themes
use, so nothing here is reconstructed. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='crimson-gold'] {
--background: #080505;
--foreground: #dbb42c;
--card: #1a0808;
--card-foreground: #dbb42c;
--popover: #1a0808;
--popover-foreground: #dbb42c;
--muted: #411010;
--muted-foreground: #bebebe;
--accent: #411010;
--accent-foreground: #dbb42c;
--primary: #9f831c;
--primary-foreground: #080505;
--secondary: #411010;
--secondary-foreground: #dbb42c;
--cta: #9f831c;
--cta-foreground: #080505;
--ctahover: #ab9237;
--success: #ffc107;
--success-foreground: #080505;
--warning: #b91c1c;
--warning-foreground: #eaeaea;
--destructive: #ee3333;
--destructive-foreground: #080505;
--info: #e68e0d;
--info-foreground: #080505;
--busy: #eac027;
--busy-foreground: #080505;
--busy-ink: #eac027;
--border: #483b3b;
--input: #483b3b;
--ring: #9f831c;
--radius: 0.625rem;
--chart-primary: #9f831c;
--chart-secondary: #d35f5f;
--status-success: #ffc107;
--status-warning: #b91c1c;
--status-in-progress: #e68e0d;
--page-bg: #080505;
--page-bg-src: '';
--panel-bg: rgb(26 8 8 / 0.72);
--panel-border: rgb(72 59 59 / 0.9);
--panel-border-bare: rgb(72 59 59 / 0.55);
--panel-raised-bg: rgb(26 8 8 / 0.95);
--panel-raised-border: rgb(72 59 59 / 1);
--panel-accent: #9f831c;
--panel-header-bg: rgb(65 16 16 / 0.6);
--panel-header-fg: #dbb42c;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a0808;
--glass-bg: rgb(26 8 8 / 0.55);
--glass-border: rgb(72 59 59 / 0.7);
--dock-bg: rgb(26 8 8 / 0.7);
--dock-border: rgb(72 59 59 / 0.8);
--dock-tooltip-bg: #411010;
--header-bg: rgb(26 8 8 / 0.8);
--header-border: rgb(72 59 59 / 0.9);
--header-fg: #dbb42c;
--header-blur: 12px;
--pixel-grid-color: rgb(96 96 96 / 0.07);
--card-grid-color: rgb(96 96 96 / 0.05);
--panel-grid-color: rgb(96 96 96 / 0.06);
--scrollbar-thumb: rgb(170 170 170 / 0.6);
--scrollbar-thumb-hover: rgb(170 170 170 / 0.85);
--scrollbar-thin-thumb: rgb(170 170 170 / 0.45);
--scrollbar-thin-thumb-hover: rgb(170 170 170 / 0.7);
--brand: #9f831c;
--brand-foreground: #080505;
--brand-primary: #9f831c;
--brand-accent: #ee0000;
--brand-warm: #eac027;
--brand-deep: #080505;
--brand-muted: #aaaaaa;
--ink: #dbb42c;
--surface-warm: #1a0808;
--naturegreen: #ffc107;
--naturegreen-foreground: #080505;
--duck-dark: #dbb42c;
--duck-teal: #ee0000;
--duck-yellow: #b91c1c;
--duck-orange: #eac027;
--duck-forest: #ffc107;
--duck-beige: #2d190c;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Crimson — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/tahfizhabib/omarchy-crimson-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='crimson'] {
--background: #0e0c12;
--foreground: #ffffff;
--card: #1a1621;
--card-foreground: #ffffff;
--popover: #1a1621;
--popover-foreground: #ffffff;
--muted: #2a2631;
--muted-foreground: #bab9bc;
--accent: #2a2631;
--accent-foreground: #ffffff;
--primary: #e7e3ed;
--primary-foreground: #0e0c12;
--secondary: #2a2631;
--secondary-foreground: #ffffff;
--cta: #e7e3ed;
--cta-foreground: #0e0c12;
--ctahover: #eae6ef;
--success: #e48a3e;
--success-foreground: #0e0c12;
--warning: #da884a;
--warning-foreground: #0e0c12;
--destructive: #b17777;
--destructive-foreground: #0e0c12;
--info: #9e96c0;
--info-foreground: #0e0c12;
--busy: #c68061;
--busy-foreground: #0e0c12;
--busy-ink: #c68061;
--border: #47444c;
--input: #47444c;
--ring: #e7e3ed;
--radius: 0.625rem;
--chart-primary: #e7e3ed;
--chart-secondary: #bba7be;
--status-success: #e48a3e;
--status-warning: #da884a;
--status-in-progress: #9e96c0;
--page-bg: #0e0c12;
--page-bg-src: '';
--panel-bg: rgb(26 22 33 / 0.72);
--panel-border: rgb(71 68 76 / 0.9);
--panel-border-bare: rgb(71 68 76 / 0.55);
--panel-raised-bg: rgb(26 22 33 / 0.95);
--panel-raised-border: rgb(71 68 76 / 1);
--panel-accent: #e7e3ed;
--panel-header-bg: rgb(42 38 49 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1621;
--glass-bg: rgb(26 22 33 / 0.55);
--glass-border: rgb(71 68 76 / 0.7);
--dock-bg: rgb(26 22 33 / 0.7);
--dock-border: rgb(71 68 76 / 0.8);
--dock-tooltip-bg: #2a2631;
--header-bg: rgb(26 22 33 / 0.8);
--header-border: rgb(71 68 76 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(129 127 133 / 0.07);
--card-grid-color: rgb(129 127 133 / 0.05);
--panel-grid-color: rgb(129 127 133 / 0.06);
--scrollbar-thumb: rgb(76 71 86 / 0.6);
--scrollbar-thumb-hover: rgb(76 71 86 / 0.85);
--scrollbar-thin-thumb: rgb(76 71 86 / 0.45);
--scrollbar-thin-thumb-hover: rgb(76 71 86 / 0.7);
--brand: #e7e3ed;
--brand-foreground: #0e0c12;
--brand-primary: #e7e3ed;
--brand-accent: #b7acc8;
--brand-warm: #c68061;
--brand-deep: #0e0c12;
--brand-muted: #4c4756;
--ink: #ffffff;
--surface-warm: #1a1621;
--naturegreen: #e48a3e;
--naturegreen-foreground: #0e0c12;
--duck-dark: #ffffff;
--duck-teal: #b7acc8;
--duck-yellow: #da884a;
--duck-orange: #c68061;
--duck-forest: #e48a3e;
--duck-beige: #312d37;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Cyberpunk 2 — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/SeanAnd/omarchy-cyberpunk-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='cyberpunk-2'] {
--background: #120000;
--foreground: #ff4040;
--card: #200000;
--card-foreground: #ff4040;
--popover: #200000;
--popover-foreground: #ff4040;
--muted: #301212;
--muted-foreground: #cd6262;
--accent: #301212;
--accent-foreground: #ff4040;
--primary: #b5e4f4;
--primary-foreground: #120000;
--secondary: #301212;
--secondary-foreground: #ff4040;
--cta: #b5e4f4;
--cta-foreground: #120000;
--ctahover: #bee7f5;
--success: #27a75c;
--success-foreground: #120000;
--warning: #ff3c3c;
--warning-foreground: #120000;
--destructive: #ff1100;
--destructive-foreground: #120000;
--info: #26bdfd;
--info-foreground: #120000;
--busy: #ff271e;
--busy-foreground: #120000;
--busy-ink: #ff271e;
--border: #4c0d0d;
--input: #501313;
--ring: #b5e4f4;
--radius: 0.625rem;
--chart-primary: #b5e4f4;
--chart-secondary: #ffcf3d;
--status-success: #27a75c;
--status-warning: #ff3c3c;
--status-in-progress: #26bdfd;
--page-bg: #120000;
--page-bg-src: '';
--panel-bg: rgb(32 0 0 / 0.72);
--panel-border: rgb(76 13 13 / 0.9);
--panel-border-bare: rgb(76 13 13 / 0.55);
--panel-raised-bg: rgb(32 0 0 / 0.95);
--panel-raised-border: rgb(76 13 13 / 1);
--panel-accent: #b5e4f4;
--panel-header-bg: rgb(48 18 18 / 0.6);
--panel-header-fg: #ff4040;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #200000;
--glass-bg: rgb(32 0 0 / 0.55);
--glass-border: rgb(76 13 13 / 0.7);
--dock-bg: rgb(32 0 0 / 0.7);
--dock-border: rgb(76 13 13 / 0.8);
--dock-tooltip-bg: #301212;
--header-bg: rgb(32 0 0 / 0.8);
--header-border: rgb(76 13 13 / 0.9);
--header-fg: #ff4040;
--header-blur: 12px;
--pixel-grid-color: rgb(154 69 69 / 0.07);
--card-grid-color: rgb(154 69 69 / 0.05);
--panel-grid-color: rgb(154 69 69 / 0.06);
--scrollbar-thumb: rgb(92 88 85 / 0.6);
--scrollbar-thumb-hover: rgb(92 88 85 / 0.85);
--scrollbar-thin-thumb: rgb(92 88 85 / 0.45);
--scrollbar-thin-thumb-hover: rgb(92 88 85 / 0.7);
--brand: #b5e4f4;
--brand-foreground: #120000;
--brand-primary: #b5e4f4;
--brand-accent: #b5e4f4;
--brand-warm: #ff271e;
--brand-deep: #120000;
--brand-muted: #5c5855;
--ink: #ff4040;
--surface-warm: #200000;
--naturegreen: #27a75c;
--naturegreen-foreground: #120000;
--duck-dark: #ff4040;
--duck-teal: #b5e4f4;
--duck-yellow: #ff3c3c;
--duck-orange: #ff271e;
--duck-forest: #27a75c;
--duck-beige: #360606;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dark XP — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/ITSZXY/dark-xp-omarchy (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dark-xp'] {
--background: #01060a;
--foreground: #a7b0bb;
--card: #010a13;
--card-foreground: #a7b0bb;
--popover: #010a13;
--popover-foreground: #a7b0bb;
--muted: #131b24;
--muted-foreground: #7c848f;
--accent: #131b24;
--accent-foreground: #a7b0bb;
--primary: #38619a;
--primary-foreground: #dfe0e0;
--secondary: #131b24;
--secondary-foreground: #a7b0bb;
--cta: #38619a;
--cta-foreground: #dfe0e0;
--ctahover: #5074a6;
--success: #2d4764;
--success-foreground: #abb4be;
--warning: #575d67;
--warning-foreground: #d9dada;
--destructive: #01336a;
--destructive-foreground: #a7b0bb;
--info: #033c82;
--info-foreground: #a7b0bb;
--busy: #2c4869;
--busy-foreground: #aeb6c0;
--busy-ink: #667a92;
--border: #212a34;
--input: #212a34;
--ring: #38619a;
--radius: 0.625rem;
--chart-primary: #38619a;
--chart-secondary: #034286;
--status-success: #2d4764;
--status-warning: #575d67;
--status-in-progress: #033c82;
--page-bg: #01060a;
--page-bg-src: '';
--panel-bg: rgb(1 10 19 / 0.72);
--panel-border: rgb(33 42 52 / 0.9);
--panel-border-bare: rgb(33 42 52 / 0.55);
--panel-raised-bg: rgb(1 10 19 / 0.95);
--panel-raised-border: rgb(33 42 52 / 1);
--panel-accent: #38619a;
--panel-header-bg: rgb(19 27 36 / 0.6);
--panel-header-fg: #a7b0bb;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #010a13;
--glass-bg: rgb(1 10 19 / 0.55);
--glass-border: rgb(33 42 52 / 0.7);
--dock-bg: rgb(1 10 19 / 0.7);
--dock-border: rgb(33 42 52 / 0.8);
--dock-tooltip-bg: #131b24;
--header-bg: rgb(1 10 19 / 0.8);
--header-border: rgb(33 42 52 / 0.9);
--header-fg: #a7b0bb;
--header-blur: 12px;
--pixel-grid-color: rgb(85 94 103 / 0.07);
--card-grid-color: rgb(85 94 103 / 0.05);
--panel-grid-color: rgb(85 94 103 / 0.06);
--scrollbar-thumb: rgb(116 123 130 / 0.6);
--scrollbar-thumb-hover: rgb(116 123 130 / 0.85);
--scrollbar-thin-thumb: rgb(116 123 130 / 0.45);
--scrollbar-thin-thumb-hover: rgb(116 123 130 / 0.7);
--brand: #38619a;
--brand-foreground: #dfe0e0;
--brand-primary: #38619a;
--brand-accent: #2e5995;
--brand-warm: #2c4869;
--brand-deep: #01060a;
--brand-muted: #747b82;
--ink: #a7b0bb;
--surface-warm: #010a13;
--naturegreen: #2d4764;
--naturegreen-foreground: #abb4be;
--duck-dark: #a7b0bb;
--duck-teal: #2e5995;
--duck-yellow: #575d67;
--duck-orange: #2c4869;
--duck-forest: #2d4764;
--duck-beige: #121b24;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dayfox — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Light theme (light), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/defer/omarchy-dayfox-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dayfox'] {
--background: #f6f2ee;
--foreground: #3d2b5a;
--card: #edeae6;
--card-foreground: #3d2b5a;
--popover: #edeae6;
--popover-foreground: #3d2b5a;
--muted: #e7e3e0;
--muted-foreground: #6c5f7c;
--accent: #e7e3e0;
--accent-foreground: #3d2b5a;
--primary: #2848a9;
--primary-foreground: #dddad6;
--secondary: #e7e3e0;
--secondary-foreground: #3d2b5a;
--cta: #2848a9;
--cta-foreground: #dddad6;
--ctahover: #233f95;
--success: #396847;
--success-foreground: #dddad6;
--warning: #ac5402;
--warning-foreground: #f0eeed;
--destructive: #a5222f;
--destructive-foreground: #dddad6;
--info: #2848a9;
--info-foreground: #dddad6;
--busy: #a93b19;
--busy-foreground: #dddad6;
--busy-ink: #a93b19;
--border: #d8d3d5;
--input: #d3ced0;
--ring: #2848a9;
--radius: 0.625rem;
--chart-primary: #2848a9;
--chart-secondary: #6e33ce;
--status-success: #396847;
--status-warning: #ac5402;
--status-in-progress: #2848a9;
--page-bg: #f6f2ee;
--page-bg-src: '';
--panel-bg: rgb(237 234 230 / 0.72);
--panel-border: rgb(216 211 213 / 0.9);
--panel-border-bare: rgb(216 211 213 / 0.55);
--panel-raised-bg: rgb(237 234 230 / 0.95);
--panel-raised-border: rgb(216 211 213 / 1);
--panel-accent: #2848a9;
--panel-header-bg: rgb(231 227 224 / 0.6);
--panel-header-fg: #3d2b5a;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #edeae6;
--glass-bg: rgb(237 234 230 / 0.55);
--glass-border: rgb(216 211 213 / 0.7);
--dock-bg: rgb(237 234 230 / 0.7);
--dock-border: rgb(216 211 213 / 0.8);
--dock-tooltip-bg: #e7e3e0;
--header-bg: rgb(237 234 230 / 0.8);
--header-border: rgb(216 211 213 / 0.9);
--header-fg: #3d2b5a;
--header-blur: 12px;
--pixel-grid-color: rgb(139 129 145 / 0.07);
--card-grid-color: rgb(139 129 145 / 0.05);
--panel-grid-color: rgb(139 129 145 / 0.06);
--scrollbar-thumb: rgb(83 76 69 / 0.6);
--scrollbar-thumb-hover: rgb(83 76 69 / 0.85);
--scrollbar-thin-thumb: rgb(83 76 69 / 0.45);
--scrollbar-thin-thumb-hover: rgb(83 76 69 / 0.7);
--brand: #2848a9;
--brand-foreground: #dddad6;
--brand-primary: #2848a9;
--brand-accent: #287980;
--brand-warm: #a93b19;
--brand-deep: #e7e3e0;
--brand-muted: #534c45;
--ink: #3d2b5a;
--surface-warm: #edeae6;
--naturegreen: #396847;
--naturegreen-foreground: #dddad6;
--duck-dark: #3d2b5a;
--duck-teal: #287980;
--duck-yellow: #ac5402;
--duck-orange: #a93b19;
--duck-forest: #396847;
--duck-beige: #dbd7d8;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dead Space — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/atif-1402/omarchy-deadspace-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dead-space'] {
--background: #010101;
--foreground: #ffffff;
--card: #010101;
--card-foreground: #ffffff;
--popover: #010101;
--popover-foreground: #ffffff;
--muted: #131313;
--muted-foreground: #b3b3b3;
--accent: #131313;
--accent-foreground: #ffffff;
--primary: #f6f5f5;
--primary-foreground: #010101;
--secondary: #131313;
--secondary-foreground: #ffffff;
--cta: #f6f5f5;
--cta-foreground: #010101;
--ctahover: #f7f6f6;
--success: #a49a9a;
--success-foreground: #010101;
--warning: #b2aaaa;
--warning-foreground: #010101;
--destructive: #958a8a;
--destructive-foreground: #010101;
--info: #c1baba;
--info-foreground: #010101;
--busy: #a49a9a;
--busy-foreground: #010101;
--busy-ink: #a49a9a;
--border: #333333;
--input: #333333;
--ring: #f6f5f5;
--radius: 0.625rem;
--chart-primary: #f6f5f5;
--chart-secondary: #cfcaca;
--status-success: #a49a9a;
--status-warning: #b2aaaa;
--status-in-progress: #c1baba;
--page-bg: #010101;
--page-bg-src: '';
--panel-bg: rgb(1 1 1 / 0.72);
--panel-border: rgb(51 51 51 / 0.9);
--panel-border-bare: rgb(51 51 51 / 0.55);
--panel-raised-bg: rgb(1 1 1 / 0.95);
--panel-raised-border: rgb(51 51 51 / 1);
--panel-accent: #f6f5f5;
--panel-header-bg: rgb(19 19 19 / 0.6);
--panel-header-fg: #ffffff;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #010101;
--glass-bg: rgb(1 1 1 / 0.55);
--glass-border: rgb(51 51 51 / 0.7);
--dock-bg: rgb(1 1 1 / 0.7);
--dock-border: rgb(51 51 51 / 0.8);
--dock-tooltip-bg: #131313;
--header-bg: rgb(1 1 1 / 0.8);
--header-border: rgb(51 51 51 / 0.9);
--header-fg: #ffffff;
--header-blur: 12px;
--pixel-grid-color: rgb(115 115 115 / 0.07);
--card-grid-color: rgb(115 115 115 / 0.05);
--panel-grid-color: rgb(115 115 115 / 0.06);
--scrollbar-thumb: rgb(221 219 219 / 0.6);
--scrollbar-thumb-hover: rgb(221 219 219 / 0.85);
--scrollbar-thin-thumb: rgb(221 219 219 / 0.45);
--scrollbar-thin-thumb-hover: rgb(221 219 219 / 0.7);
--brand: #f6f5f5;
--brand-foreground: #010101;
--brand-primary: #f6f5f5;
--brand-accent: #dedada;
--brand-warm: #a49a9a;
--brand-deep: #010101;
--brand-muted: #dddbdb;
--ink: #ffffff;
--surface-warm: #010101;
--naturegreen: #a49a9a;
--naturegreen-foreground: #010101;
--duck-dark: #ffffff;
--duck-teal: #dedada;
--duck-yellow: #b2aaaa;
--duck-orange: #a49a9a;
--duck-forest: #a49a9a;
--duck-beige: #1a1a1a;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Deckard — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/OldJobobo/omarchy-deckard-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='deckard'] {
--background: #001128;
--foreground: #eef1d7;
--card: #001e49;
--card-foreground: #eef1d7;
--popover: #001e49;
--popover-foreground: #eef1d7;
--muted: #122e56;
--muted-foreground: #a7b2ac;
--accent: #122e56;
--accent-foreground: #eef1d7;
--primary: #d4fcfc;
--primary-foreground: #001128;
--secondary: #122e56;
--secondary-foreground: #eef1d7;
--cta: #d4fcfc;
--cta-foreground: #001128;
--ctahover: #d9fcfc;
--success: #5a9f72;
--success-foreground: #001128;
--warning: #d2df6f;
--warning-foreground: #001128;
--destructive: #c24e13;
--destructive-foreground: #f8f9ee;
--info: #1fb1a5;
--info-foreground: #001128;
--busy: #ca9741;
--busy-foreground: #001128;
--busy-ink: #ca9741;
--border: #2f4765;
--input: #2f4765;
--ring: #d4fcfc;
--radius: 0.625rem;
--chart-primary: #d4fcfc;
--chart-secondary: #4b64fd;
--status-success: #5a9f72;
--status-warning: #d2df6f;
--status-in-progress: #1fb1a5;
--page-bg: #001128;
--page-bg-src: '';
--panel-bg: rgb(0 30 73 / 0.72);
--panel-border: rgb(47 71 101 / 0.9);
--panel-border-bare: rgb(47 71 101 / 0.55);
--panel-raised-bg: rgb(0 30 73 / 0.95);
--panel-raised-border: rgb(47 71 101 / 1);
--panel-accent: #d4fcfc;
--panel-header-bg: rgb(18 46 86 / 0.6);
--panel-header-fg: #eef1d7;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #001e49;
--glass-bg: rgb(0 30 73 / 0.55);
--glass-border: rgb(47 71 101 / 0.7);
--dock-bg: rgb(0 30 73 / 0.7);
--dock-border: rgb(47 71 101 / 0.8);
--dock-tooltip-bg: #122e56;
--header-bg: rgb(0 30 73 / 0.8);
--header-border: rgb(47 71 101 / 0.9);
--header-fg: #eef1d7;
--header-blur: 12px;
--pixel-grid-color: rgb(107 125 137 / 0.07);
--card-grid-color: rgb(107 125 137 / 0.05);
--panel-grid-color: rgb(107 125 137 / 0.06);
--scrollbar-thumb: rgb(63 116 192 / 0.6);
--scrollbar-thumb-hover: rgb(63 116 192 / 0.85);
--scrollbar-thin-thumb: rgb(63 116 192 / 0.45);
--scrollbar-thin-thumb-hover: rgb(63 116 192 / 0.7);
--brand: #d4fcfc;
--brand-foreground: #001128;
--brand-primary: #d4fcfc;
--brand-accent: #83f0f1;
--brand-warm: #ca9741;
--brand-deep: #001128;
--brand-muted: #3f74c0;
--ink: #eef1d7;
--surface-warm: #001e49;
--naturegreen: #5a9f72;
--naturegreen-foreground: #001128;
--duck-dark: #eef1d7;
--duck-teal: #83f0f1;
--duck-yellow: #d2df6f;
--duck-orange: #ca9741;
--duck-forest: #5a9f72;
--duck-beige: #183357;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Delete — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/signaldirective/delete-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='delete'] {
--background: #090b11;
--foreground: #f2f1fa;
--card: #11141e;
--card-foreground: #f2f1fa;
--popover: #11141e;
--popover-foreground: #f2f1fa;
--muted: #22242e;
--muted-foreground: #afafb8;
--accent: #22242e;
--accent-foreground: #f2f1fa;
--primary: #e2e3ee;
--primary-foreground: #090b11;
--secondary: #22242e;
--secondary-foreground: #f2f1fa;
--cta: #e2e3ee;
--cta-foreground: #090b11;
--ctahover: #e5e6f0;
--success: #7f86b8;
--success-foreground: #090b11;
--warning: #a19ece;
--warning-foreground: #090b11;
--destructive: #aba8d7;
--destructive-foreground: #090b11;
--info: #7575aa;
--info-foreground: #090b11;
--busy: #a6a3d3;
--busy-foreground: #090b11;
--busy-ink: #a6a3d3;
--border: #3d3f49;
--input: #3d3f49;
--ring: #e2e3ee;
--radius: 0.625rem;
--chart-primary: #e2e3ee;
--chart-secondary: #9895c7;
--status-success: #7f86b8;
--status-warning: #a19ece;
--status-in-progress: #7575aa;
--page-bg: #090b11;
--page-bg-src: '';
--panel-bg: rgb(17 20 30 / 0.72);
--panel-border: rgb(61 63 73 / 0.9);
--panel-border-bare: rgb(61 63 73 / 0.55);
--panel-raised-bg: rgb(17 20 30 / 0.95);
--panel-raised-border: rgb(61 63 73 / 1);
--panel-accent: #e2e3ee;
--panel-header-bg: rgb(34 36 46 / 0.6);
--panel-header-fg: #f2f1fa;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #11141e;
--glass-bg: rgb(17 20 30 / 0.55);
--glass-border: rgb(61 63 73 / 0.7);
--dock-bg: rgb(17 20 30 / 0.7);
--dock-border: rgb(61 63 73 / 0.8);
--dock-tooltip-bg: #22242e;
--header-bg: rgb(17 20 30 / 0.8);
--header-border: rgb(61 63 73 / 0.9);
--header-fg: #f2f1fa;
--header-blur: 12px;
--pixel-grid-color: rgb(118 119 129 / 0.07);
--card-grid-color: rgb(118 119 129 / 0.05);
--panel-grid-color: rgb(118 119 129 / 0.06);
--scrollbar-thumb: rgb(122 130 154 / 0.6);
--scrollbar-thumb-hover: rgb(122 130 154 / 0.85);
--scrollbar-thin-thumb: rgb(122 130 154 / 0.45);
--scrollbar-thin-thumb-hover: rgb(122 130 154 / 0.7);
--brand: #e2e3ee;
--brand-foreground: #090b11;
--brand-primary: #e2e3ee;
--brand-accent: #a9accb;
--brand-warm: #a6a3d3;
--brand-deep: #090b11;
--brand-muted: #7a829a;
--ink: #f2f1fa;
--surface-warm: #11141e;
--naturegreen: #7f86b8;
--naturegreen-foreground: #090b11;
--duck-dark: #f2f1fa;
--duck-teal: #a9accb;
--duck-yellow: #a19ece;
--duck-orange: #a6a3d3;
--duck-forest: #7f86b8;
--duck-beige: #282a34;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Delorean — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/jbnunn/omarchy-delorean-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='delorean'] {
--background: #070112;
--foreground: #f9f9f9;
--card: #0d0221;
--card-foreground: #f9f9f9;
--popover: #0d0221;
--popover-foreground: #f9f9f9;
--muted: #1e1431;
--muted-foreground: #b2afb8;
--accent: #1e1431;
--accent-foreground: #f9f9f9;
--primary: #52fff7;
--primary-foreground: #070112;
--secondary: #1e1431;
--secondary-foreground: #f9f9f9;
--cta: #52fff7;
--cta-foreground: #070112;
--ctahover: #67fff8;
--success: #05ffa1;
--success-foreground: #070112;
--warning: #fffb96;
--warning-foreground: #070112;
--destructive: #ff2a6d;
--destructive-foreground: #070112;
--info: #01cdfe;
--info-foreground: #070112;
--busy: #ff9382;
--busy-foreground: #070112;
--busy-ink: #ff9382;
--border: #3b324b;
--input: #3b324b;
--ring: #52fff7;
--radius: 0.625rem;
--chart-primary: #52fff7;
--chart-secondary: #b967ff;
--status-success: #05ffa1;
--status-warning: #fffb96;
--status-in-progress: #01cdfe;
--page-bg: #070112;
--page-bg-src: '';
--panel-bg: rgb(13 2 33 / 0.72);
--panel-border: rgb(59 50 75 / 0.9);
--panel-border-bare: rgb(59 50 75 / 0.55);
--panel-raised-bg: rgb(13 2 33 / 0.95);
--panel-raised-border: rgb(59 50 75 / 1);
--panel-accent: #52fff7;
--panel-header-bg: rgb(30 20 49 / 0.6);
--panel-header-fg: #f9f9f9;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0d0221;
--glass-bg: rgb(13 2 33 / 0.55);
--glass-border: rgb(59 50 75 / 0.7);
--dock-bg: rgb(13 2 33 / 0.7);
--dock-border: rgb(59 50 75 / 0.8);
--dock-tooltip-bg: #1e1431;
--header-bg: rgb(13 2 33 / 0.8);
--header-border: rgb(59 50 75 / 0.9);
--header-fg: #f9f9f9;
--header-blur: 12px;
--pixel-grid-color: rgb(119 113 130 / 0.07);
--card-grid-color: rgb(119 113 130 / 0.05);
--panel-grid-color: rgb(119 113 130 / 0.06);
--scrollbar-thumb: rgb(76 26 87 / 0.6);
--scrollbar-thumb-hover: rgb(76 26 87 / 0.85);
--scrollbar-thin-thumb: rgb(76 26 87 / 0.45);
--scrollbar-thin-thumb-hover: rgb(76 26 87 / 0.7);
--brand: #52fff7;
--brand-foreground: #070112;
--brand-primary: #52fff7;
--brand-accent: #05ffa1;
--brand-warm: #ff9382;
--brand-deep: #070112;
--brand-muted: #4c1a57;
--ink: #f9f9f9;
--surface-warm: #0d0221;
--naturegreen: #05ffa1;
--naturegreen-foreground: #070112;
--duck-dark: #f9f9f9;
--duck-teal: #05ffa1;
--duck-yellow: #fffb96;
--duck-orange: #ff9382;
--duck-forest: #05ffa1;
--duck-beige: #251b37;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Demon — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/HANCORE-linux/omarchy-demon-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='demon'] {
--background: #080808;
--foreground: #e0d4c2;
--card: #0f0f0f;
--card-foreground: #e0d4c2;
--popover: #0f0f0f;
--popover-foreground: #e0d4c2;
--muted: #202020;
--muted-foreground: #a1998c;
--accent: #202020;
--accent-foreground: #e0d4c2;
--primary: #7a7d80;
--primary-foreground: #080808;
--secondary: #202020;
--secondary-foreground: #e0d4c2;
--cta: #7a7d80;
--cta-foreground: #080808;
--ctahover: #8a8d8f;
--success: #586259;
--success-foreground: #e3d8c8;
--warning: #e24c00;
--warning-foreground: #080808;
--destructive: #bb1c1c;
--destructive-foreground: #e3d8c8;
--info: #5c624b;
--info-foreground: #e3d8c8;
--busy: #cf340e;
--busy-foreground: #f3f3f3;
--busy-ink: #d44826;
--border: #383632;
--input: #383632;
--ring: #7a7d80;
--radius: 0.625rem;
--chart-primary: #7a7d80;
--chart-secondary: #655e56;
--status-success: #586259;
--status-warning: #e24c00;
--status-in-progress: #5c624b;
--page-bg: #080808;
--page-bg-src: '';
--panel-bg: rgb(15 15 15 / 0.72);
--panel-border: rgb(56 54 50 / 0.9);
--panel-border-bare: rgb(56 54 50 / 0.55);
--panel-raised-bg: rgb(15 15 15 / 0.95);
--panel-raised-border: rgb(56 54 50 / 1);
--panel-accent: #7a7d80;
--panel-header-bg: rgb(32 32 32 / 0.6);
--panel-header-fg: #e0d4c2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0f0f0f;
--glass-bg: rgb(15 15 15 / 0.55);
--glass-border: rgb(56 54 50 / 0.7);
--dock-bg: rgb(15 15 15 / 0.7);
--dock-border: rgb(56 54 50 / 0.8);
--dock-tooltip-bg: #202020;
--header-bg: rgb(15 15 15 / 0.8);
--header-border: rgb(56 54 50 / 0.9);
--header-fg: #e0d4c2;
--header-blur: 12px;
--pixel-grid-color: rgb(109 104 96 / 0.07);
--card-grid-color: rgb(109 104 96 / 0.05);
--panel-grid-color: rgb(109 104 96 / 0.06);
--scrollbar-thumb: rgb(107 104 103 / 0.6);
--scrollbar-thumb-hover: rgb(107 104 103 / 0.85);
--scrollbar-thin-thumb: rgb(107 104 103 / 0.45);
--scrollbar-thin-thumb-hover: rgb(107 104 103 / 0.7);
--brand: #7a7d80;
--brand-foreground: #080808;
--brand-primary: #7a7d80;
--brand-accent: #7a7d80;
--brand-warm: #cf340e;
--brand-deep: #080808;
--brand-muted: #6b6867;
--ink: #e0d4c2;
--surface-warm: #0f0f0f;
--naturegreen: #586259;
--naturegreen-foreground: #e3d8c8;
--duck-dark: #e0d4c2;
--duck-teal: #7a7d80;
--duck-yellow: #e24c00;
--duck-orange: #cf340e;
--duck-forest: #586259;
--duck-beige: #242321;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Desert Gleam — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Light theme (light), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/avis3nna/desert-gleam (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='desert-gleam'] {
--background: #faf7f3;
--foreground: #3a3644;
--card: #f1eeea;
--card-foreground: #3a3644;
--popover: #f1eeea;
--popover-foreground: #3a3644;
--muted: #ebe8e4;
--muted-foreground: #68656d;
--accent: #ebe8e4;
--accent-foreground: #3a3644;
--primary: #7a6d9e;
--primary-foreground: #fbfbfb;
--secondary: #ebe8e4;
--secondary-foreground: #3a3644;
--cta: #7a6d9e;
--cta-foreground: #fbfbfb;
--ctahover: #6b608b;
--success: #7a9977;
--success-foreground: #2c2933;
--warning: #c68d53;
--warning-foreground: #332f3c;
--destructive: #b36667;
--destructive-foreground: #111110;
--info: #7a6d9e;
--info-foreground: #fbfbfb;
--busy: #bd7a5d;
--busy-foreground: #24222b;
--busy-ink: #97624a;
--border: #dbd8d6;
--input: #d6d3d1;
--ring: #7a6d9e;
--radius: 0.625rem;
--chart-primary: #7a6d9e;
--chart-secondary: #d4886a;
--status-success: #7a9977;
--status-warning: #c68d53;
--status-in-progress: #7a6d9e;
--page-bg: #faf7f3;
--page-bg-src: '';
--panel-bg: rgb(241 238 234 / 0.72);
--panel-border: rgb(219 216 214 / 0.9);
--panel-border-bare: rgb(219 216 214 / 0.55);
--panel-raised-bg: rgb(241 238 234 / 0.95);
--panel-raised-border: rgb(219 216 214 / 1);
--panel-accent: #7a6d9e;
--panel-header-bg: rgb(235 232 228 / 0.6);
--panel-header-fg: #3a3644;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #f1eeea;
--glass-bg: rgb(241 238 234 / 0.55);
--glass-border: rgb(219 216 214 / 0.7);
--dock-bg: rgb(241 238 234 / 0.7);
--dock-border: rgb(219 216 214 / 0.8);
--dock-tooltip-bg: #ebe8e4;
--header-bg: rgb(241 238 234 / 0.8);
--header-border: rgb(219 216 214 / 0.9);
--header-fg: #3a3644;
--header-blur: 12px;
--pixel-grid-color: rgb(139 136 139 / 0.07);
--card-grid-color: rgb(139 136 139 / 0.05);
--panel-grid-color: rgb(139 136 139 / 0.06);
--scrollbar-thumb: rgb(216 203 192 / 0.6);
--scrollbar-thumb-hover: rgb(216 203 192 / 0.85);
--scrollbar-thin-thumb: rgb(216 203 192 / 0.45);
--scrollbar-thin-thumb-hover: rgb(216 203 192 / 0.7);
--brand: #7a6d9e;
--brand-foreground: #fbfbfb;
--brand-primary: #7a6d9e;
--brand-accent: #5f8d7c;
--brand-warm: #bd7a5d;
--brand-deep: #ebe8e4;
--brand-muted: #d8cbc0;
--ink: #3a3644;
--surface-warm: #f1eeea;
--naturegreen: #7a9977;
--naturegreen-foreground: #2c2933;
--duck-dark: #3a3644;
--duck-teal: #5f8d7c;
--duck-yellow: #c68d53;
--duck-orange: #bd7a5d;
--duck-forest: #7a9977;
--duck-beige: #dfdcd9;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Desert Twilight — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/avis3nna/desert-twilight (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='desert-twilight'] {
--background: #0e0e13;
--foreground: #f0f0f2;
--card: #1a1922;
--card-foreground: #f0f0f2;
--popover: #1a1922;
--popover-foreground: #f0f0f2;
--muted: #2a2931;
--muted-foreground: #b0b0b4;
--accent: #2a2931;
--accent-foreground: #f0f0f2;
--primary: #94d4c4;
--primary-foreground: #0e0e13;
--secondary: #2a2931;
--secondary-foreground: #f0f0f2;
--cta: #94d4c4;
--cta-foreground: #0e0e13;
--ctahover: #a1d9cb;
--success: #5fb3ac;
--success-foreground: #0e0e13;
--warning: #d4a255;
--warning-foreground: #0e0e13;
--destructive: #d16b6b;
--destructive-foreground: #0e0e13;
--info: #a89dd1;
--info-foreground: #0e0e13;
--busy: #d38760;
--busy-foreground: #0e0e13;
--busy-ink: #d38760;
--border: #44434b;
--input: #44434b;
--ring: #94d4c4;
--radius: 0.625rem;
--chart-primary: #94d4c4;
--chart-secondary: #e8845c;
--status-success: #5fb3ac;
--status-warning: #d4a255;
--status-in-progress: #a89dd1;
--page-bg: #0e0e13;
--page-bg-src: '';
--panel-bg: rgb(26 25 34 / 0.72);
--panel-border: rgb(68 67 75 / 0.9);
--panel-border-bare: rgb(68 67 75 / 0.55);
--panel-raised-bg: rgb(26 25 34 / 0.95);
--panel-raised-border: rgb(68 67 75 / 1);
--panel-accent: #94d4c4;
--panel-header-bg: rgb(42 41 49 / 0.6);
--panel-header-fg: #f0f0f2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #1a1922;
--glass-bg: rgb(26 25 34 / 0.55);
--glass-border: rgb(68 67 75 / 0.7);
--dock-bg: rgb(26 25 34 / 0.7);
--dock-border: rgb(68 67 75 / 0.8);
--dock-tooltip-bg: #2a2931;
--header-bg: rgb(26 25 34 / 0.8);
--header-border: rgb(68 67 75 / 0.9);
--header-fg: #f0f0f2;
--header-blur: 12px;
--pixel-grid-color: rgb(122 122 128 / 0.07);
--card-grid-color: rgb(122 122 128 / 0.05);
--panel-grid-color: rgb(122 122 128 / 0.06);
--scrollbar-thumb: rgb(58 57 77 / 0.6);
--scrollbar-thumb-hover: rgb(58 57 77 / 0.85);
--scrollbar-thin-thumb: rgb(58 57 77 / 0.45);
--scrollbar-thin-thumb-hover: rgb(58 57 77 / 0.7);
--brand: #94d4c4;
--brand-foreground: #0e0e13;
--brand-primary: #94d4c4;
--brand-accent: #7dbba3;
--brand-warm: #d38760;
--brand-deep: #0e0e13;
--brand-muted: #3a394d;
--ink: #f0f0f2;
--surface-warm: #1a1922;
--naturegreen: #5fb3ac;
--naturegreen-foreground: #0e0e13;
--duck-dark: #f0f0f2;
--duck-teal: #7dbba3;
--duck-yellow: #d4a255;
--duck-orange: #d38760;
--duck-forest: #5fb3ac;
--duck-beige: #2f2f37;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Dotrb — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/dotsilva/omarchy-dotrb-theme (colors.toml (16-colour ANSI)), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dotrb'] {
--background: #0a0a0a;
--foreground: #f5f5f5;
--card: #121212;
--card-foreground: #f5f5f5;
--popover: #121212;
--popover-foreground: #f5f5f5;
--muted: #232323;
--muted-foreground: #b1b1b1;
--accent: #232323;
--accent-foreground: #f5f5f5;
--primary: #b4b4b4;
--primary-foreground: #0a0a0a;
--secondary: #232323;
--secondary-foreground: #f5f5f5;
--cta: #b4b4b4;
--cta-foreground: #0a0a0a;
--ctahover: #bdbdbd;
--success: #b14242;
--success-foreground: #f5f5f5;
--warning: #f0be66;
--warning-foreground: #0a0a0a;
--destructive: #d66e36;
--destructive-foreground: #0a0a0a;
--info: #4a8b8b;
--info-foreground: #0a0a0a;
--busy: #e3964e;
--busy-foreground: #0a0a0a;
--busy-ink: #e3964e;
--border: #3f3f3f;
--input: #3f3f3f;
--ring: #b4b4b4;
--radius: 0.625rem;
--chart-primary: #b4b4b4;
--chart-secondary: #a7a7a7;
--status-success: #b14242;
--status-warning: #f0be66;
--status-in-progress: #4a8b8b;
--page-bg: #0a0a0a;
--page-bg-src: '';
--panel-bg: rgb(18 18 18 / 0.72);
--panel-border: rgb(63 63 63 / 0.9);
--panel-border-bare: rgb(63 63 63 / 0.55);
--panel-raised-bg: rgb(18 18 18 / 0.95);
--panel-raised-border: rgb(63 63 63 / 1);
--panel-accent: #b4b4b4;
--panel-header-bg: rgb(35 35 35 / 0.6);
--panel-header-fg: #f5f5f5;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #121212;
--glass-bg: rgb(18 18 18 / 0.55);
--glass-border: rgb(63 63 63 / 0.7);
--dock-bg: rgb(18 18 18 / 0.7);
--dock-border: rgb(63 63 63 / 0.8);
--dock-tooltip-bg: #232323;
--header-bg: rgb(18 18 18 / 0.8);
--header-border: rgb(63 63 63 / 0.9);
--header-fg: #f5f5f5;
--header-blur: 12px;
--pixel-grid-color: rgb(120 120 120 / 0.07);
--card-grid-color: rgb(120 120 120 / 0.05);
--panel-grid-color: rgb(120 120 120 / 0.06);
--scrollbar-thumb: rgb(148 148 148 / 0.6);
--scrollbar-thumb-hover: rgb(148 148 148 / 0.85);
--scrollbar-thin-thumb: rgb(148 148 148 / 0.45);
--scrollbar-thin-thumb-hover: rgb(148 148 148 / 0.7);
--brand: #b4b4b4;
--brand-foreground: #0a0a0a;
--brand-primary: #b4b4b4;
--brand-accent: #b4b4b4;
--brand-warm: #e3964e;
--brand-deep: #0a0a0a;
--brand-muted: #949494;
--ink: #f5f5f5;
--surface-warm: #121212;
--naturegreen: #b14242;
--naturegreen-foreground: #f5f5f5;
--duck-dark: #f5f5f5;
--duck-teal: #b4b4b4;
--duck-yellow: #f0be66;
--duck-orange: #e3964e;
--duck-forest: #b14242;
--duck-beige: #292929;
--logo-src: '/officer-logo.svg';
}
+97
View File
@@ -0,0 +1,97 @@
/*
Drac — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/ShehabShaef/omarchy-drac-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='drac'] {
--background: #090b16;
--foreground: #f2f4f8;
--card: #101428;
--card-foreground: #f2f4f8;
--popover: #101428;
--popover-foreground: #f2f4f8;
--muted: #212437;
--muted-foreground: #aeb1ba;
--accent: #212437;
--accent-foreground: #f2f4f8;
--primary: #a4ffff;
--primary-foreground: #090b16;
--secondary: #212437;
--secondary-foreground: #f2f4f8;
--cta: #a4ffff;
--cta-foreground: #090b16;
--ctahover: #afffff;
--success: #1e90ff;
--success-foreground: #090b16;
--warning: #f1fa8c;
--warning-foreground: #090b16;
--destructive: #ff5555;
--destructive-foreground: #090b16;
--info: #50fa7b;
--info-foreground: #090b16;
--busy: #f8a871;
--busy-foreground: #090b16;
--busy-ink: #f8a871;
--border: #3c4051;
--input: #3c4051;
--ring: #a4ffff;
--radius: 0.625rem;
--chart-primary: #a4ffff;
--chart-secondary: #ff79c6;
--status-success: #1e90ff;
--status-warning: #f1fa8c;
--status-in-progress: #50fa7b;
--page-bg: #090b16;
--page-bg-src: '';
--panel-bg: rgb(16 20 40 / 0.72);
--panel-border: rgb(60 64 81 / 0.9);
--panel-border-bare: rgb(60 64 81 / 0.55);
--panel-raised-bg: rgb(16 20 40 / 0.95);
--panel-raised-border: rgb(60 64 81 / 1);
--panel-accent: #a4ffff;
--panel-header-bg: rgb(33 36 55 / 0.6);
--panel-header-fg: #f2f4f8;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #101428;
--glass-bg: rgb(16 20 40 / 0.55);
--glass-border: rgb(60 64 81 / 0.7);
--dock-bg: rgb(16 20 40 / 0.7);
--dock-border: rgb(60 64 81 / 0.8);
--dock-tooltip-bg: #212437;
--header-bg: rgb(16 20 40 / 0.8);
--header-border: rgb(60 64 81 / 0.9);
--header-fg: #f2f4f8;
--header-blur: 12px;
--pixel-grid-color: rgb(118 121 134 / 0.07);
--card-grid-color: rgb(118 121 134 / 0.05);
--panel-grid-color: rgb(118 121 134 / 0.06);
--scrollbar-thumb: rgb(85 85 85 / 0.6);
--scrollbar-thumb-hover: rgb(85 85 85 / 0.85);
--scrollbar-thin-thumb: rgb(85 85 85 / 0.45);
--scrollbar-thin-thumb-hover: rgb(85 85 85 / 0.7);
--brand: #a4ffff;
--brand-foreground: #090b16;
--brand-primary: #a4ffff;
--brand-accent: #8be9fd;
--brand-warm: #f8a871;
--brand-deep: #090b16;
--brand-muted: #555555;
--ink: #f2f4f8;
--surface-warm: #101428;
--naturegreen: #1e90ff;
--naturegreen-foreground: #090b16;
--duck-dark: #f2f4f8;
--duck-teal: #8be9fd;
--duck-yellow: #f1fa8c;
--duck-orange: #f8a871;
--duck-forest: #1e90ff;
--duck-beige: #272a3d;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dracula — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/catlee/omarchy-dracula-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dracula'] {
--background: #16171e;
--foreground: #f8f8f2;
--card: #282a36;
--card-foreground: #f8f8f2;
--popover: #282a36;
--popover-foreground: #f8f8f2;
--muted: #373944;
--muted-foreground: #bababa;
--accent: #373944;
--accent-foreground: #f8f8f2;
--primary: #a4ffff;
--primary-foreground: #16171e;
--secondary: #373944;
--secondary-foreground: #f8f8f2;
--cta: #a4ffff;
--cta-foreground: #16171e;
--ctahover: #afffff;
--success: #50fa7b;
--success-foreground: #16171e;
--warning: #f1fa8c;
--warning-foreground: #16171e;
--destructive: #ff5555;
--destructive-foreground: #16171e;
--info: #bd93f9;
--info-foreground: #16171e;
--busy: #f8a871;
--busy-foreground: #16171e;
--busy-ink: #f8a871;
--border: #51525b;
--input: #51525b;
--ring: #a4ffff;
--radius: 0.625rem;
--chart-primary: #a4ffff;
--chart-secondary: #ff79c6;
--status-success: #50fa7b;
--status-warning: #f1fa8c;
--status-in-progress: #bd93f9;
--page-bg: #16171e;
--page-bg-src: '';
--panel-bg: rgb(40 42 54 / 0.72);
--panel-border: rgb(81 82 91 / 0.9);
--panel-border-bare: rgb(81 82 91 / 0.55);
--panel-raised-bg: rgb(40 42 54 / 0.95);
--panel-raised-border: rgb(81 82 91 / 1);
--panel-accent: #a4ffff;
--panel-header-bg: rgb(55 57 68 / 0.6);
--panel-header-fg: #f8f8f2;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #282a36;
--glass-bg: rgb(40 42 54 / 0.55);
--glass-border: rgb(81 82 91 / 0.7);
--dock-bg: rgb(40 42 54 / 0.7);
--dock-border: rgb(81 82 91 / 0.8);
--dock-tooltip-bg: #373944;
--header-bg: rgb(40 42 54 / 0.8);
--header-border: rgb(81 82 91 / 0.9);
--header-fg: #f8f8f2;
--header-blur: 12px;
--pixel-grid-color: rgb(134 135 139 / 0.07);
--card-grid-color: rgb(134 135 139 / 0.05);
--panel-grid-color: rgb(134 135 139 / 0.06);
--scrollbar-thumb: rgb(98 114 164 / 0.6);
--scrollbar-thumb-hover: rgb(98 114 164 / 0.85);
--scrollbar-thin-thumb: rgb(98 114 164 / 0.45);
--scrollbar-thin-thumb-hover: rgb(98 114 164 / 0.7);
--brand: #a4ffff;
--brand-foreground: #16171e;
--brand-primary: #a4ffff;
--brand-accent: #8be9fd;
--brand-warm: #f8a871;
--brand-deep: #16171e;
--brand-muted: #6272a4;
--ink: #f8f8f2;
--surface-warm: #282a36;
--naturegreen: #50fa7b;
--naturegreen-foreground: #16171e;
--duck-dark: #f8f8f2;
--duck-teal: #8be9fd;
--duck-yellow: #f1fa8c;
--duck-orange: #f8a871;
--duck-forest: #50fa7b;
--duck-beige: #3d3f49;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dragon — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/thatmechguy/omarchy-dragon-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dragon'] {
--background: #0d070c;
--foreground: #ebe5d6;
--card: #180d15;
--card-foreground: #ebe5d6;
--popover: #180d15;
--popover-foreground: #ebe5d6;
--muted: #281e25;
--muted-foreground: #aca49c;
--accent: #281e25;
--accent-foreground: #ebe5d6;
--primary: #95caca;
--primary-foreground: #0d070c;
--secondary: #281e25;
--secondary-foreground: #ebe5d6;
--cta: #95caca;
--cta-foreground: #0d070c;
--ctahover: #a2d0d0;
--success: #78a682;
--success-foreground: #0d070c;
--warning: #f7e1c7;
--warning-foreground: #0d070c;
--destructive: #ff3d5a;
--destructive-foreground: #0d070c;
--info: #5298c4;
--info-foreground: #0d070c;
--busy: #fb8f91;
--busy-foreground: #0d070c;
--busy-ink: #fb8f91;
--border: #41373b;
--input: #41373b;
--ring: #95caca;
--radius: 0.625rem;
--chart-primary: #95caca;
--chart-secondary: #c479a9;
--status-success: #78a682;
--status-warning: #f7e1c7;
--status-in-progress: #5298c4;
--page-bg: #0d070c;
--page-bg-src: '';
--panel-bg: rgb(24 13 21 / 0.72);
--panel-border: rgb(65 55 59 / 0.9);
--panel-border-bare: rgb(65 55 59 / 0.55);
--panel-raised-bg: rgb(24 13 21 / 0.95);
--panel-raised-border: rgb(65 55 59 / 1);
--panel-accent: #95caca;
--panel-header-bg: rgb(40 30 37 / 0.6);
--panel-header-fg: #ebe5d6;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #180d15;
--glass-bg: rgb(24 13 21 / 0.55);
--glass-border: rgb(65 55 59 / 0.7);
--dock-bg: rgb(24 13 21 / 0.7);
--dock-border: rgb(65 55 59 / 0.8);
--dock-tooltip-bg: #281e25;
--header-bg: rgb(24 13 21 / 0.8);
--header-border: rgb(65 55 59 / 0.9);
--header-fg: #ebe5d6;
--header-blur: 12px;
--pixel-grid-color: rgb(119 110 108 / 0.07);
--card-grid-color: rgb(119 110 108 / 0.05);
--panel-grid-color: rgb(119 110 108 / 0.06);
--scrollbar-thumb: rgb(58 26 34 / 0.6);
--scrollbar-thumb-hover: rgb(58 26 34 / 0.85);
--scrollbar-thin-thumb: rgb(58 26 34 / 0.45);
--scrollbar-thin-thumb-hover: rgb(58 26 34 / 0.7);
--brand: #95caca;
--brand-foreground: #0d070c;
--brand-primary: #95caca;
--brand-accent: #7abaa9;
--brand-warm: #fb8f91;
--brand-deep: #0d070c;
--brand-muted: #3a1a22;
--ink: #ebe5d6;
--surface-warm: #180d15;
--naturegreen: #78a682;
--naturegreen-foreground: #0d070c;
--duck-dark: #ebe5d6;
--duck-teal: #7abaa9;
--duck-yellow: #f7e1c7;
--duck-orange: #fb8f91;
--duck-forest: #78a682;
--duck-beige: #2d2328;
--logo-src: '/officer-logo.svg';
}
@@ -0,0 +1,97 @@
/*
Dreamwave — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
Dark theme (dark), by luminance of its own background — the source repo carries no mode flag of its own to read.
Source: https://github.com/RiO7MAKK3R/omarchy-dreamwave-theme (alacritty.toml), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4
colors.toml — some tokens here are reconstructed, not authored. Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
[data-theme='dreamwave'] {
--background: #06040c;
--foreground: #f5eafd;
--card: #0a0716;
--card-foreground: #f5eafd;
--popover: #0a0716;
--popover-foreground: #f5eafd;
--muted: #1b1826;
--muted-foreground: #afa6b8;
--accent: #1b1826;
--accent-foreground: #f5eafd;
--primary: #e37bd1;
--primary-foreground: #06040c;
--secondary: #1b1826;
--secondary-foreground: #f5eafd;
--cta: #e37bd1;
--cta-foreground: #06040c;
--ctahover: #e68bd7;
--success: #7f049e;
--success-foreground: #f5eafd;
--warning: #790aac;
--warning-foreground: #f5eafd;
--destructive: #a70b68;
--destructive-foreground: #f5eafd;
--info: #ca2cb0;
--info-foreground: #ffffff;
--busy: #900b8a;
--busy-foreground: #f5eafd;
--busy-ink: #b154ad;
--border: #383443;
--input: #383443;
--ring: #e37bd1;
--radius: 0.625rem;
--chart-primary: #e37bd1;
--chart-secondary: #a604e9;
--status-success: #7f049e;
--status-warning: #790aac;
--status-in-progress: #ca2cb0;
--page-bg: #06040c;
--page-bg-src: '';
--panel-bg: rgb(10 7 22 / 0.72);
--panel-border: rgb(56 52 67 / 0.9);
--panel-border-bare: rgb(56 52 67 / 0.55);
--panel-raised-bg: rgb(10 7 22 / 0.95);
--panel-raised-border: rgb(56 52 67 / 1);
--panel-accent: #e37bd1;
--panel-header-bg: rgb(27 24 38 / 0.6);
--panel-header-fg: #f5eafd;
--panel-radius: 0.75rem;
--panel-blur: 12px;
--card-bg: #0a0716;
--glass-bg: rgb(10 7 22 / 0.55);
--glass-border: rgb(56 52 67 / 0.7);
--dock-bg: rgb(10 7 22 / 0.7);
--dock-border: rgb(56 52 67 / 0.8);
--dock-tooltip-bg: #1b1826;
--header-bg: rgb(10 7 22 / 0.8);
--header-border: rgb(56 52 67 / 0.9);
--header-fg: #f5eafd;
--header-blur: 12px;
--pixel-grid-color: rgb(116 109 126 / 0.07);
--card-grid-color: rgb(116 109 126 / 0.05);
--panel-grid-color: rgb(116 109 126 / 0.06);
--scrollbar-thumb: rgb(140 83 183 / 0.6);
--scrollbar-thumb-hover: rgb(140 83 183 / 0.85);
--scrollbar-thin-thumb: rgb(140 83 183 / 0.45);
--scrollbar-thin-thumb-hover: rgb(140 83 183 / 0.7);
--brand: #e37bd1;
--brand-foreground: #06040c;
--brand-primary: #e37bd1;
--brand-accent: #c429b2;
--brand-warm: #900b8a;
--brand-deep: #06040c;
--brand-muted: #8c53b7;
--ink: #f5eafd;
--surface-warm: #0a0716;
--naturegreen: #7f049e;
--naturegreen-foreground: #f5eafd;
--duck-dark: #f5eafd;
--duck-teal: #c429b2;
--duck-yellow: #790aac;
--duck-orange: #900b8a;
--duck-forest: #7f049e;
--duck-beige: #221e2d;
--logo-src: '/officer-logo.svg';
}

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