Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamic dark theme based on CSS class selector #1803

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#### Features 🚀

- Dark theme maybe enabled via a custom CSS class: `--dark-theme-class theme-dark`.

#### Improvements 🧹

#### Bugfixes ⛑️
4 changes: 4 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ still be applied and this may produce unexpected results. We plan on resolving t
making style maps in D2 light/dark mode specific. See
.Lk https://github.com/terrastruct/d2/issues/831
.Ns .
.It Fl -dark-theme-class Ar
The CSS class to enable dark mode. When left unset prefers-color-scheme media query is used. See
.Lk https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
.Ns .
.It Fl s , -sketch Ar false
Renders the diagram to look like it was sketched by hand
.Ns .
Expand Down
19 changes: 13 additions & 6 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
darkThemeClassFlag := ms.Opts.String("D2_DARK_THEME_CLASS", "dark-theme-class", "", "", "the CSS class to enable dark mode. When left unset prefers-color-scheme media query is used. See https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme.")
develar marked this conversation as resolved.
Show resolved Hide resolved
padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
if err != nil {
return err
Expand Down Expand Up @@ -285,6 +286,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
darkThemeFlag = nil
}
if darkThemeClassFlag != nil {
ms.Log.Warn.Printf("--dark-theme-class cannot be used while exporting to another format other than .svg")
darkThemeClassFlag = nil
}
}
var pw png.Playwright
if outputFormat.requiresPNGRenderer() {
Expand All @@ -301,12 +306,13 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}

renderOpts := d2svg.RenderOpts{
Pad: padFlag,
Sketch: sketchFlag,
Center: centerFlag,
ThemeID: themeFlag,
DarkThemeID: darkThemeFlag,
Scale: scale,
Pad: padFlag,
Sketch: sketchFlag,
Center: centerFlag,
ThemeID: themeFlag,
DarkThemeID: darkThemeFlag,
DarkThemeClass: *darkThemeClassFlag,
Scale: scale,
}

if *watchFlag {
Expand Down Expand Up @@ -849,6 +855,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
Center: opts.Center,
ThemeID: opts.ThemeID,
DarkThemeID: opts.DarkThemeID,
DarkThemeClass: opts.DarkThemeClass,
MasterID: opts.MasterID,
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
Expand Down
3 changes: 3 additions & 0 deletions d2lib/d2.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOp
if renderOpts.DarkThemeID == nil {
renderOpts.DarkThemeID = config.DarkThemeID
}
if renderOpts.DarkThemeClass == "" {
renderOpts.DarkThemeClass = *config.DarkThemeClass
}
if renderOpts.Sketch == nil {
renderOpts.Sketch = config.Sketch
}
Expand Down
2 changes: 1 addition & 1 deletion d2renderers/d2animate/d2animate.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO

d2svg.EmbedFonts(buf, diagramHash, svgsStr, rootDiagram.FontFamily, rootDiagram.GetNestedCorpus())

themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID, renderOpts.ThemeOverrides, renderOpts.DarkThemeOverrides)
themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID, renderOpts.DarkThemeClass, renderOpts.ThemeOverrides, renderOpts.DarkThemeOverrides)
if err != nil {
return nil, err
}
Expand Down
147 changes: 81 additions & 66 deletions d2renderers/d2svg/d2svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type RenderOpts struct {
Center *bool
ThemeID *int64
DarkThemeID *int64
DarkThemeClass string
ThemeOverrides *d2target.ThemeOverrides
DarkThemeOverrides *d2target.ThemeOverrides
Font string
Expand Down Expand Up @@ -1758,6 +1759,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
pad := DEFAULT_PADDING
themeID := d2themescatalog.NeutralDefault.ID
darkThemeID := DEFAULT_DARK_THEME
darkThemeClass := ""
var scale *float64
if opts != nil {
if opts.Pad != nil {
Expand All @@ -1774,6 +1776,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
themeID = *opts.ThemeID
}
darkThemeID = opts.DarkThemeID
darkThemeClass = opts.DarkThemeClass
scale = opts.Scale
}

Expand Down Expand Up @@ -1856,7 +1859,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
upperBuf := &bytes.Buffer{}
if opts.MasterID == "" {
EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus()) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
themeStylesheet, err := ThemeCSS(diagramHash, &themeID, darkThemeID, opts.ThemeOverrides, opts.DarkThemeOverrides)
themeStylesheet, err := ThemeCSS(diagramHash, &themeID, darkThemeID, darkThemeClass, opts.ThemeOverrides, opts.DarkThemeOverrides)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2018,96 +2021,108 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}

// TODO include only colors that are being used to reduce size
func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64, overrides, darkOverrides *d2target.ThemeOverrides) (stylesheet string, err error) {
func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64, darkThemeClass string, overrides, darkOverrides *d2target.ThemeOverrides) (stylesheet string, err error) {
if themeID == nil {
themeID = &d2themescatalog.NeutralDefault.ID
}
out, err := singleThemeRulesets(diagramHash, *themeID, overrides)

classPrefix := "." + diagramHash
out, err := singleThemeRulesets(classPrefix, *themeID, overrides)
if err != nil {
return "", err
}

if darkThemeID != nil {
darkOut, err := singleThemeRulesets(diagramHash, *darkThemeID, darkOverrides)
hasThemeClass := len(darkThemeClass) != 0
if hasThemeClass {
classPrefix = "." + darkThemeClass + " " + classPrefix
}
darkOut, err := singleThemeRulesets(classPrefix, *darkThemeID, darkOverrides)
if err != nil {
return "", err
}
out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", darkOut)

if hasThemeClass {
out += darkOut
} else {
out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", darkOut)
}
}

return out, nil
}

func singleThemeRulesets(diagramHash string, themeID int64, overrides *d2target.ThemeOverrides) (rulesets string, err error) {
func singleThemeRulesets(classPrefix string, themeID int64, overrides *d2target.ThemeOverrides) (rulesets string, err error) {
out := ""
theme := d2themescatalog.Find(themeID)
theme.ApplyOverrides(overrides)

// Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} {
out += fmt.Sprintf(`
.%s .%s-N1{%s:%s;}
.%s .%s-N2{%s:%s;}
.%s .%s-N3{%s:%s;}
.%s .%s-N4{%s:%s;}
.%s .%s-N5{%s:%s;}
.%s .%s-N6{%s:%s;}
.%s .%s-N7{%s:%s;}
.%s .%s-B1{%s:%s;}
.%s .%s-B2{%s:%s;}
.%s .%s-B3{%s:%s;}
.%s .%s-B4{%s:%s;}
.%s .%s-B5{%s:%s;}
.%s .%s-B6{%s:%s;}
.%s .%s-AA2{%s:%s;}
.%s .%s-AA4{%s:%s;}
.%s .%s-AA5{%s:%s;}
.%s .%s-AB4{%s:%s;}
.%s .%s-AB5{%s:%s;}`,
diagramHash,
%s .%s-N1{%s:%s;}
%s .%s-N2{%s:%s;}
%s .%s-N3{%s:%s;}
%s .%s-N4{%s:%s;}
%s .%s-N5{%s:%s;}
%s .%s-N6{%s:%s;}
%s .%s-N7{%s:%s;}
%s .%s-B1{%s:%s;}
%s .%s-B2{%s:%s;}
%s .%s-B3{%s:%s;}
%s .%s-B4{%s:%s;}
%s .%s-B5{%s:%s;}
%s .%s-B6{%s:%s;}
%s .%s-AA2{%s:%s;}
%s .%s-AA4{%s:%s;}
%s .%s-AA5{%s:%s;}
%s .%s-AB4{%s:%s;}
%s .%s-AB5{%s:%s;}`,
classPrefix,
property, property, theme.Colors.Neutrals.N1,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N2,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N3,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N4,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N5,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N6,
diagramHash,
classPrefix,
property, property, theme.Colors.Neutrals.N7,
diagramHash,
classPrefix,
property, property, theme.Colors.B1,
diagramHash,
classPrefix,
property, property, theme.Colors.B2,
diagramHash,
classPrefix,
property, property, theme.Colors.B3,
diagramHash,
classPrefix,
property, property, theme.Colors.B4,
diagramHash,
classPrefix,
property, property, theme.Colors.B5,
diagramHash,
classPrefix,
property, property, theme.Colors.B6,
diagramHash,
classPrefix,
property, property, theme.Colors.AA2,
diagramHash,
classPrefix,
property, property, theme.Colors.AA4,
diagramHash,
classPrefix,
property, property, theme.Colors.AA5,
diagramHash,
classPrefix,
property, property, theme.Colors.AB4,
diagramHash,
classPrefix,
property, property, theme.Colors.AB5,
)
}

// Appendix
out += fmt.Sprintf(".appendix text.text{fill:%s}", theme.Colors.Neutrals.N1)
out += fmt.Sprintf("%s .appendix text.text{fill:%s}", classPrefix, theme.Colors.Neutrals.N1)

// Markdown specific rulesets
out += fmt.Sprintf(".md{--color-fg-default:%s;--color-fg-muted:%s;--color-fg-subtle:%s;--color-canvas-default:%s;--color-canvas-subtle:%s;--color-border-default:%s;--color-border-muted:%s;--color-neutral-muted:%s;--color-accent-fg:%s;--color-accent-emphasis:%s;--color-attention-subtle:%s;--color-danger-fg:%s;}",
out += fmt.Sprintf("%s .md{--color-fg-default:%s;--color-fg-muted:%s;--color-fg-subtle:%s;--color-canvas-default:%s;--color-canvas-subtle:%s;--color-border-default:%s;--color-border-muted:%s;--color-neutral-muted:%s;--color-accent-fg:%s;--color-accent-emphasis:%s;--color-attention-subtle:%s;--color-danger-fg:%s;}",
classPrefix,
theme.Colors.Neutrals.N1, theme.Colors.Neutrals.N2, theme.Colors.Neutrals.N3,
theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6,
theme.Colors.B1, theme.Colors.B2,
Expand All @@ -2123,105 +2138,105 @@ func singleThemeRulesets(diagramHash string, themeID int64, overrides *d2target.
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B1, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B1, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B2)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B2, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B2, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B3)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B3, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B3, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B4)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B4, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B4, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B5)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B5, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B5, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B6)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B6, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.B6, lc, blendMode(lc))

// AA
lc, err = color.LuminanceCategory(theme.Colors.AA2)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA2, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.AA2, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.AA4)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA4, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.AA4, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.AA5)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA5, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.AA5, lc, blendMode(lc))

// AB
lc, err = color.LuminanceCategory(theme.Colors.AB4)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AB4, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.AB4, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.AB5)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AB5, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.AB5, lc, blendMode(lc))

// Neutrals
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N1)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N1, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N1, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N2)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N2, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N2, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N3)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N3, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N3, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N4)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N4, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N4, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N5)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N5, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N5, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N6)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N6, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N6, lc, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N7)
if err != nil {
return "", err
}
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N7, lc, blendMode(lc))
out += fmt.Sprintf("%s .sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", classPrefix, color.N7, lc, blendMode(lc))

if theme.IsDark() {
out += ".light-code{display: none}"
out += ".dark-code{display: block}"
out += classPrefix + " .light-code{display: none}"
out += classPrefix + " .dark-code{display: block}"
} else {
out += ".light-code{display: block}"
out += ".dark-code{display: none}"
out += classPrefix + " .light-code{display: block}"
out += classPrefix + " .dark-code{display: none}"
}

return out, nil
Expand Down