Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/soliplex_frontend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
library;

export 'src/core/app_module.dart' show AppModule, ModuleRoutes;
export 'src/core/branding.dart' show BrandLogo, SoliplexBranding;
export 'src/core/shell.dart' show runSoliplexShell;
export 'src/core/shell_config.dart' show ShellConfig;
export 'src/interfaces/auth_state.dart'
Expand Down
91 changes: 91 additions & 0 deletions lib/src/core/branding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';

import '../design/design.dart';

/// Brand identity for a Soliplex shell: accent colors for light and dark
/// themes, a display name, and one or two logo widgets.
///
/// Whitelabel authors construct one of these and hand it to the flavor.
/// Everything in the design system except the brand-driven `primary` stays
/// Soliplex (surfaces, container tones, status colors, radii, typography) so
/// the platform identity stays legible across flavors.
@immutable
class SoliplexBranding {
const SoliplexBranding({
required this.accentLight,
required this.accentDark,
required this.appName,
required this.logoLight,
this.logoDark,
this.logoGlow,
});

/// Brand accent for the light theme. Drives `primary` and its readable
/// `onPrimary` foreground via [SoliplexColors.fromAccent]; container tones
/// and every other slot stay neutral Soliplex.
final Color accentLight;

/// Brand accent for the dark theme. Same derivation as [accentLight].
final Color accentDark;

/// Used as `MaterialApp.title` and surfaced through the auth + versions
/// modules.
final String appName;

/// Logo widget used in light mode and as the fallback in dark mode when
/// [logoDark] is not provided.
final Widget logoLight;

/// Optional dedicated dark-mode logo. When null, [BrandLogo] renders
/// [logoLight] wrapped in a [SoliplexGlow] backplate so dark-on-light
/// institutional marks stay legible against the dark surface.
final Widget? logoDark;

/// Glow color for the dark-mode fallback when [logoDark] is null. Ignored
/// when [logoDark] is provided. When null, [BrandLogo] derives a soft halo
/// from the active theme's `onSurface`.
final Color? logoGlow;

static const _soliplexLogoAsset = 'assets/branding/soliplex/logo_1024.png';
static const _soliplexLogoSize = 64.0;

/// Default Soliplex branding. The logo resolves against the running app's
/// own asset bundle (the bare asset path), so this is correct when
/// `soliplex_frontend` is the runnable app. A consumer that imports
/// `soliplex_frontend` as a library is expected to supply its own
/// [SoliplexBranding] with its own logo.
static SoliplexBranding get soliplex => SoliplexBranding(
accentLight: lightSoliplexColors.primary,
accentDark: darkSoliplexColors.primary,
appName: 'Soliplex',
logoLight: Image.asset(
_soliplexLogoAsset,
width: _soliplexLogoSize,
height: _soliplexLogoSize,
),
);
}

/// Renders the brand mark for the current theme brightness, applying a glow
/// backplate when only a single light-mode logo is provided.
class BrandLogo extends StatelessWidget {
const BrandLogo({super.key, required this.branding});

/// 8-bit alpha (~0.7 opacity) of the theme-derived fallback halo.
static const _fallbackGlowAlpha = 179;

final SoliplexBranding branding;

@override
Widget build(BuildContext context) {
final brightness = Theme.of(context).brightness;
if (brightness == Brightness.light) {
return branding.logoLight;
}
final dark = branding.logoDark;
if (dark != null) return dark;
final glow = branding.logoGlow ??
Theme.of(context).colorScheme.onSurface.withAlpha(_fallbackGlowAlpha);
return SoliplexGlow(color: glow, child: branding.logoLight);
}
}
4 changes: 3 additions & 1 deletion lib/src/core/shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ class _SoliplexShellState extends State<SoliplexShell> {
overrides: widget.config.overrides,
child: MaterialApp.router(
title: widget.config.appName,
theme: widget.config.theme,
theme: widget.config.lightTheme,
darkTheme: widget.config.darkTheme,
themeMode: widget.config.themeMode,
routerConfig: _router,
),
);
Expand Down
20 changes: 12 additions & 8 deletions lib/src/core/shell_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'router.dart';

class ShellConfig {
final String appName;
final Widget? logo;
final ThemeData theme;
final ThemeData lightTheme;
final ThemeData? darkTheme;
final ThemeMode themeMode;
final String initialRoute;
final Listenable? refreshListenable;

Expand All @@ -28,8 +29,9 @@ class ShellConfig {

ShellConfig._internal({
required this.appName,
this.logo,
required this.theme,
required this.lightTheme,
this.darkTheme,
this.themeMode = ThemeMode.system,
this.initialRoute = '/',
required List<RouteBase> routes,
required List<Override> overrides,
Expand Down Expand Up @@ -57,16 +59,18 @@ class ShellConfig {
static Future<ShellConfig> fromModules({
required List<AppModule> modules,
required String appName,
Widget? logo,
required ThemeData theme,
required ThemeData lightTheme,
ThemeData? darkTheme,
ThemeMode themeMode = ThemeMode.system,
String initialRoute = '/',
Listenable? refreshListenable,
}) async {
final coordinator = _AppModuleCoordinator(modules);
return ShellConfig._internal(
appName: appName,
logo: logo,
theme: theme,
lightTheme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
initialRoute: initialRoute,
routes: coordinator.routes,
overrides: coordinator.overrides,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/design/design.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export 'color/color_scheme_extensions.dart';
export 'effects/glow.dart';
export 'theme/markdown_theme_extension.dart';
export 'theme/theme.dart';
export 'theme/theme_extensions.dart';
export 'tokens/breakpoints.dart';
Expand Down
52 changes: 52 additions & 0 deletions lib/src/design/effects/glow.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';

/// A soft radial glow rendered *behind* [child] — a backplate for brand
/// artwork that can't be inverted for the current theme (institutional
/// logos, non-monochrome marks).
///
/// The glow is a [RadialGradient] that bleeds outside the widget's layout
/// bounds, so [SoliplexGlow] takes exactly [child]'s size and does not
/// disturb surrounding layout. The glow is brightest at the center and
/// fades to fully transparent at its rim.
class SoliplexGlow extends StatelessWidget {
const SoliplexGlow({
super.key,
required this.color,
required this.child,
this.extent = 16,
});

/// Color of the glow at its center; fades to transparent at the rim.
final Color color;

/// How far the glow radiates beyond each edge of [child], in logical
/// pixels.
final double extent;

final Widget child;

@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Positioned(
left: -extent,
right: -extent,
top: -extent,
bottom: -extent,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [color, color.withAlpha(0)],
),
),
),
),
child,
],
);
}
}
53 changes: 41 additions & 12 deletions lib/src/design/theme/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,38 @@ import '../tokens/colors.dart';
import '../tokens/radii.dart';
import '../tokens/spacing.dart';
import '../tokens/typography.dart';
import 'markdown_theme_extension.dart';
import 'theme_extensions.dart';

ThemeData soliplexLightTheme({SoliplexColors colors = lightSoliplexColors}) {
ThemeData soliplexLightTheme({SoliplexColors colors = lightSoliplexColors}) =>
_buildTheme(colors: colors, brightness: Brightness.light);

ThemeData soliplexDarkTheme({SoliplexColors colors = darkSoliplexColors}) =>
_buildTheme(colors: colors, brightness: Brightness.dark);

ThemeData _buildTheme({
required SoliplexColors colors,
required Brightness brightness,
}) {
final textTheme = soliplexTextTheme(colors);
final colorScheme = ColorScheme(
brightness: Brightness.light,
// Primary
brightness: brightness,
primary: colors.primary,
onPrimary: colors.onPrimary,
primaryContainer: colors.primaryContainer,
onPrimaryContainer: colors.onPrimaryContainer,
// Secondary
secondary: colors.secondary,
onSecondary: colors.onSecondary,
secondaryContainer: colors.muted,
onSecondaryContainer: colors.mutedForeground,
// Tertiary
tertiary: colors.tertiary,
onTertiary: colors.onTertiary,
tertiaryContainer: colors.tertiaryContainer,
onTertiaryContainer: colors.onTertiaryContainer,
// Error
error: colors.destructive,
onError: colors.onDestructive,
errorContainer: colors.errorContainer,
onErrorContainer: colors.onErrorContainer,
// Surface
surface: colors.background,
onSurface: colors.foreground,
onSurfaceVariant: colors.mutedForeground,
Expand All @@ -41,21 +46,18 @@ ThemeData soliplexLightTheme({SoliplexColors colors = lightSoliplexColors}) {
surfaceContainerHighest: colors.surfaceContainerHighest,
surfaceDim: colors.accent,
surfaceBright: colors.background,
// Outline
outline: colors.outline,
outlineVariant: colors.outlineVariant,
// Inverse
inverseSurface: colors.primary,
onInverseSurface: colors.onPrimary,
inversePrimary: colors.inversePrimary,
// Utility
shadow: const Color(0xFF000000),
scrim: const Color(0xFF000000),
surfaceTint: colors.primary,
);

return ThemeData(
brightness: Brightness.light,
brightness: brightness,
colorScheme: colorScheme,
appBarTheme: AppBarTheme(
backgroundColor: colors.onPrimary,
Expand Down Expand Up @@ -177,7 +179,7 @@ ThemeData soliplexLightTheme({SoliplexColors colors = lightSoliplexColors}) {
vertical: SoliplexSpacing.s1,
),
secondarySelectedColor: colors.primary.withAlpha(25),
brightness: Brightness.light,
brightness: brightness,
),
checkboxTheme: CheckboxThemeData(
shape: RoundedRectangleBorder(
Expand Down Expand Up @@ -228,6 +230,33 @@ ThemeData soliplexLightTheme({SoliplexColors colors = lightSoliplexColors}) {
),
),
),
MarkdownThemeExtension(
h1: textTheme.titleLarge,
h2: textTheme.titleMedium,
h3: textTheme.titleSmall,
body: textTheme.bodyMedium,
code: textTheme.bodyMedium?.copyWith(
backgroundColor: colorScheme.surfaceContainerHighest,
),
link: TextStyle(
color: colors.link,
decoration: TextDecoration.underline,
decorationColor: colors.link,
),
codeBlockDecoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(soliplexRadii.md),
),
blockquoteDecoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
border: Border(
left: BorderSide(
color: colorScheme.outlineVariant,
width: 3,
),
),
),
),
],
);
}
Loading
Loading