Files
Garage-SDKs/garage_ui/tool/export_design_tokens_test.dart
ImBenjiandClaude Opus 5.5 b5e5ed5b75 Add a design system for Claude Design, generated from garage_ui
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
2026-09-23 23:19:02 +01:00

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());
});
}