Horae follows the Invoicer design system: editorial warm-dark surfaces, serif display type, humanist sans-serif for UI, and monospace numerals — an accountancy aesthetic that suits time-tracking and invoicing. Visual prototypes with pixel-accurate examples live in design/project/dark.dc.html.
All colors are CSS custom properties in assets/css/horae.css.
| Token | Value | Usage |
|---|---|---|
--color-bg |
#100F0C |
Page background (void) |
--color-bg-secondary |
#1A1813 |
Cards, nav bar, sidebar |
--color-bg-tertiary |
#232019 |
Table cells, raised chips |
--color-bg-overlay |
#2E2A22 |
Hover-raise over a raised surface |
--color-menu |
#201D17 |
Dropdown / popover surface (deeper than overlay) |
--color-row-hover |
#1C1A15 |
Table / ledger row hover (sinks) |
| Token | Value | Usage |
|---|---|---|
--color-text-strong |
#F6F2E9 |
Headings, wordmark, solid-button ink |
--color-text |
#EFEAE0 |
Primary body text |
--color-text-secondary |
#A29C8D |
Labels, captions, sidebar links |
--color-text-muted |
#7C7565 |
Placeholders, section headers |
--color-label |
#6A6353 |
Uppercase micro-labels, table headers |
| Token | Value | Usage |
|---|---|---|
--color-border |
#322E26 |
Card borders, nav border, table edges |
--color-border-light |
#262219 |
Table row dividers |
--color-border-strong |
#5A5446 |
Input hover, checkbox edge, control-lift |
| Token | Value | Usage |
|---|---|---|
--color-primary |
#4FB79A |
Pine-300 — logo, active links, timer, focus ring |
--color-primary-hover |
#63C9AC |
Brand button hover — brightens (with a soft glow) |
--color-primary-light |
#6ECAB0 |
Active sidebar link text |
--color-primary-bg |
rgba(79,183,154,0.14) |
Active sidebar link background |
--color-accent |
#D99A3C |
Brass — invoicing context, send/bill actions |
--color-accent-hover |
#E5AB52 |
Brass button hover — brightens |
--color-accent-bg |
rgba(217,154,60,0.14) |
Brass tint background |
--color-pine |
#1F5C4D |
Deep pine — solid fills, marks, selected states |
| Token | Value |
|---|---|
--color-success |
#3FB489 |
--color-warning |
#D6A24A |
--color-danger |
#E06661 |
--color-info |
#4FB79A (reuses pine-300) |
Semantic backgrounds use solid dark tints (--color-success-bg: #15291F, etc.) rather than rgba.
Three typefaces loaded from Google Fonts:
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=Instrument+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
| Role | Family | Token | Usage |
|---|---|---|---|
| Display / headings | Newsreader (serif) | --font-family-display |
h1–h6, brand wordmark, card titles, page titles, auth logo |
| UI / body | Instrument Sans (humanist sans) | --font-family |
Body, labels, buttons, nav links, form inputs |
| Numerals | IBM Plex Mono | --font-family-mono |
Timer, stat values, amounts, hours, rates, dates |
Sizes are --font-size-* tokens with matching text-* utilities: xs 12 · sm 14 (body, numerals) · base 16 · lg 18 (H2) · xl 20 · 2xl 24 · 3xl 30 (H1) · 4xl 34 (refreshed handoff page heading) · display 44 (hero totals, e.g. invoice "Total due"). Headings resolve to --color-text-strong. The refreshed heading uses tracking-tight (−0.015em); opt into it per surface without changing existing heading defaults.
| Token | Value | Usage |
|---|---|---|
--radius-sm |
4px |
Micro elements |
--radius |
6px |
Menu items, segmented inner, small controls |
--radius-btn |
8px |
Buttons, inputs |
--radius-lg |
11px |
Small cards, menus / popovers, toasts |
--radius-icon |
12px |
Opt-in empty-state icon tiles |
--radius-xl |
16px |
Refreshed grouped lists and empty-state cards |
--radius-panel |
20px |
Tables, modals, large panels, auth card |
--radius-full |
9999px |
Badge pills, toggles |
- Sidebar (
--sidebar-width: 264px): full-height left rail on the warm dark surface (--color-bg-secondary), 1px right border, sticky. Holds the brand, a start-timer action, grouped navigation, and the signed-in user; there is no separate top nav. Collapses to a 68px icon strip. - Content area: scrollable, generous padding,
--color-bg(#100F0C) background. - Mobile navigation (up to 768px): a labelled toggle expands the same sidebar above the content. It stays in normal document flow, so short screens can scroll to every link. Selecting a link closes the panel and focuses the content; Escape closes it and returns focus to the toggle. Desktop width and collapse state are preserved independently.
- Narrow content: page headers and actions wrap; the admin subnavigation moves above its panel when the two columns no longer fit. Dense project/time grids and the day-total strip scroll within their own containers. Project names and badges wrap inside an aligned name column instead of overlapping budget figures.
Full-height left rail. The brand row leads with the Horae mark and wordmark plus the golden accent dot. Nav rows default to --color-text-secondary; hover darkens the background to --color-bg-tertiary; the active row raises to --color-bg-tertiary with a --color-border hairline and swaps its glyph for a leading pine dot. Section labels: all-caps, wide letter-spacing, muted warm gray.
HH:MM:SS in IBM Plex Mono (--font-family-mono), pine-300 when stopped, green (--color-success) when running.
Wrapped in .table-container (border + 20px panel radius). Headers: uppercase, 0.1em letter-spacing, transparent surface, label-tone ink (--color-label). Row hover sinks to --color-row-hover. Last row has no bottom border.
Inputs on --color-bg (void — darkest), 8px radius, 44px tall, warm border #3d382e. Idle hover lifts the border to --color-border-strong and warms the fill; focus ring is --color-primary border + 18% pine glow. Instrument Sans for all form text.
Pill shape (9999px radius, 4px 12px). Every variant — including neutral — carries a 7px status dot (via ::before) over a tinted background + warm border. Neutral uses the --color-menu surface with a --color-border-strong dot.
Control-lg is 44px tall (12px 20px). Brand fills brighten on hover with a soft glow; neutral controls lift a shade and raise their border. Weights: primary/accent 700, solid 600, secondary/danger/ghost 500.
- Primary: pine-300 fill, dark green text
#0d211b; hover →--color-primary-hover+--glow-primary - Solid: deep-pine fill,
--color-text-strongink — form submits; hover →--color-pine-hover+--glow-pine - Secondary: tertiary surface,
--color-border-inputborder; hover raises the border to--color-border-strong - Accent: brass fill
#D99A3C, dark text — "Send invoice" and billing actions; hover →--color-accent-hover+--glow-accent - Danger: transparent with dark red border
#6e3634; hover tints and lifts the border to--color-border-danger-hover - Ghost: transparent pine-ink link; hover → pine-tint background
- One component per file in
src/components/ - Props use
snake_case - Components are
#[component]functions returningElement - State uses
use_signal(local) oruse_resource(async server data) - No global mutable state — pass data down via props or context
Empty-state variants are opt-in: .empty-state-icon-tile uses the 52px tile,
12px radius and 22px glyph tokens; .empty-state-copy limits copy to 380px with
1.55 line height. Compose card spacing/radius and text-subtle in markup. The
existing .empty-state, circular icon and typography defaults are unchanged.
Everything is driven by CSS custom properties in :root (assets/css/horae.css) —
never hardcode a colour, spacing, or radius in a rule; reference the token so the
palette stays themeable and consistent. Notable token groups:
- Colour:
--color-{bg,bg-secondary,bg-tertiary,bg-overlay,menu,row-hover},--color-{text-strong,text,text-secondary,text-subtle,text-muted,label},--color-primary*,--color-accent*, and semantic--color-{success,warning,danger,info}with-bg/-fg/-linetints. - Foreground-on-fill:
--color-on-{primary,accent,pine}(text over a solid control). - Chrome:
--color-border,--color-border-input,--color-border-strong,--color-border-danger,--color-border-danger-hover,--ring/--ring-soft(focus glows). - Hover fills:
--color-{primary,accent}-hover,--color-pine-hover, and the brand glows--glow-{primary,accent,pine}. - Elevation:
--shadow-{sm,md,menu,modal}. - Scale:
--space-1..16,--font-size-*(xs–4xl,display),--radius-*(sm, base,btn,lg,xl,panel,full).rounded-btnuses the existing 8px control radius;rounded-xladds the refreshed handoff's 16px grouped-list radius without changing existing cards.
On top of the tokens is a Tailwind-style utility layer — flex,
items-center, justify-between, gap-4, p-4, directional pt-/pb-/pl-/pr-,
text-sm, text-display, font-semibold, text-secondary, text-label,
bg-secondary, bg-menu, rounded-lg, rounded-panel, shadow-menu, … plus
responsive variants (md:flex-row, lg:grid-cols-3). The numeric spacing scale
matches Tailwind's (p-4 = --space-4 = 1rem). Compose utilities in markup for
layout/spacing; reach for a semantic component class (.btn, .card, .badge,
.banner, .collapse, .nav-item) for anything reused.
This layer is generated from the design scale by crates/horae/build.rs (the
Node-free equivalent of a Tailwind build) into assets/css/horae-utils.css, which
app.rs loads alongside horae.css. It is plain committed CSS at runtime. The
build script regenerates it on cargo build and only rewrites the file when the
content changes — so editing the scale in crates/horae/build.rs and rebuilding
keeps horae-utils.css in sync, and a stale committed copy can't slip through
(the sandboxed nix build fails if the checked-in file doesn't match).
Tokens and semantic component classes stay hand-written in horae.css; the
generator owns only the mechanical utility + responsive matrix.
Menu and Combobox accept optional trigger_class utilities. Their default
remains btn btn-secondary btn-sm; a surface may opt into text-sm px-4 without
changing other callers or adding page-specific descendant overrides.
| Route | Component | Description |
|---|---|---|
/auth/login |
Login |
Email + password form |
/auth/register |
Register |
Registration (admin only after first user) |
/ |
Dashboard |
Stats overview + active timer |
/clients |
ClientList |
Table of clients |
/clients/:id |
ClientDetail |
Projects under client |
/projects |
ProjectList |
All projects |
/projects/:id |
ProjectDetail |
Project tasks + time entries |
/time |
TimeList |
Time entry list with filters |
/invoices |
InvoiceList |
Invoice table |
/invoices/:id |
InvoiceDetail |
Invoice line items |
/admin/users |
AdminUsers |
User management |
/settings |
Settings |
App + plugin settings |
- Keyboard navigation throughout
- ARIA labels on icon-only buttons
- Color contrast meets WCAG AA —
#EFEAE0on#1A1813is ~9:1 - Form inputs have associated
<label>elements - Shared
Modaldialogs stay mounted and use nativeshowModal()/close()for background inertness and focus restoration. Escape, backdrop clicks and Cancel dismiss them unless a mutation is pending; the panel scrolls within the viewport on short screens. - Shared
Menuuses a native popover in the top layer so tables cannot clip it. It opens above or below its trigger within the viewport, dismisses on outside click or anchor scrolling/resizing, and supports arrows, Home/End, initial-letter navigation, Escape and Tab. Each caller supplies a stable, unique ID for the trigger/menu association. The browser owns visibility; the shared script supplies positioning and menu keyboard behavior. - Status indicated by text + color (never color alone) — badges include both dot and text label
- Timer state is reactive: the running timer increments via
use_interval - All data mutations go through
#[server]functions — never direct fetch calls - Optimistic UI where appropriate; rollback on error
- Loading states shown inline, not full-page spinners