Skip to main content

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 with X-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-aware data-confirm dialog — pairs with the confirm_dialog() macro in templates/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_url must be a plain URL (no ? or #): the menu row appends ?next=<current page>. install refuses anything else. It also clears the environment's template cache, so a template rendered before install picks up the translation.
  • install is 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.
  • install adds jinja2.ext.i18n, installs newstyle gettext / ngettext / pgettext / npgettext (and _) bound to the request's locale, and sets the globals locale (read at render time; ui_base.html puts it on <html lang>), i18n_switch_url and ch_i18n (used by the package's own helpers).
  • LocaleMiddleware is pure ASGI (a BaseHTTPMiddleware would not carry the context variable to the endpoint). language_of must be a sync function: an async one is refused at start, since its coroutine would never be awaited. A value outside SUPPORTED, None, or an exception from language_of gives the default locale (the middleware's default_locale if passed, else the one install set); 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, npgettext and get_locale from coffeehouse_ui.i18n. Module-level strings (label dictionaries) use lazy_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 &amp; 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 .pot lacks, 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

  • master is the single long-lived branch (renamed from dev on 2026-04-22 for ecosystem-wide naming consistency; previously this repo shipped from dev).
  • 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.0 on master for consumer apps to pin.
  • See agents/shared/50-dev-workflow.md and agents/shared/97-pr-workflow.md for 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.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for coffeehouse-ui 0.13.2
File Size Uploaded Provenance Actions
coffeehouse_ui-0.13.2.tar.gz 519.8 kB With provenance Details for coffeehouse_ui-0.13.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for coffeehouse-ui 0.13.2
File Interpreter ABI Platform Actions
coffeehouse_ui-0.13.2-py3-none-any.whl Python 3 none any Details for coffeehouse_ui-0.13.2-py3-none-any.whl

Total release size: 1.0 MB

Release files / coffeehouse_ui-0.13.2.tar.gz

Download URL coffeehouse_ui-0.13.2.tar.gz
Size 519.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0ad5afa19a839c1434dd949e24de4e788f2417b26380bf9b8253691a116e0d9d
BLAKE2b-256 checksum
How to use checksums
3b14bc5fba35b00b3a38ec469c9e2ca3ef59e19644f8d1a41144e7691f5f441c
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 3, 2026.

Transparency log

Release files / coffeehouse_ui-0.13.2-py3-none-any.whl

Download URL coffeehouse_ui-0.13.2-py3-none-any.whl
Size 527.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7a006c8808b3e1b950cbf3da1333933fece71f68b6a881c4e5b0d0c707dce3c
BLAKE2b-256 checksum
How to use checksums
8d33b10318467dd28989ae440d81c94b4390a81bb8086f654a5bcfacb03ab655
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 3, 2026.

Transparency log

Security for coffeehouse-ui 0.13.2

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.

Select from 2 files

Attestation for coffeehouse_ui-0.13.2.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 3, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page