|
| 1 | +21. Support application tokens |
| 2 | +------------------------------ |
| 3 | + |
| 4 | +Status |
| 5 | +====== |
| 6 | + |
| 7 | +Accepted |
| 8 | + |
| 9 | +Context |
| 10 | +======= |
| 11 | + |
| 12 | +Paragon's design token system (see `ADR 0019: Scaling Paragon's styles |
| 13 | +architecture with design tokens |
| 14 | +<0019-scaling-styles-with-design-tokens.rst>`__) prefixes every emitted CSS |
| 15 | +variable with ``--pgn-``. This is intentional for tokens that belong to the |
| 16 | +Paragon design system: it namespaces them, keeps them from colliding with |
| 17 | +consumer CSS, and reflects their origin. |
| 18 | + |
| 19 | +However, Open edX micro-frontends (MFEs) define their own CSS variables for |
| 20 | +component-level customization, and those variables are *not* part of Paragon. |
| 21 | +They use names chosen by the MFE itself. For example, ``frontend-app-catalog`` |
| 22 | +defines this in its home banner stylesheet: |
| 23 | + |
| 24 | +.. code-block:: scss |
| 25 | +
|
| 26 | + background-color: var(--catalog-home-page-banner-background-color, var(--pgn-color-gray-500)); |
| 27 | +
|
| 28 | +The fallback (``var(--pgn-color-gray-500)``) covers default rendering when no |
| 29 | +MFE-specific override is supplied. Brand package authors who want to customize |
| 30 | +the catalog banner separately are expected to set |
| 31 | +``--catalog-home-page-banner-background-color`` to a value of their choosing. |
| 32 | + |
| 33 | +With Paragon's existing token tooling, brand package authors cannot do this. |
| 34 | +Every token built through ``paragon build-tokens`` is unconditionally prefixed |
| 35 | +with ``pgn``. A brand package author defining a token at path |
| 36 | +``catalog.home-page.banner.background-color`` would emit a CSS variable named |
| 37 | +``--pgn-catalog-home-page-banner-background-color`` — a name no MFE actually |
| 38 | +reads. |
| 39 | + |
| 40 | +This is the gap reported in `issue #4274 |
| 41 | +<https://github.com/openedx/paragon/issues/4274>`__: brand package authors |
| 42 | +should be able to use the existing token system to customize MFE CSS |
| 43 | +variables, but the system has no mechanism for tokens that don't belong to |
| 44 | +Paragon. |
| 45 | + |
| 46 | +Decision |
| 47 | +======== |
| 48 | + |
| 49 | +We will add a third top-level token directory, ``apps/``, alongside ``core/`` |
| 50 | +and ``themes/``. Tokens placed under ``apps/<app-name>/`` will be emitted |
| 51 | +**without** the ``--pgn-`` prefix into a new build artifact at |
| 52 | +``<buildDir>/apps/<app-name>/variables.css``. References from app tokens to |
| 53 | +Paragon tokens will be preserved as ``var(--pgn-…)`` so theme variation |
| 54 | +continues to flow through automatically. |
| 55 | + |
| 56 | +The decision breaks down into three sub-decisions, each with a meaningful |
| 57 | +rejected alternative. |
| 58 | + |
| 59 | +1. Apps emit unprefixed CSS variables; the variable name is the JSON path |
| 60 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 61 | + |
| 62 | +The app build runs with ``prefix: ''`` and a token defined at JSON path |
| 63 | +``catalog.home-page.banner.background-color`` emits the CSS variable |
| 64 | +``--catalog-home-page-banner-background-color``. The brand package author has |
| 65 | +full control over the resulting CSS name through the path they choose. |
| 66 | + |
| 67 | +**Rejected alternative: a per-app prefix (e.g. ``--<app-name>-…``) injected |
| 68 | +automatically by the build.** The reason this fails is that MFE CSS variable |
| 69 | +conventions vary across the Open edX codebase. Some MFEs use the unprefixed |
| 70 | +app slug (``--catalog-…``), some use the full package name |
| 71 | +(``--frontend-app-catalog-…``), some use neither. Any single auto-prefix |
| 72 | +scheme would either require brand package authors to write extra path |
| 73 | +segments to compensate, or would simply fail to match the MFE's actual |
| 74 | +variable name. Letting the JSON path be the source of truth is the only |
| 75 | +flexible answer. |
| 76 | + |
| 77 | +**Rejected alternative: keep the ``--pgn-`` prefix and ask MFEs to adopt |
| 78 | +``--pgn-…`` for their own variables.** The ``--pgn-`` prefix marks a variable |
| 79 | +as belonging to the Paragon design system. MFE-defined variables are *not* |
| 80 | +part of Paragon — they belong to the MFE that defines them — so prefixing |
| 81 | +them with ``--pgn-`` would be a category error regardless of any practical |
| 82 | +considerations. Asking MFEs to migrate would also shift a non-trivial burden |
| 83 | +across the ecosystem and contradict variable conventions MFEs have already |
| 84 | +published, but the namespacing argument is the load-bearing one: app tokens |
| 85 | +fundamentally aren't Paragon tokens. |
| 86 | + |
| 87 | +2. App tokens are inlined into each theme variant's CSS bundle |
| 88 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 89 | + |
| 90 | +The per-theme ``index.css`` produced by ``build-tokens`` adds an ``@import`` |
| 91 | +line per discovered app, and ``postcss-import`` resolves those imports during |
| 92 | +the SCSS compilation step in ``build-scss``. The published |
| 93 | +``dist/<variant>.min.css`` therefore already contains every app's |
| 94 | +custom-property declarations. |
| 95 | + |
| 96 | +This means no MFE-side runtime change is required to consume app tokens. |
| 97 | +Every MFE that loads a brand's ``light.min.css`` (via |
| 98 | +``MFE_CONFIG["PARAGON_THEME_URLS"]``) automatically receives every app's |
| 99 | +overrides. |
| 100 | + |
| 101 | +**Rejected alternative (deferred): ship per-app CSS files in ``dist/`` and |
| 102 | +extend the runtime manifest.** A natural future shape is one CSS file per app |
| 103 | +at ``dist/apps/<app>.min.css``, advertised through a new ``apps`` section of |
| 104 | +``theme-urls.json``, with Paragon's runtime loading only the app file |
| 105 | +matching the current MFE. This is more efficient when many apps have brand |
| 106 | +overrides — each MFE downloads only its own. We didn't do it because it |
| 107 | +requires changes across multiple repositories (Paragon's runtime, the |
| 108 | +MFE-side convention for "what's my app name", the manifest schema, every |
| 109 | +brand package's build), all to optimize a cost (theme bundle size) that |
| 110 | +isn't yet a real problem. Brands typically override a small number of apps, |
| 111 | +so the linear growth of the bundled approach is bounded. We can revisit |
| 112 | +this once volume justifies it. |
| 113 | + |
| 114 | +**Rejected alternative: convention-based URLs without a manifest.** MFEs |
| 115 | +could try-load ``<brandOverride-base>/apps/<self>.min.css`` and fall back |
| 116 | +silently if the file is absent. This is the worst of both worlds: it requires |
| 117 | +MFE-side changes (so it doesn't deliver the bundled approach's "no MFE |
| 118 | +change" benefit), but it provides no manifest for discovery and produces 404 |
| 119 | +noise in production logs. |
| 120 | + |
| 121 | +3. App builds include a theme variant for reference-resolution vocabulary |
| 122 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 123 | + |
| 124 | +The app build's ``include`` covers Paragon's ``core/`` *and* one theme |
| 125 | +variant (``themes/light/``). This makes paths like ``color.gray.500`` and |
| 126 | +``color.brand.500`` available to ``style-dictionary``'s reference resolver, |
| 127 | +so an app token writing ``{ "$value": "{color.gray.500}" }`` correctly emits |
| 128 | +``var(--pgn-color-gray-500)`` in the output. |
| 129 | + |
| 130 | +The choice of theme variant is incidental — only the path layout matters for |
| 131 | +reference resolution; values fall out at runtime against whichever Paragon |
| 132 | +theme CSS is loaded. ``light`` is hardcoded in |
| 133 | +``getAppStyleDictionaryConfig`` because it is the only theme variant Paragon |
| 134 | +ships today. |
| 135 | + |
| 136 | +**Rejected alternative: include only ``core/``.** A natural-looking simpler |
| 137 | +design, but it fails: many tokens that brand package authors will want to |
| 138 | +reference (colors in particular) are declared in ``themes/<variant>/`` files, |
| 139 | +not in ``core/``. Without a theme in scope, those references would not |
| 140 | +resolve and the build would fail or warn. |
| 141 | + |
| 142 | +This points at an architectural smell — the coupling of "the path exists" |
| 143 | +and "the path has this value in this theme" — that is captured in `issue |
| 144 | +#4275 <https://github.com/openedx/paragon/issues/4275>`__ as a future |
| 145 | +refactor. A semantic-vs-primitive token split would let app builds reference |
| 146 | +a theme-invariant schema in ``core/`` and remove the implicit light-theme |
| 147 | +dependency. That refactor is out of scope here; the hardcoded ``light`` is a |
| 148 | +deliberate workaround that this ADR records so it can be revisited. |
| 149 | + |
| 150 | +Consequences |
| 151 | +============ |
| 152 | + |
| 153 | +* **Brand package authors gain a token-level mechanism for MFE |
| 154 | + customization.** What previously required hand-rolled CSS now flows through |
| 155 | + the same JSON-driven design token pipeline as Paragon's own tokens. |
| 156 | + |
| 157 | +* **No MFE-side change is required.** Brand packages continue to publish |
| 158 | + ``dist/<variant>.min.css`` that MFEs load via |
| 159 | + ``MFE_CONFIG["PARAGON_THEME_URLS"]``. App overrides arrive as additional |
| 160 | + ``:root`` declarations in that same file. MFEs that don't read a particular |
| 161 | + ``--<app>-…`` variable simply ignore the declaration; the theme bundle |
| 162 | + grows linearly with the number of apps a brand actually overrides. |
| 163 | + |
| 164 | +* **The ``--pgn-`` prefix is no longer a hard invariant of the token |
| 165 | + system.** Output prefixes now depend on which top-level directory a token |
| 166 | + lives under. Consumers writing tokens don't need to reason about it — the |
| 167 | + directory choice does it for them. |
| 168 | + |
| 169 | +* **Per-app CSS files are not currently emitted to ``dist/``.** Apps are |
| 170 | + bundled into the theme variant's CSS rather than served independently. If |
| 171 | + app overrides become widespread enough that per-MFE loading is worth it, a |
| 172 | + future iteration can revisit decision (2) above. |
| 173 | + |
| 174 | +* **App tokens reach into ``themes/`` for reference resolution, not just |
| 175 | + ``core/``.** Many of the paths a brand package author will want to |
| 176 | + reference — colors especially — are declared in ``themes/<variant>/``, not |
| 177 | + in ``core/``. The app build accommodates that today by including |
| 178 | + ``themes/light`` in its style-dictionary ``include`` purely for vocabulary. |
| 179 | + Issue `#4275 <https://github.com/openedx/paragon/issues/4275>`__ tracks |
| 180 | + splitting Paragon's tokens into a theme-invariant schema (in ``core/``) |
| 181 | + and theme-specific values (in ``themes/``), at which point app builds |
| 182 | + would only need ``core/``. |
| 183 | + |
| 184 | +Resources |
| 185 | +========= |
| 186 | + |
| 187 | +* `Issue #4274 — Handle non-pgn-prefixed tokens from MFEs |
| 188 | + <https://github.com/openedx/paragon/issues/4274>`__ |
| 189 | +* `Issue #4275 — Decouple semantic token declarations from theme-specific |
| 190 | + values <https://github.com/openedx/paragon/issues/4275>`__ |
| 191 | +* `ADR 0019 — Scaling Paragon's styles architecture with design tokens |
| 192 | + <0019-scaling-styles-with-design-tokens.rst>`__ |
0 commit comments