First, the design system gets broken down into three parts: palette, theme, and config.
A palette is a set of values that admins can manage directly. Palette fields accept raw input (numbers, hex codes, etc). A palette would look like this:
palette = {
"color1-faded": "#xxx",
"color1-light": "#xxx",
"color1-bright": "#xxx",
"color1-main": "#xxx",
"color1-dark": "#xxx",
"color2-faded": "#xxx",
"color2-light": "#xxx",
"color2-bright": "#xxx",
"color2-main": "#xxx",
"color2-dark": "#xxx",
"color3-faded": "#xxx",
"color3-light": "#xxx",
"color3-bright": "#xxx",
"color3-main": "#xxx",
"color3-dark": "#xxx",
"color4-faded": "#xxx",
"color4-light": "#xxx",
"color4-bright": "#xxx",
"color4-main": "#xxx",
"color4-dark": "#xxx",
"neutral-white": "#xxx",
"neutral-gray05": "#xxx",
"neutral-gray10": "#xxx",
"neutral-gray30": "#xxx",
"neutral-gray40": "#xxx",
"neutral-gray60": "#xxx",
"neutral-gray70": "#xxx",
"neutral-gray80": "#xxx",
"neutral-gray90": "#xxx",
"neutral-black": "#xxx",
}
The second half of the design system is a theme. Theme is a schema that holds keys of palette values. A theme value never holds a raw value, it always references a field out of the palette.
theme = {
"brand-color": <palette.keys>,
"brand-color-tint": <palette.keys>,
"white-mode-primary-foreground": <palette.keys>,
"white-mode-secondary-foreground": <palette.keys>,
"white-mode-accent": <palette.keys>,
"white-mode-hover": <palette.keys>,
"white-mode-ui": <palette.keys>,
"white-mode-button": <palette.keys>,
"white-mode-background": <palette.keys>,
"black-mode-primary-foreground": <palette.keys>,
"black-mode-secondary-foreground": <palette.keys>,
"black-mode-accent": <palette.keys>,
"black-mode-hover": <palette.keys>,
"black-mode-ui": <palette.keys>,
"black-mode-button": <palette.keys>,
"black-mode-background": <palette.keys>,
"light-mode-primary-foreground": <palette.keys>,
"light-mode-secondary-foreground": <palette.keys>,
"light-mode-accent": <palette.keys>,
"light-mode-hover": <palette.keys>,
"light-mode-ui": <palette.keys>,
"light-mode-button": <palette.keys>,
"light-mode-background": <palette.keys>,
"dark-mode-primary-foreground": <palette.keys>,
"dark-mode-secondary-foreground": <palette.keys>,
"dark-mode-accent": <palette.keys>,
"dark-mode-hover": <palette.keys>,
"dark-mode-ui": <palette.keys>,
"dark-mode-button": <palette.keys>,
"dark-mode-background": <palette.keys>,
}
With this breakdown, it becomes relatively straight forward to render a hash of theme values populated with palette fields.
Lastly, we get to configuration. This is where theme settings are mapped to actual frontend components. This looks a lot like site config settings as they exist now. Style config settings can select from characteristics of the theme schema. For example:
config = {
"sidebar-widget-theme-mode": <"light-mode", "dark-mode", ...>,
"breaker-bar-theme-mode": "dark-mode"
}
Now the implementation will need to be managed at several levels.
In SCSS, variables would need to be defined as composite fields. These would accept config references and other partial namespace references.
background-color: composite-var(--sidebar-widget-theme-mode, "background");
When rendered as CSS, these composite values simply get a recognizable token format that would allow for post-processing.
background-color: COMPOSITE____sidebar_widget_theme_mode____background;
Now when our proprietary Webpack plugin runs to transform the CSS into ERB, we'd simply recognize these larger token patterns and write them as template functions into the ERB markup.
background-color: <%= composite_var(@theme[:sidebar_widget_theme_mode], "background") %>
Finally, Ruby render context adds corresponding view helper methods that would allow variables to be assembled into dynamic references at runtime. This would allow for, say, "light-mode"
to be read from component config, namespaced as "light-mode-background"
by the template function, and then reference the precompiled value from theme data.
def composite_var(prefix, namespace)
@theme["#{prefix}_#{namespace}"]
end