mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(mobile): restore channel icons and align chat colors with desktop (#8478)
## Summary - Restores `#` and lock icons for public and private channels on Home, with matching loading placeholders. - Updates the dark-mode navigation colors to the desktop design-system tokens (read and unread text, section headings). - Replaces the chat color picker with the seven desktop accents (Gray, Chartreuse, Flame, Magenta, Iris, Cobalt, Jade). Saved legacy colors map to the closest one. - Uses white text in your own bubbles, and black where white falls below 4.5:1 (Chartreuse, dark-mode Gray). Links (underlined), quotes, tables and code use the same text color. - Insets code blocks in bubbles evenly, with corners concentric to the bubble and even inner padding. - The swipe-to-reply indicator uses the chosen chat color. ## Testing - `flutter analyze` and `dart format` are clean. The full mobile suite passes (3,705 tests). - Installed and checked on a Pixel 10 and an iPhone Air against a real community. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Queue <874bf2d815c2c4b459b90438c669c57f2702f3a76bb6e16fa97184259aac3e9e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,8 +27,8 @@ class _ChannelTile extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final contentColor = isMuted
|
||||
? navigationSecondaryForeground(context)
|
||||
: navigationPrimaryForeground(context);
|
||||
? navigationMetadataForeground(context)
|
||||
: channelNavigationForeground(context);
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(Radii.md),
|
||||
@@ -55,6 +55,22 @@ class _ChannelTile extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _kChannelLabelGap),
|
||||
] else ...[
|
||||
SizedBox(
|
||||
width: _kChannelLeadingWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Icon(
|
||||
channel.visibility == 'private'
|
||||
? BuzzIcons.lock
|
||||
: BuzzIcons.hash,
|
||||
key: ValueKey('channel-icon-${channel.id}'),
|
||||
size: _kChannelIconSize,
|
||||
color: contentColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _kChannelLabelGap),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -73,7 +89,12 @@ class _ChannelTile extends ConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: contentListTitleTextStyle.copyWith(
|
||||
color: contentColor,
|
||||
color: isMuted
|
||||
? contentColor
|
||||
: channelNavigationForeground(
|
||||
context,
|
||||
unread: isUnread,
|
||||
),
|
||||
fontWeight: isUnread ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
@@ -92,7 +113,7 @@ class _ChannelTile extends ConsumerWidget {
|
||||
child: Icon(
|
||||
BuzzIcons.bellOff,
|
||||
size: 12,
|
||||
color: context.colors.onSurface.withValues(alpha: 0.4),
|
||||
color: navigationMetadataForeground(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -115,9 +115,7 @@ class _CustomSectionHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sectionColor = context.colors.brightness == Brightness.dark
|
||||
? const Color(0xFF595959)
|
||||
: const Color(0xFF959595);
|
||||
final sectionColor = navigationMetadataForeground(context);
|
||||
return GestureDetector(
|
||||
onTap: onToggle,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -144,7 +142,7 @@ class _CustomSectionHeader extends StatelessWidget {
|
||||
icon: Icon(
|
||||
BuzzIcons.ellipsisVertical,
|
||||
size: _kChannelIconSize,
|
||||
color: sectionColor,
|
||||
color: navigationSecondaryForeground(context),
|
||||
),
|
||||
onPressed: () async {
|
||||
final value = await showAnchoredPopover<String>(
|
||||
@@ -468,9 +466,7 @@ class _SectionHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sectionColor = context.colors.brightness == Brightness.dark
|
||||
? const Color(0xFF595959)
|
||||
: const Color(0xFF959595);
|
||||
final sectionColor = navigationMetadataForeground(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onToggle,
|
||||
@@ -499,7 +495,7 @@ class _SectionHeader extends StatelessWidget {
|
||||
icon: Icon(
|
||||
BuzzIcons.ellipsisVertical,
|
||||
size: _kChannelIconSize,
|
||||
color: sectionColor,
|
||||
color: navigationSecondaryForeground(context),
|
||||
),
|
||||
onPressed: () async {
|
||||
final value = await showAnchoredPopover<String>(
|
||||
@@ -551,7 +547,10 @@ class _SectionLabel extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.quarter),
|
||||
_SectionChevron(expanded: expanded, color: color),
|
||||
_SectionChevron(
|
||||
expanded: expanded,
|
||||
color: navigationSecondaryForeground(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ class _ChannelSkeletonSection extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: _kChannelLeadingWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SkeletonBar(
|
||||
width: _kChannelIconSize,
|
||||
height: _kChannelIconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _kChannelLabelGap),
|
||||
SkeletonBar(
|
||||
key: Key('channels-skeleton-row-label-$index'),
|
||||
width: widths[index],
|
||||
|
||||
@@ -200,12 +200,9 @@ class MessageContent extends HookConsumerWidget {
|
||||
final markdownTheme = GptMarkdownTheme.of(context);
|
||||
final chatColors = ChatColors.of(context);
|
||||
final ownTextColor = chatColors.foreground;
|
||||
// Desktop uses blue links on neutral bubbles and the bubble ink on colors.
|
||||
final linkColor = ownMessage && ownTextColor == Colors.white
|
||||
? ownTextColor
|
||||
: context.colors.brightness == Brightness.dark
|
||||
? const Color(0xFF83C4FF)
|
||||
: const Color(0xFF0B5FA8);
|
||||
// Own-bubble links share the bubble ink; markdown marks them by underline.
|
||||
final linkColor = ownMessage ? ownTextColor : context.colors.tertiary;
|
||||
final bubbleInk = ownMessage ? ownTextColor : context.colors.onSurface;
|
||||
final baseTextStyle =
|
||||
(baseStyle ??
|
||||
context.textTheme.bodyMedium?.copyWith(
|
||||
@@ -361,12 +358,27 @@ class MessageContent extends HookConsumerWidget {
|
||||
child: GptMarkdown(
|
||||
text,
|
||||
style: style,
|
||||
styleSheet: bubble
|
||||
? GptMarkdownStyleSheet(
|
||||
blockQuote: BlockQuoteStyle(barColor: bubbleInk),
|
||||
hr: HrStyle(color: bubbleInk.withValues(alpha: 0.3)),
|
||||
table: TableStyle(
|
||||
borderColor: bubbleInk.withValues(alpha: 0.3),
|
||||
headerBackground: Colors.transparent,
|
||||
headerTextStyle: TextStyle(color: bubbleInk),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
followLinkColor: false,
|
||||
// normalizeBareLinks() already turns bare URLs into Markdown links;
|
||||
// gpt_markdown 1.2.0 autolinks by default, so both would run.
|
||||
autolink: false,
|
||||
codeBuilder: (context, name, code, closed) =>
|
||||
_MessageCodeBlock(name: name, code: code),
|
||||
codeBuilder: (context, name, code, closed) => _MessageCodeBlock(
|
||||
name: name,
|
||||
code: code,
|
||||
bubble: bubble,
|
||||
ink: bubble && ownMessage ? ownTextColor : null,
|
||||
),
|
||||
linkBuilder: (context, linkText, url, linkStyle) => _buildLink(
|
||||
context,
|
||||
ref,
|
||||
@@ -426,7 +438,7 @@ class MessageContent extends HookConsumerWidget {
|
||||
child: LocalSendBubbleSurface(
|
||||
color: ownMessage
|
||||
? chatColors.outgoing
|
||||
: context.colors.secondaryContainer,
|
||||
: receivedMessageSurface(context.colors.brightness),
|
||||
shape: const _MessageBubbleShape(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@@ -445,6 +457,21 @@ class MessageContent extends HookConsumerWidget {
|
||||
),
|
||||
child: GptMarkdownTheme(
|
||||
gptThemeData: markdownTheme.copyWith(
|
||||
linkColor: linkColor,
|
||||
// A neutral chip would put white ink on a light
|
||||
// fill; tint the accent instead.
|
||||
inlineCode: ownMessage
|
||||
? markdownTheme.inlineCode.copyWith(
|
||||
color: ownTextColor,
|
||||
backgroundColor: chatCodeTint(
|
||||
ownTextColor,
|
||||
0.18,
|
||||
),
|
||||
borderColor: ownTextColor.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
h1: markdownTheme.h1?.copyWith(
|
||||
color: ownMessage
|
||||
? ownTextColor
|
||||
@@ -869,7 +896,18 @@ class _MessageCodeBlock extends HookWidget {
|
||||
final String name;
|
||||
final String code;
|
||||
|
||||
const _MessageCodeBlock({required this.name, required this.code});
|
||||
/// Whether the block sits inside a message bubble.
|
||||
final bool bubble;
|
||||
|
||||
/// Text color on an accent bubble; null keeps the neutral highlighted card.
|
||||
final Color? ink;
|
||||
|
||||
const _MessageCodeBlock({
|
||||
required this.name,
|
||||
required this.code,
|
||||
this.bubble = false,
|
||||
this.ink,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -884,83 +922,101 @@ class _MessageCodeBlock extends HookWidget {
|
||||
});
|
||||
}
|
||||
|
||||
final ink = this.ink;
|
||||
// A fence's closing newline would otherwise render as a blank last line.
|
||||
final shownCode = code.trimRight();
|
||||
final codeBaseStyle = TextStyle(
|
||||
fontFamily: CodeStyle.fontFamily,
|
||||
fontSize: CodeStyle.fontSize,
|
||||
height: CodeStyle.lineHeight,
|
||||
color: context.colors.onSurface,
|
||||
color: ink ?? context.colors.onSurface,
|
||||
);
|
||||
final isDark = context.theme.brightness == Brightness.dark;
|
||||
final codeTheme = isDark ? highlightDarkTheme : highlightLightTheme;
|
||||
// Syntax colors are tuned for neutral cards and lose contrast on accents.
|
||||
final codeSpans = useMemoized(
|
||||
() => highlightCode(code, name, codeTheme, codeBaseStyle),
|
||||
[code, name, isDark],
|
||||
() => ink == null
|
||||
? highlightCode(shownCode, name, codeTheme, codeBaseStyle)
|
||||
: [TextSpan(text: shownCode)],
|
||||
[code, name, isDark, ink],
|
||||
);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: Grid.half),
|
||||
// In a bubble, the vertical margin brings the bubble's 8px vertical
|
||||
// padding up to its 16px side padding, so the block is inset evenly and
|
||||
// its corners stay concentric with the bubble's.
|
||||
margin: bubble
|
||||
? const EdgeInsets.symmetric(vertical: Grid.xs - Grid.xxs)
|
||||
: const EdgeInsets.only(top: Grid.half),
|
||||
decoration: BoxDecoration(
|
||||
color: CodeStyle.background(context.colors),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: CodeStyle.border(context.colors)),
|
||||
color: ink == null
|
||||
? CodeStyle.background(context.colors)
|
||||
: chatCodeTint(ink, 0.14),
|
||||
borderRadius: BorderRadius.circular(
|
||||
bubble ? Radii.container - Grid.xs : 12,
|
||||
),
|
||||
border: Border.all(
|
||||
color:
|
||||
ink?.withValues(alpha: 0.24) ?? CodeStyle.border(context.colors),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (name.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Grid.twelve,
|
||||
top: Grid.half + Grid.quarter,
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Even 12px insets; the right side also reserves the copy button.
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Grid.twelve,
|
||||
Grid.twelve,
|
||||
Grid.twelve + 28,
|
||||
Grid.twelve,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Grid.twelve,
|
||||
name.isEmpty ? Grid.half + Grid.quarter : Grid.quarter,
|
||||
44,
|
||||
Grid.half + Grid.quarter,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (name.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: Grid.half),
|
||||
child: Text(
|
||||
name,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: ink ?? context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: RichText(
|
||||
softWrap: false,
|
||||
text: TextSpan(style: codeBaseStyle, children: codeSpans),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: Grid.quarter,
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: IconButton(
|
||||
onPressed: handleCopy,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
icon: Icon(
|
||||
isCopied.value ? BuzzIcons.check : BuzzIcons.copy,
|
||||
size: 14,
|
||||
color: isCopied.value
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: Grid.half + Grid.quarter,
|
||||
right: Grid.half + Grid.quarter,
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: IconButton(
|
||||
onPressed: handleCopy,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
icon: Icon(
|
||||
isCopied.value ? BuzzIcons.check : BuzzIcons.copy,
|
||||
size: 14,
|
||||
color:
|
||||
ink ??
|
||||
(isCopied.value
|
||||
? context.colors.primary
|
||||
: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
: context.colors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -139,11 +139,10 @@ class MessageSwipeReply extends HookConsumerWidget {
|
||||
child: child,
|
||||
builder: (context, child) {
|
||||
final progress = (offset.value / 72).clamp(0.0, 1.0);
|
||||
final accent = context.appColors.accent;
|
||||
final filledForeground =
|
||||
ThemeData.estimateBrightnessForColor(accent) == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
// Follows the chosen message color, like the user's own bubbles.
|
||||
final chatColors = ChatColors.of(context);
|
||||
final accent = chatColors.outgoing;
|
||||
final filledForeground = chatColors.foreground;
|
||||
return Stack(
|
||||
key: layoutKey,
|
||||
clipBehavior: Clip.none,
|
||||
|
||||
@@ -79,6 +79,12 @@ const accentColors = [
|
||||
dark: Color(0xFF818CF8),
|
||||
wireValue: '#6366f1',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Chartreuse',
|
||||
light: Color(0xFFD7D72E),
|
||||
dark: Color(0xFFD7D72E),
|
||||
wireValue: '#d7d72e',
|
||||
),
|
||||
];
|
||||
|
||||
Color accentColorForScheme(ColorScheme scheme, int accentIndex) {
|
||||
|
||||
@@ -100,3 +100,17 @@ LinearGradient? buzzTopSectionGradient(
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Desktop navigation ink distinguishes read destinations from unread labels.
|
||||
Color channelNavigationForeground(BuildContext context, {bool unread = false}) {
|
||||
if (unread || Theme.of(context).brightness != Brightness.dark) {
|
||||
return navigationPrimaryForeground(context);
|
||||
}
|
||||
return Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
|
||||
/// Metadata ink used by Home section headings and muted destinations.
|
||||
Color navigationMetadataForeground(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFFA4A4A4)
|
||||
: const Color(0xFF5F5F5F);
|
||||
|
||||
@@ -2,59 +2,82 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'accent_colors.dart';
|
||||
|
||||
/// Desktop message palette, in picker order. Wire values remain compatible
|
||||
/// with existing community preferences; fills match buzz-app bubble tokens.
|
||||
/// Desktop accent families in picker order. Fills are accent-9 from buzz-app's
|
||||
/// design-system tokens (4ad00a8a).
|
||||
///
|
||||
/// Wire values are desktop's v1 `community-theme` accents for the same color
|
||||
/// family, so a synced preference means the same color on every client. The
|
||||
/// cross-client mapping lives in `test-fixtures/community-theme-accents.json`.
|
||||
const chatColorChoices = [
|
||||
AccentColor(
|
||||
name: 'Neutral',
|
||||
light: Color(0xFFE9E9EA),
|
||||
dark: Color(0xFF2E2E2E),
|
||||
name: 'Gray',
|
||||
light: Color(0xFF525252),
|
||||
dark: Color(0xFFC7C7C7),
|
||||
wireValue: 'neutral',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Blue',
|
||||
light: Color(0xFF0077DB),
|
||||
dark: Color(0xFF0077DB),
|
||||
wireValue: '#3b82f6',
|
||||
name: 'Chartreuse',
|
||||
light: Color(0xFFD7D72E),
|
||||
dark: Color(0xFFD7D72E),
|
||||
wireValue: '#d7d72e',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Green',
|
||||
light: Color(0xFF008934),
|
||||
dark: Color(0xFF008934),
|
||||
wireValue: '#22c55e',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Orange',
|
||||
light: Color(0xFFC95100),
|
||||
dark: Color(0xFFC95100),
|
||||
name: 'Flame',
|
||||
light: Color(0xFFD93A0C),
|
||||
dark: Color(0xFFD93A0C),
|
||||
wireValue: '#f97316',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Pink',
|
||||
light: Color(0xFFD33785),
|
||||
dark: Color(0xFFD33785),
|
||||
name: 'Magenta',
|
||||
light: Color(0xFFCC1F7A),
|
||||
dark: Color(0xFFCC1F7A),
|
||||
wireValue: '#ec4899',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Lilac',
|
||||
light: Color(0xFFA94AD8),
|
||||
dark: Color(0xFFA94AD8),
|
||||
name: 'Iris',
|
||||
light: Color(0xFF6C4EF0),
|
||||
dark: Color(0xFF6C4EF0),
|
||||
wireValue: '#c0a2f1',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Cobalt',
|
||||
light: Color(0xFF2B4FE0),
|
||||
dark: Color(0xFF2B4FE0),
|
||||
wireValue: '#3b82f6',
|
||||
),
|
||||
AccentColor(
|
||||
name: 'Jade',
|
||||
light: Color(0xFF0B8467),
|
||||
dark: Color(0xFF0B8467),
|
||||
wireValue: '#22c55e',
|
||||
),
|
||||
];
|
||||
|
||||
/// Resolves old community colors using desktop's retired-color aliases.
|
||||
/// Resolves a v1 accent: each choice's own value, desktop's remaining legacy
|
||||
/// values via their closest family, and family names written by earlier
|
||||
/// preview builds. Anything else falls back to Chartreuse.
|
||||
int chatColorIndex(String wireValue) {
|
||||
final normalized = switch (wireValue) {
|
||||
'#06b6d4' || '#6366f1' => '#3b82f6',
|
||||
'#06b6d4' => '#22c55e',
|
||||
'#6366f1' => '#3b82f6',
|
||||
'#ef4444' => '#ec4899',
|
||||
'#a855f7' => '#c0a2f1',
|
||||
_ => wireValue,
|
||||
};
|
||||
final index = chatColorChoices.indexWhere(
|
||||
(color) => color.wireValue == normalized,
|
||||
(color) =>
|
||||
color.wireValue == normalized || color.name.toLowerCase() == normalized,
|
||||
);
|
||||
return index < 0 ? 0 : index;
|
||||
return index < 0 ? 1 : index;
|
||||
}
|
||||
|
||||
/// Desktop-compatible value for a family name written by earlier preview
|
||||
/// builds, so those preferences sync again once republished.
|
||||
String? previewFamilyWireValue(String value) {
|
||||
for (final color in chatColorChoices) {
|
||||
if (color.name.toLowerCase() == value) return color.wireValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resolves a picker swatch for the active appearance mode.
|
||||
@@ -62,3 +85,23 @@ Color chatColorForScheme(ColorScheme scheme, int index) {
|
||||
final color = chatColorChoices[index];
|
||||
return scheme.brightness == Brightness.dark ? color.dark : color.light;
|
||||
}
|
||||
|
||||
/// Bubble text is white unless white falls below 4.5:1 on the fill, as on
|
||||
/// Chartreuse and dark-mode Gray; those light fills take black instead.
|
||||
Color chatForegroundForScheme(ColorScheme scheme, int index) {
|
||||
final fill = chatColorForScheme(scheme, index).computeLuminance();
|
||||
return 1.05 / (fill + .05) >= 4.5 ? Colors.white : Colors.black;
|
||||
}
|
||||
|
||||
/// Translucent code surface behind [ink] on an accent bubble. It shades toward
|
||||
/// the opposite of the ink, so it can only raise contrast over the fill.
|
||||
Color chatCodeTint(Color ink, double opacity) =>
|
||||
(ink == Colors.white ? Colors.black : Colors.white).withValues(
|
||||
alpha: opacity,
|
||||
);
|
||||
|
||||
/// Received messages use their own surface rather than a control fill.
|
||||
Color receivedMessageSurface(Brightness brightness) =>
|
||||
brightness == Brightness.dark
|
||||
? const Color(0xFF292929)
|
||||
: const Color(0xFFF3F3F4);
|
||||
|
||||
@@ -25,8 +25,7 @@ abstract final class CodeStyle {
|
||||
static final fontSizeFactor = fontSize / messageBodyTextStyle.fontSize!;
|
||||
|
||||
/// Fill behind a code surface.
|
||||
static Color background(ColorScheme scheme) =>
|
||||
scheme.surfaceContainerHighest.withValues(alpha: 0.6);
|
||||
static Color background(ColorScheme scheme) => scheme.surfaceContainerHighest;
|
||||
|
||||
/// Outline around a code surface.
|
||||
static Color border(ColorScheme scheme) =>
|
||||
|
||||
@@ -14,7 +14,7 @@ const lightColorScheme = ColorScheme(
|
||||
onSecondary: Color(0xFFFFFFFF),
|
||||
secondaryContainer: Color(0xFFF0F0F0),
|
||||
onSecondaryContainer: Color(0xFF0A0A0A),
|
||||
tertiary: Color(0xFF0D74CE),
|
||||
tertiary: Color(0xFF0B5FA8),
|
||||
onTertiary: Color(0xFFFFFFFF),
|
||||
tertiaryContainer: Color(0xFFE6F4FE),
|
||||
onTertiaryContainer: Color(0xFF113264),
|
||||
@@ -42,37 +42,37 @@ const lightColorScheme = ColorScheme(
|
||||
|
||||
const darkColorScheme = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: Color(0xFFE5E5E5),
|
||||
onPrimary: Color(0xFF0F0F0F),
|
||||
primaryContainer: Color(0xFF2A2A2A),
|
||||
onPrimaryContainer: Color(0xFFF5F5F5),
|
||||
secondary: Color(0xFFC1C1C1),
|
||||
onSecondary: Color(0xFF0F0F0F),
|
||||
secondaryContainer: Color(0xFF2A2A2A),
|
||||
onSecondaryContainer: Color(0xFFF5F5F5),
|
||||
tertiary: Color(0xFF70B8FF),
|
||||
onTertiary: Color(0xFF0F0F0F),
|
||||
primary: Color(0xFFFFFFFF),
|
||||
onPrimary: Color(0xFF000000),
|
||||
primaryContainer: Color(0xFF282828),
|
||||
onPrimaryContainer: Color(0xFFFFFFFF),
|
||||
secondary: Color(0xFFC7C7C7),
|
||||
onSecondary: Color(0xFF000000),
|
||||
secondaryContainer: Color(0xFF282828),
|
||||
onSecondaryContainer: Color(0xFFFFFFFF),
|
||||
tertiary: Color(0xFF83C4FF),
|
||||
onTertiary: Color(0xFF000000),
|
||||
tertiaryContainer: Color(0xFF0D2847),
|
||||
onTertiaryContainer: Color(0xFFC2E6FF),
|
||||
error: Color(0xFFFF9592),
|
||||
onError: Color(0xFF191111),
|
||||
errorContainer: Color(0xFF3B1219),
|
||||
onErrorContainer: Color(0xFFFFD1D9),
|
||||
surface: Color(0xFF1C1C1C),
|
||||
onSurface: Color(0xFFF5F5F5),
|
||||
onSurfaceVariant: Color(0xFFC1C1C1),
|
||||
outline: Color(0xFF333333),
|
||||
outlineVariant: Color(0xFF333333),
|
||||
inverseSurface: Color(0xFFE5E5E5),
|
||||
onInverseSurface: Color(0xFF0F0F0F),
|
||||
surface: Color(0xFF000000),
|
||||
onSurface: Color(0xFFFFFFFF),
|
||||
onSurfaceVariant: Color(0xFFC7C7C7),
|
||||
outline: Color(0xFF1A1A1A),
|
||||
outlineVariant: Color(0xFF1A1A1A),
|
||||
inverseSurface: Color(0xFFC7C7C7),
|
||||
onInverseSurface: Color(0xFF000000),
|
||||
inversePrimary: Color(0xFF262626),
|
||||
shadow: Color(0xFF000000),
|
||||
scrim: Color(0xFF000000),
|
||||
surfaceContainerLowest: Color(0xFF0F0F0F),
|
||||
surfaceContainerLow: Color(0xFF161616),
|
||||
surfaceContainer: Color(0xFF232323),
|
||||
surfaceContainerHigh: Color(0xFF2A2A2A),
|
||||
surfaceContainerHighest: Color(0xFF2A2A2A),
|
||||
surfaceContainerLowest: Color(0xFF000000),
|
||||
surfaceContainerLow: Color(0xFF101010),
|
||||
surfaceContainer: Color(0xFF1A1A1A),
|
||||
surfaceContainerHigh: Color(0xFF282828),
|
||||
surfaceContainerHighest: Color(0xFF282828),
|
||||
surfaceTint: Colors.transparent,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'accent_colors.dart';
|
||||
import 'chat_colors.dart';
|
||||
import 'theme_catalog.dart';
|
||||
import 'theme_provider.dart' show effectiveTheme, schemeForAppearanceMode;
|
||||
|
||||
const communityThemeDTag = 'community-theme';
|
||||
const defaultCommunityTheme = CommunityThemePreference(
|
||||
theme: 'buzz',
|
||||
accent: '#3b82f6',
|
||||
// Seeded without a picker action, so it must stay readable by older clients.
|
||||
accent: '#3b82f6', // Cobalt
|
||||
followSystem: true,
|
||||
);
|
||||
|
||||
@@ -32,13 +34,16 @@ class CommunityThemePreference {
|
||||
json['theme'] is! String ||
|
||||
findTheme(json['theme'] as String) == null ||
|
||||
json['accent'] is! String ||
|
||||
accentIndexForWireValue(json['accent'] as String) == null ||
|
||||
(accentIndexForWireValue(json['accent'] as String) == null &&
|
||||
previewFamilyWireValue(json['accent'] as String) == null) ||
|
||||
json['followSystem'] is! bool) {
|
||||
throw const FormatException('Invalid community theme preference');
|
||||
}
|
||||
return CommunityThemePreference(
|
||||
theme: json['theme'] as String,
|
||||
accent: json['accent'] as String,
|
||||
accent:
|
||||
previewFamilyWireValue(json['accent'] as String) ??
|
||||
json['accent'] as String,
|
||||
followSystem: json['followSystem'] as bool,
|
||||
);
|
||||
}
|
||||
@@ -157,7 +162,9 @@ class CommunityThemeStorage {
|
||||
};
|
||||
return CommunityThemePreference(
|
||||
theme: resolvedTheme,
|
||||
accent: legacyAccentWireValue(legacyAccent),
|
||||
accent: legacyAccent == null
|
||||
? defaultCommunityTheme.accent
|
||||
: legacyAccentWireValue(legacyAccent),
|
||||
followSystem: mode == ThemeMode.system,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ ThemeData mobileAppearanceTheme(Brightness brightness, String chatColor) {
|
||||
...theme.extensions.values,
|
||||
ChatColors(
|
||||
outgoing: chatColorForScheme(theme.colorScheme, index),
|
||||
foreground: index == 0 ? theme.colorScheme.onSurface : Colors.white,
|
||||
foreground: chatForegroundForScheme(theme.colorScheme, index),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'app_colors.dart';
|
||||
/// Shared neutral background for mobile controls and selected tabs.
|
||||
Color neutralControlSurface(Brightness brightness) =>
|
||||
brightness == Brightness.dark
|
||||
? const Color(0xFF595959)
|
||||
? const Color(0xFF101010)
|
||||
: const Color(0xFFF0F0F0);
|
||||
|
||||
extension AppThemeExtension on BuildContext {
|
||||
|
||||
Reference in New Issue
Block a user