Skip to content

Commit 3e25b68

Browse files
authored
feat(tokens): wire style-dictionary — design-system.dtcg.json is now SoT (#4)
Closes the manual-mirror gap between `design-system.dtcg.json` (the file designers edit in Penpot) and the Sass partials Odoo compiles. After this PR the JSON is the single source of truth; SCSS regenerates on demand. ┌─────────────────────────────────────────┐ │ design-system.dtcg.json ← edit this │ └──────────────────┬──────────────────────┘ │ pnpm run tokens ┌─────────────┴───────────────┐ ▼ ▼ _tokens.generated.scss dist/brand_variables.scss (loaded by manifest) (drop into ledoweb_branding) ## What's in the PR - `package.json` — pins `style-dictionary@^5.4.0` (the v5 line reads DTCG `$value`/`$type` natively, including shadow object → string). - `style-dictionary.config.js` — two platforms: - `scss-ods-tokens` writes `odoo_design_system/static/src/scss/_tokens.generated.scss` (bucket palette + extension palette + `:root` CSS custom properties for size / shadow / motion). - `scss-brand` writes `dist/brand_variables.scss` (overrides for `$o-enterprise-color` / `$o-community-color` / `$o-brand-*` plus the portal chrome variables; consumer copies the file into their branding module). - `_tokens.scss` (renamed in role): now just hand-written hash helpers + selector-generating mixins. The palette data + :root vars moved to the generated file. `_tokens.scss` references the generated maps by name; manifest order makes them in-scope before the mixins execute. - Manifest assets reordered so `_tokens.generated.scss` loads first in both `web.assets_backend` and `web.assets_frontend`. - `scripts/check-tokens-in-sync.mjs` — the CI gate. Runs the generator and `git diff --exit-code` against the committed files; prints the drift and exits 1 if anyone forgot to re-run after editing the JSON. - `pnpm run tokens` / `pnpm run tokens:watch` / `pnpm run tokens:check` scripts in `package.json`. - `_tokens.scss` mixins now read `--ods-card-shadow-hover`, `--ods-card-lift-duration`, and `--ods-card-lift-easing` from CSS vars instead of hard-coding values — they were already in the JSON but unused. - `readme/USAGE.md` documents the pipeline with the diagram + workflow steps. `readme/ROADMAP.md` strikes through the wired follow-up. ## Verification - `pnpm run tokens` → both files regenerate, no diff against committed - `pnpm run tokens:check` → exits 0 (✓ both files match) - libsass compiles the concatenated bundle to 15 023 bytes of CSS (no warnings, no errors) - Anchor extension keys (`image`, `media`, etc.) are filtered out of the runtime Sass map — they exist in the JSON for designer reference but no markup carries `data-ext="image"`. ## Not in scope The brand-logo follow-up (tokenize `#D97706` → `currentColor`) ships as a separate PR on ledoent/erp's `ledoweb_branding` module, since the logo file lives there.
1 parent 68d1f6e commit 3e25b68

11 files changed

Lines changed: 1365 additions & 69 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
__pycache__/
33
.tmp-screens/
44
.DS_Store
5+
node_modules/

dist/brand_variables.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Generated by `pnpm run tokens` (in odoo-design-system) from
2+
// odoo_design_system/static/src/tokens/design-system.dtcg.json.
3+
//
4+
// DO NOT EDIT BY HAND. Change the JSON and re-run.
5+
6+
// Drop this file into your branding module's `static/src/scss/` and list
7+
// it in `web._assets_primary_variables` so Bootstrap compiles $primary
8+
// correctly against it.
9+
// Color resolution chain in Odoo 18/19:
10+
// $o-enterprise-color → base-1 palette 'o-color-1' → $-main-color
11+
// → 'primary' → $primary. Overriding $o-enterprise-color here
12+
// (before primary_variables.scss !default) cascades through the
13+
// entire system.
14+
$o-enterprise-color: #d97706;
15+
$o-community-color: #d97706;
16+
$o-brand-odoo: #d97706;
17+
$o-brand-primary: #d97706;
18+
19+
// Lighter variant for hover states
20+
$o-brand-lighten: #f59e0b;
21+
// Portal chrome
22+
$o-portal-nav-dark: #1a1a1e;
23+
$o-portal-sidebar-light: #fafaf8;
24+
$o-portal-border: #e5e5e5;

odoo_design_system/__manifest__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
],
1616
"assets": {
1717
"web.assets_backend": [
18-
# SCSS — tokens must concatenate first, then component styles.
18+
# SCSS — `_tokens.generated.scss` carries the palette maps + :root
19+
# CSS variables (regenerated from `static/src/tokens/design-system.dtcg.json`
20+
# by `pnpm run tokens`); `_tokens.scss` adds the hand-written
21+
# hash helpers + mixins that consume those maps. Order matters:
22+
# the generated maps must be in scope before the helpers reference
23+
# them. `components.scss` then consumes the mixins.
24+
"odoo_design_system/static/src/scss/_tokens.generated.scss",
1925
"odoo_design_system/static/src/scss/_tokens.scss",
2026
"odoo_design_system/static/src/scss/components.scss",
2127
"odoo_design_system/static/src/scss/showcase.scss",
@@ -35,6 +41,7 @@
3541
"web.assets_frontend": [
3642
# Tokens are also available on the portal/website side so OCA
3743
# frontend modules can consume `--ods-accent` etc.
44+
"odoo_design_system/static/src/scss/_tokens.generated.scss",
3845
"odoo_design_system/static/src/scss/_tokens.scss",
3946
],
4047
},

odoo_design_system/readme/ROADMAP.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ Likely next steps:
1818
card", "form-view hero block" — full layouts rather than just
1919
components.
2020
- Translation (`.pot`) once strings stabilise.
21-
- **DTCG → SCSS generator.** `static/src/tokens/design-system.dtcg.json`
22-
ships alongside `_tokens.scss` today, but the SCSS is hand-mirrored.
23-
Wire up `style-dictionary` (or a small Python script) so the SCSS
24-
partial regenerates from the DTCG JSON at build time. Then designers
25-
edit tokens in Penpot, export DTCG, commit it, and the SCSS follows
26-
automatically.
21+
- ~~**DTCG → SCSS generator.**~~ Shipped: `style-dictionary` wired up
22+
via `pnpm run tokens`. The JSON is now the single source of truth;
23+
`_tokens.generated.scss` and `dist/brand_variables.scss` regenerate
24+
from it. `pnpm run tokens:check` is the CI gate against drift.
2725
- **Tokenized brand logo.** Move `ledoweb_branding/static/src/img/logo.svg`
2826
into a per-tenant override that uses `currentColor` (or
2927
`var(--o-color-primary)`) for the fill, so changing

odoo_design_system/readme/USAGE.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,41 @@ reference them by their static path:
5858

5959
## Design-tool source (Penpot / Figma)
6060

61-
The same tokens also ship as a [W3C Design Tokens Community Group](https://design-tokens.github.io/community-group/format/) JSON file at
61+
The same tokens ship as a [W3C Design Tokens Community Group](https://design-tokens.github.io/community-group/format/) JSON file at
6262
`static/src/tokens/design-system.dtcg.json`. This is the file designers
6363
import into Penpot (Tools → Design Tokens → Import) or any other tool
64-
that speaks DTCG.
64+
that speaks DTCG. It's the **single source of truth** — the Sass
65+
partials in `static/src/scss/` are generated from it.
6566

66-
Today the JSON and `_tokens.scss` are hand-mirrored: change one, update
67-
the other in the same commit. A follow-up will add a `style-dictionary`
68-
build step that regenerates the SCSS from the JSON automatically (see
69-
ROADMAP).
67+
The pipeline:
68+
69+
```
70+
┌──────────────────────────────────────────┐
71+
│ design-system.dtcg.json │ ← edit this
72+
│ (designers edit via Penpot UI, devs │
73+
│ edit directly — same file either way) │
74+
└────────────────┬─────────────────────────┘
75+
76+
pnpm run tokens (style-dictionary)
77+
78+
┌──────────────┴───────────────┐
79+
▼ ▼
80+
odoo_design_system/static/src/ dist/brand_variables.scss
81+
scss/_tokens.generated.scss (drop into your branding module)
82+
(auto-loaded by manifest)
83+
```
84+
85+
After editing the JSON:
86+
87+
```bash
88+
pnpm install # once
89+
pnpm run tokens # regenerates both SCSS outputs
90+
git diff # review
91+
git add … && git commit
92+
```
93+
94+
A `pnpm run tokens:check` script verifies the JSON and SCSS are in
95+
sync — wire it into your pre-commit / CI to fail fast on drift.
7096

7197
## Living docs
7298

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Generated by `pnpm run tokens` from
2+
// odoo_design_system/static/src/tokens/design-system.dtcg.json.
3+
//
4+
// DO NOT EDIT BY HAND. Change the JSON and re-run.
5+
6+
7+
// ---------------------------------------------------------------------------
8+
// Sass-level palettes — internal source of truth for the generated
9+
// per-identity selectors emitted by the mixins in _tokens-helpers.scss.
10+
11+
$ods-bucket-palette: (
12+
1: #4263eb,
13+
2: #1098ad,
14+
3: #2f9e44,
15+
4: #f08c00,
16+
5: #d6336c,
17+
6: #ae3ec9,
18+
7: #5f3dc4,
19+
8: #495057,
20+
);
21+
22+
$ods-extension-accents: (
23+
"pdf": #d6336c,
24+
"doc": #1971c2,
25+
"docx": #1971c2,
26+
"odt": #1971c2,
27+
"rtf": #1971c2,
28+
"txt": #495057,
29+
"md": #495057,
30+
"rst": #495057,
31+
"xls": #2f9e44,
32+
"xlsx": #2f9e44,
33+
"ods": #2f9e44,
34+
"csv": #2f9e44,
35+
"ppt": #e8590c,
36+
"pptx": #e8590c,
37+
"odp": #e8590c,
38+
"zip": #5f3dc4,
39+
"tar": #5f3dc4,
40+
"gz": #5f3dc4,
41+
"rar": #5f3dc4,
42+
"jpg": #d6336c,
43+
"jpeg": #d6336c,
44+
"png": #d6336c,
45+
"gif": #d6336c,
46+
"svg": #d6336c,
47+
"webp": #d6336c,
48+
"mp4": #ae3ec9,
49+
"mov": #ae3ec9,
50+
"mkv": #ae3ec9,
51+
"webm": #ae3ec9,
52+
"mp3": #ae3ec9,
53+
"wav": #ae3ec9,
54+
"flac": #ae3ec9,
55+
"ogg": #ae3ec9,
56+
);
57+
58+
// ---------------------------------------------------------------------------
59+
// CSS-side tokens — exposed on :root so any component can read them.
60+
61+
:root {
62+
--ods-card-radius: 6px;
63+
--ods-chip-radius: 999px;
64+
--ods-tile-size: 56px;
65+
--ods-avatar-size: 22px;
66+
--ods-spine-width: 3px;
67+
--ods-card-lift-duration: 150ms;
68+
--ods-card-lift-easing: ease;
69+
--ods-card-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.08);
70+
--ods-card-shadow-hover: 0px 4px 18px 0px rgba(0, 0, 0, 0.08);
71+
}

odoo_design_system/static/src/scss/_tokens.scss

Lines changed: 13 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,16 @@
33
//
44
// Shared design tokens for OCA UI work on Odoo 19.
55
//
6-
// This partial is concatenated at the top of both web.assets_backend and
7-
// web.assets_frontend by __manifest__.py — Sass maps + mixins below are
8-
// therefore visible to every other SCSS file in the same bundle (Odoo's
9-
// asset compiler rejects relative @import for security, so the
10-
// prepend-via-manifest pattern is the load mechanism).
11-
12-
// ---------------------------------------------------------------------------
13-
// Sass-level palettes — internal source of truth for the generated
14-
// per-identity selectors below.
15-
16-
// Eight buckets. Anything that needs a stable per-identity tint (a
17-
// directory name, a creator initial, an asset type) hashes into this map.
18-
// Colors chosen to be visually distinct at small chip sizes and to read
19-
// cleanly over both light and dark Odoo themes.
20-
$ods-bucket-palette: (
21-
1: #4263eb,
22-
2: #1098ad,
23-
3: #2f9e44,
24-
4: #f08c00,
25-
5: #d6336c,
26-
6: #ae3ec9,
27-
7: #5f3dc4,
28-
8: #495057,
29-
);
30-
31-
// Per-extension accents. Grouped by family so adding an extension only
32-
// touches one row. Consumers read the generated --ods-accent variable;
33-
// the map itself is never exposed.
34-
$ods-extension-accents: (
35-
"pdf": #d6336c,
36-
"doc": #1971c2, "docx": #1971c2, "odt": #1971c2, "rtf": #1971c2,
37-
"txt": #495057, "md": #495057, "rst": #495057,
38-
"xls": #2f9e44, "xlsx": #2f9e44, "ods": #2f9e44, "csv": #2f9e44,
39-
"ppt": #e8590c, "pptx": #e8590c, "odp": #e8590c,
40-
"zip": #5f3dc4, "tar": #5f3dc4, "gz": #5f3dc4, "rar": #5f3dc4,
41-
"jpg": #d6336c, "jpeg": #d6336c, "png": #d6336c, "gif": #d6336c,
42-
"svg": #d6336c, "webp": #d6336c,
43-
"mp4": #ae3ec9, "mov": #ae3ec9, "mkv": #ae3ec9, "webm": #ae3ec9,
44-
"mp3": #ae3ec9, "wav": #ae3ec9, "flac": #ae3ec9, "ogg": #ae3ec9,
45-
);
6+
// The palette maps + :root CSS custom properties are auto-generated from
7+
// `static/src/tokens/design-system.dtcg.json` into `_tokens.generated.scss`
8+
// (see top-level `style-dictionary.config.js`). __manifest__.py lists the
9+
// generated partial *before* this one, so `$ods-bucket-palette`,
10+
// `$ods-extension-accents`, and every `--ods-*` variable are already in
11+
// scope by the time the helpers below execute.
12+
//
13+
// This file holds the hand-written half: pure-Sass hash helpers + the
14+
// data-attribute-keyed selector mixins. They never need to change unless
15+
// the contract (bucket count, mixin signatures) changes.
4616

4717
// ---------------------------------------------------------------------------
4818
// Pure-Sass deterministic hash. No JS hook is required — the markup
@@ -98,14 +68,14 @@ $ods-extension-accents: (
9868
--ods-accent: var(--o-gray-500, #adb5bd);
9969
position: relative;
10070
transition:
101-
transform 0.15s ease,
102-
box-shadow 0.15s ease;
71+
transform var(--ods-card-lift-duration, 150ms) var(--ods-card-lift-easing, ease),
72+
box-shadow var(--ods-card-lift-duration, 150ms) var(--ods-card-lift-easing, ease);
10373

10474
&::before {
10575
content: "";
10676
position: absolute;
10777
inset: 0 auto 0 0;
108-
width: 3px;
78+
width: var(--ods-spine-width, 3px);
10979
background: var(--ods-accent);
11080
opacity: 0.85;
11181
border-top-left-radius: inherit;
@@ -119,16 +89,3 @@ $ods-extension-accents: (
11989
}
12090
}
12191
}
122-
123-
// ---------------------------------------------------------------------------
124-
// CSS-side tokens — exposed on :root so any component can read them.
125-
126-
:root {
127-
--ods-card-radius: 6px;
128-
--ods-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
129-
--ods-card-shadow-hover: 0 4px 18px rgba(0, 0, 0, 0.08);
130-
--ods-chip-radius: 999px;
131-
--ods-tile-size: 56px;
132-
--ods-avatar-size: 22px;
133-
--ods-spine-width: 3px;
134-
}

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "odoo-design-system",
3+
"version": "19.0.1.0.0",
4+
"private": true,
5+
"description": "Build pipeline: regenerates Odoo-side SCSS partials from design-system.dtcg.json (the single source of truth for designers via Penpot/Figma).",
6+
"type": "module",
7+
"scripts": {
8+
"tokens": "style-dictionary build --config style-dictionary.config.js",
9+
"tokens:watch": "style-dictionary build --config style-dictionary.config.js --watch",
10+
"tokens:check": "node scripts/check-tokens-in-sync.mjs"
11+
},
12+
"license": "LGPL-3.0",
13+
"packageManager": "pnpm@10.33.2",
14+
"devDependencies": {
15+
"style-dictionary": "^5.4.0"
16+
}
17+
}

0 commit comments

Comments
 (0)