// Dumps the real theme values out to design_system/, so the design system // never has to be typed out by hand (and never drifts from the package). // // cd garage_ui && flutter test tool/export_design_tokens_test.dart // // its a "test" only because thats the easiest way to get a flutter binding // without an app. it writes files, it doesnt assert anything interesting. import "dart:convert"; import "dart:io"; import "package:flutter/widgets.dart"; import "package:flutter_test/flutter_test.dart"; import "package:garage_ui/theme.dart"; String _hex(Color c) { final argb = c.toARGB32(); final a = (argb >> 24) & 0xff; final rgb = (argb & 0xffffff).toRadixString(16).padLeft(6, "0"); if (a == 0xff) return "#$rgb"; return "#$rgb${a.toRadixString(16).padLeft(2, "0")}"; } Map _colours(ColourScheme s) => { "background": _hex(s.background), "foreground": _hex(s.foreground), "chrome": _hex(s.chrome), "surface-sunken": _hex(s.surfaceSunken), "card": _hex(s.card), "muted": _hex(s.muted), "muted-foreground": _hex(s.mutedForeground), "row-text": _hex(s.rowText), "row-hovered": _hex(s.rowHovered), "popover": _hex(s.popover), "popover-border": _hex(s.popoverBorder), "popover-item-hovered": _hex(s.popoverItemHovered), "tooltip-background": _hex(s.tooltipBackground), "tooltip-border": _hex(s.tooltipBorder), "primary": _hex(s.primary), "primary-hovered": _hex(s.primaryHovered), "primary-foreground": _hex(s.primaryForeground), "secondary": _hex(s.secondary), "secondary-hovered": _hex(s.secondaryHovered), "secondary-foreground": _hex(s.secondaryForeground), "destructive": _hex(s.destructive), "control-fill": _hex(s.controlFill), "control-fill-hovered": _hex(s.controlFillHovered), "control-fill-focused": _hex(s.controlFillFocused), "control-border": _hex(s.controlBorder), "switch-track-inactive": _hex(s.switchTrackInactive), "border": _hex(s.border), "divider": _hex(s.divider), "panel-border": _hex(s.panelBorder), "panel-border-highlighted": _hex(s.panelBorderHighlighted), "properties-section-border": _hex(s.propertiesSectionBorder), "ring": _hex(s.ring), "chart-1": _hex(s.chart1), "chart-2": _hex(s.chart2), "chart-3": _hex(s.chart3), "chart-4": _hex(s.chart4), "chart-5": _hex(s.chart5), }; Map _density(Density d) { final t = ThemeData(colorScheme: GarageSchemes.carbon, density: d); return { "font-size": d.fontSize, "line-height": d.lineHeight, "line-box": d.lineBox, "control-height": d.controlHeight, "control-padding-x": d.buttonPaddingX, "control-padding-y": d.controlPaddingY, "control-gap": d.controlGap, "control-border-width": d.controlBorderWidth, "menu-row-height": d.menuRowHeight, "popup-row-height": d.popupRowHeight, "explorer-row-height": d.explorerRowHeight, "chrome-bar-height": d.chromeBarHeight, "property-row-height": d.propertyRowHeight, // the label + subtitle stack a SettingsRow is sized off "label-column-height": d.labelColumnHeight, "properties-section-gap": d.propertiesSectionGap, "explorer-row-base-padding": d.explorerRowBasePadding, "explorer-row-end-padding": d.explorerRowEndPadding, "list-row-indent": d.listRowIndent, "container-gap": d.containerGap, "container-padding": d.containerPadding, "gap-xxs": d.gapXxs, "gap-xs": d.gapXs, "gap-sm": d.gapSm, "gap-md": d.gapMd, "gap-lg": d.gapLg, "gap-xl": d.gapXl, "gap-xxl": d.gapXxl, "text-xxs": d.textXxs, "text-xs": d.textXs, "text-sm": d.textSm, "text-lg": d.textLg, "icon-sm": t.iconTheme.small.size ?? d.iconSize, "icon-md": t.iconTheme.medium.size ?? 20, "icon-lg": t.iconTheme.large.size ?? 24, }; } String _num(num n) => n == n.roundToDouble() ? n.round().toString() : n.toString(); void main() { test("export design tokens", () { final out = Directory("../design_system"); out.createSync(recursive: true); final t = ThemeData(colorScheme: GarageSchemes.carbon); final schemes = { "carbon": _colours(GarageSchemes.carbon), "dark": _colours(GarageSchemes.dark), "light": _colours(GarageSchemes.light), }; final densities = { "compact": _density(const Density.compact()), "normal": _density(const Density.normal()), "product": _density(const Density.product()), }; final shape = { "radius-xs": t.radiusXs, "radius-sm": t.radiusSm, "radius-md": t.radiusMd, "radius-lg": t.radiusLg, "radius-xl": t.radiusXl, "radius-xxl": t.radiusXxl, "panel-radius": t.panelRadius, "panel-gap": t.panelGap, "panel-border-width": 1.15, "property-split": 0.4, "property-label-gap": 10, "dialog-max-width": 350, }; File("${out.path}/tokens.json").writeAsStringSync( const JsonEncoder.withIndent(" ").convert({ "_generated": "by garage_ui/tool/export_design_tokens_test.dart - dont edit by hand", "fonts": {"sans": "Geist", "mono": "Geist Mono"}, "weights": {"body": 300, "control": 400, "medium": 500, "semibold": 600}, "schemes": schemes, "densities": densities, "shape": shape, }), ); final css = StringBuffer() ..writeln("/* garage_ui design tokens. Generated by garage_ui/tool/export_design_tokens_test.dart") ..writeln(" - dont edit by hand, rerun the export.") ..writeln(" Scheme: data-scheme=\"carbon\" (default) | \"dark\" | \"light\"") ..writeln(" Density: data-density=\"product\" (default) | \"normal\" | \"compact\" */") ..writeln() ..writeln("@import url(\"https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap\");") ..writeln(); void block(String selector, Map vars) { css.writeln("$selector {"); vars.forEach((k, v) => css.writeln(" --$k: $v;")); css.writeln("}"); css.writeln(); } block(":root", { "font-sans": "\"Geist\", system-ui, sans-serif", "font-mono": "\"Geist Mono\", ui-monospace, monospace", "weight-body": "300", "weight-control": "400", "weight-medium": "500", "weight-semibold": "600", for (final e in shape.entries) e.key: (e.key == "property-split") ? _num(e.value) : "${_num(e.value)}px", }); schemes.forEach((name, vars) { final sel = name == "carbon" ? ":root, [data-scheme=\"carbon\"]" : "[data-scheme=\"$name\"]"; block(sel, vars); }); // the default block has to come FIRST. `:root` and `[data-density=x]` are // the same specificity, so whichever is later wins - with product last, // quietly rendered product final densityOrder = ["product", "compact", "normal"]; for (final name in densityOrder) { final vars = densities[name]!; final sel = name == "product" ? ":root, [data-density=\"product\"]" : "[data-density=\"$name\"]"; block(sel, { for (final e in vars.entries) e.key: e.key == "line-height" ? _num(e.value) : "${_num(e.value)}px", }); } File("${out.path}/tokens.css").writeAsStringSync(css.toString()); }); }