From 5ed7a95eb4959b6f08fed0739a228ea3a3df586f Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 26 May 2026 15:58:35 +0100 Subject: [PATCH 1/4] feat(app-theming):spec definition for React and GSP pages theming system --- openspec/changes/app-theming/.openspec.yaml | 2 + openspec/changes/app-theming/THEME.md | 224 ++++++++ openspec/changes/app-theming/design.md | 125 +++++ openspec/changes/app-theming/proposal.md | 50 ++ .../app-theming/specs/app-theming/spec.md | 82 +++ openspec/changes/app-theming/tasks.md | 35 ++ openspec/changes/app-theming/theme.css | 490 ++++++++++++++++++ openspec/config.yaml | 2 +- 8 files changed, 1009 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/app-theming/.openspec.yaml create mode 100644 openspec/changes/app-theming/THEME.md create mode 100644 openspec/changes/app-theming/design.md create mode 100644 openspec/changes/app-theming/proposal.md create mode 100644 openspec/changes/app-theming/specs/app-theming/spec.md create mode 100644 openspec/changes/app-theming/tasks.md create mode 100644 openspec/changes/app-theming/theme.css diff --git a/openspec/changes/app-theming/.openspec.yaml b/openspec/changes/app-theming/.openspec.yaml new file mode 100644 index 00000000000..fd706ac496e --- /dev/null +++ b/openspec/changes/app-theming/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-26 diff --git a/openspec/changes/app-theming/THEME.md b/openspec/changes/app-theming/THEME.md new file mode 100644 index 00000000000..7c71a4495b9 --- /dev/null +++ b/openspec/changes/app-theming/THEME.md @@ -0,0 +1,224 @@ +# OpenBoxes — UNICEF Tajikistan Theme + +Custom navy theming for OpenBoxes (React + Groovy pages), inspired by the +visual language of the Tajik Ministry of Health & Social Protection +(moh.tj). This document is the source of truth — drop `theme.css` into the +project and follow the rules below when restyling new screens. + +> **For Claude (or any agent picking this up):** read this file first, then +> read `theme.css`. The theme is implemented as CSS custom properties on +> `:root` — re‑theming is a matter of overriding those variables, not +> rewriting components. Do **not** invent new colors. Do **not** change +> element positions or add brand imagery to existing OB pages — match the +> existing OpenBoxes layout 1:1 and only swap the visual tokens. + +--- + +## 1. Principles + +1. **Theme tokens only, not layout.** OpenBoxes already has a working + information architecture. We are reskinning, not redesigning. Match the + existing DOM and element order — change only colors, borders, type and + small chrome details (badges, focus rings, header strips). +2. **One palette, one font stack.** All brand color flows from + `--ob-primary`. All UI type is Inter. IDs / lot numbers / shipment + identifiers use JetBrains Mono. +3. **Semantic chart colors stay.** Green = good / in‑stock, + Red = critical / out‑of‑stock, Blue = neutral / shipped, + Amber = warning. Do not retheme these to the brand color. +4. **Sparingly use the accent.** Tajik gold (`--ob-accent`, `#C9A24B`) is + reserved for active‑tab underlines, login lock icon, and rare emphasis. + Never for backgrounds, buttons, or chart series. +5. **No emoji, no decorative SVG illustrations, no gradients beyond the + thin header strips.** This is a clinical supply‑chain tool. + +--- + +## 2. Color tokens + +All defined in `theme.css` as CSS custom properties on `:root`. Refer to +them by variable name, never hex. + +| Token | Hex | Use | +|---|---|---| +| `--ob-primary` | `#1F4FA8` | Brand navy. Buttons, links, active nav, header strip, focus rings. | +| `--ob-primary-dark` | `#163F8A` | Button hover, gradient bottom. | +| `--ob-primary-darker` | `#0F2E68` | Deepest navy, used rarely (login gradient terminus). | +| `--ob-primary-soft` | `#E8EEF8` | Active-row tint, chip bg, badge bg. | +| `--ob-primary-softer` | `#F4F7FC` | Hover-row tint, card-header bg, table-header bg, footer bg. | +| `--ob-accent` | `#C9A24B` | Tajik gold. Active-tab underline; lock icon on login header. | +| `--ob-bg` | `#F6F8FB` | Page background. | +| `--ob-surface` | `#FFFFFF` | Card / panel surface. | +| `--ob-border` | `#E5E9F0` | Hairline divider. | +| `--ob-border-strong` | `#CDD4E0` | Form inputs, segmented buttons. | +| `--ob-ink` | `#0F1F3D` | Primary text. | +| `--ob-ink-2` | `#2C3A55` | Secondary text / cell body. | +| `--ob-muted` | `#5A6678` | Captions, footer, table headers. | +| `--ob-muted-2` | `#8893A4` | Placeholder text. | +| `--ob-green / red / amber / info` | semantic | Use only for status, charts, alerts. | +| `--ob-chart-1..5` | series | Bar/line/donut series colors. | + +### Retheming +To produce another palette variant, override only `--ob-primary*` and +`--ob-accent`. All cards, tables, buttons, badges, focus rings and active +states inherit from these. See `OpenBoxes Theme.html` → "Palette +variants" section for three working examples (MoH Navy, Cobalt, Deep +Ink). + +--- + +## 3. Typography + +``` +font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", + system-ui, sans-serif; +mono-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, + monospace; +``` + +| Use | Size | Weight | +|---|---|---| +| Page title | 18 | 700 | +| Card title (uppercase, tracked) | 13 | 600 | +| Body / table cells | 13–14 | 400–500 | +| Stat numerics | 36–44 | 700 | +| Form labels (uppercase, tracked) | 11–12 | 600 | +| Footnotes, build strings | 11 | 400 | +| Identifiers (shipment, lot, etc.) | 11.5–12 | 600 / mono | + +Apply `.mono { font-family: var(--ob-font-mono); }` for ID columns. Never +use mono for descriptive text. + +--- + +## 4. Component conventions + +### Header (two‑tier) +Navy ribbon (`--ob-primary`, 12px white text, 6px vertical pad) over a +white nav bar (64px tall, 14px medium‑weight items). Active nav item +takes `--ob-primary` text and a 3px `--ob-accent` underline. + +### Page title bar (Groovy pages) +White strip, 18px bold ink text, 3px gold accent bar before the title. +Pattern: `.ob-page-title`. + +### Cards +- 10px radius, 1px `--ob-border`, light shadow. +- Card header has a faint gradient from `--ob-primary-softer` → white, + border-bottom 1px `--ob-border`. +- Card title: 13px, uppercase, 0.04em tracking, `--ob-ink`. +- Card tools (info / kebab) use `--ob-muted-2`. + +### Buttons +- Primary `.ob-btn` — navy fill, white text, 6px radius, 600 weight. +- Secondary `.ob-btn.secondary` — white fill, `--ob-border-strong` border; + hover swaps border + text to navy with `--ob-primary-softer` tint. +- Ghost `.ob-btn.ghost` — transparent, navy text. + +### Forms +- Inputs: 1px `--ob-border-strong`, 6px radius, 9–12px vertical pad. +- Focus: `--ob-primary` border + 3px navy 15%‑alpha ring. +- Labels: 11–12px, uppercase, 600, `--ob-muted`, 0.05em tracking. +- Checkboxes: `accent-color: var(--ob-primary)`. + +### Tables (groovy) +- Header cells: 11px uppercase 600, `--ob-muted`, bg + `--ob-primary-softer`, 0.06em tracking. +- Body cells: 13px, `--ob-ink-2`, 12px vertical pad. +- Row hover: `--ob-primary-softer`. +- Numeric columns: `.right` + tabular‑nums. +- Pagination footer lives inside the table card with + `--ob-primary-softer` bg. + +### Status pills +Use the `.pill-status` pattern + a color modifier: +- `ps-pending` (blue), `ps-shipped` (purple), `ps-received` (green), + `ps-delayed` (red), `ps-draft` (neutral grey). + +### Chips / badges +`.ob-chip` (`--ob-primary-soft` bg, navy text) with semantic +modifiers `.green`, `.red`, `.amber`, `.neutral`. + +### Login +Centered 460px card. Header strip is navy gradient with a gold +padlock; body holds the original OB elements (two inputs, button, +"Not registered?" link). **Do not** add logos, ribbons, or marketing +copy to the real login page — keep parity with the existing OB form. + +--- + +## 5. What to retheme vs. leave alone + +| Element | Retheme? | Notes | +|---|---|---| +| Navigation, header, footer | ✅ | Apply two‑tier + gold underline pattern. | +| Cards, panels, tabs | ✅ | Soft navy tint headers; navy primary tabs. | +| Tables, pagination | ✅ | Light navy header & hover; navy active page button. | +| Buttons, links, focus rings | ✅ | All flow from `--ob-primary`. | +| Chart axes, grid, tick labels | ✅ | Use `--ob-border` / `--ob-muted-2`. | +| **Chart data series** | ❌ | Keep green/red/blue/amber semantic. | +| **Status colors (success/error/warn)** | ❌ | Keep semantic. | +| **Element positions, DOM order, copy** | ❌ | Match real OB pages 1:1. | +| **Imagery / illustrations** | ❌ | Don't add. Keep the UNICEF logo where it already lives; nowhere else. | + +--- + +## 6. How to apply this to the OpenBoxes codebase + +### Quick path (single‑theme override) +1. Copy `theme.css` into the project's static assets folder + (`grails-app/assets/stylesheets/` for Groovy pages, + `src/styles/` for React). +2. Import it **after** OpenBoxes' own stylesheets so the + `:root` variables and any overlapping selectors win. +3. Map OpenBoxes' existing class names to our utilities where + sensible, or add `:root` variable overrides that target the + bootstrap‑era class names in `theme.css`. (Example: map + `.btn-primary` to use `--ob-primary`.) + +### Codebase path (recommended) +Extract the tokens into a SCSS partial that overrides OpenBoxes' own +brand variables: + +```scss +// _theme-unicef-tjk.scss +$brand-primary: #1F4FA8; +$brand-primary-dark: #163F8A; +$brand-accent: #C9A24B; +$body-bg: #F6F8FB; +$panel-bg: #FFFFFF; +$border-color: #E5E9F0; +// …continue mapping the rest of this doc. +``` + +Then import this partial first in the OB build pipeline so the +existing component SCSS picks up the new values without further +changes. + +### Working with Claude +When asking Claude to apply this theme to a new OpenBoxes screen, paste +into the chat (or attach via `CLAUDE.md` at the repo root): + +> Follow the OpenBoxes UNICEF‑TJK theme as defined in `THEME.md`. Apply +> the navy palette via `theme.css` tokens only. Do not change element +> positions, copy, or layout — only retheme the existing markup. + +Reference `OpenBoxes Theme.html` as the visual ground truth — open it +side‑by‑side while restyling. + +--- + +## 7. Files in this package + +- `theme.css` — the canonical token + utility stylesheet. Drop into the + app. +- `THEME.md` — this document. +- `OpenBoxes Theme.html` — interactive design canvas with: + - Hero dashboard with live `Tweaks` panel for primary/accent + - Themed Login, Browse Inventory, Inbound Stock Movements + - Three palette variants of the dashboard +- `pages/dashboard.html`, `pages/inventory.html`, `pages/movements.html`, + `pages/login.html` — standalone themed mocks consuming `theme.css`. + +When the canonical palette changes, update **only** `theme.css` and +the hex tables in this document. All mocks will update automatically. diff --git a/openspec/changes/app-theming/design.md b/openspec/changes/app-theming/design.md new file mode 100644 index 00000000000..3ecce8c0b4a --- /dev/null +++ b/openspec/changes/app-theming/design.md @@ -0,0 +1,125 @@ +## Context + +OpenBoxes serves two kinds of pages from one Grails app: +- **React pages** — a `react` GSP layout shell whose Webpack bundle CSS is loaded in `` + (`grails-app/views/common/react.gsp`). Color system lives in `src/css/colors.scss`, which + already defines a `:root { --blue-primary: …; --color-red: … }` block; newer components consume + `var(--blue-primary)`. +- **GSP pages** — the `main` layout (`grails-app/views/layouts/main.gsp`) loading asset-pipeline + CSS (`grails-app/assets/stylesheets/*.css`), much of it hardcoded hex. + +A Claude-designed theme package now exists (`THEME.md` + `theme.css`, the "UNICEF Tajikistan +Navy" theme inspired by moh.tj). `theme.css` is the canonical artifact: a `:root` block of `--ob-*` +custom properties plus an `.ob-*` utility-class layer. `THEME.md` is the written source of truth — +reskin, not redesign; override only `--ob-primary*` and `--ob-accent` to produce palette variants. + +There is no single brand source in the live app today. We want `theme.css` to be that source for +both stacks, without modifying upstream stylesheets and without disturbing existing OB layouts. + +## Goals / Non-Goals + +**Goals:** +- Make `theme.css` the single in-code source of truth for colors + fonts, edited per customer branch. +- Recolor **both** React and GSP pages from it. +- Zero edits to upstream stylesheets (`colors.scss`, `main.scss`, `grails.css`, component `.scss`). +- Custom code isolated under `custom/` paths; only surgical, documented upstream touches. +- Reskin only — no change to DOM order, element positions, or copy on existing OB pages. + +**Non-Goals:** +- Migrating the ~30% hardcoded-hex values in upstream CSS to tokens. +- Runtime admin UI, DB-backed theme, or per-location theming. +- External web fonts; new layout/IA; brand imagery beyond where the logo already lives. + +## Decisions + +**1. `theme.css` is the token + utility source; no Groovy token map, no token-generating taglib.** +The dropped-in `theme.css` owns the `:root` `--ob-*` tokens and the `.ob-*` utilities. Retheming is +editing its `--ob-primary*` / `--ob-accent` values — nothing is generated from code. +- *Supersedes the earlier plan* of a `ThemeTokens.groovy` map + an `obtheme` taglib: redundant now + that a canonical CSS file exists. + +**2. Deliver via the Grails asset pipeline, injected into both layout heads.** +Place the theme CSS under `grails-app/assets/stylesheets/custom/` and inject a single +`` line into the `` of `main.gsp` (GSP pages) and `react.gsp` (React pages). +Both are GSP layouts, so the asset link reaches both stacks; React needs no webpack import (the +asset is served same-origin and loads on React pages too). +- *Alternative — webpack `@import` of theme.css into the bundle:* rejected; would only reach React + and duplicate delivery. + +**3. A small "bridge" file recolors existing OB chrome; uses a specificity bump so it always wins.** +`theme.css` only defines `--ob-*`; existing OB components read `--blue-primary`, `--color-red`, etc. +A companion `themeBridge.css` maps those to the new tokens and applies the base font: +```css +:root:root { /* 0,2,0 beats the bundle's :root (0,1,0) regardless of load order */ + --blue-primary: var(--ob-primary); + --blue-500: var(--ob-primary); + --blue-700: var(--ob-primary-dark); + --color-red: var(--ob-red); + --color-green: var(--ob-green); + --color-yellow: var(--ob-amber); +} +html body { font-family: var(--ob-font); } /* beats upstream `body {…}` without !important */ +``` +The React bundle's `:root` defaults load in `` after the head, so the bump is what makes the +override win — without editing `colors.scss`/`main.scss`/`grails.css` or using `!important`. + +**4. Do NOT blanket-apply `theme.css`'s global reset/body rules to existing pages.** +`theme.css` was authored for standalone mocks and includes `*,*::before,*::after{box-sizing}` and a +full `body{…}` restyle. Bootstrap 4.6 already sets `box-sizing: border-box`, so that is a no-op; the +`body` background/color/size could shift existing pages. To honor "reskin, not redesign," the live +wiring loads the **tokens** (`:root`), the **`.ob-*` utilities** (new namespace, inert on existing +markup), and the **bridge** (recolors via OB's own vars). Any opinionated global `body`/reset rules +from the mock are scoped or omitted at apply time, verified against real OB pages. + +**5. Curated self-hosted fonts: Inter + JetBrains Mono.** +`theme.css` references `"Inter"` (UI) and `"JetBrains Mono"` (IDs/lots). `@font-face` for both lives +in `grails-app/assets/stylesheets/custom/themeFonts.css` with woff2 under +`grails-app/assets/fonts/custom/theme/`, served same-origin so both stacks use them. Inter is also +bundled on React via `@fontsource/inter` today; self-hosting unifies it for GSP without an external +request. + +**Bundling:** an asset manifest `grails-app/assets/stylesheets/custom/obTheme.css` uses +`//= require` to pull in `themeFonts`, `theme`, and `themeBridge`, so each layout head needs only one +`` line. + +## Risks / Trade-offs + +- **Mock `theme.css` global rules disturb existing layouts** → Load tokens + utilities + bridge; + scope/omit the mock's `body`/reset; verify real OB pages render unchanged in structure (Decision 4). +- **Specificity-bump fragility** → If upstream ever sets a brand var at higher specificity than + `:root:root`, the bridge could lose. Low risk (vars live at `:root`); documented in `themeBridge.css`. +- **GSP hardcoded-hex unaffected** → Pages using literal hex won't recolor. Accepted per scope. +- **Upstream layout-head edits are merge points** → One `` line each in + `main.gsp`/`react.gsp`; listed below for the merge hitlist. + +## Migration Plan + +Additive; no DB, no data migration. Deploy = ship the new custom asset files + the two layout +one-liners. Rollback = remove the two `` lines (pages fall back to upstream +`colors.scss` defaults) and delete the custom assets. No persisted state to unwind. + +## Upstream touch points + +| File | Edit | Reason | +|---|---|---| +| `grails-app/views/layouts/main.gsp` | add one `` in `` (after `application.css`) | load theme on all GSP pages | +| `grails-app/views/layouts/react.gsp` | add one `` in `` | load theme on all React pages | + +Optional (only if needed): same line in `bootstrap.gsp`, `mobile.gsp`, `print.gsp`, `email.gsp`. +No other upstream files are modified. `THEME.md` and `theme.css` move from this change folder to +their runtime homes (`theme.css` → `grails-app/assets/stylesheets/custom/`; `THEME.md` → repo root +or `.claude/docs/`, referenced from `CLAUDE.md` UI conventions). + +## Deploy status + +- Implemented on: `release/est/tjk/0.9.7` (pending). +- Replayed onto other customer branches: none yet. +- Submitted upstream: no. + +## Open Questions + +- Final home for `THEME.md` (repo root vs `.claude/docs/`) and how it's referenced from `CLAUDE.md`. +- Which of `theme.css`'s global `body`/reset rules (if any) are safe to apply app-wide vs must be + scoped to `.ob-*` containers — resolved by visual diff against real OB pages during apply. +- Whether to promote the mechanism to the EST shared layer with a neutral default theme, leaving + only token values per customer branch. diff --git a/openspec/changes/app-theming/proposal.md b/openspec/changes/app-theming/proposal.md new file mode 100644 index 00000000000..7d3874ce0d9 --- /dev/null +++ b/openspec/changes/app-theming/proposal.md @@ -0,0 +1,50 @@ +## Why + +OpenBoxes renders pages from two stacks — React (Webpack bundle) and Grails/GSP +(server-rendered) — each with its own styling delivery. There is no single place to set +brand colors and fonts, so rebranding a deployment (e.g. the Tajikistan MoH look on +`release/est/tjk/0.9.7`) means hunting through SCSS, asset-pipeline CSS, and hardcoded hex +values across both stacks. We want one in-code theme that recolors both stacks from a single +source of truth. + +## What Changes + +- Adopt the dropped-in **`theme.css`** (Claude-designed "UNICEF Tajikistan Navy" theme) as the + single in-code source of truth: a `:root` block of `--ob-*` tokens plus an `.ob-*` utility layer. + Each customer branch rebrands by overriding `--ob-primary*` and `--ob-accent` only. `THEME.md` is + the written reskin-not-redesign convention. +- **Deliver via the Grails asset pipeline**, injected into the React and GSP layout heads with one + `` line each — CSS variables are the one styling primitive both stacks share. +- Add a small **bridge** stylesheet that maps the existing brand-driving palette variables + (`--blue-primary`, `--color-red`, …) to the new `--ob-*` tokens and applies the base font, so + current chrome recolors immediately with **zero edits to upstream CSS** (a `:root:root` + specificity bump makes the override win the cascade regardless of load order; no `!important`). +- Bundle a **curated, self-hosted font set** (Inter + JetBrains Mono; no external web-font requests). +- Reskin only — preserve existing DOM order, layout, and copy on OB pages. + +Out of scope (deliberately): migrating the ~30% hardcoded-hex values scattered in upstream CSS, +runtime admin UI, DB-backed config, per-location theming, external fonts, layout/IA changes. + +## Capabilities + +### New Capabilities +- `app-theming`: A central, in-code theme (colors + fonts) expressed as CSS custom properties and + injected into every page head, so both the React and Grails/GSP stacks render with one brand + source of truth, without modifying upstream stylesheets. + +### Modified Capabilities + + +## Impact + +- **New custom code** (isolated, merge-safe), all under `grails-app/assets/.../custom/`: + - `stylesheets/custom/theme.css` — canonical tokens + `.ob-*` utilities (the dropped-in file). + - `stylesheets/custom/themeBridge.css` — maps OB vars → `--ob-*` (specificity bump) + base font. + - `stylesheets/custom/themeFonts.css` + `fonts/custom/theme/` — self-hosted Inter + JetBrains Mono. + - `stylesheets/custom/obTheme.css` — asset manifest requiring the three above. + - `THEME.md` — reskin convention doc (repo root or `.claude/docs/`), referenced from `CLAUDE.md`. +- **Upstream touch points** (surgical, one line each — listed in design.md): + - `grails-app/views/layouts/main.gsp` — add `` in ``. + - `grails-app/views/layouts/react.gsp` — add `` in ``. +- **No** Groovy/taglib, **no** DB/migration, **no** new runtime npm dependency, **no** changes to + `colors.scss`/`main.scss`/`grails.css`. diff --git a/openspec/changes/app-theming/specs/app-theming/spec.md b/openspec/changes/app-theming/specs/app-theming/spec.md new file mode 100644 index 00000000000..01bcade3c1b --- /dev/null +++ b/openspec/changes/app-theming/specs/app-theming/spec.md @@ -0,0 +1,82 @@ +## ADDED Requirements + +### Requirement: Single in-code theme source of truth +The system SHALL define theme colors and the base font family in a single in-code theme stylesheet +(`theme.css`) that is edited per deployment/customer branch to rebrand the application. The theme +SHALL be expressed as named CSS custom properties on `:root` (colors) plus base font families, and +SHALL support producing palette variants by overriding only the `--ob-primary*` and `--ob-accent` +tokens. + +#### Scenario: Changing a theme color recolors the app +- **WHEN** a developer changes a color value in the theme definition file and the application is restarted +- **THEN** pages render with the new color without any other source file being edited + +#### Scenario: Theme defines semantic tokens +- **WHEN** the theme definition is read +- **THEN** it provides semantic tokens (e.g. `--ob-primary`, `--ob-accent`, `--ob-font-base`) usable by new custom components + +### Requirement: Theme applies to both React and GSP pages +The system SHALL apply the theme to both React-rendered pages and Grails/GSP server-rendered pages +from the single theme source, by injecting the theme as a `:root` CSS-custom-properties `