coffeehouse-ui
Shared UI package for the Coffee House digital ecosystem. Pip-installable Python package providing Jinja2 base templates, shared components, CSS theme, JS utilities, brand assets, and the unified permission UX used by Digi, Grind, and Beany (compute_effective(dept ∪ grants − revokes) resolver + three shared Jinja macros for the dept-matrix / role-tri-state / simulate admin pages).
Quick Start
Install from PyPI, pinned exactly, in your app's requirements.txt:
coffeehouse-ui==0.11.0
Integrate in your FastAPI/Starlette app:
from coffeehouse_ui import TEMPLATE_DIR as UI_TEMPLATE_DIR, STATIC_DIR as UI_STATIC_DIR
from starlette.templating import Jinja2Templates
from starlette.staticfiles import StaticFiles
templates = Jinja2Templates(directory=[str(app_templates_dir), str(UI_TEMPLATE_DIR)])
templates.env.globals["app_name"] = "YourApp"
app.mount("/ui", StaticFiles(directory=str(UI_STATIC_DIR)), name="ui_static")
Your app's base.html extends ui_base.html:
{% extends "ui_base.html" %}
{% block title %}My Page{% endblock %}
{% block content %}...{% endblock %}
What's Included
Templates
ui_base.html— Canonical base template with Inter font, DaisyUI, Tailwind, custom theme, dark mode, HTMX, skip-to-content, toast container, CSRF/toast/loading JS- 12 shared components — navbar, sidebar, app_switcher, flash, page_header, data_table, empty_state, stat_card, pagination, form_field, confirm_modal, loading
Static Assets
- CSS:
coffeehouse.min.css(purged Tailwind 3 + DaisyUI 4, 18KB gzip) +coffeehouse-theme.css - JS:
theme.js,toast.js,loading.js,csrf.js,session.js(marks timer-fired HTMX requests withX-Background-Request: true, so an app answers a poll on a lapsed session with an empty 204 instead of navigating the page),confirm.js(submitter-awaredata-confirmdialog — pairs with theconfirm_dialog()macro intemplates/macros/confirm.html),htmx.min.js(v2.0.4),culori.min.js(color conversion for Auth brand admin) - Images:
favicon.svg(coffee cup)
Theme
Custom DaisyUI themes coffeehouse (light) and coffeehouse-dark (dark) with a warm espresso/brown palette. Dark mode toggle persists to localStorage.
The sans stack is Inter, "Noto Sans Armenian", …. Inter has no Armenian glyphs; both Noto Sans Armenian faces (static/fonts/, OFL) are declared in coffeehouse-theme.css with unicode-range: U+0530-058F, U+FB13-FB17, so a browser downloads one only on a page that renders Armenian text. Never preload them.
Translations
coffeehouse_ui.i18n translates an app's UI per request with gettext catalogs (Babel). English is the source text and the default; the supported locales are SUPPORTED = ("en", "hy"). Each person's choice is saved by Auth and carried in the session; the app reads it and hands it to the middleware.
An app that never calls install is unaffected. Every package template renders byte for byte as it did in v0.12.1 without the i18n extension (tests/unit/test_i18n_noop.py compares every template against outputs captured from v0.12.1, both without install and with install at en). The guarantee is for Starlette's default Jinja2Templates environment, which every consumer uses: with trim_blocks or lstrip_blocks switched on, some whitespace around the new template tags differs.
Opting in
from coffeehouse_ui.i18n import LocaleMiddleware, install
install(
templates.env, # the app's Jinja environment
translations_dir=APP_DIR / "translations",
domain="messages", # default
default_locale="en", # default
switch_url=f"{AUTH_SERVICE_URL}/me/language", # language row in the user menu; no ? or #
)
def language_of(request) -> str | None: # the app reads its own session
user = current_user_or_none(request)
return user.get("language") if user else None
app.add_middleware(LocaleMiddleware, language_of=language_of) # first call = innermost
switch_urlmust be a plain URL (no?or#): the menu row appends?next=<current page>.installrefuses anything else. It also clears the environment's template cache, so a template rendered beforeinstallpicks up the translation.installis process-wide: the catalogs live at module level so Python code (lazy label dictionaries, toasts, PDF chrome) translates without a Jinja environment in hand. One app per process; a second call replaces the catalogs for every environment.installaddsjinja2.ext.i18n, installs newstylegettext/ngettext/pgettext/npgettext(and_) bound to the request's locale, and sets the globalslocale(read at render time;ui_base.htmlputs it on<html lang>),i18n_switch_urlandch_i18n(used by the package's own helpers).LocaleMiddlewareis pure ASGI (aBaseHTTPMiddlewarewould not carry the context variable to the endpoint).language_ofmust be a sync function: an async one is refused at start, since its coroutine would never be awaited. A value outsideSUPPORTED,None, or an exception fromlanguage_ofgives the default locale (the middleware'sdefault_localeif passed, else the oneinstallset); the first exception is logged at WARNING with its traceback, later ones at DEBUG. The package never reads the session itself and does not depend on coffeehouse-common.- Python code imports
gettext/_,ngettext,pgettext,npgettextandget_localefromcoffeehouse_ui.i18n. Module-level strings (label dictionaries) uselazy_gettext/lazy_pgettext, which re-evaluate at every render.
Catalog layout
<app>/translations/messages.pot # extracted from the app's source
<app>/translations/hy/LC_MESSAGES/messages.po # Armenian; required (a header-only file is enough to start)
coffeehouse_ui/translations/coffeehouse_ui.pot # the package's chrome (Logout, Cancel, pagination, ...)
coffeehouse_ui/translations/hy/LC_MESSAGES/coffeehouse_ui.po
Only .po / .pot are committed; install compiles them in memory at start, so there is no .mo file and no build step. Entries marked #, fuzzy are left out, so they render English until someone confirms them. The app's catalog is merged over the package's: an app may reword shared chrome by translating the same msgid. A missing app catalog for a supported locale is a startup error, not a silent English fallback.
What a translation may contain (app catalogs)
Newstyle _() and {% trans %} return Markup: an app's catalog text, msgid and msgstr alike, is trusted HTML and is not escaped, while named placeholders (_("Hello %(name)s", name=user.name)) are escaped. So app catalogs hold plain sentences: no & or other entities, never user data in a msgid, and markup only when the sentence really is HTML. A message is a whole sentence with named placeholders, never assembled from fragments; a short word with several meanings carries a context (pgettext("audit", "Close")).
The package's own templates
Package templates must still render in an environment without the i18n extension, where {% trans %} does not parse and _ is undefined, so they never use either. They use the macros in templates/macros/i18n.html:
{% from "macros/i18n.html" import ch_t, ch_nt, ch_data %}
{{ ch_t("Logout") }}
{{ ch_t("from dept: %(dept)s", dept=slug) }}
{{ ch_nt("%(num)d role", "%(num)d roles", n) }}
<div id="x"{{ ch_data("data-empty-label", "(empty)") }}>
Without install they output the English literal; with it they translate through ch_i18n, which returns a plain str. Either way the result is escaped exactly as the literal was, which is why they are not _() (whose Markup would render Login & Users differently). ch_data emits the attribute only when the current locale translates the message, so a script keeps its English fallback and the markup at en is unchanged. Where one sentence cannot reproduce the old markup byte for byte (a multi-line paragraph with a link), the template keeps the old English markup verbatim and switches to a whole-sentence block only when ch_i18n.translates(msgid) is true.
Refresh the package catalog after changing a package string (keywords come from babel.cfg):
pybabel extract -F babel.cfg --sort-output --project coffeehouse-ui -o coffeehouse_ui/translations/coffeehouse_ui.pot .
pybabel update -D coffeehouse_ui -d coffeehouse_ui/translations -i coffeehouse_ui/translations/coffeehouse_ui.pot
Checks for an app's tests
pot_drift(pot_path, root=..., method_map=..., options_map=..., keywords=...) -> (missing, stale): messages a fresh extraction finds that the.potlacks, and the reverse, compared as(msgctxt, msgid, msgid_plural)with locations ignored.read_mapping("babel.cfg")returns the(method_map, options_map)pair.catalog_report(po_path) -> (untranslated, fuzzy): msgid lists; fail a test on the first, report the second.
CSS Build
CSS is built from src/input.css using Tailwind 3 + DaisyUI 4. The Tailwind config scans templates across all ecosystem apps.
npm install # First time only
npm run build # Rebuild after adding new Tailwind classes
npm run watch # Watch mode for development
Rebuild when a new Tailwind utility class is used in any app template that wasn't previously in the CSS bundle. You rarely need to do this by hand any more: coffeehouse-ecosystem's daily css-rebuild-cron rebuilds against every consumer's master and opens a chore(css): rebuild shared stylesheet (vX.Y.Z) PR here when the bundle differs; on merge css-rebuild-release.yml tags it, PyPI publishes, and the consumer pins bump automatically.
Template Linter
python scripts/lint_templates.py --all
Checks all ecosystem templates for CDN links, missing role="alert", missing scope="col", and hardcoded color classes.
Project Structure
coffeehouse_ui/
├── __init__.py # Exports TEMPLATE_DIR, STATIC_DIR, __version__
├── i18n/ # Translation: install, LocaleMiddleware, gettext callables, checks
├── translations/ # The package's chrome catalog (.pot + hy .po)
├── templates/
│ ├── ui_base.html
│ ├── macros/i18n.html # ch_t / ch_nt / ch_data for package templates
│ └── components/ # 12 shared Jinja2 components
└── static/
├── css/ # Built Tailwind + theme CSS
├── fonts/ # Inter, Noto Sans Armenian (woff2 + licences)
├── js/ # Theme, toast, loading, CSRF, HTMX
└── img/ # Favicon
Consumer Apps
| App | Repo |
|---|---|
| Auth | rubencfh/coffeehouse-auth |
| Beany/KMS | rubencfh/Beany |
| Digi | rubencfh/Digi |
| Grind | rubencfh/Grind |
| CHQR | rubencfh/CHQR |
Development
First-time setup:
pip install -e ".[dev]"
pip install -r requirements-dev.txt # pulls coffeehouse-common[testing] + pinned tooling
pre-commit install # wires ruff-format + ruff + hygiene hooks on each commit
Run the checks that CI runs:
pytest # unit + integration tests with coverage
ruff format --check . && ruff check . # format + lint
mypy coffeehouse_ui # lenient typecheck
bandit -c pyproject.toml -r coffeehouse_ui scripts -ll # SAST
pre-commit run --all-files runs the on-commit subset in one go. gitleaks runs in CI only (Windows Application Control blocks the Go-built local hook; install the native binary manually if you want local secret scanning).
The full pipeline (lint / typecheck / pytest w/ coverage / secret-scan / SAST / SCA) runs on every push and PR via .github/workflows/ci.yml, which is a thin caller of rubencfh/coffeehouse-ecosystem/.github/workflows/reusable-python-ci.yml@master. As of the TBD cutover (2026-04-22), the suite is a merge gate on master: 6 CI jobs + CodeRabbit review = 7 required status checks. Direct git push origin master is blocked; changes land through short-lived feature branches + PRs.
Branch Strategy — Trunk-Based Development
masteris the single long-lived branch (renamed fromdevon 2026-04-22 for ecosystem-wide naming consistency; previously this repo shipped fromdev).- Every change goes through a short-lived feature branch off
master+ PR + squash merge. Feature branches auto-delete on merge. - Tag releases as
v0.x.0onmasterfor consumer apps to pin. - See
agents/shared/50-dev-workflow.mdandagents/shared/97-pr-workflow.mdfor the full flow.
Version
Current: 0.3.1
Brand API (optional)
Set templates.env.globals["auth_service_url"] to your Auth base URL (e.g. https://auth.coffeehouse.tools). ui_base.html then loads active brand CSS and images from GET {auth_service_url}/api/brand/active on each session (cached in sessionStorage for 30 seconds). Omit or leave empty if you do not use the shared brand feature.
Metadata
Release files for coffeehouse-ui 0.13.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| coffeehouse_ui-0.13.0.tar.gz | 519.8 kB | With provenance | Details for coffeehouse_ui-0.13.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| coffeehouse_ui-0.13.0-py3-none-any.whl | Python 3 | none | any | Details for coffeehouse_ui-0.13.0-py3-none-any.whl |
Total release size: 1.0 MB
Release files / coffeehouse_ui-0.13.0.tar.gz
| Download URL | coffeehouse_ui-0.13.0.tar.gz |
|---|---|
| Size | 519.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a202aadff904d7c4e6759ca26ec8e86ec438a7bd45fcb8bd1b01bf6dbb6f25e4
|
|
BLAKE2b-256 checksum How to use checksums |
ea505ef98b7d1108efdc7382aea2538282eafb4be98c376fb0e095db5aaa6617
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / coffeehouse_ui-0.13.0-py3-none-any.whl
| Download URL | coffeehouse_ui-0.13.0-py3-none-any.whl |
|---|---|
| Size | 527.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd100598a44fd4a86e777f6634794096d357e40d4c52da58f22f667cdae85616
|
|
BLAKE2b-256 checksum How to use checksums |
ff4f3ae9529e328a50521c25d52d4676a72a91b963be1157741ad1b9ae57a755
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for coffeehouse-ui 0.13.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for coffeehouse_ui-0.13.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.