feat: Implement "Heritage Gallery Editorial" design system with updated app theme and new design documentation.

This commit is contained in:
Suherdy Yacob 2026-03-27 11:30:44 +07:00
parent 544439d571
commit 8431a8dc13
5 changed files with 176 additions and 64 deletions

93
DESIGN.md Normal file
View File

@ -0,0 +1,93 @@
# Design System Strategy: Heritage Gallery Editorial
## 1. Overview & Creative North Star
The Creative North Star for this design system is **"The Modern Curator."** We are not building a standard utility app; we are designing a digital gallery that honors a rich culinary heritage through a modern, high-end editorial lens.
To achieve this, we move beyond the rigid "bootstrap" look. We break the template by embracing **Intentional Asymmetry**—where whitespace is as much a design element as the content itself. Overlapping elements, such as images bleeding off the edge or overlapping surface containers, create a sense of physical depth. By utilizing the **Plus Jakarta Sans** typeface at extreme scale contrasts, we establish an authoritative yet welcoming voice that feels premium, bespoke, and intentional.
---
## 2. Colors
Our palette is rooted in the warmth of tradition but executed with modern precision.
### The Palette
- **Primary (`#E1251B`)**: A bold, vibrant red used for high-impact CTAs and core brand moments.
- **Secondary (`#FFBF3C`)**: A warm gold, used to draw attention to interactive accents and "Mapan" highlights.
- **Tertiary (`#CA8342`)**: An earthy, wood-tone brown used for sophisticated detailing and organic warmth.
- **Background (`#EADFD2`)**: A soft, parchment-like neutral that provides the "gallery wall" for our content.
### The "No-Line" Rule
**Explicit Instruction:** Do not use 1px solid borders to section off content. Traditional lines are "noise" that clutter the heritage aesthetic. Boundaries must be defined solely through background color shifts. For example, a `surface-container-low` section should sit directly on a `surface` background to define its territory.
### Surface Hierarchy & Nesting
Treat the UI as a series of physical layers—stacked sheets of fine paper.
- Use the **Surface Tiers** (`surface-container-lowest` to `highest`) to create depth.
- **Nesting:** An inner card should be `surface-container-lowest` (pure white) when placed on a `surface-container-low` section. This creates a soft, natural "lift" without the need for artificial borders.
### The "Glass & Gradient" Rule
To add "soul" to the interface:
- **Glassmorphism:** Use semi-transparent versions of `surface` colors with a `backdrop-blur` for floating navigation bars or modal overlays.
- **Signature Gradients:** Use subtle transitions from `primary` (`#BB0004`) to `primary-container` (`#E1251B`) on large buttons or Hero backgrounds. This prevents the "flat-web" look and adds professional polish.
---
## 3. Typography
We use **Plus Jakarta Sans** to balance contemporary geometric shapes with warm, humanistic curves.
- **Display Scale (`display-lg` to `sm`)**: Used for "Hero" moments and editorial storytelling. Use extreme negative letter-spacing (-0.02em) to create a high-fashion, compact look.
- **Headline & Title Scale**: These are your signposts. Use `headline-lg` for section headers with generous top-padding to let the "curation" breathe.
- **Body & Labels**: `body-lg` (1rem) is our standard for readability. Use `label-md` for metadata, ensuring high contrast using the `on-surface-variant` token.
The hierarchy is designed to be **Editorial First**: large headings should feel like magazine titles, while body text provides the "curator's notes" in a clean, legible block.
---
## 4. Elevation & Depth
In this design system, depth is felt, not seen. We favor **Tonal Layering** over structural shadows.
### The Layering Principle
Depth is achieved by "stacking" the surface-container tiers. For instance, a floating action card should use `surface-container-lowest` to pop against a `surface-dim` background.
### Ambient Shadows
When a physical "lift" is required (e.g., a floating bottom sheet):
- **Diffusion:** Shadows must be extra-diffused (Blur: 20px-40px).
- **Opacity:** Keep opacity between 4% and 8%.
- **Tinting:** Never use pure black. Tint the shadow with `on-surface` (`#201B13`) to mimic natural, ambient light reflecting off the heritage wood tones.
### The "Ghost Border" Fallback
If accessibility requires a container edge, use a **Ghost Border**: the `outline-variant` token at 15% opacity. Standard 100% opaque borders are strictly forbidden.
---
## 5. Components
### Buttons
- **Primary**: `primary` background with `on-primary` text. Apply a `xl` (0.75rem) roundedness for a modern, tactile feel.
- **Secondary**: `secondary-container` (`#FEBE3B`) with `on-secondary-container`. This acts as a "warm" alternative for secondary actions.
- **Glass Variant**: For buttons sitting on imagery, use a semi-transparent `surface` with a heavy blur.
### Cards & Lists
- **No Dividers:** Forbid the use of horizontal rules. Separate list items using the **Spacing Scale** (e.g., `spacing-4`) or by alternating between `surface` and `surface-container-low`.
- **Imagery**: Cards should feature high-quality photography that bleeds to the top and sides, emphasizing the "Gallery" aesthetic.
### Chips & Inputs
- **Chips**: Use `surface-container-high` for unselected states and `tertiary` for selected states.
- **Input Fields**: Use a "minimalist-underlined" or "soft-filled" style. Forbid the heavy "boxed" input. The background should be `surface-container-lowest`.
### Signature Component: The "Heritage Overlay"
A specific component for this system: An image container with a `tertiary-container` (`#A56526`) accent tab in the corner, holding a `label-sm` tag. This mimics the labeling of artifacts in a gallery.
---
## 6. Do's and Don'ts
### Do
- **Do** use intentional white space. If you think a section needs more room, double the spacing.
- **Do** overlap elements. Let a product image break the container of a card to create 3D interest.
- **Do** use the `tertiary` wood-tones for subtle accents like icons or small labels to tie into the "Heritage" material.
### Don't
- **Don't** use 1px solid black or grey borders. This immediately destroys the "Modern Gallery" feel.
- **Don't** use standard drop shadows. If it looks like a "box shadow," its too heavy.
- **Don't** crowd the layout. If the user feels overwhelmed, the "Curator" has failed.
- **Don't** use pure black (`#000000`) for text. Use `on-surface` (`#201B13`) to keep the typography feeling organic and soft.

View File

@ -86,13 +86,7 @@ class _BranchesScreenState extends State<BranchesScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceContainerLow, color: AppTheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // Spec rules: "Don't use standard drop shadows"
BoxShadow(
color: AppTheme.onSurface.withOpacity(0.04),
blurRadius: 16,
offset: const Offset(0, 4),
)
]
), ),
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),

View File

@ -74,23 +74,30 @@ class _LoginScreenState extends State<LoginScreen> {
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 48.0), padding: const EdgeInsets.only(
left: 32.0,
right: 32.0,
top: 80.0,
bottom: 48.0,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 64), const SizedBox(height: 64),
Text( Text(
'Mie Mapan\nMembership', // Editorial high-end entry 'Mie Mapan\nMembership', // Editorial high-end entry
style: Theme.of( style: Theme.of(context).textTheme.displayLarge?.copyWith(
context, color: AppTheme.primary,
).textTheme.displayMedium?.copyWith(color: AppTheme.primary), letterSpacing: -1.0, // High-fashion compact look
fontSize: 48,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Sign in to access your culinary loyalty tier and discover exclusive offers.', 'Sign in to access your culinary loyalty tier and discover exclusive offers.',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
const SizedBox(height: 56), const SizedBox(height: 80),
TextField( TextField(
controller: _usernameController, controller: _usernameController,
decoration: const InputDecoration( decoration: const InputDecoration(

View File

@ -66,23 +66,16 @@ class _LoyaltyDashboardState extends State<LoyaltyDashboard> {
), ),
) )
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 40.0),
itemCount: _loyaltyCards.length, itemCount: _loyaltyCards.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final card = _loyaltyCards[index]; final card = _loyaltyCards[index];
return Container( return Container(
margin: const EdgeInsets.only(bottom: 32), margin: const EdgeInsets.only(bottom: 40),
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(32),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceContainerLow, color: AppTheme.surfaceContainerHighest, // Soft Lift without shadow
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: AppTheme.onSurface.withOpacity(0.06),
blurRadius: 24,
offset: const Offset(0, 8),
)
]
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -102,7 +95,7 @@ class _LoyaltyDashboardState extends State<LoyaltyDashboard> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.secondaryContainer, color: AppTheme.secondaryContainer,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(1000), // full explicit roundedness
), ),
child: Text( child: Text(
'Gold Member', 'Gold Member',

View File

@ -2,22 +2,23 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
class AppTheme { class AppTheme {
// Mapan Core Tokens // Mapan Core Tokens (Heritage Gallery)
static const Color primary = Color(0xFFB20000); static const Color primary = Color(0xFFE1251B);
static const Color primaryContainer = Color(0xFFE00101); static const Color primaryContainer = Color(0xFFBB0004);
static const Color secondary = Color(0xFF825500); static const Color secondary = Color(0xFFCA8342);
static const Color secondaryContainer = Color(0xFFFEB23D); static const Color secondaryContainer = Color(0xFFFFBF3C);
static const Color onSecondaryContainer = Color(0xFF6E4700); static const Color onSecondaryContainer = Color(0xFFEADFD2);
// Surface Hierarchy // Surface Hierarchy
static const Color surface = Color(0xFFFCF9F8); static const Color surface = Color(0xFFFFF8F3);
static const Color surfaceContainer = Color(0xFFF0EDED); static const Color surfaceContainer = Color(0xFFF7ECDF);
static const Color surfaceContainerLow = Color(0xFFF6F3F2); static const Color surfaceContainerLow = Color(0xFFFDF2E5);
static const Color surfaceContainerHighest = Color(0xFFE5E2E1); static const Color surfaceContainerLowest = Colors.white;
static const Color surfaceContainerHighest = Color(0xFFECE1D4);
// Text & On-Colors // Text & On-Colors
static const Color onSurface = Color(0xFF1C1B1B); static const Color onSurface = Color(0xFF201B13);
static const Color onSurfaceVariant = Color(0xFF5E3F3A); static const Color onSurfaceVariant = Color(0xFF5D3F3B);
static const Color onPrimary = Colors.white; static const Color onPrimary = Colors.white;
/// The Signature 135-degree CTA Gradient for main buttons. /// The Signature 135-degree CTA Gradient for main buttons.
@ -46,21 +47,45 @@ class AppTheme {
error: Color(0xFFBA1A1A), error: Color(0xFFBA1A1A),
), ),
textTheme: baseTheme.textTheme.copyWith( textTheme: baseTheme.textTheme.copyWith(
displayLarge: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold, letterSpacing: -0.5), displayLarge: GoogleFonts.plusJakartaSans(
displayMedium: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), color: onSurface,
displaySmall: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
headlineMedium: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), letterSpacing: -0.5,
titleLarge: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w600), ),
titleMedium: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w600), displayMedium: GoogleFonts.plusJakartaSans(
titleSmall: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w500), color: onSurface,
bodyLarge: GoogleFonts.beVietnamPro(color: onSurfaceVariant), fontWeight: FontWeight.bold,
bodyMedium: GoogleFonts.beVietnamPro(color: onSurfaceVariant), ),
bodySmall: GoogleFonts.beVietnamPro(color: onSurfaceVariant), displaySmall: GoogleFonts.plusJakartaSans(
labelLarge: GoogleFonts.beVietnamPro(color: onSurfaceVariant), color: onSurface,
fontWeight: FontWeight.bold,
),
headlineMedium: GoogleFonts.plusJakartaSans(
color: onSurface,
fontWeight: FontWeight.bold,
),
titleLarge: GoogleFonts.plusJakartaSans(
color: onSurface,
fontWeight: FontWeight.w600,
),
titleMedium: GoogleFonts.plusJakartaSans(
color: onSurface,
fontWeight: FontWeight.w600,
),
titleSmall: GoogleFonts.plusJakartaSans(
color: onSurface,
fontWeight: FontWeight.w500,
),
bodyLarge: GoogleFonts.plusJakartaSans(color: onSurfaceVariant),
bodyMedium: GoogleFonts.plusJakartaSans(color: onSurfaceVariant),
bodySmall: GoogleFonts.plusJakartaSans(color: onSurfaceVariant),
labelLarge: GoogleFonts.plusJakartaSans(color: onSurfaceVariant),
), ),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
foregroundColor: onPrimary, foregroundColor: onPrimary,
backgroundColor: primaryContainer, // Fallback if no gradient is used backgroundColor: primaryContainer, // Fallback if no gradient is used
elevation: 0, elevation: 0,
@ -74,19 +99,19 @@ class AppTheme {
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: surfaceContainerHighest, fillColor: surfaceContainerLowest, // Spec: soft filled background
// Using "Ghost Border" logic at 15% opacity contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder( border: const UnderlineInputBorder(
borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, // Minimum/No outline by default
borderSide: const BorderSide(color: Color(0x26946E68)), borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: const UnderlineInputBorder(
borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none,
borderSide: const BorderSide(color: Color(0x26946E68)), borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: UnderlineInputBorder(
borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: primary.withOpacity(0.5), width: 2),
borderSide: BorderSide(color: primary.withOpacity(0.4), width: 2), borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
), ),
labelStyle: const TextStyle(color: onSurfaceVariant), labelStyle: const TextStyle(color: onSurfaceVariant),
), ),
@ -96,9 +121,9 @@ class AppTheme {
elevation: 0, elevation: 0,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
titleTextStyle: GoogleFonts.plusJakartaSans( titleTextStyle: GoogleFonts.plusJakartaSans(
color: onSurface, color: onSurface,
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold,
), ),
), ),
); );