From 86d24dd6efbf6461e986f258d19c957cd714e6f9 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Tue, 8 Sep 2026 10:49:16 +0200 Subject: [PATCH 01/10] refactor to use pip installable design system --- requirements.txt | 6 ++ src/app.py | 19 ++++- src/assets/css/main.css | 78 +++++++++---------- src/theme.py | 161 +++++----------------------------------- 4 files changed, 77 insertions(+), 187 deletions(-) diff --git a/requirements.txt b/requirements.txt index e2070ec..c7e11f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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. +# For local development against an unpublished change, override this with: +# pip install -e C:\projects\4subsea-design-system +git+https://github.com/4Subsea/4subsea-design-system.git diff --git a/src/app.py b/src/app.py index c8db985..5013d66 100644 --- a/src/app.py +++ b/src/app.py @@ -11,12 +11,24 @@ 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 @@ -32,12 +44,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}/") +def design_system_static(filename): + return flask.send_from_directory(DESIGN_SYSTEM_DIR, filename) + + def nav_bar(pathname): """ Side navigation, built from the page registry so it never needs editing. diff --git a/src/assets/css/main.css b/src/assets/css/main.css index cdbe314..cd39d96 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -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; @@ -188,8 +175,8 @@ body, 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 { @@ -219,14 +206,17 @@ 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); + color: var(--text-link); background: var(--light-grey, #f4f6f8); border-left-color: var(--turquoise); } diff --git a/src/theme.py b/src/theme.py index c83d862..015be7c 100644 --- a/src/theme.py +++ b/src/theme.py @@ -1,156 +1,33 @@ -"""4Subsea theme: colours, typography and the Plotly template. +"""4Subsea theme: registers the Plotly template from the design system. -Translated from the 4Subsea PBI style guide -https://miro.com/app/board/uXjVHbD9HV0=/ on 2026-07-30. +The actual tokens, palettes and the Plotly template itself live in the +`4subsea-design-system` package (theme_4insight.py there, backed by +colors_and_type.css + dataviz.css) - see requirements.txt for how it's +installed. This file's only job is to register that template as Plotly's +default on import, and to publish COLORS so tests can assert the +registration held. -Importing this module registers the template as Plotly's default, so any figure -built afterwards picks it up without asking. Everything that needs a brand -colour should import it from here rather than hardcoding a hex value. - -Notes on what the template cannot cover ---------------------------------------- -- Visual borders (1px, 4px corners, border grey 1) are page chrome, not figure - chrome - they live in assets/css/main.css, which mirrors these constants as - CSS custom properties. -- The guide's sizes are PowerPoint/PBI points, used here as Plotly font sizes, - which are pixels. SCALE below converts between the two; keep it in step with - the --size-* variables in main.css. +Importing this module registers the template as Plotly's default, so any +figure built afterwards picks it up without asking. """ -import plotly.graph_objects as go import plotly.io as pio -# ── UI colours ──────────────────────────────────────────────────────────────── -BORDER_GREY_1 = "#e6e6e6" -BORDER_GREY_2 = "#d2d3ce" -LIGHT_GREY = "#f8f9fa" -GREY = "#a8a9a5" -DARK_GREY = "#7e7f7c" -DARK_BLUE = "#012b5d" -TURQUOISE = "#00a0b0" -BODY_TEXT = "#002023" - -UI_COLORS = { - "border_grey_1": BORDER_GREY_1, - "border_grey_2": BORDER_GREY_2, - "light_grey": LIGHT_GREY, - "grey": GREY, - "dark_grey": DARK_GREY, - "dark_blue": DARK_BLUE, - "turquoise": TURQUOISE, - "body_text": BODY_TEXT, -} - -# ── Data colours — the guide says to use them in this order ─────────────────── -COLORS = [ - "#012b5d", # Dark blue - "#00a0b0", # Turquoise - "#f3776f", # Red - "#feb272", # Orange - "#a8a9a5", # Border grey 2 / grey - "#87d8f8", # Light blue - "#bdf4eb", # Mint - "#8e00b0", # Purple - "#716fb3", # Muted purple - "#95b000", # Olive -] - -# ── Typography ──────────────────────────────────────────────────────────────── -# Guide sizes are in points; SCALE converts them to pixel sizes. -# Raise or lower this one number to scale every font in every figure. -SCALE = 1.6 - -FONT_FAMILY = "Trebuchet MS, Arial, sans-serif" - -BODY_SIZE = round(10 * SCALE) # body text, legend entries, category labels -TITLE_SIZE = round(12 * SCALE) # visual title -AXIS_TITLE_SIZE = round(9 * SCALE) # axis title -AXIS_VALUE_SIZE = round(8 * SCALE) # axis values / scales -TABLE_HEADER_SIZE = round(9 * SCALE) # mirrored by --size-table-head in main.css +from foursubsea_design_system.theme_4insight import plotly_template -# ── Grid ────────────────────────────────────────────────────────────────────── -GRID_COLOR = BORDER_GREY_1 - - -def rgba(hex_color, alpha): - """'#012b5d' -> 'rgba(1,43,93,alpha)'""" - r, g, b = (int(hex_color.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4)) - return f"rgba({r},{g},{b},{alpha})" - - -def make_template() -> go.layout.Template: - axis = dict( - showgrid=True, - gridcolor=GRID_COLOR, - gridwidth=1, - zeroline=False, - showline=False, - ticks="", - # Axis title: size 9, dark grey - title=dict(font=dict(size=AXIS_TITLE_SIZE, color=DARK_GREY)), - # Axis values / scales: size 8, grey - tickfont=dict(size=AXIS_VALUE_SIZE, color=GREY), - exponentformat="power", - showexponent="all", - ) - - return go.layout.Template( - layout=go.Layout( - # Body text: Trebuchet MS, size 10, body text colour - font=dict(family=FONT_FAMILY, size=BODY_SIZE, color=BODY_TEXT), - # Visual title: size 12, grey, left aligned - title=dict( - font=dict(family=FONT_FAMILY, size=TITLE_SIZE, color=GREY), - x=0, - xanchor="left", - xref="paper", - ), - colorway=COLORS, - # No fixed width - visuals stretch to the width of their container - width=None, - paper_bgcolor="white", - plot_bgcolor="white", - margin=dict(l=60, r=20, t=48, b=48), - xaxis=axis, - yaxis=axis, - # Legend entries: size 10, body text. The guide notes a legend title - # is usually not needed, so it is off by default. Legends sit in a - # vertical block to the right, as in the guide's scatter example. - legend=dict( - title=dict(text=""), - x=1.02, - y=1.0, - xanchor="left", - yanchor="top", - orientation="v", - bgcolor="rgba(0,0,0,0)", - borderwidth=0, - font=dict(size=BODY_SIZE, color=BODY_TEXT), - ), - # Zoom/pan controls, top right by default. main.css gives the bar a - # border so it reads as chrome rather than data. - modebar=dict( - orientation="h", - bgcolor="rgba(255,255,255,0.85)", - color=GREY, - activecolor=TURQUOISE, - ), - colorscale=dict(sequential=[[0, "#bdf4eb"], [0.5, TURQUOISE], [1, DARK_BLUE]]), - ), - data=dict( - scatter=[go.Scatter(line=dict(width=2), marker=dict(size=6))], - scattergl=[go.Scattergl(line=dict(width=2), marker=dict(size=6))], - bar=[go.Bar(marker=dict(line=dict(width=0)))], - ), - ) +TEMPLATE_NAME = "4subsea" def register_theme(set_as_default: bool = True) -> None: - """Register the 4Subsea template with Plotly.""" - pio.templates["4subsea"] = make_template() + """Register the design system's Plotly template.""" + pio.templates[TEMPLATE_NAME] = plotly_template() if set_as_default: - pio.templates.default = "4subsea" + pio.templates.default = TEMPLATE_NAME -# Register theme on import so that themes are available after import wihout needing to call register_theme() +# Register theme on import so that themes are available after import without needing to call register_theme() register_theme() + +COLORS = tuple(pio.templates[TEMPLATE_NAME].layout.colorway) +"""The registered template's colorway, read back from the template itself so +it can't drift from what got registered.""" From 634f5503634d429cf3768dadf745f26abbae60ce Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Tue, 8 Sep 2026 10:57:46 +0200 Subject: [PATCH 02/10] update requirements to install from remote --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index c7e11f5..38f66d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ 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. -# For local development against an unpublished change, override this with: -# pip install -e C:\projects\4subsea-design-system -git+https://github.com/4Subsea/4subsea-design-system.git +# 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@packaging-test From 8798141a958162efd0c9fbd7af120745b68e66e2 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 9 Sep 2026 11:38:22 +0200 Subject: [PATCH 03/10] update style to use tokens --- src/assets/css/main.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index cd39d96..ee88363 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -46,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; @@ -86,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 { @@ -217,7 +217,7 @@ body, .sidebar-link:hover { color: var(--text-link); - background: var(--light-grey, #f4f6f8); + background: var(--light-grey); border-left-color: var(--turquoise); } @@ -256,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; @@ -305,9 +305,9 @@ 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; @@ -363,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); } /* -------------------------------------------------------------------------- @@ -378,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; From be88548013e9a4c728547e1ccf662713318ffb5a Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 9 Sep 2026 11:50:58 +0200 Subject: [PATCH 04/10] add pytest that checks that styling is applied --- requirements.txt | 2 +- src/assets/css/main.css | 12 +++---- tests/test_style_tokens.py | 69 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tests/test_style_tokens.py diff --git a/requirements.txt b/requirements.txt index 38f66d6..243bca8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ python-dotenv>=1.0 # 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@packaging-test +git+https://github.com/4Subsea/4subsea-design-system.git@packaging-test \ No newline at end of file diff --git a/src/assets/css/main.css b/src/assets/css/main.css index ee88363..5d52c75 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -118,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; } @@ -137,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 */ } @@ -147,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; } @@ -167,7 +167,7 @@ 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 */ } @@ -314,8 +314,8 @@ body, --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; } diff --git a/tests/test_style_tokens.py b/tests/test_style_tokens.py new file mode 100644 index 0000000..9172a04 --- /dev/null +++ b/tests/test_style_tokens.py @@ -0,0 +1,69 @@ +"""Guards against hardcoded colors/fonts creeping back into src/ instead of +using the foursubsea_design_system tokens (see CLAUDE.md "Layout and +Styling"). A hardcoded value is only allowed when its line carries an +`allow-hardcoded: ` comment, so any exception is a deliberate, +documented one rather than a silent regression. +""" + +import pathlib +import re + +import pytest + +SRC = pathlib.Path(__file__).resolve().parents[1] / "src" + +HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b") +RGB_FUNC = re.compile(r"\brgba?\([^)]*\)") +ALLOW_MARKER = "allow-hardcoded" + + +def _css_files(): + return sorted((SRC / "assets" / "css").glob("*.css")) + + +def _py_files(): + return sorted(p for p in SRC.rglob("*.py") if "__pycache__" not in p.parts) + + +def _matches(path, pattern): + hits = [] + for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if ALLOW_MARKER in line: + continue + if pattern.search(line): + hits.append(f"{path.relative_to(SRC.parent)}:{lineno}: {line.strip()}") + return hits + + +@pytest.mark.parametrize("path", _css_files(), ids=lambda p: p.name) +def test_css_has_no_hardcoded_colors(path): + violations = _matches(path, HEX_COLOR) + _matches(path, RGB_FUNC) + assert not violations, ( + "Hardcoded color(s) found - use a design-system token (var(--...)) " + "instead, or mark a deliberate exception with an inline " + "`allow-hardcoded: ` comment:\n" + "\n".join(violations) + ) + + +@pytest.mark.parametrize("path", _css_files(), ids=lambda p: p.name) +def test_css_font_family_uses_tokens(path): + violations = [] + for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if ALLOW_MARKER in line: + continue + if "font-family" in line and "var(" not in line: + violations.append(f"{path.name}:{lineno}: {line.strip()}") + assert not violations, ( + "font-family should reference a design-system font token " + "(var(--font-...)):\n" + "\n".join(violations) + ) + + +@pytest.mark.parametrize("path", _py_files(), ids=lambda p: p.relative_to(SRC)) +def test_python_has_no_hardcoded_colors(path): + violations = _matches(path, HEX_COLOR) + assert not violations, ( + "Hardcoded color(s) found in Python - pull the value from " + "foursubsea_design_system.theme_4insight (T / FILL / LINE / ...) " + "instead:\n" + "\n".join(violations) + ) From 3141ae4ff6d95d6b585985f8657f0bcdab824f52 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Thu, 10 Sep 2026 09:13:27 +0200 Subject: [PATCH 05/10] update link to 4subsea design system repo --- requirements.txt | 2 +- src/theme.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 243bca8..9e6a88e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ python-dotenv>=1.0 # 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@packaging-test \ No newline at end of file +git+https://github.com/4Subsea/4subsea-design-system.git \ No newline at end of file diff --git a/src/theme.py b/src/theme.py index 015be7c..abc7033 100644 --- a/src/theme.py +++ b/src/theme.py @@ -12,7 +12,6 @@ """ import plotly.io as pio - from foursubsea_design_system.theme_4insight import plotly_template TEMPLATE_NAME = "4subsea" From cbdb3179c14335c6d3976480174707ebe618d300 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Thu, 10 Sep 2026 09:22:22 +0200 Subject: [PATCH 06/10] update docs --- CLAUDE.md | 4 ++-- README.md | 44 ++++++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 499d3de..20a0c84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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: ` 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. diff --git a/README.md b/README.md index c2e2114..353e8ef 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: ` comment. ## Sample data @@ -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 From 0e1dc2456059b250aed88f23873020d0c3babca4 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Mon, 14 Sep 2026 14:23:40 +0200 Subject: [PATCH 07/10] test access token --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58fcdba..0ea5bd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,10 @@ jobs: requirements.txt requirements-dev.txt + - name: Configure git auth for private design-system repo + run: | + git config --global url."https://x-access-token:${{ secrets.DESIGN_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" + - name: Install run: | python -m pip install --upgrade pip From 853e35dcd90d7f4a590542c0c9e5fc558a0f477c Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Mon, 14 Sep 2026 14:26:44 +0200 Subject: [PATCH 08/10] try again --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea5bd1..9f112b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: From fbaf11b886b29d7722474ad69d92260a91d44340 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Mon, 14 Sep 2026 14:28:37 +0200 Subject: [PATCH 09/10] try again --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f112b3..b62ee59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,11 @@ jobs: requirements-dev.txt - name: Configure git auth for private design-system repo + env: + DESIGN_ACCESS_TOKEN: ${{ secrets.DESIGN_ACCESS_TOKEN }} run: | - git config --global url."https://x-access-token:${{ secrets.DESIGN_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" + echo "token length: ${#DESIGN_ACCESS_TOKEN}" + git config --global url."https://x-access-token:${DESIGN_ACCESS_TOKEN}@github.com/".insteadOf "https://github.com/" - name: Install run: | From 4078c6bda4e35185e9c0df83b7701d949220adec Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Thu, 17 Sep 2026 13:44:05 +0200 Subject: [PATCH 10/10] fix typo --- src/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.py b/src/app.py index 5013d66..e6c748d 100644 --- a/src/app.py +++ b/src/app.py @@ -4,6 +4,7 @@ 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