diff --git a/d2cli/main.go b/d2cli/main.go
index 40f0617e8..5c92efb5e 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -273,8 +273,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
layout := plugin.Layout
opts := &d2lib.CompileOptions{
- Layout: layout,
- Ruler: ruler,
+ Layout: layout,
+ Ruler: ruler,
+ ThemeID: themeID,
}
if sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
diff --git a/d2exporter/export.go b/d2exporter/export.go
index e1b38992d..16ba3787b 100644
--- a/d2exporter/export.go
+++ b/d2exporter/export.go
@@ -9,6 +9,7 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
+ "oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/color"
)
@@ -20,22 +21,25 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
if fontFamily == nil {
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
}
+ if g.Theme != nil && g.Theme.SpecialRules.Mono {
+ fontFamily = go2.Pointer(d2fonts.SourceCodePro)
+ }
diagram.FontFamily = fontFamily
diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range g.Objects {
- diagram.Shapes[i] = toShape(g.Objects[i])
+ diagram.Shapes[i] = toShape(g.Objects[i], g.Theme)
}
diagram.Connections = make([]d2target.Connection, len(g.Edges))
for i := range g.Edges {
- diagram.Connections[i] = toConnection(g.Edges[i])
+ diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
}
return diagram, nil
}
-func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
+func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText {
@@ -46,6 +50,23 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
shape.SecondaryAccentColor = color.AA2
shape.NeutralAccentColor = color.N2
}
+
+ // Theme options that change more than color
+ if theme != nil {
+ if theme.SpecialRules.OuterContainerDoubleBorder {
+ if obj.Level() == 1 && len(obj.ChildrenArray) > 0 {
+ shape.DoubleBorder = true
+ }
+ }
+ if theme.SpecialRules.ContainerDots {
+ if len(obj.ChildrenArray) > 0 {
+ shape.FillPattern = "dots"
+ }
+ }
+ if theme.SpecialRules.Mono {
+ shape.FontFamily = "mono"
+ }
+ }
}
func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
@@ -102,7 +123,7 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
}
}
-func toShape(obj *d2graph.Object) d2target.Shape {
+func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value)
shape.ID = obj.AbsID()
@@ -127,7 +148,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
}
applyStyles(shape, obj)
- applyTheme(shape, obj)
+ applyTheme(shape, obj, theme)
shape.Color = text.GetColor(shape.Italic)
applyStyles(shape, obj)
@@ -168,7 +189,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
return *shape
}
-func toConnection(edge *d2graph.Edge) d2target.Connection {
+func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
connection := d2target.BaseConnection()
connection.ID = edge.AbsID()
connection.ZIndex = edge.ZIndex
@@ -208,7 +229,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
connection.DstLabel = edge.DstArrowhead.Label.Value
}
}
-
+ if theme != nil && theme.SpecialRules.NoCornerRadius {
+ connection.BorderRadius = 0
+ }
if edge.Attributes.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64)
}
@@ -258,6 +281,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
if edge.Attributes.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value)
}
+ if theme != nil && theme.SpecialRules.Mono {
+ connection.FontFamily = "mono"
+ }
if edge.Attributes.Style.Font != nil {
connection.FontFamily = edge.Attributes.Style.Font.Value
}
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index b8955a878..129a2f39a 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -17,6 +17,8 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2target"
+ "oss.terrastruct.com/d2/d2themes"
+ "oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/shape"
@@ -43,6 +45,8 @@ type Graph struct {
Layers []*Graph `json:"layers,omitempty"`
Scenarios []*Graph `json:"scenarios,omitempty"`
Steps []*Graph `json:"steps,omitempty"`
+
+ Theme *d2themes.Theme `json:"theme,omitempty"`
}
func NewGraph() *Graph {
@@ -1251,6 +1255,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
+ if g.Theme != nil && g.Theme.SpecialRules.Mono {
+ tmp := d2fonts.SourceCodePro
+ fontFamily = &tmp
+ }
+
for _, obj := range g.Objects {
obj.Box = &geo.Box{}
@@ -1291,6 +1300,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
+ if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
+ obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value)
+ }
+
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
return err
@@ -1408,6 +1421,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
+ if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
+ edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value)
+ }
+
dims := GetTextDimensions(mtexts, ruler, edge.Text(), fontFamily)
if dims == nil {
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
@@ -1650,3 +1667,15 @@ func (obj *Object) IsDescendantOf(ancestor *Object) bool {
}
return obj.Parent.IsDescendantOf(ancestor)
}
+
+// ApplyTheme applies themes on the graph level
+// This is different than on the render level, which only changes colors
+// A theme applied on the graph level applies special rules that change the graph
+func (g *Graph) ApplyTheme(themeID int64) error {
+ theme := d2themescatalog.Find(themeID)
+ if theme == (d2themes.Theme{}) {
+ return fmt.Errorf("theme %d not found", themeID)
+ }
+ g.Theme = &theme
+ return nil
+}
diff --git a/d2lib/d2.go b/d2lib/d2.go
index e6347d303..060204ea9 100644
--- a/d2lib/d2.go
+++ b/d2lib/d2.go
@@ -22,6 +22,7 @@ type CompileOptions struct {
MeasuredTexts []*d2target.MText
Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error
+ ThemeID int64
// FontFamily controls the font family used for all texts that are not the following:
// - code
@@ -51,6 +52,11 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) {
+ err := g.ApplyTheme(opts.ThemeID)
+ if err != nil {
+ return nil, err
+ }
+
if len(g.Objects) > 0 {
err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil {
diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go
index 16b04dae9..5d578e019 100644
--- a/d2renderers/d2sketch/sketch_test.go
+++ b/d2renderers/d2sketch/sketch_test.go
@@ -22,6 +22,7 @@ import (
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
+ "oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@@ -505,6 +506,54 @@ darker: {
style.font-color: "#fff"
style.fill: "#000"
}
+`,
+ },
+ {
+ name: "terminal",
+ themeID: d2themescatalog.Terminal.ID,
+ script: `network: {
+ cell tower: {
+ satellites: {
+ shape: stored_data
+ style.multiple: true
+ }
+
+ transmitter
+
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ }
+
+ online portal: {
+ ui: { shape: hexagon }
+ }
+
+ data processor: {
+ storage: {
+ shape: cylinder
+ style.multiple: true
+ }
+ }
+
+ cell tower.transmitter -> data processor.storage: phone logs
+}
+
+user: {
+ shape: person
+ width: 130
+}
+
+user -> network.cell tower: make call
+user -> network.online portal.ui: access {
+ style.stroke-dash: 3
+}
+
+api server -> network.online portal.ui: display
+api server -> logs: persist
+logs: { shape: page; style.multiple: true }
+
+network.data processor -> api server
`,
},
{
@@ -1218,6 +1267,7 @@ func run(t *testing.T, tc testCase) {
Ruler: ruler,
Layout: layout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
+ ThemeID: tc.themeID,
})
if !tassert.Nil(t, err) {
return
diff --git a/d2renderers/d2sketch/testdata/terminal/sketch.exp.svg b/d2renderers/d2sketch/testdata/terminal/sketch.exp.svg
new file mode 100644
index 000000000..e72fdceff
--- /dev/null
+++ b/d2renderers/d2sketch/testdata/terminal/sketch.exp.svg
@@ -0,0 +1,150 @@
+
\ No newline at end of file
diff --git a/d2renderers/d2svg/appendix/appendix_test.go b/d2renderers/d2svg/appendix/appendix_test.go
index c1f3fdc11..26126327e 100644
--- a/d2renderers/d2svg/appendix/appendix_test.go
+++ b/d2renderers/d2svg/appendix/appendix_test.go
@@ -136,8 +136,9 @@ func run(t *testing.T, tc testCase) {
}
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
- Ruler: ruler,
- Layout: d2dagrelayout.DefaultLayout,
+ Ruler: ruler,
+ Layout: d2dagrelayout.DefaultLayout,
+ ThemeID: tc.themeID,
})
if !tassert.Nil(t, err) {
return
diff --git a/d2renderers/d2svg/dark_theme/dark_theme_test.go b/d2renderers/d2svg/dark_theme/dark_theme_test.go
index 3e3c64974..caf81ce00 100644
--- a/d2renderers/d2svg/dark_theme/dark_theme_test.go
+++ b/d2renderers/d2svg/dark_theme/dark_theme_test.go
@@ -430,6 +430,7 @@ func run(t *testing.T, tc testCase) {
Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
+ ThemeID: 200,
})
if !tassert.Nil(t, err) {
return
diff --git a/d2themes/d2themes.go b/d2themes/d2themes.go
index f7fd5e327..7b6e1c267 100644
--- a/d2themes/d2themes.go
+++ b/d2themes/d2themes.go
@@ -8,6 +8,16 @@ type Theme struct {
ID int64 `json:"id"`
Name string `json:"name"`
Colors ColorPalette `json:"colors"`
+
+ SpecialRules SpecialRules `json:"specialRules,omitempty"`
+}
+
+type SpecialRules struct {
+ Mono bool `json:"mono"`
+ NoCornerRadius bool `json:"noCornerRadius"`
+ OuterContainerDoubleBorder bool `json:"outerContainerDoubleBorder"`
+ ContainerDots bool `json:"containerDots"`
+ CapsLock bool `json:"capsLock"`
}
func (t *Theme) IsDark() bool {
diff --git a/d2themes/d2themescatalog/catalog.go b/d2themes/d2themescatalog/catalog.go
index 117465547..7a8b5e2aa 100644
--- a/d2themes/d2themescatalog/catalog.go
+++ b/d2themes/d2themescatalog/catalog.go
@@ -22,6 +22,7 @@ var LightCatalog = []d2themes.Theme{
EarthTones,
EvergladeGreen,
ButteredToast,
+ Terminal,
}
var DarkCatalog = []d2themes.Theme{
diff --git a/d2themes/d2themescatalog/terminal.go b/d2themes/d2themescatalog/terminal.go
new file mode 100644
index 000000000..79a07e8fc
--- /dev/null
+++ b/d2themes/d2themescatalog/terminal.go
@@ -0,0 +1,42 @@
+package d2themescatalog
+
+import "oss.terrastruct.com/d2/d2themes"
+
+var Terminal = d2themes.Theme{
+ ID: 300,
+ Name: "Terminal",
+ Colors: d2themes.ColorPalette{
+ Neutrals: TerminalNeutral,
+
+ B1: "#000410",
+ B2: "#0000E4",
+ B3: "#5AA4DC",
+ B4: "#E7E9EE",
+ B5: "#F5F6F9",
+ B6: "#FFFFFF",
+
+ AA2: "#008566",
+ AA4: "#45BBA5",
+ AA5: "#7ACCBD",
+
+ AB4: "#F1C759",
+ AB5: "#F9E088",
+ },
+ SpecialRules: d2themes.SpecialRules{
+ Mono: true,
+ NoCornerRadius: true,
+ OuterContainerDoubleBorder: true,
+ ContainerDots: true,
+ CapsLock: true,
+ },
+}
+
+var TerminalNeutral = d2themes.Neutral{
+ N1: "#000410",
+ N2: "#0000B8",
+ N3: "#9499AB",
+ N4: "#CFD2DD",
+ N5: "#C3DEF3",
+ N6: "#EEF1F8",
+ N7: "#FFFFFF",
+}
diff --git a/docs/examples/lib/3-lowlevel/lowlevel.go b/docs/examples/lib/3-lowlevel/lowlevel.go
index 6f8420cbb..55bcea233 100644
--- a/docs/examples/lib/3-lowlevel/lowlevel.go
+++ b/docs/examples/lib/3-lowlevel/lowlevel.go
@@ -17,6 +17,7 @@ import (
// Remember to add if err != nil checks in production.
func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
+ graph.ApplyTheme(d2themescatalog.NeutralDefault.ID)
ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler, nil)
_ = d2dagrelayout.Layout(context.Background(), graph, nil)
diff --git a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf
index cd314f85f..d282d6326 100644
Binary files a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf and b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf differ
diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go
index c3339063c..7b71d909b 100644
--- a/e2etests/e2e_test.go
+++ b/e2etests/e2e_test.go
@@ -42,6 +42,7 @@ func TestE2E(t *testing.T) {
t.Run("measured", testMeasured)
t.Run("unicode", testUnicode)
t.Run("root", testRoot)
+ t.Run("themes", testThemes)
}
func testSanity(t *testing.T) {
@@ -86,6 +87,7 @@ type testCase struct {
dagreFeatureError string
elkFeatureError string
expErr string
+ themeID int64
}
func runa(t *testing.T, tcs []testCase) {
@@ -163,6 +165,7 @@ func run(t *testing.T, tc testCase) {
Ruler: ruler,
MeasuredTexts: tc.mtexts,
Layout: layout,
+ ThemeID: tc.themeID,
})
if tc.expErr != "" {
@@ -204,7 +207,7 @@ func run(t *testing.T, tc testCase) {
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: 0,
- ThemeID: 0,
+ ThemeID: tc.themeID,
})
assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755)
diff --git a/e2etests/testdata/themes/terminal/dagre/board.exp.json b/e2etests/testdata/themes/terminal/dagre/board.exp.json
new file mode 100644
index 000000000..aaff0b214
--- /dev/null
+++ b/e2etests/testdata/themes/terminal/dagre/board.exp.json
@@ -0,0 +1,1104 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "network",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 275
+ },
+ "width": 410,
+ "height": 1225,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "NETWORK",
+ "fontSize": 28,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 116,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "network.cell tower",
+ "type": "rectangle",
+ "pos": {
+ "x": 96,
+ "y": 340
+ },
+ "width": 294,
+ "height": 317,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "CELL TOWER",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 143,
+ "labelHeight": 31,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.cell tower.satellites",
+ "type": "stored_data",
+ "pos": {
+ "x": 163,
+ "y": 372
+ },
+ "width": 161,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "SATELLITES",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 96,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.cell tower.transmitter",
+ "type": "rectangle",
+ "pos": {
+ "x": 168,
+ "y": 559
+ },
+ "width": 151,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "TRANSMITTER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 106,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.online portal",
+ "type": "rectangle",
+ "pos": {
+ "x": 20,
+ "y": 1319
+ },
+ "width": 157,
+ "height": 151,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ONLINE PORTAL",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 185,
+ "labelHeight": 31,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.online portal.ui",
+ "type": "hexagon",
+ "pos": {
+ "x": 71,
+ "y": 1360
+ },
+ "width": 65,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "UI",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 18,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.data processor",
+ "type": "rectangle",
+ "pos": {
+ "x": 147,
+ "y": 814
+ },
+ "width": 192,
+ "height": 182,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "DATA PROCESSOR",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 200,
+ "labelHeight": 31,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.data processor.storage",
+ "type": "cylinder",
+ "pos": {
+ "x": 187,
+ "y": 846
+ },
+ "width": 112,
+ "height": 118,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "STORAGE",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 67,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "user",
+ "type": "person",
+ "pos": {
+ "x": 82,
+ "y": 0
+ },
+ "width": 130,
+ "height": 87,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B3",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "USER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "api server",
+ "type": "rectangle",
+ "pos": {
+ "x": 450,
+ "y": 1076
+ },
+ "width": 142,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "API SERVER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 97,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "logs",
+ "type": "page",
+ "pos": {
+ "x": 480,
+ "y": 1313
+ },
+ "width": 82,
+ "height": 87,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AB4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "LOGS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 37,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[0]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 218,
+ "y": 439
+ },
+ {
+ "x": 182.4,
+ "y": 487
+ },
+ {
+ "x": 182.5,
+ "y": 511.2
+ },
+ {
+ "x": 218.5,
+ "y": 560
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[1]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 243,
+ "y": 439
+ },
+ {
+ "x": 243.2,
+ "y": 487
+ },
+ {
+ "x": 243.25,
+ "y": 511.2
+ },
+ {
+ "x": 243.25,
+ "y": 560
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[2]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 268,
+ "y": 439
+ },
+ {
+ "x": 304,
+ "y": 487
+ },
+ {
+ "x": 304,
+ "y": 511.2
+ },
+ {
+ "x": 268,
+ "y": 560
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.(cell tower.transmitter -> data processor.storage)[0]",
+ "src": "network.cell tower.transmitter",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.data processor.storage",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "PHONE LOGS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 96,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 243.25,
+ "y": 625.5
+ },
+ {
+ "x": 243.25,
+ "y": 651.1
+ },
+ {
+ "x": 243.25,
+ "y": 669.6
+ },
+ {
+ "x": 243.25,
+ "y": 687.75
+ },
+ {
+ "x": 243.25,
+ "y": 705.9
+ },
+ {
+ "x": 243.2,
+ "y": 792.2
+ },
+ {
+ "x": 243,
+ "y": 847
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(user -> network.cell tower)[0]",
+ "src": "user",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "MAKE CALL",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 86,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 172,
+ "y": 87
+ },
+ {
+ "x": 229,
+ "y": 156.2
+ },
+ {
+ "x": 243.25,
+ "y": 248.2
+ },
+ {
+ "x": 243.25,
+ "y": 305
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(user -> network.online portal.ui)[0]",
+ "src": "user",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.online portal.ui",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 3,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "label": "ACCESS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 58,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 128,
+ "y": 87
+ },
+ {
+ "x": 86.6,
+ "y": 156.2
+ },
+ {
+ "x": 76.25,
+ "y": 185.6
+ },
+ {
+ "x": 76.25,
+ "y": 203.75
+ },
+ {
+ "x": 76.25,
+ "y": 221.9
+ },
+ {
+ "x": 76.25,
+ "y": 244
+ },
+ {
+ "x": 76.25,
+ "y": 259
+ },
+ {
+ "x": 76.25,
+ "y": 274
+ },
+ {
+ "x": 76.25,
+ "y": 300.6
+ },
+ {
+ "x": 76.25,
+ "y": 325.5
+ },
+ {
+ "x": 76.25,
+ "y": 350.4
+ },
+ {
+ "x": 76.25,
+ "y": 385.7
+ },
+ {
+ "x": 76.25,
+ "y": 413.75
+ },
+ {
+ "x": 76.25,
+ "y": 441.8
+ },
+ {
+ "x": 76.25,
+ "y": 479.2
+ },
+ {
+ "x": 76.25,
+ "y": 507.25
+ },
+ {
+ "x": 76.25,
+ "y": 535.3
+ },
+ {
+ "x": 76.25,
+ "y": 570.6
+ },
+ {
+ "x": 76.25,
+ "y": 595.5
+ },
+ {
+ "x": 76.25,
+ "y": 620.4
+ },
+ {
+ "x": 76.25,
+ "y": 649.1
+ },
+ {
+ "x": 76.25,
+ "y": 667.25
+ },
+ {
+ "x": 76.25,
+ "y": 685.4
+ },
+ {
+ "x": 76.25,
+ "y": 709.6
+ },
+ {
+ "x": 76.25,
+ "y": 727.75
+ },
+ {
+ "x": 76.25,
+ "y": 745.9
+ },
+ {
+ "x": 76.25,
+ "y": 779.8
+ },
+ {
+ "x": 76.25,
+ "y": 812.5
+ },
+ {
+ "x": 76.25,
+ "y": 845.2
+ },
+ {
+ "x": 76.25,
+ "y": 888.8
+ },
+ {
+ "x": 76.25,
+ "y": 921.5
+ },
+ {
+ "x": 76.25,
+ "y": 954.2
+ },
+ {
+ "x": 76.25,
+ "y": 986
+ },
+ {
+ "x": 76.25,
+ "y": 1001
+ },
+ {
+ "x": 76.25,
+ "y": 1016
+ },
+ {
+ "x": 76.25,
+ "y": 1042.6
+ },
+ {
+ "x": 76.25,
+ "y": 1067.5
+ },
+ {
+ "x": 76.25,
+ "y": 1092.4
+ },
+ {
+ "x": 76.25,
+ "y": 1127.7
+ },
+ {
+ "x": 76.25,
+ "y": 1155.75
+ },
+ {
+ "x": 76.25,
+ "y": 1183.8
+ },
+ {
+ "x": 79.6,
+ "y": 1282.6
+ },
+ {
+ "x": 93,
+ "y": 1361
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(api server -> network.online portal.ui)[0]",
+ "src": "api server",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.online portal.ui",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "DISPLAY",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 69,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 450.25,
+ "y": 1126
+ },
+ {
+ "x": 194.64999999999998,
+ "y": 1187.2
+ },
+ {
+ "x": 127.4,
+ "y": 1282.6
+ },
+ {
+ "x": 114,
+ "y": 1361
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(api server -> logs)[0]",
+ "src": "api server",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "logs",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "PERSIST",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 521.25,
+ "y": 1142
+ },
+ {
+ "x": 521.25,
+ "y": 1190.4
+ },
+ {
+ "x": 521.2,
+ "y": 1273
+ },
+ {
+ "x": 521,
+ "y": 1313
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(network.data processor -> api server)[0]",
+ "src": "network.data processor",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "api server",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 243.25,
+ "y": 996.5
+ },
+ {
+ "x": 243.25,
+ "y": 1020.1
+ },
+ {
+ "x": 284.65,
+ "y": 1038.4
+ },
+ {
+ "x": 450.25,
+ "y": 1088
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/themes/terminal/dagre/sketch.exp.svg b/e2etests/testdata/themes/terminal/dagre/sketch.exp.svg
new file mode 100644
index 000000000..f85057442
--- /dev/null
+++ b/e2etests/testdata/themes/terminal/dagre/sketch.exp.svg
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+NETWORKUSERAPI SERVERLOGSCELL TOWERONLINE PORTALDATA PROCESSORSATELLITESTRANSMITTERUISTORAGE SENDSENDSENDPHONE LOGSMAKE CALL ACCESSDISPLAYPERSIST
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/themes/terminal/elk/board.exp.json b/e2etests/testdata/themes/terminal/elk/board.exp.json
new file mode 100644
index 000000000..b673875c3
--- /dev/null
+++ b/e2etests/testdata/themes/terminal/elk/board.exp.json
@@ -0,0 +1,895 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "network",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 311
+ },
+ "width": 611,
+ "height": 902,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "NETWORK",
+ "fontSize": 28,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 116,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "network.cell tower",
+ "type": "rectangle",
+ "pos": {
+ "x": 62,
+ "y": 361
+ },
+ "width": 261,
+ "height": 413,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "CELL TOWER",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 143,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.cell tower.satellites",
+ "type": "stored_data",
+ "pos": {
+ "x": 112,
+ "y": 411
+ },
+ "width": 161,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "SATELLITES",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 96,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.cell tower.transmitter",
+ "type": "rectangle",
+ "pos": {
+ "x": 117,
+ "y": 658
+ },
+ "width": 151,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "TRANSMITTER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 106,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.online portal",
+ "type": "rectangle",
+ "pos": {
+ "x": 343,
+ "y": 366
+ },
+ "width": 230,
+ "height": 169,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ONLINE PORTAL",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 185,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.online portal.ui",
+ "type": "hexagon",
+ "pos": {
+ "x": 418,
+ "y": 416
+ },
+ "width": 80,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "UI",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 18,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "network.data processor",
+ "type": "rectangle",
+ "pos": {
+ "x": 70,
+ "y": 945
+ },
+ "width": 245,
+ "height": 218,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "fillPattern": "dots",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "DATA PROCESSOR",
+ "fontSize": 24,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 200,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "network.data processor.storage",
+ "type": "cylinder",
+ "pos": {
+ "x": 136,
+ "y": 995
+ },
+ "width": 112,
+ "height": 118,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "STORAGE",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 67,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "user",
+ "type": "person",
+ "pos": {
+ "x": 314,
+ "y": 12
+ },
+ "width": 130,
+ "height": 87,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B3",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "USER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "api server",
+ "type": "rectangle",
+ "pos": {
+ "x": 592,
+ "y": 59
+ },
+ "width": 142,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "API SERVER",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 97,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "logs",
+ "type": "page",
+ "pos": {
+ "x": 703,
+ "y": 311
+ },
+ "width": 82,
+ "height": 87,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AB4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": true,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "LOGS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 37,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[0]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 143,
+ "y": 477
+ },
+ {
+ "x": 143.5,
+ "y": 658
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[1]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 192,
+ "y": 477
+ },
+ {
+ "x": 192.5,
+ "y": 658
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.cell tower.(satellites -> transmitter)[2]",
+ "src": "network.cell tower.satellites",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower.transmitter",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "SEND",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 38,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 241,
+ "y": 477
+ },
+ {
+ "x": 241.5,
+ "y": 658
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "network.(cell tower.transmitter -> data processor.storage)[0]",
+ "src": "network.cell tower.transmitter",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.data processor.storage",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "PHONE LOGS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 96,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 192.5,
+ "y": 724
+ },
+ {
+ "x": 193,
+ "y": 995
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(user -> network.cell tower)[0]",
+ "src": "user",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.cell tower",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "MAKE CALL",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 86,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 358,
+ "y": 99
+ },
+ {
+ "x": 357.58333333333337,
+ "y": 165
+ },
+ {
+ "x": 236.83333333333334,
+ "y": 165
+ },
+ {
+ "x": 236.83333333333334,
+ "y": 361
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(user -> network.online portal.ui)[0]",
+ "src": "user",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.online portal.ui",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 3,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "label": "ACCESS",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 58,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 401,
+ "y": 99
+ },
+ {
+ "x": 400.9166666666667,
+ "y": 165
+ },
+ {
+ "x": 444.6666666666667,
+ "y": 165
+ },
+ {
+ "x": 445,
+ "y": 416
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(api server -> network.online portal.ui)[0]",
+ "src": "api server",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "network.online portal.ui",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "DISPLAY",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 69,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 627.5,
+ "y": 125
+ },
+ {
+ "x": 627.5,
+ "y": 266
+ },
+ {
+ "x": 471.33333333333337,
+ "y": 266
+ },
+ {
+ "x": 471,
+ "y": 416
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(api server -> logs)[0]",
+ "src": "api server",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "logs",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "PERSIST",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 698.5,
+ "y": 125
+ },
+ {
+ "x": 698.5,
+ "y": 165
+ },
+ {
+ "x": 744,
+ "y": 165
+ },
+ {
+ "x": 744,
+ "y": 311
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(network.data processor -> api server)[0]",
+ "src": "network.data processor",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "api server",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "mono",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 136.5,
+ "y": 1163
+ },
+ {
+ "x": 136.5,
+ "y": 1258
+ },
+ {
+ "x": 663,
+ "y": 1258
+ },
+ {
+ "x": 663,
+ "y": 125
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/themes/terminal/elk/sketch.exp.svg b/e2etests/testdata/themes/terminal/elk/sketch.exp.svg
new file mode 100644
index 000000000..72a1e2fbf
--- /dev/null
+++ b/e2etests/testdata/themes/terminal/elk/sketch.exp.svg
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+NETWORKUSERAPI SERVERLOGSCELL TOWERONLINE PORTALDATA PROCESSORSATELLITESTRANSMITTERUISTORAGE SENDSENDSENDPHONE LOGSMAKE CALL ACCESSDISPLAYPERSIST
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/themes_test.go b/e2etests/themes_test.go
new file mode 100644
index 000000000..47c384956
--- /dev/null
+++ b/e2etests/themes_test.go
@@ -0,0 +1,64 @@
+package e2etests
+
+import (
+ _ "embed"
+ "testing"
+
+ "oss.terrastruct.com/d2/d2themes/d2themescatalog"
+)
+
+func testThemes(t *testing.T) {
+ tcs := []testCase{
+ {
+ name: "terminal",
+ themeID: d2themescatalog.Terminal.ID,
+ script: `
+network: {
+ cell tower: {
+ satellites: {
+ shape: stored_data
+ style.multiple: true
+ }
+
+ transmitter
+
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ }
+
+ online portal: {
+ ui: { shape: hexagon }
+ }
+
+ data processor: {
+ storage: {
+ shape: cylinder
+ style.multiple: true
+ }
+ }
+
+ cell tower.transmitter -> data processor.storage: phone logs
+}
+
+user: {
+ shape: person
+ width: 130
+}
+
+user -> network.cell tower: make call
+user -> network.online portal.ui: access {
+ style.stroke-dash: 3
+}
+
+api server -> network.online portal.ui: display
+api server -> logs: persist
+logs: { shape: page; style.multiple: true }
+
+network.data processor -> api server
+`,
+ },
+ }
+
+ runa(t, tcs)
+}