9.9 KiB
Gramps Web Frontend
This is the frontend for Gramps Web, a genealogical research PWA. It talks to the Gramps Web API backend.
Tech stack
- LitElement (Web Components) — every UI element is a custom element
- Rollup v4 for production builds (
rollup.config.js);@web/dev-serverfor development - Material Web Components — UI primitives; the codebase contains a mix of the old v1
@material/mwc-*(<mwc-button>etc.) and the new v2@material/web(<md-button>etc.). New code should usemd-*elements only. - D3.js — genealogical charts (fan chart, tree chart, relationship chart, Y-DNA lineage)
- MapLibre GL — interactive maps
- MDI icons via
@mdi/js— SVG path constants rendered through<grampsjs-icon>. The codebase also contains older<mwc-icon>usage. New code should always use<grampsjs-icon>.
Development
Dev container
The repository includes a .devcontainer/ configuration. After opening in VS Code Dev Containers, dependencies are installed automatically. Start the dev server with:
npm start
This starts @web/dev-server on port 8001 with hot reload. The dev server does not include a backend — API calls will fail unless a backend is also running.
Build
npm run build # outputs to dist/
Environment variables used at build time:
API_URL— backend API origin (default: empty string, i.e. same origin). In dev, the hardcoded fallback ishttp://localhost:5555.BASE_DIR— base path if the app is not served from/
Project structure
src/
GrampsJs.js # Root LitElement component (<gramps-js>)
gramps-js.js # Entry point (registers the custom element)
api.js # All API calls, auth helpers, localStorage utilities
appState.js # App-wide state initialisation
color.js # Gramps internal hex format converters (tag colours etc.)
config.js # Runtime config (window.grampsjsConfig); see below
strings.js # i18n string keys and language list
theme.js # Colour theme: applyScheme(), applyColors(), DEFAULT_PRIMARY/SECONDARY
util.js # Shared helpers
components/ # Reusable components
views/ # Page-level components
mixins/ # LitElement mixins
charts/ # D3.js visualisation components
lang/ # Translation JSON files (one per language code)
test/ # Unit tests (minimal coverage currently)
Component conventions
Naming
All component files and classes use the Grampsjs prefix with lowercase js (e.g. GrampsjsPersonCard, not GrampsJsPersonCard). A handful of older files use GrampsJs — these are legacy and should not be followed.
Base classes
| Base class | Use for |
|---|---|
GrampsjsView |
Page-level views (only renders when active) |
GrampsjsConnectedComponent |
Components that fetch API data on mount |
LitElement + GrampsjsAppStateMixin |
Everything else that needs appState or translations |
Icons
Import the SVG path constant from @mdi/js and pass it to <grampsjs-icon>:
import {mdiInformation} from '@mdi/js'
// in render():
html`<grampsjs-icon path="${mdiInformation}"></grampsjs-icon>`
Optional attributes: color, height, width, rotate.
For action icon color (add/edit/delete buttons), use color="var(--mdc-theme-secondary)" — this is derived from the active secondary seed colour and adapts to light/dark mode.
Mixins
GrampsjsAppStateMixin— addsappStateproperty and the_(key)translation helperGrampsjsStaleDataMixin— marks data as stale ondb:changedevents; refetches when the component becomes active
Data flow
Data flows down via Lit properties, and up via custom events using fireEvent. There is no shared mutable state between siblings — communicate through a common parent.
State management
State is a plain object (appState) initialised in appState.js and passed down as a Lit property. There is no Redux or other store. The API methods are attached to appState and should always be used instead of calling api.js functions directly:
const result = await this.appState.apiGet('/api/people/')
await this.appState.apiPost('/api/people/', payload)
await this.appState.apiPut('/api/people/handle', payload)
await this.appState.apiDelete('/api/people/handle')
api.js is still the right place to import utility helpers that are not HTTP calls: getMediaUrl, getThumbnailUrl, getExporterUrl, getSettings, updateSettings, addBookmark, etc.
Auth
JWTs are stored in localStorage (access_token, refresh_token, id_token). The Auth class in api.js handles proactive token refresh. OIDC (OpenID Connect) login/logout is also supported.
Permissions are decoded from JWT claims and surfaced through appState.permissions:
appState.permissions.canAdd
appState.permissions.canEdit
appState.permissions.canViewPrivate
appState.permissions.canManageUsers
appState.permissions.canUseChat
appState.permissions.canUpgradeTree
i18n
All translatable text must go through the _(key) method (available in any component that uses GrampsjsAppStateMixin).
There are two sources of strings:
- Gramps strings — keys that exist in the Gramps desktop app's translation system. Add them to the
grampsStringsarray insrc/strings.js. The backend serves translated versions of these. - Frontend-only strings — add the English text to
lang/en.json. Translations are managed via Weblate.
Do not add frontend-only strings to grampsStrings, and do not add Gramps strings to lang/en.json.
Custom events
Components communicate via window-level custom events:
| Event | Meaning |
|---|---|
db:changed |
A mutation succeeded; stale data should be refreshed |
user:loggedout |
Auth was cleared |
settings:changed |
User settings were updated |
grampsjs:error |
A component encountered an error (bubbles up to root) |
Fire events with the fireEvent(target, eventName, detail?) helper from util.js.
db:changed and dbChanged
apiPutPostDelete (called by appState.apiPost, apiPut, apiDelete) fires db:changed on window automatically whenever a mutation succeeds, including 202 background-task responses. The dbChanged option (default true) controls this.
Do not fire db:changed manually after an apiPost/apiPut/apiDelete call — it will be a duplicate. If you need to suppress the automatic event (rare), pass {dbChanged: false} in the options and fire it yourself at the right moment.
Runtime configuration
src/config.js is excluded from the bundle and copied as-is to dist/config.js. Deployers can mount a replacement file on top in Docker to set window.grampsjsConfig options (e.g. custom API URL, OAuth provider configuration). The default file just sets window.grampsjsConfig = {}.
Code style
Enforced by ESLint + Prettier (run automatically on commit via husky/lint-staged):
npm run lint # check
npm run format # fix
Prettier config (from package.json): single quotes, no semicolons, no arrow-function parens for single args, no bracket spacing.
Both .js and .ts files are linted and formatted.
TypeScript
TypeScript is optional — new files may be .ts, existing .js files stay as-is. The two coexist freely.
- Production build compiles
.tsvia@rollup/plugin-typescript; dev server transpiles with esbuild (no type-checking) - Run
npm run typecheckfor a standalone type-check (also runs in CI) - Lit components: use
@customElement,@property,@statefromlit/decorators.js;experimentalDecoratorsanduseDefineForClassFields: falseare already set intsconfig.json
Tests
npm test # run once with coverage
npm run test:watch
Runner: Vitest with happy-dom (no browser needed), coverage via @vitest/coverage-v8. Tests live in test/unit/ and cover pure functions. Import from vitest directly. Component tests requiring a real DOM are out of scope for now.
Opening issues and pull requests
If you are drafting a GitHub issue or pull request on a user's behalf, these rules apply in addition to CONTRIBUTING.md.
- Write as the user's assistant. Do not adopt a human persona, and do not hide that the text was drafted by an agent.
- Describe the problem. Any solution proposal goes in the clearly marked optional section, in a few sentences at most.
- Keep issue bodies under ~300 words. A maintainer reads every word.
- Do not open a pull request for a non-trivial change unless an issue exists and a maintainer has replied to it. An issue and a pull request opened minutes apart do not satisfy this.
- Do not restate the problem in a pull request body. Link the issue and describe the change.
- Check whether the report is about installing, deploying or configuring Gramps Web. If so, it belongs in the forum.
- Do not write greetings, thanks or other pleasantries in the user's name. Leave the human parts of the message to the human, and never delete or shorten what they wrote there.