Tokens come out of garage_ui/tool/export_design_tokens_test.dart, the components are CSS on top of them, and the cards cover foundations, components and whole hub and Arcs & Angles screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
198 lines
7.3 KiB
Dart
198 lines
7.3 KiB
Dart
// 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<String, String> _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<String, num> _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<String, String> 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,
|
|
// <html data-density="compact"> 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());
|
|
});
|
|
}
|