Mitra Bestie §1–§3: shell + Undangan + popup + chat polish
Brings the mitra app to figma-bestie parity for Home (§1), Undangan
inbox with Curhat Baru + Perpanjang tabs (§2), and the incoming-popup
+ active-chat flow (§3). Home now lives inside a StatefulShellRoute
with BestieTabBar so Profil + Undangan + Home share one shell.
- Shell: features/shell/ (StatefulShellRoute, BestieTabBar, 3 branches)
- Undangan: features/undangan/ — Curhat Baru reads
chatRequestProvider.pendingInvites; row Terima delegates accept to
the notifier and ChatRequestOverlay owns nav (no double-push).
Perpanjang tab stubbed (empty state) until backend exposes
pendingExtensionsProvider.
- Profil: features/profile/ — Bestie-styled stub
- Home: refactored to body-only (shell owns chrome)
- Popup: chat_request_overlay + chat_request_notifier updated to
serve the list rows, not just the modal
- Chat: mitra_chat_screen polish
- Theme: accentAmber tokens for the Perpanjang tab + halo_orb widget
(loading spinner used by undangan list states)
- Login: replace broken GoRouterState location guard with
_expectOtpPush flag — was stacking duplicate /otp pages on OTP
resend (see project-otp-nav-bug-fixed-2026-05-21)
Maestro:
- 17 new flows under .maestro/flows/ts-mitra-{1,2,3}-* covering home
online/offline variants, undangan empty/populated/tolak states,
popup curhat-baru → accept → chat → ended banner, plus popup
dismiss/expire/cancelled edge cases
- 4 new §A OTP flows (07/08/09/10) for invalid/mismatch/expired/cooldown
- Helper scripts: force_mitra_online/offline, force_pairing_timeout,
force_session_expires_at, delete_mitra_status_row,
customer_blast_now (js), customer_cancel_latest_blast
- Backend: POST /internal/_test/delete-mitra-status-row supports the
"fresh mitra with no status row" test setup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ class HaloTokens {
|
||||
static const Color danger = Color(0xFFD86B6B);
|
||||
static const Color border = Color(0xFFF0E4E8);
|
||||
|
||||
// Amber accent — used by the "Perpanjang Curhat" tab (BestieInvitesExtend
|
||||
// in figma-bestie/project/screens/v5.jsx).
|
||||
static const Color accentAmber = Color(0xFFD97706);
|
||||
static const Color accentAmberSoft = Color(0xFFFFE3A8);
|
||||
static const Color accentAmberBg = Color(0xFFFBEFE8);
|
||||
|
||||
// Font family names — must match the `family:` entries in pubspec.yaml.
|
||||
// Falls back to system fonts when the .ttf assets are not bundled.
|
||||
static const String fontDisplay = 'BricolageGrotesque';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
enum HaloButtonVariant { primary, secondary, ghost }
|
||||
enum HaloButtonVariant { primary, secondary, ghost, soft, dark }
|
||||
|
||||
enum HaloButtonSize { sm, md, lg }
|
||||
|
||||
@@ -93,6 +93,52 @@ class HaloButton extends StatelessWidget {
|
||||
child: child,
|
||||
);
|
||||
break;
|
||||
case HaloButtonVariant.soft:
|
||||
button = ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: HaloTokens.brandSofter,
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
disabledBackgroundColor: HaloTokens.brandSoft,
|
||||
disabledForegroundColor: HaloTokens.inkMuted,
|
||||
elevation: 0,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
break;
|
||||
case HaloButtonVariant.dark:
|
||||
button = Container(
|
||||
decoration: disabled
|
||||
? null
|
||||
: const BoxDecoration(
|
||||
borderRadius: HaloRadius.pill,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x402A1820),
|
||||
offset: Offset(0, 6),
|
||||
blurRadius: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: HaloTokens.ink,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: HaloTokens.inkMuted,
|
||||
disabledForegroundColor: Colors.white70,
|
||||
elevation: 0,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (fullWidth) {
|
||||
|
||||
124
mitra_app/lib/core/theme/widgets/halo_orb.dart
Normal file
124
mitra_app/lib/core/theme/widgets/halo_orb.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
/// Decorative gradient blob avatar — the "Bestie" abstract avatar (not a face).
|
||||
///
|
||||
/// Ported from `mitra_app/figma-bestie/project/screens/primitives.jsx` (HBOrb).
|
||||
/// The JSX renders a CSS `radial-gradient` plus two soft white highlight blobs
|
||||
/// stacked over it. In Flutter we approximate with a `Stack`:
|
||||
/// - base: `Container` w/ `RadialGradient` from top-left
|
||||
/// - overlay 1: large soft white blob in upper-left (primary specular)
|
||||
/// - overlay 2: smaller dimmer blob in lower-right (secondary highlight,
|
||||
/// opposite the JSX which is also lower-right but very faint — keeping
|
||||
/// the same position for visual parity)
|
||||
/// - plus an outer drop shadow tinted by the seed's primary color.
|
||||
///
|
||||
/// Approximation note: CSS `filter: blur(6px)` on a sibling layer is emulated
|
||||
/// with low-opacity white circles. It reads as "soft highlight" at a glance
|
||||
/// without needing `ImageFilter.blur` (which would require ClipOval + BackdropFilter).
|
||||
class HaloOrb extends StatelessWidget {
|
||||
const HaloOrb({
|
||||
super.key,
|
||||
this.size = 120,
|
||||
this.seed = 0,
|
||||
});
|
||||
|
||||
/// Diameter in logical pixels.
|
||||
final double size;
|
||||
|
||||
/// 0–4 selects a deterministic color pair from the warm palette seeds.
|
||||
/// Out-of-range values are folded with modulo.
|
||||
final int seed;
|
||||
|
||||
/// Warm-palette seed table — mirrors `HBOrb` colors in primitives.jsx:68–73.
|
||||
static const List<List<Color>> _seeds = [
|
||||
[HaloTokens.brand, HaloTokens.accent],
|
||||
[HaloTokens.brandDark, HaloTokens.lilac],
|
||||
[HaloTokens.accent, HaloTokens.brand],
|
||||
[HaloTokens.lilac, HaloTokens.brand],
|
||||
[HaloTokens.mint, HaloTokens.brand],
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pair = _seeds[seed.abs() % _seeds.length];
|
||||
final primary = pair[0];
|
||||
final secondary = pair[1];
|
||||
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Base radial gradient + drop shadow.
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
center: const Alignment(-0.4, -0.4), // 30% / 30% in JSX
|
||||
radius: 0.85,
|
||||
colors: [primary, secondary],
|
||||
stops: const [0.0, 0.7],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primary.withValues(alpha: 0.25),
|
||||
offset: const Offset(0, 8),
|
||||
blurRadius: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Inner shadow approximation — darker rim, bottom-right.
|
||||
// Re-used the JSX `inset -8px -8px 20px rgba(0,0,0,0.12)` intent.
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
center: const Alignment(0.6, 0.6),
|
||||
radius: 0.85,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.0),
|
||||
Colors.black.withValues(alpha: 0.12),
|
||||
],
|
||||
stops: const [0.7, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Top-left specular highlight (larger, brighter).
|
||||
Positioned(
|
||||
top: size * 0.12,
|
||||
left: size * 0.20,
|
||||
child: Container(
|
||||
width: size * 0.32,
|
||||
height: size * 0.24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom-right faint glint.
|
||||
Positioned(
|
||||
bottom: size * 0.14,
|
||||
right: size * 0.18,
|
||||
child: Container(
|
||||
width: size * 0.18,
|
||||
height: size * 0.14,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export 'halo_button.dart';
|
||||
export 'halo_orb.dart';
|
||||
|
||||
Reference in New Issue
Block a user