Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
# checkout's own GITHUB_TOKEN credential, once persisted, matches
# any github.com URL and overrides the DESIGN_ACCESS_TOKEN below.
persist-credentials: false

- uses: actions/setup-python@v5
with:
Expand All @@ -21,6 +25,13 @@ jobs:
requirements.txt
requirements-dev.txt

- name: Configure git auth for private design-system repo
env:
DESIGN_ACCESS_TOKEN: ${{ secrets.DESIGN_ACCESS_TOKEN }}
run: |
echo "token length: ${#DESIGN_ACCESS_TOKEN}"
git config --global url."https://x-access-token:${DESIGN_ACCESS_TOKEN}@github.com/".insteadOf "https://github.com/"
Comment on lines +30 to +33

- name: Install
run: |
python -m pip install --upgrade pip
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
python-dashboard-template/
├── src/
│ ├── app.py # Main application file
│ ├── theme.py # Colours, type scale and the Plotly template
│ ├── theme.py # Registers the Plotly template from the foursubsea_design_system package
│ ├── memory_log.py # Opt-in dev aid: prints RSS memory to the terminal, see LOG_MEMORY
│ ├── assets/ # Static files (CSS, images, sample data)
│ └── pages/ # One module per page, each with dash.register_page
Expand Down Expand Up @@ -42,7 +42,7 @@ python-dashboard-template/

## Layout and Styling
- **Custom Style Sheets**: For external stylesheets and CSS files, put core layout styles, layout grids, and structural overrides into custom files inside the `assets/` directory.
- **Theme File**: Use a shared `theme.py` or `theme.js` containing color constants, spacing scales, and font definitions to pass values systematically.
- **Theme File**: Colors, type scale and spacing come from the `foursubsea_design_system` pip package, not from constants defined in this repo. `theme.py` only registers the package's Plotly template as Plotly's default on import; CSS consumes the same tokens as custom properties from the package's `colors_and_type.css` (served by `app.py`, referenced via `var(--...)` in `assets/css/main.css`); Python code that needs a raw value (e.g. for a conditional format) reads it from `foursubsea_design_system.theme_4insight`. Never hardcode a color or font in `src/` — `tests/test_style_tokens.py` enforces this, with `allow-hardcoded: <reason>` as the documented escape hatch.
- **Inline Styles**: Use inline Python dictionaries (`style={"marginRight": "10px"}`) only for highly dynamic, runtime-computed values (e.g., styling a component color based on a callback threshold). Avoid static inline styling blocks as much as possible.
- **Code Format**: Run `black` for Python formatting and Prettier for CSS formatting.

Expand Down
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ edit it; `.env` is gitignored):
```
src/
├── app.py Dash shell: side navigation, mock 4insight header
├── theme.py colours, type scale and the Plotly template
├── theme.py registers the Plotly template from the design-system package
├── memory_log.py dev aid: prints RSS memory usage, see LOG_MEMORY
├── pages/
│ ├── introduction.py landing page: revision log AgGrid and a free-text notes box
Expand Down Expand Up @@ -100,19 +100,29 @@ default.

## Styling / theming

Colours, fonts and spacing live in two places that must be kept in step:

- `src/theme.py` — the single source of truth as Python constants
(`UI_COLORS`, `COLORS`, font sizes). Importing it registers a Plotly
template as the default, so any figure built anywhere in the app picks up
the palette and typography automatically — no chart-by-chart styling.
- `src/assets/css/main.css` — the same palette and type scale as CSS custom
properties, used for page chrome, the sidebar, the mock header, and AgGrid's
theme variables (AgGrid isn't a Plotly figure, so it reads the CSS
variables rather than `theme.py` directly).

If you change `theme.py`'s `SCALE` or a size constant, change the matching
`--size-*` variable in `main.css` too — nothing enforces this automatically.
Colours, fonts and spacing are not defined in this repo — they live in the
`foursubsea-design-system` pip package (installed via `requirements.txt`,
imported as `foursubsea_design_system`), which is the single source of
truth. Two things in `src/` consume it:

- `src/theme.py` — imports `foursubsea_design_system.theme_4insight`'s
Plotly template and registers it as Plotly's default on import, so any
figure built anywhere in the app picks up the palette and typography
automatically — no chart-by-chart styling. It also re-exports `COLORS`
(read back from the registered template) for tests to assert against.
- `src/assets/css/main.css` — the same tokens as CSS custom properties,
loaded from the package's `colors_and_type.css` (served by `app.py` at
`/design-system/...`, since the package ships its assets inside itself
rather than under `assets/`). `main.css` only ever points app-local
variable names (`--dark-blue`, `--size-title`, ...) at the design system's
own tokens (`--secondary-500`, `--fs-h6`, ...) — it never hardcodes a
value itself.

Any Python code that needs a raw value (e.g. a conditional-formatting
threshold) should read it from `foursubsea_design_system.theme_4insight`
rather than hardcoding it. `tests/test_style_tokens.py` enforces that no
hardcoded color or font-family sneaks into `src/`'s CSS or Python; a
deliberate exception needs an inline `allow-hardcoded: <reason>` comment.

## Sample data

Expand All @@ -131,8 +141,10 @@ pytest
Covers page registration, that each page's layout builds, that every
callback's component IDs actually exist in its page, the mock header (absent
by default, carries the logo, placeholder title and spacer note when
configured), and that the analytics chart actually renders through the
"4subsea" Plotly theme rather than silently falling back to Plotly's default.
configured), that the analytics chart actually renders through the
"4subsea" Plotly theme rather than silently falling back to Plotly's default,
and that no hardcoded color or font-family has crept into `src/`'s CSS or
Python outside of a documented `allow-hardcoded` exception.

## Contributing

Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ plotly>=6.0
pandas>=2.2

python-dotenv>=1.0

# Design tokens, fonts and the Plotly theme (theme.py, assets/css/main.css).
# Imports as `foursubsea_design_system` - see theme.py.
# TODO: pinned to the packaging-test branch while that's under test - switch
# back to @main (or drop the @ref entirely) once it's merged.
git+https://github.com/4Subsea/4subsea-design-system.git
20 changes: 19 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,32 @@
pages/ and registers itself with dash.register_page, so adding a page means
adding one file - plus updating EXPECTED_PAGES/CALLBACK_IDS in
tests/test_app.py, which is the one thing that doesn't update itself.

"""

import os
import pathlib

import dash
import dash_bootstrap_components as dbc
import flask
from dash import Dash, Input, Output, callback, dcc, html
from dotenv import load_dotenv
from foursubsea_design_system import theme_4insight

import memory_log
import theme # registers the "4subsea" Plotly template

# The design system ships its CSS and fonts inside the installed package
# (see requirements.txt), not under assets/, so Dash's automatic assets-folder
# serving can't reach them. This route serves them straight from wherever pip
# put the package - no copying, always the version that's actually installed.
# foursubsea_design_system itself has no __file__ (it's an implicit namespace
# package - package-dir maps it straight onto the design system's repo root,
# see that repo's pyproject.toml), so locate it via a real module inside it.
DESIGN_SYSTEM_DIR = pathlib.Path(theme_4insight.__file__).resolve().parent
DESIGN_SYSTEM_URL_PREFIX = "/design-system"

PAGE_TITLE = "Dashboard Template"

# Read environment variables from .env in the repo root, if present
Expand All @@ -32,12 +45,17 @@
app = Dash(
__name__,
use_pages=True,
external_stylesheets=[dbc.themes.BOOTSTRAP],
external_stylesheets=[dbc.themes.BOOTSTRAP, f"{DESIGN_SYSTEM_URL_PREFIX}/colors_and_type.css"],
suppress_callback_exceptions=True, # Set true for multi-page apps to avoid raising exceptions.
title=PAGE_TITLE,
)


@app.server.route(f"{DESIGN_SYSTEM_URL_PREFIX}/<path:filename>")
def design_system_static(filename):
return flask.send_from_directory(DESIGN_SYSTEM_DIR, filename)
Comment on lines +54 to +56


def nav_bar(pathname):
"""
Side navigation, built from the page registry so it never needs editing.
Expand Down
108 changes: 49 additions & 59 deletions src/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,37 @@
https://miro.com/app/board/uXjVHbD9HV0=/
========================================================================== */

/* Colours, fonts and the base type/spacing tokens all come from the
4subsea-design-system package's colors_and_type.css (loaded as an external
stylesheet - see app.py). The names below are kept as a stable, app-local
vocabulary for this file - each just points at the design system's token,
so there is one place any of these values is actually written. Sizes
follow theme_4insight.py's two-size-by-role split (FS_SMALL / FS_TITLE)
rather than the old five-step ladder, so this file's chrome text matches
the Plotly figures'. */
:root {
/* FONT */
--font-main: 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;

/* COLORS, UI */
--border-grey-1: #e6e6e6;
--border-grey-2: #d2d3ce;
--light-grey: #f8f9fa;
--grey: #a8a9a5;
--dark-grey: #7e7f7c;
--dark-blue: #012b5d;
--turquoise: #00a0b0;
--body-text: #002023;

/* COLORS, data (use them in this order) */
--data-1: #012b5d;
--data-2: #00a0b0;
--data-3: #f3776f;
--data-4: #feb272;
--data-5: #a8a9a5;
--data-6: #87d8f8;
--data-7: #bdf4eb;
--data-8: #8e00b0;
--data-9: #716fb3;
--data-10: #95b000;

/* TYPE SCALE — guide sizes are points, rendered here at 1.6x as pixels.
Keep in step with SCALE in theme.py. */
--size-axis-value: 13px;
/* 8 */
--size-axis-title: 14px;
/* 9 */
--size-table-head: 14px;
/* 9 */
--size-body: 16px;
/* 10 */
--size-title: 19px;
/* 12 */
--size-page-title: 34px;
--font-main: var(--font-sans);

--border-grey-1: var(--surface-border);
--border-grey-2: var(--gray-500);
--light-grey: var(--surface-muted);
--grey: var(--gray-600);
--dark-grey: var(--gray-700);
--dark-blue: var(--secondary-500);
--turquoise: var(--primary-500);
--body-text: var(--text-body);

/* TYPE SCALE */
--size-axis-title: var(--fs-small);
/* also table headers */
--size-table-head: var(--fs-small);
--size-body: var(--fs-body);
--size-title: var(--fs-h6);
/* visual titles, slicer headers, sidebar links */

/* LAYOUT */
--gap: 16px;
--gap: var(--space-4);
/* "two spaces between visuals" */
--radius: 4px;
--max-width: 1800px;
/* content stops filling very wide monitors */
--narrow-width: 1100px;
Expand All @@ -59,7 +46,7 @@ body {
padding: 0;
font-family: var(--font-main) !important;
color: var(--body-text);
background-color: #ffffff;
background-color: var(--surface-page);
font-size: var(--size-body);
line-height: 1.5;
font-weight: 400;
Expand Down Expand Up @@ -99,7 +86,7 @@ body,
flex: 0 0 var(--sidebar-width);
border-right: 1px solid var(--border-grey-1);
padding: 20px 0;
background: #ffffff;
background: var(--surface-page);
}

.content {
Expand Down Expand Up @@ -131,7 +118,7 @@ body,
display: flex;
flex-direction: column;
padding: 8px 18px 0;
background: #16191c;
background: #16191c; /* allow-hardcoded: mocks 4insight's own real header, not this app's design */
font-size: 12px;
letter-spacing: 0.02em;
}
Expand All @@ -150,7 +137,7 @@ body,

.mock-4insight-note {
line-height: 22px;
color: #6b7075;
color: #6b7075; /* allow-hardcoded: mocks 4insight's own real header, not this app's design */
/* quiet: it is a note to the developer, not part of the design */
}

Expand All @@ -160,7 +147,7 @@ body,
margin-top: 4px;
font-size: 30px;
line-height: 1.3;
color: #ffffff;
color: #ffffff; /* allow-hardcoded: mocks 4insight's own real header, not this app's design */
font-weight: 700;
}

Expand All @@ -180,16 +167,16 @@ body,
font-size: 11px;
line-height: 1;
letter-spacing: 0.02em;
color: rgba(255, 255, 255, 0.55);
color: rgba(255, 255, 255, 0.55); /* allow-hardcoded: mocks 4insight's own real header, not this app's design */
/* quiet: it is a note to the developer, not part of the design */
}

/* --------------------------------------------------------------------------
Headings
-------------------------------------------------------------------------- */

/* The page title lives in 4insight's header, not in the app. --size-page-title
is kept for anything that needs a large heading later. */
/* The page title lives in 4insight's header, not in the app - nothing here
needs a page-title-sized heading. */

/* Footnote under a visual: small, dark grey, sits inside the visual border */
.footnote {
Expand Down Expand Up @@ -219,15 +206,18 @@ body,
padding: 9px 18px 9px 15px;
font-size: var(--size-title);
line-height: 1.25;
color: var(--turquoise);
/* --turquoise (--primary-500) is 3.16:1 as text on white and fails WCAG AA;
--text-link is the design system's token for teal text, at 4.70:1. The
border below stays --turquoise since it's decoration, not text. */
color: var(--text-link);
text-decoration: none;
border-left: 3px solid transparent;
transition: color 0.12s ease, border-color 0.12s ease, background-color 0.12s ease;
}

.sidebar-link:hover {
color: var(--turquoise);
background: var(--light-grey, #f4f6f8);
color: var(--text-link);
background: var(--light-grey);
border-left-color: var(--turquoise);
}

Expand Down Expand Up @@ -266,14 +256,14 @@ body,
border: 1px solid var(--border-grey-1);
border-radius: var(--radius);
padding: 12px;
background-color: #ffffff;
background-color: var(--surface-page);
}

/* Filter panel — used by the Analytics page's dropdown filter. Just the box
and heading style; the original multi-section/radio-group slicer layout
isn't here, because nothing in this template uses more than one filter. */
.slicer {
background-color: #ffffff;
background-color: var(--surface-page);
border: 1px solid var(--border-grey-1);
border-radius: var(--radius);
padding: 12px;
Expand Down Expand Up @@ -315,17 +305,17 @@ body,
--ag-font-family: var(--font-main);
--ag-font-size: var(--size-body);
--ag-foreground-color: var(--body-text);
--ag-background-color: #ffffff;
--ag-background-color: var(--surface-page);
--ag-odd-row-background-color: var(--light-grey);
--ag-header-background-color: #ffffff;
--ag-header-background-color: var(--surface-page);
--ag-header-foreground-color: var(--dark-grey);
--ag-border-color: transparent;
--ag-row-border-color: transparent;
--ag-borders: none;
--ag-header-column-separator-display: none;
--ag-header-column-resize-handle-display: none;
--ag-row-hover-color: rgba(0, 160, 176, 0.06);
--ag-selected-row-background-color: rgba(1, 43, 93, 0.06);
--ag-row-hover-color: rgba(0, 160, 176, 0.06); /* allow-hardcoded: alpha blend of --primary-500, no rgb-triplet token exists */
--ag-selected-row-background-color: rgba(1, 43, 93, 0.06); /* allow-hardcoded: alpha blend of --secondary-500, no rgb-triplet token exists */
--ag-grid-size: 5px;
border-top: 2px solid var(--dark-grey) !important;
}
Expand Down Expand Up @@ -373,7 +363,7 @@ body,
.js-plotly-plot .modebar--horizontal {
border: 1px solid var(--border-grey-1);
border-radius: var(--radius);
background-color: #ffffff;
background-color: var(--surface-page);
}

/* --------------------------------------------------------------------------
Expand All @@ -388,7 +378,7 @@ img {
input[type=submit],
button {
background-color: var(--turquoise);
color: #ffffff;
color: var(--text-inverse);
border: none;
min-height: 32px;
width: 200px;
Expand Down
Loading
Loading