This commit is contained in:
Ho Ngoc Hai
2026-05-23 18:37:02 +07:00
parent f15d91ee29
commit 76d75c753b
3993 changed files with 403 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
@*
EN: Auth CTA button with color variants (orange, blue, green, outline, ghost).
VI: Nút CTA xác thực với các biến thể màu (cam, xanh dương, xanh lá, outline, ghost).
*@
<button type="@ButtonType"
class="auth-btn auth-btn--@Variant @CssClass"
disabled="@(Disabled || Loading)"
aria-label="@AriaLabel"
aria-busy="@(Loading ? "true" : "false")"
aria-disabled="@((Disabled || Loading) ? "true" : "false")"
@onclick="OnClick">
@if (Loading)
{
<span class="spinner-small" aria-hidden="true"></span>
}
@if (IconName != null)
{
<i data-lucide="@IconName" aria-hidden="true"></i>
}
@ChildContent
</button>
@code {
/// <summary>
/// EN: Button color variant. Options: orange, blue, green, outline, ghost.
/// VI: Biến thể màu. Tùy chọn: orange, blue, green, outline, ghost.
/// </summary>
[Parameter] public string Variant { get; set; } = "orange";
/// <summary>
/// EN: HTML button type attribute.
/// VI: Attribute type của button.
/// </summary>
[Parameter] public string ButtonType { get; set; } = "button";
/// <summary>
/// EN: Whether the button is disabled.
/// VI: Button có bị disable không.
/// </summary>
[Parameter] public bool Disabled { get; set; }
/// <summary>
/// EN: Whether to show a loading spinner.
/// VI: Có hiển thị spinner loading không.
/// </summary>
[Parameter] public bool Loading { get; set; }
/// <summary>
/// EN: Optional Lucide icon name.
/// VI: Tên icon Lucide tùy chọn.
/// </summary>
[Parameter] public string? IconName { get; set; }
/// <summary>
/// EN: Additional CSS classes.
/// VI: CSS class bổ sung.
/// </summary>
[Parameter] public string? CssClass { get; set; }
/// <summary>
/// EN: Click event callback.
/// VI: Callback sự kiện click.
/// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
/// <summary>
/// EN: Accessible label for screen readers (required when button has no visible text).
/// VI: Nhãn accessible cho screen reader (bắt buộc khi button không có text hiển thị).
/// </summary>
[Parameter] public string? AriaLabel { get; set; }
/// <summary>
/// EN: Button content.
/// VI: Nội dung button.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
}

View File

@@ -0,0 +1,157 @@
@*
EN: Reusable auth card wrapper — centered form container with icon, title, subtitle.
VI: Card xác thực tái sử dụng — container form giữa trang với icon, tiêu đề, mô tả.
*@
<div class="auth-card @(Compact ? "auth-card--compact" : "") @(Transparent ? "auth-card--transparent" : "") @CssClass">
@if (BackLink != null)
{
<a href="@BackLink" class="auth-back-link">
<i data-lucide="arrow-left"></i> @BackLinkText
</a>
}
@if (ShowHeader)
{
<div class="auth-header">
@if (Icon != null)
{
<div class="auth-icon @IconClass">
<i data-lucide="@Icon"></i>
</div>
}
@if (RoleBadge != null)
{
<span class="auth-role-badge @RoleBadgeClass">@RoleBadge</span>
}
@if (Title != null)
{
<h1 class="auth-heading">@Title</h1>
}
@if (Subtitle != null)
{
<p class="auth-subheading">@Subtitle</p>
}
@if (SecurityHint != null)
{
<div class="auth-security-hint @SecurityHintClass">
<i data-lucide="@SecurityHintIcon"></i>
@SecurityHint
</div>
}
</div>
}
@ChildContent
@if (FooterContent != null)
{
<div class="auth-footer">
@FooterContent
</div>
}
</div>
@code {
/// <summary>
/// EN: Card title text.
/// VI: Tiêu đề card.
/// </summary>
[Parameter] public string? Title { get; set; }
/// <summary>
/// EN: Subtitle/description text shown below the title.
/// VI: Mô tả hiển thị dưới tiêu đề.
/// </summary>
[Parameter] public string? Subtitle { get; set; }
/// <summary>
/// EN: Lucide icon name shown above the title.
/// VI: Tên icon Lucide hiển thị trên tiêu đề.
/// </summary>
[Parameter] public string? Icon { get; set; }
/// <summary>
/// EN: CSS class for the icon (e.g., auth-icon--blue, auth-icon--orange).
/// VI: CSS class cho icon (VD: auth-icon--blue, auth-icon--orange).
/// </summary>
[Parameter] public string IconClass { get; set; } = "auth-icon--blue";
/// <summary>
/// EN: Role badge text (e.g., "QUẢN TRỊ", "NHÂN VIÊN").
/// VI: Text badge vai trò (VD: "QUẢN TRỊ", "NHÂN VIÊN").
/// </summary>
[Parameter] public string? RoleBadge { get; set; }
/// <summary>
/// EN: CSS class for the role badge.
/// VI: CSS class cho role badge.
/// </summary>
[Parameter] public string RoleBadgeClass { get; set; } = "auth-role-badge--blue";
/// <summary>
/// EN: Security hint message (e.g., "Khu vực bảo mật cao").
/// VI: Thông báo bảo mật.
/// </summary>
[Parameter] public string? SecurityHint { get; set; }
/// <summary>
/// EN: Icon for the security hint.
/// VI: Icon cho thông báo bảo mật.
/// </summary>
[Parameter] public string SecurityHintIcon { get; set; } = "shield-alert";
/// <summary>
/// EN: CSS class for the security hint.
/// VI: CSS class cho thông báo bảo mật.
/// </summary>
[Parameter] public string SecurityHintClass { get; set; } = "auth-security-hint--warning";
/// <summary>
/// EN: Whether to show the header section.
/// VI: Có hiển thị phần header không.
/// </summary>
[Parameter] public bool ShowHeader { get; set; } = true;
/// <summary>
/// EN: Use compact gap (28px vs 32px).
/// VI: Dùng gap nhỏ hơn (28px thay vì 32px).
/// </summary>
[Parameter] public bool Compact { get; set; }
/// <summary>
/// EN: Transparent background (used inside split panels).
/// VI: Nền trong suốt (dùng trong split panel).
/// </summary>
[Parameter] public bool Transparent { get; set; }
/// <summary>
/// EN: Back link URL.
/// VI: URL link quay lại.
/// </summary>
[Parameter] public string? BackLink { get; set; }
/// <summary>
/// EN: Back link text.
/// VI: Text link quay lại.
/// </summary>
[Parameter] public string BackLinkText { get; set; } = "Quay lại đăng nhập";
/// <summary>
/// EN: Additional CSS classes.
/// VI: CSS class bổ sung.
/// </summary>
[Parameter] public string? CssClass { get; set; }
/// <summary>
/// EN: Main content slot.
/// VI: Slot nội dung chính.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>
/// EN: Footer content slot.
/// VI: Slot nội dung footer.
/// </summary>
[Parameter] public RenderFragment? FooterContent { get; set; }
}

View File

@@ -0,0 +1,75 @@
@*
EN: Dark-themed auth input with label, prefix icon, and action button (e.g., password toggle).
VI: Input xác thực dark theme với label, icon prefix, và action button (VD: toggle mật khẩu).
*@
<div class="auth-field">
@if (Label != null || ForgotPasswordLink != null)
{
<div class="auth-label-row">
@if (Label != null)
{
<label class="auth-label" for="@InputId">@Label</label>
}
@if (ForgotPasswordLink != null)
{
<a href="@ForgotPasswordLink" class="auth-label-link">@ForgotPasswordText</a>
}
</div>
}
<div class="auth-input-wrapper">
@if (PrefixIcon != null)
{
<span class="auth-input-icon">
<i data-lucide="@PrefixIcon"></i>
</span>
}
<input id="@InputId"
type="@_currentType"
class="auth-input @(PrefixIcon == null ? "auth-input--no-icon" : "")"
placeholder="@Placeholder"
value="@Value"
autocomplete="@AutoComplete"
@oninput="OnInput"
@onchange="OnChange" />
@if (InputType == "password")
{
<button type="button" class="auth-input-action" @onclick="TogglePassword" title="Toggle password visibility">
<i data-lucide="@(_showPassword ? "eye-off" : "eye")"></i>
</button>
}
</div>
</div>
@code {
private bool _showPassword = false;
private string _currentType => InputType == "password" && _showPassword ? "text" : InputType;
[Parameter] public string? Label { get; set; }
[Parameter] public string InputType { get; set; } = "text";
[Parameter] public string? Placeholder { get; set; }
[Parameter] public string? Value { get; set; }
[Parameter] public string? PrefixIcon { get; set; }
[Parameter] public string? InputId { get; set; }
[Parameter] public string? AutoComplete { get; set; }
[Parameter] public string? ForgotPasswordLink { get; set; }
[Parameter] public string ForgotPasswordText { get; set; } = "Quên mật khẩu?";
[Parameter] public EventCallback<ChangeEventArgs> ValueChanged { get; set; }
private async Task OnInput(ChangeEventArgs e)
{
Value = e.Value?.ToString();
await ValueChanged.InvokeAsync(e);
}
private async Task OnChange(ChangeEventArgs e)
{
Value = e.Value?.ToString();
await ValueChanged.InvokeAsync(e);
}
private void TogglePassword()
{
_showPassword = !_showPassword;
}
}

View File

@@ -0,0 +1,95 @@
@*
EN: Brand panel for split auth layouts — shows logo, title, description, and trust stats.
VI: Panel thương hiệu cho layout chia đôi — hiển thị logo, tiêu đề, mô tả, thống kê.
*@
<div class="auth-brand-panel @PanelClass">
<div class="auth-brand-logo">@LogoText</div>
<h2 class="auth-brand-title">@Title</h2>
<p class="auth-brand-desc">@Description</p>
@if (ShowStats)
{
<div class="auth-brand-stats">
@foreach (var stat in Stats)
{
<div class="auth-brand-stat">
<span class="auth-brand-stat-value">@stat.Value</span>
<span class="auth-brand-stat-label">@stat.Label</span>
</div>
}
</div>
}
@if (Features != null && Features.Count > 0)
{
<div class="auth-brand-features">
@foreach (var feature in Features)
{
<div class="auth-brand-feature">
<i data-lucide="check"></i>
<span>@feature</span>
</div>
}
</div>
}
@ChildContent
</div>
@code {
/// <summary>
/// EN: Panel variant class (auth-brand-panel--orange or auth-brand-panel--dark).
/// VI: Class biến thể panel (auth-brand-panel--orange hoặc auth-brand-panel--dark).
/// </summary>
[Parameter] public string PanelClass { get; set; } = "auth-brand-panel--orange";
/// <summary>
/// EN: Logo character/text displayed in the brand panel.
/// VI: Ký tự/text logo hiển thị trong brand panel.
/// </summary>
[Parameter] public string LogoText { get; set; } = "a";
/// <summary>
/// EN: Brand panel title.
/// VI: Tiêu đề brand panel.
/// </summary>
[Parameter] public string Title { get; set; } = "aPOS Branch";
/// <summary>
/// EN: Description text.
/// VI: Text mô tả.
/// </summary>
[Parameter] public string Description { get; set; } = "";
/// <summary>
/// EN: Whether to show trust stats.
/// VI: Có hiển thị thống kê tin cậy không.
/// </summary>
[Parameter] public bool ShowStats { get; set; } = true;
/// <summary>
/// EN: Trust statistics data.
/// VI: Dữ liệu thống kê tin cậy.
/// </summary>
[Parameter] public List<BrandStat> Stats { get; set; } = new()
{
new("1,200+", "Cửa hàng"),
new("50K+", "Giao dịch/ngày"),
new("99.9%", "Uptime")
};
/// <summary>
/// EN: Feature list items.
/// VI: Danh sách tính năng.
/// </summary>
[Parameter] public List<string>? Features { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>
/// EN: Brand stat record.
/// VI: Record thống kê thương hiệu.
/// </summary>
public record BrandStat(string Value, string Label);
}

View File

@@ -0,0 +1,99 @@
@*
EN: 6-digit OTP input with auto-focus, auto-advance, and backspace support.
VI: Input OTP 6 chữ số với auto-focus, tự chuyển ô, và hỗ trợ backspace.
*@
@inject IJSRuntime JS
<div class="auth-otp-group"
role="group"
aria-label="@GroupAriaLabel">
@for (int i = 0; i < DigitCount; i++)
{
var index = i;
<input id="otp-@index"
type="text"
inputmode="numeric"
maxlength="1"
autocomplete="one-time-code"
aria-label="@($"Digit {index + 1} of {DigitCount}")"
class="auth-otp-input @(UseBlueTheme ? "auth-otp-input--blue" : "") @(!string.IsNullOrEmpty(_digits[index]) ? "auth-otp-input--filled" : "")"
value="@_digits[index]"
@oninput="(e) => HandleInput(e, index)"
@onkeydown="(e) => HandleKeyDown(e, index)" />
}
</div>
@code {
private string[] _digits = new string[6];
/// <summary>
/// EN: Number of OTP digits.
/// VI: Số chữ số OTP.
/// </summary>
[Parameter] public int DigitCount { get; set; } = 6;
/// <summary>
/// EN: Use blue theme (for 2FA authenticator).
/// VI: Dùng theme xanh dương (cho 2FA authenticator).
/// </summary>
[Parameter] public bool UseBlueTheme { get; set; }
/// <summary>
/// EN: Accessible group label for screen readers.
/// VI: Nhãn nhóm accessible cho screen reader.
/// </summary>
[Parameter] public string GroupAriaLabel { get; set; } = "Enter OTP code";
/// <summary>
/// EN: Callback when all digits are entered.
/// VI: Callback khi tất cả chữ số được nhập.
/// </summary>
[Parameter] public EventCallback<string> OnComplete { get; set; }
protected override void OnInitialized()
{
_digits = new string[DigitCount];
}
private async Task HandleInput(ChangeEventArgs e, int index)
{
var value = e.Value?.ToString() ?? "";
// EN: Only allow numeric input
// VI: Chỉ cho phép nhập số
if (!string.IsNullOrEmpty(value) && !char.IsDigit(value[0]))
{
_digits[index] = "";
return;
}
_digits[index] = value;
if (!string.IsNullOrEmpty(value) && index < DigitCount - 1)
{
// EN: Auto-advance to next input
// VI: Tự động chuyển sang ô tiếp theo
await JS.InvokeVoidAsync("focusOtpInput", index + 1);
}
// EN: Check if all digits are filled
// VI: Kiểm tra tất cả ô đã nhập xong chưa
if (_digits.All(d => !string.IsNullOrEmpty(d)))
{
var code = string.Join("", _digits);
await OnComplete.InvokeAsync(code);
}
}
private async Task HandleKeyDown(KeyboardEventArgs e, int index)
{
if (e.Key == "Backspace" && string.IsNullOrEmpty(_digits[index]) && index > 0)
{
// EN: Move to previous input on backspace
// VI: Chuyển về ô trước khi nhấn backspace
_digits[index - 1] = "";
await JS.InvokeVoidAsync("focusOtpInput", index - 1);
}
}
}

View File

@@ -0,0 +1,61 @@
@*
EN: Social login buttons — Google, Apple, Zalo with divider.
VI: Nút đăng nhập bên thứ ba — Google, Apple, Zalo với divider.
*@
<div class="auth-divider">@DividerText</div>
<div class="auth-social-group">
@* EN: Google — official multi-color "G" logo *@
<button type="button" class="auth-social-btn auth-social-btn--google" title="Google" @onclick="OnGoogleClick">
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18A11.96 11.96 0 0 0 1 12c0 1.94.46 3.77 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
</button>
@* EN: Apple — official Apple logo *@
<button type="button" class="auth-social-btn auth-social-btn--apple" title="Apple" @onclick="OnAppleClick">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.52-3.23 0-1.44.62-2.2.44-3.06-.4C3.79 16.17 4.36 9.53 8.76 9.28c1.25.07 2.12.72 2.88.76.99-.2 1.94-.78 3-.84 1.42-.1 2.5.42 3.2 1.33-2.92 1.69-2.23 5.42.53 6.46-.62 1.6-1.42 3.19-2.95 3.29h-.37zM12 9.22c-.12-2.24 1.73-4.25 3.87-4.22.32 2.36-2.12 4.38-3.87 4.22z"/>
</svg>
</button>
@* EN: Facebook — official F logo *@
<button type="button" class="auth-social-btn auth-social-btn--facebook" title="Facebook" @onclick="OnFacebookClick">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</button>
@if (ShowZalo)
{
@* EN: Zalo — Z letter in brand blue *@
<button type="button" class="auth-social-btn auth-social-btn--zalo" title="Zalo" @onclick="OnZaloClick">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.373 0 0 4.925 0 11s5.373 11 12 11c1.125 0 2.215-.152 3.253-.437l3.694 1.327a.75.75 0 0 0 .966-.932l-1.152-3.572C22.018 16.037 24 13.696 24 11 24 4.925 18.627 0 12 0zm-3.2 14.4H5.6V13h2.08l-2.48-3.76V8h3.6v1.4H7.12l2.48 3.76v1.24zm5.4-.8c0 .44-.36.8-.8.8h-2.8c-.44 0-.8-.36-.8-.8V8.8c0-.44.36-.8.8-.8h2.8c.44 0 .8.36.8.8v4.8zm3.2.8h-1.6V8h1.6v6.4z"/>
</svg>
</button>
}
</div>
@code {
/// <summary>
/// EN: Divider text between form and social buttons.
/// VI: Text divider giữa form và social buttons.
/// </summary>
[Parameter] public string DividerText { get; set; } = "Hoặc đăng nhập bằng";
/// <summary>
/// EN: Whether to show Zalo button (Vietnam market).
/// VI: Có hiển thị nút Zalo không (thị trường VN).
/// </summary>
[Parameter] public bool ShowZalo { get; set; } = true;
[Parameter] public EventCallback OnGoogleClick { get; set; }
[Parameter] public EventCallback OnAppleClick { get; set; }
[Parameter] public EventCallback OnFacebookClick { get; set; }
[Parameter] public EventCallback OnZaloClick { get; set; }
}

View File

@@ -0,0 +1,66 @@
@using System.Globalization
@inject NavigationManager Navigation
@inject IJSRuntime JS
<MudMenu Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight" LockScroll="true">
<ActivatorContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mr-2 cursor-pointer">
<MudText Typo="Typo.button" Style="font-family: var(--font-heading);">
@GetCurrentLabel()
</MudText>
<MudIcon Icon="@Icons.Material.Rounded.Language" Size="Size.Small" />
</MudStack>
</ActivatorContent>
<ChildContent>
<MudMenuItem OnClick="@(() => SwitchLanguage("vi-VN"))">
<MudStack Row="true" Spacing="2">
<MudText>🇻🇳</MudText>
<MudText>Tiếng Việt</MudText>
</MudStack>
</MudMenuItem>
<MudMenuItem OnClick="@(() => SwitchLanguage("en-US"))">
<MudStack Row="true" Spacing="2">
<MudText>🇺🇸</MudText>
<MudText>English</MudText>
</MudStack>
</MudMenuItem>
</ChildContent>
</MudMenu>
@code {
private string GetCurrentLabel()
{
var culture = CultureInfo.CurrentUICulture.Name;
return culture.StartsWith("vi", StringComparison.OrdinalIgnoreCase) ? "VI" : "EN";
}
private async Task SwitchLanguage(string targetCulture)
{
// Save to localStorage for persistence across page reloads
await JS.InvokeVoidAsync("localStorage.setItem", "aPOS_culture", targetCulture);
// Force full page reload to reinitialize the WASM app with new culture
var currentUri = Navigation.Uri;
var uri = new Uri(currentUri);
// Build clean URL (strip any culture segments from path)
var path = uri.AbsolutePath;
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
// Remove existing culture segment if present
if (segments.Length > 0 && (
segments[0].Equals("vi-VN", StringComparison.OrdinalIgnoreCase) ||
segments[0].Equals("en-US", StringComparison.OrdinalIgnoreCase) ||
segments[0].Equals("vi", StringComparison.OrdinalIgnoreCase) ||
segments[0].Equals("en", StringComparison.OrdinalIgnoreCase)))
{
segments = segments.Skip(1).ToArray();
}
var cleanPath = "/" + string.Join('/', segments);
if (cleanPath == "/") cleanPath = "";
// Navigate to root with forceLoad to reinitialize WASM
Navigation.NavigateTo($"{cleanPath}", forceLoad: true);
}
}

View File

@@ -0,0 +1,80 @@
// <auto-generated>
// EN: This file is auto-generated by the Style Dictionary pipeline.
// Do NOT edit manually. Update tokens/*.json and re-run the build.
// VI: File này được tạo tự động bởi Style Dictionary pipeline.
// KHÔNG chỉnh sửa thủ công. Cập nhật tokens/*.json và build lại.
// </auto-generated>
namespace GoodGo.BlazorUi.DesignTokens;
/// <summary>
/// EN: Auto-generated design token constants. Maps 1:1 with CSS custom properties.
/// VI: Hằng số design token tự động. Ánh xạ 1:1 với CSS custom properties.
/// </summary>
public static class DesignTokens
{
public const string BorderRadiusBase = "6px"; // Default border radius
public const string BorderRadiusLg = "10px"; // Large border radius
public const string BorderRadiusXl = "14px"; // XL border radius
public const string BorderRadius2xl = "20px"; // 2XL border radius
public const string ColorPrimitiveNeutral0 = "#ffffff"; // Pure white
public const string ColorPrimitiveNeutral50 = "#fafafa"; // Near white
public const string ColorPrimitiveNeutral100 = "#ADADB0"; // Light gray
public const string ColorPrimitiveNeutral200 = "#8B8B90"; // Medium gray
public const string ColorPrimitiveNeutral300 = "#6B6B70"; // Muted gray
public const string ColorPrimitiveNeutral400 = "#3A3A3E"; // Dark gray
public const string ColorPrimitiveNeutral500 = "#2A2A2E"; // Darker gray
public const string ColorPrimitiveNeutral600 = "#1F1F23"; // Surface border
public const string ColorPrimitiveNeutral700 = "#1A1A1D"; // Elevated surface
public const string ColorPrimitiveNeutral800 = "#111113"; // Base surface
public const string ColorPrimitiveNeutral900 = "#0A0A0B"; // Page background
public const string ColorPrimitiveNeutral950 = "#050506"; // Near black
public const string ColorPrimitiveAccent400 = "#FF8A4C"; // Orange light
public const string ColorPrimitiveAccent500 = "#FF5C00"; // Orange primary
public const string ColorPrimitiveAccent600 = "#E05200"; // Orange dark
public const string ColorPrimitiveSuccess500 = "#22C55E"; // Success green
public const string ColorPrimitiveWhite = "#ffffff";
public const string ColorPrimitiveBlack = "#000000";
public const string ColorPrimitiveOverlay = "rgba(0, 0, 0, 0.6)";
public const string ColorSemanticBgPage = "#0A0A0B"; // Page background
public const string ColorSemanticBgSurface = "#111113"; // Card / panel surface
public const string ColorSemanticBgElevated = "#1A1A1D"; // Elevated elements
public const string ColorSemanticBgInteractive = "#2A2A2E"; // Interactive backgrounds
public const string ColorSemanticBgSurfaceHover = "#1F1F23"; // Hover state
public const string ColorSemanticBgOverlay = "rgba(10, 10, 11, 0.9)"; // Modal overlay
public const string ColorSemanticTextPrimary = "#ffffff"; // Primary text
public const string ColorSemanticTextSecondary = "#ADADB0"; // Secondary text
public const string ColorSemanticTextTertiary = "#8B8B90"; // Tertiary / helper text
public const string ColorSemanticTextDisabled = "#6B6B70"; // Disabled text
public const string ColorSemanticTextMuted = "rgba(255, 255, 255, 0.8)"; // Muted white text
public const string ColorSemanticTextInverse = "#0A0A0B"; // Inverse (dark on light)
public const string ColorSemanticAccentPrimary = "#FF5C00"; // Brand orange
public const string ColorSemanticAccentLight = "#FF8A4C"; // Light orange
public const string ColorSemanticAccentGlow = "rgba(255, 92, 0, 0.15)"; // Subtle glow
public const string ColorSemanticAccentGlowStrong = "rgba(255, 92, 0, 0.3)"; // Strong glow
public const string ColorSemanticBorderSubtle = "#1F1F23"; // Subtle divider
public const string ColorSemanticBorderDefault = "#2A2A2E"; // Default border
public const string ColorSemanticBorderStrong = "#3A3A3E"; // Strong border
public const string ColorSemanticActionPrimaryBg = "#FF5C00"; // CTA button bg
public const string ColorSemanticActionPrimaryBgHover = "#E05200"; // CTA button hover
public const string ColorSemanticActionPrimaryText = "#ffffff"; // CTA button text
public const string ColorSemanticActionSecondaryBg = "transparent"; // Ghost button bg
public const string ColorSemanticActionSecondaryBgHover = "rgba(255, 255, 255, 0.05)"; // Ghost button hover
public const string ColorSemanticActionSecondaryText = "#ffffff"; // Ghost button text
public const string ColorSemanticActionSecondaryBorder = "#2A2A2E"; // Ghost button border
public const string ColorSemanticStatusSuccess = "#22C55E"; // Success state
public const string Spacing1 = "0.25rem"; // 4px
public const string Spacing2 = "0.5rem"; // 8px
public const string Spacing3 = "0.75rem"; // 12px
public const string Spacing4 = "1rem"; // 16px
public const string Spacing5 = "1.25rem"; // 20px
public const string Spacing6 = "1.5rem"; // 24px
public const string Spacing8 = "2rem"; // 32px
public const string Spacing10 = "2.5rem"; // 40px
public const string Spacing12 = "3rem"; // 48px
public const string Spacing16 = "4rem"; // 64px
public const string Spacing20 = "5rem"; // 80px
public const string Spacing24 = "6rem"; // 96px
public const string FontFamilyHeading = "'Roboto', system-ui, sans-serif"; // Heading / display font
public const string FontFamilyBody = "'Roboto', system-ui, sans-serif"; // Body text font
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<!--
EN: Razor Class Library — shared Blazor UI components for GoodGo platform.
Consumed by web-client-tpos-net and web-client-base-net.
VI: Razor Class Library — thư viện component Blazor dùng chung cho nền tảng GoodGo.
Được dùng bởi web-client-tpos-net và web-client-base-net.
-->
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AddRazorSupportForMvc>false</AddRazorSupportForMvc>
<AssemblyName>GoodGo.BlazorUi</AssemblyName>
<RootNamespace>GoodGo.BlazorUi</RootNamespace>
<Version>1.0.0</Version>
<Description>Shared Blazor UI component library for GoodGo platform</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.2" />
<PackageReference Include="MudBlazor" Version="8.15.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using MudBlazor
@using GoodGo.BlazorUi
@using GoodGo.BlazorUi.Components.Auth
@using GoodGo.BlazorUi.Components.Common