Rebrand CommGate to gsmnode and adopt new design system
Rename the whole project from CommGate to gsmnode and re-skin every surface onto the new signal-green + ink design system (Space Grotesk / IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark, lowercase gsm+node wordmark). - Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme, new logo/favicon assets; both builds verified. - Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive launcher icons; rename Android applicationId/package to app.gsmnode.phone; bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified. - API Server (Go): panel routes, User-Agent, health service id, branding. - Home Assistant Plugin: rename integration domain sms_gateway to gsmnode (folder, DOMAIN, services, entity ids, classes); py_compile verified. - Update all READMEs; add .gitignore (excludes Design/, build artifacts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// gsmnode design tokens (see Design/SMS Gateway logo design/tokens in the
|
||||
/// project root). Brand is signal-green #2E9E6B on an ink/paper cool scale.
|
||||
abstract class GsmColors {
|
||||
// Signal green (brand) ramp
|
||||
static const green100 = Color(0xFFE4F4EC);
|
||||
static const green200 = Color(0xFFBEE6D0);
|
||||
static const green300 = Color(0xFF6BC79A); // lifted text on dark
|
||||
static const green500 = Color(0xFF2E9E6B); // primary
|
||||
static const green600 = Color(0xFF278A5C); // hover
|
||||
static const green700 = Color(0xFF1F6E49); // active
|
||||
static const greenOnDark = Color(0xFF4FB985); // accent on ink surfaces
|
||||
|
||||
// Ink & paper
|
||||
static const ink = Color(0xFF12161C); // primary text / dark surface
|
||||
static const ink900 = Color(0xFF0A0D11); // deepest surface
|
||||
static const ink800 = Color(0xFF1A1F27);
|
||||
static const paper = Color(0xFFFAFAF9); // page bg
|
||||
|
||||
// Cool gray ramp
|
||||
static const gray50 = Color(0xFFF4F5F4);
|
||||
static const gray100 = Color(0xFFE9EBEA);
|
||||
static const gray200 = Color(0xFFDCDFDE); // subtle border
|
||||
static const gray300 = Color(0xFFC2C7C6); // strong border
|
||||
static const gray400 = Color(0xFF9AA0A2); // muted text
|
||||
static const gray500 = Color(0xFF6B7278);
|
||||
static const gray600 = Color(0xFF4A5157);
|
||||
static const gray700 = Color(0xFF333A40); // secondary text
|
||||
|
||||
// Semantic (light)
|
||||
static const warning = Color(0xFFC68A2E);
|
||||
static const warningTint = Color(0xFFF6EAD6);
|
||||
static const danger = Color(0xFFC64A3E);
|
||||
static const dangerTint = Color(0xFFF6E1DC);
|
||||
|
||||
// Dark surfaces
|
||||
static const dBase = Color(0xFF0A0D11);
|
||||
static const dSunken = Color(0xFF0E1216);
|
||||
static const dCard = Color(0xFF14191F);
|
||||
static const dRaised = Color(0xFF1A2027);
|
||||
static const dBorderSubtle = Color(0xFF262B33);
|
||||
static const dBorderStrong = Color(0xFF363C46);
|
||||
static const dTextPrimary = Color(0xFFF5F6F4);
|
||||
static const dTextSecondary = Color(0xFFC2C7C6);
|
||||
static const dTextMuted = Color(0xFF9AA0A2);
|
||||
|
||||
// Semantic (dark — lifted for contrast)
|
||||
static const dWarning = Color(0xFFE0A84E);
|
||||
static const dDanger = Color(0xFFE0655B);
|
||||
}
|
||||
|
||||
/// Semantic role colors that flip between light and dark, exposed as a
|
||||
/// ThemeExtension so screens theme through roles, not raw ramp values.
|
||||
class GsmSemantic extends ThemeExtension<GsmSemantic> {
|
||||
const GsmSemantic({
|
||||
required this.pageBg,
|
||||
required this.sunkenBg,
|
||||
required this.card,
|
||||
required this.borderSubtle,
|
||||
required this.borderStrong,
|
||||
required this.textPrimary,
|
||||
required this.textSecondary,
|
||||
required this.textMuted,
|
||||
required this.brandTint,
|
||||
required this.success,
|
||||
required this.successTint,
|
||||
required this.warning,
|
||||
required this.warningTint,
|
||||
required this.danger,
|
||||
required this.dangerTint,
|
||||
});
|
||||
|
||||
final Color pageBg;
|
||||
final Color sunkenBg;
|
||||
final Color card;
|
||||
final Color borderSubtle;
|
||||
final Color borderStrong;
|
||||
final Color textPrimary;
|
||||
final Color textSecondary;
|
||||
final Color textMuted;
|
||||
final Color brandTint;
|
||||
final Color success;
|
||||
final Color successTint;
|
||||
final Color warning;
|
||||
final Color warningTint;
|
||||
final Color danger;
|
||||
final Color dangerTint;
|
||||
|
||||
static const light = GsmSemantic(
|
||||
pageBg: GsmColors.paper,
|
||||
sunkenBg: GsmColors.gray50,
|
||||
card: Colors.white,
|
||||
borderSubtle: GsmColors.gray200,
|
||||
borderStrong: GsmColors.gray300,
|
||||
textPrimary: GsmColors.ink,
|
||||
textSecondary: GsmColors.gray700,
|
||||
textMuted: GsmColors.gray500,
|
||||
brandTint: GsmColors.green100,
|
||||
success: GsmColors.green500,
|
||||
successTint: GsmColors.green100,
|
||||
warning: GsmColors.warning,
|
||||
warningTint: GsmColors.warningTint,
|
||||
danger: GsmColors.danger,
|
||||
dangerTint: GsmColors.dangerTint,
|
||||
);
|
||||
|
||||
static const dark = GsmSemantic(
|
||||
pageBg: GsmColors.dBase,
|
||||
sunkenBg: GsmColors.dSunken,
|
||||
card: GsmColors.dCard,
|
||||
borderSubtle: GsmColors.dBorderSubtle,
|
||||
borderStrong: GsmColors.dBorderStrong,
|
||||
textPrimary: GsmColors.dTextPrimary,
|
||||
textSecondary: GsmColors.dTextSecondary,
|
||||
textMuted: GsmColors.dTextMuted,
|
||||
brandTint: Color(0x292E9E6B), // green-500 @ 16%
|
||||
success: GsmColors.green300,
|
||||
successTint: Color(0x2E2E9E6B),
|
||||
warning: GsmColors.dWarning,
|
||||
warningTint: Color(0x2EC68A2E),
|
||||
danger: GsmColors.dDanger,
|
||||
dangerTint: Color(0x2EC64A3E),
|
||||
);
|
||||
|
||||
@override
|
||||
GsmSemantic copyWith({Color? pageBg}) => this;
|
||||
|
||||
@override
|
||||
GsmSemantic lerp(ThemeExtension<GsmSemantic>? other, double t) => this;
|
||||
}
|
||||
|
||||
extension GsmThemeX on BuildContext {
|
||||
GsmSemantic get cg => Theme.of(this).extension<GsmSemantic>()!;
|
||||
}
|
||||
|
||||
/// A mono (JetBrains Mono) style for IDs, numbers, timestamps and eyebrows.
|
||||
TextStyle gsmMono({
|
||||
double size = 12,
|
||||
Color? color,
|
||||
FontWeight weight = FontWeight.w500,
|
||||
double letterSpacing = 0,
|
||||
}) =>
|
||||
GoogleFonts.jetBrainsMono(
|
||||
fontSize: size,
|
||||
color: color,
|
||||
fontWeight: weight,
|
||||
letterSpacing: letterSpacing,
|
||||
);
|
||||
|
||||
/// Display (Space Grotesk) style for titles & metrics.
|
||||
TextStyle gsmDisplay({
|
||||
double size = 20,
|
||||
Color? color,
|
||||
FontWeight weight = FontWeight.w700,
|
||||
}) =>
|
||||
GoogleFonts.spaceGrotesk(
|
||||
fontSize: size,
|
||||
color: color,
|
||||
fontWeight: weight,
|
||||
letterSpacing: -0.02 * size,
|
||||
);
|
||||
|
||||
ThemeData _base(Brightness brightness, GsmSemantic s) {
|
||||
final isDark = brightness == Brightness.dark;
|
||||
final scheme = ColorScheme(
|
||||
brightness: brightness,
|
||||
primary: GsmColors.green500,
|
||||
onPrimary: Colors.white,
|
||||
secondary: isDark ? GsmColors.greenOnDark : GsmColors.green600,
|
||||
onSecondary: Colors.white,
|
||||
error: s.danger,
|
||||
onError: Colors.white,
|
||||
surface: s.card,
|
||||
onSurface: s.textPrimary,
|
||||
outline: s.borderStrong,
|
||||
outlineVariant: s.borderSubtle,
|
||||
surfaceContainerLowest: s.sunkenBg,
|
||||
surfaceContainerLow: s.pageBg,
|
||||
surfaceContainer: s.card,
|
||||
);
|
||||
|
||||
// Body & UI in IBM Plex Sans; titles/metrics reach for Space Grotesk.
|
||||
final textTheme = GoogleFonts.ibmPlexSansTextTheme(
|
||||
(isDark ? ThemeData.dark() : ThemeData.light()).textTheme,
|
||||
).apply(bodyColor: s.textPrimary, displayColor: s.textPrimary);
|
||||
|
||||
const controlRadius = BorderRadius.all(Radius.circular(8));
|
||||
const cardRadius = BorderRadius.all(Radius.circular(18));
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: s.pageBg,
|
||||
textTheme: textTheme,
|
||||
extensions: [s],
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: s.card,
|
||||
foregroundColor: s.textPrimary,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
shape: Border(bottom: BorderSide(color: s.borderSubtle)),
|
||||
titleTextStyle: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: s.textPrimary,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: s.card,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: cardRadius,
|
||||
side: BorderSide(color: s.borderSubtle),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: GsmColors.green500,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: const RoundedRectangleBorder(borderRadius: controlRadius),
|
||||
textStyle: GoogleFonts.ibmPlexSans(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return GsmColors.green500.withValues(alpha: 0.45);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) return GsmColors.green700;
|
||||
if (states.contains(WidgetState.hovered)) return GsmColors.green600;
|
||||
return GsmColors.green500;
|
||||
}),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: s.textPrimary,
|
||||
side: BorderSide(color: s.borderStrong),
|
||||
shape: const RoundedRectangleBorder(borderRadius: controlRadius),
|
||||
textStyle: GoogleFonts.ibmPlexSans(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: isDark ? GsmColors.greenOnDark : GsmColors.green600,
|
||||
textStyle: GoogleFonts.ibmPlexSans(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: s.card,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: controlRadius,
|
||||
borderSide: BorderSide(color: s.borderStrong),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: controlRadius,
|
||||
borderSide: BorderSide(color: s.borderStrong),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: controlRadius,
|
||||
borderSide: BorderSide(color: GsmColors.green500, width: 2),
|
||||
),
|
||||
labelStyle: TextStyle(color: s.textSecondary),
|
||||
hintStyle: TextStyle(color: s.textMuted),
|
||||
),
|
||||
dividerTheme: DividerThemeData(color: s.borderSubtle, thickness: 1),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: isDark ? GsmColors.dRaised : GsmColors.ink,
|
||||
contentTextStyle: GoogleFonts.ibmPlexSans(color: Colors.white),
|
||||
shape: const RoundedRectangleBorder(borderRadius: controlRadius),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData gsmnodeLightTheme() => _base(Brightness.light, GsmSemantic.light);
|
||||
ThemeData gsmnodeDarkTheme() => _base(Brightness.dark, GsmSemantic.dark);
|
||||
Reference in New Issue
Block a user