Compare commits

...

34 Commits

Author SHA1 Message Date
b4df7201f5 Merge branch 'dev' 2026-04-23 15:27:27 +03:00
b0ac7426b7 Build [v1.14.19] 2026-04-23 15:27:27 +03:00
941761ad83 fix(09): fix syntax error in ExportPanel component 2026-04-23 15:27:27 +03:00
20ace033a2 Merge branch 'dev' 2026-04-23 15:24:27 +03:00
c21ed699ec Build [v1.14.18] 2026-04-23 15:24:27 +03:00
f77582fdcd fix(09): improve Caddy WebSocket support for Next.js HMR 2026-04-23 15:24:27 +03:00
34ae803c7a Merge branch 'dev' 2026-04-23 15:19:55 +03:00
654becfd86 Build [v1.14.17] 2026-04-23 15:19:55 +03:00
dcc3f692f3 fix(09): exact match export buttons to UI standard 2026-04-23 15:19:54 +03:00
fb2947cfd0 Merge branch 'dev' 2026-04-23 15:18:14 +03:00
1d22f1e85e Build [v1.14.16] 2026-04-23 15:18:14 +03:00
b87b1eceae fix(09): match export UI styles and fix backend user_id error 2026-04-23 15:18:14 +03:00
5ed8c6c7db Merge branch 'dev' 2026-04-23 15:14:59 +03:00
f2fb81024e Build [v1.14.15] 2026-04-23 15:14:59 +03:00
76f0e83a65 fix(09): refine export UI buttons and fix 404 2026-04-23 15:14:59 +03:00
14ffecafdd Merge branch 'dev' 2026-04-23 15:08:46 +03:00
c85a7b78a6 Build [v1.14.14] 2026-04-23 15:08:46 +03:00
6f9716c32b fix(09): resolve export data quality and integrate UI 2026-04-23 15:08:46 +03:00
2ce0f84cd3 Merge branch 'dev' 2026-04-23 14:51:52 +03:00
96f4a5d228 Build [v1.14.13] 2026-04-23 14:51:52 +03:00
827dfcdf2f fix(08): fix infinite recursion in config manager 2026-04-23 14:51:51 +03:00
e770938de8 Merge branch 'dev' 2026-04-23 14:45:07 +03:00
9afe335384 Build [v1.14.12] 2026-04-23 14:45:07 +03:00
c14a28b093 fix(08): fix AI key status detection and enable saving to secrets.yaml 2026-04-23 14:45:07 +03:00
593c3ba5e6 Merge branch 'dev' 2026-04-23 14:04:08 +03:00
70523be677 Merge branch 'dev' 2026-04-23 13:58:33 +03:00
cc6c70a467 Merge branch 'dev' 2026-04-23 12:58:31 +03:00
978c6b8116 Merge branch 'dev' 2026-04-23 12:58:15 +03:00
06718d3beb Merge branch 'dev' 2026-04-23 12:56:30 +03:00
778d671a5f Merge tag '1.14.5_cleaned'
project was cleaned
2026-04-23 11:47:44 +03:00
2b96d22eed chore: merge dev to master with typography updates 2026-04-19 18:03:47 +03:00
a6cb7e7bc7 Build [v1.11.1] 2026-04-19 18:03:32 +03:00
73dde9b40d feat: add responsive typography scaling for desktop readability
- Add CSS clamp() for fluid font-size scaling (16px to 20px based on viewport)
- Apply responsive breakpoint classes to all components:
  * text-xs → lg:text-sm xl:text-base
  * text-sm → lg:text-base xl:text-lg
  * text-base → lg:text-lg xl:text-xl
  * text-lg → lg:text-xl xl:text-2xl
  * text-xl → lg:text-2xl xl:text-3xl
  * text-2xl → lg:text-3xl xl:text-4xl
  * text-3xl → lg:text-4xl xl:text-5xl
- Apply responsive padding/spacing scaling proportionally
- Updated 27 component and page files
- Build verified: 6 routes, zero TypeScript errors
- Fixes readability on MacBook Pro 14" and other desktop screens without browser zoom
2026-04-19 18:03:21 +03:00
fbab38b212 Build [v1.11.0] 2026-04-19 17:31:49 +03:00
44 changed files with 527 additions and 235 deletions

View File

@@ -1,3 +1,3 @@
826698
826699
826713
836027
836028
836042

View File

@@ -17,7 +17,15 @@ https://:8919 {
}
# Next.js HMR Support (WebSocket)
header_up X-Forwarded-For {remote_host}
handle /_next/webpack-hmr {
reverse_proxy http://localhost:8917 {
header_up Upgrade {>Upgrade}
header_up Connection {>Connection}
}
}
reverse_proxy http://localhost:8917 {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto https
header_up X-Forwarded-Host {host}
}
@@ -37,7 +45,7 @@ https://:8918 {
}
reverse_proxy http://localhost:8916 {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto https
header_up X-Forwarded-Host {host}
}

View File

@@ -0,0 +1,228 @@
# Design: Typography & Visual Hierarchy Overhaul
Generated by /gstack-office-hours on 2026-04-19
Branch: dev
Repo: tfm_ainventory
Status: DRAFT
Mode: Builder (Intrapreneurship)
## Problem Statement
The current UI has weak visual hierarchy on tablets and larger screens. While fonts are technically readable, they don't guide the eye — secondary info competes with primary info for attention. Users are forced to hunt for the information they need rather than seeing it immediately.
**Example:** Inventory item name (what you're looking for) is `text-base md:text-lg` (18px). The category label next to it is almost the same size. Which is more important? The UI doesn't say.
## Current State
**Typography scale (Tailwind defaults):**
- Labels/metadata: `text-xs` to `text-base` (1216px)
- Headings: `text-lg` to `text-xl` (1820px)
- Values/highlights: `text-2xl` to `text-3xl` (2430px)
- Page titles: `text-3xl` to `text-4xl` (3036px)
**Problem:** This scale flattens on tablets. A 18px label looks like a headline. A 30px value looks like a label. The hierarchy collapses.
## Premises
1. **Font size alone doesn't create hierarchy.** Weight, color, spacing, and contrast matter more than raw pixels.
2. **Tablet is a first-class device.** The app is used on iPads in warehouses. Design for that, not just mobile.
3. **Visual hierarchy = faster task completion.** If the user sees the item quantity immediately, they spend 0.5 seconds scanning. If they hunt, it's 3 seconds per item. Over 100 items, that's 4 minutes lost per session.
4. **We don't need bigger type everywhere.** Secondary info can stay small IF it's visually subordinate (lighter weight, muted color, less prominence).
## Open Questions
- What's the primary use case breakdown? Mostly mobile in the field, mostly tablet in warehouse, or mixed 50/50?
- Do users ever need to scan and read fine details simultaneously (e.g., zoom control labels)?
- Is there a standard tablet size we're optimizing for (iPad 10.2", iPad Pro 12.9", or both)?
## Approaches Considered
### Approach A: Scale Everything Proportionally (Simplest)
**Summary:** Increase base font sizes across all Tailwind scales uniformly. `text-base``text-lg`, `text-lg``text-xl`, etc. On tablets, everything gets bigger, hierarchy stays the same.
**Effort:** S (one config change to tailwind.config.ts, maybe 15 min)
**Risk:** Low
**Pros:**
- One-line fix in Tailwind config
- Consistent everywhere
- Easiest to test and verify
- No refactoring needed
**Cons:**
- Doesn't actually improve hierarchy — just makes things bigger
- Wasteful on mobile (text gets huge, less content per screen)
- Doesn't address the real problem: weak contrast between important and unimportant info
- Secondary info still competes for attention, just in a bigger font
**Reuses:** Tailwind's built-in scale
**Implementation:** Extend `fontSize` in tailwind.config.ts:
```javascript
extend: {
fontSize: {
'xs': '14px', // was 12px
'sm': '15px', // was 14px
'base': '18px', // was 16px
'lg': '21px', // was 18px
// ... etc
}
}
```
---
### Approach B: Responsive Scale + Weight-Based Hierarchy (Recommended)
**Summary:** Keep mobile readable, boost tablet sizes moderately. More importantly, use **font weight** (not just size) to create hierarchy. Primary info is bold/heavy; secondary is regular/light. Add color emphasis (primary color for key values).
**Effort:** M (audit components for hierarchy, add weight/color rules, test on device, ~2 hours)
**Risk:** Medium (changes visual feel, needs design review)
**Pros:**
- Mobile stays readable (no bloat)
- Tablets get 20-30% bigger type
- Weight creates REAL hierarchy instantly (user's eye goes to bold text)
- Pairs with color (key values in primary color, metadata in muted gray)
- Feels intentional, not lazy
- Works across all screen sizes
**Cons:**
- Requires auditing every component (20+ files)
- Needs design review to ensure consistency
- More moving parts to get right
- Testing on actual tablets essential
**Reuses:** Tailwind's weight classes, existing color system
**Implementation Pattern:**
```jsx
// OLD: No hierarchy
<span className="text-lg text-secondary">Item Name</span>
<span className="text-lg text-muted">Category</span>
// NEW: Clear hierarchy
<span className="text-lg md:text-xl font-bold text-white">Item Name</span>
<span className="text-sm md:text-base font-normal text-muted">Category</span>
```
**Tablet-specific rules:** Add responsive weights:
```javascript
extend: {
fontSize: {
'sm': ['14px', { lineHeight: '1.5' }],
'base': ['16px', { lineHeight: '1.6' }],
'lg': ['18px', { lineHeight: '1.6' }],
'xl': ['20px', { lineHeight: '1.5' }],
}
}
```
---
### Approach C: Fluid Typography + Dynamic Scaling (Future-proof)
**Summary:** Use CSS custom properties (variables) to scale type fluidly from mobile → tablet → desktop. Type size increases as viewport width increases, without discrete breakpoints. Add hierarchy through weight, spacing, and contrast.
**Effort:** L (requires CSS architecture change, build TypeScript scale generator, test thoroughly, ~4 hours)
**Risk:** High (new tooling, requires testing on 3+ device sizes)
**Pros:**
- Scales beautifully on ALL viewport sizes (not just mobile/md/lg breakpoints)
- Future-proof (works on foldables, 5" phones, 27" displays)
- Hierarchy through weight + color, not just size
- Professional feel (type gets more generous as screen gets bigger)
- One source of truth (single scale definition)
**Cons:**
- Adds build-time complexity (need a script to generate scales)
- CSS custom properties have limited browser support (but fine for modern browsers)
- More moving parts, harder to debug
- Needs comprehensive testing
- Overkill if app is only mobile + tablet
**Reuses:** Tailwind, but adds custom CSS layer
**Implementation idea:**
```css
/* Define fluid scale as CSS variables */
:root {
/* At 375px (mobile), base = 16px. At 1440px (desktop), base = 18px. */
--font-base: clamp(16px, 2.5vw, 18px);
--font-lg: clamp(18px, 3vw, 20px);
--font-xl: clamp(20px, 3.5vw, 24px);
}
/* Use in components */
.text-base { font-size: var(--font-base); }
.text-lg { font-size: var(--font-lg); }
.text-xl { font-size: var(--font-xl); }
```
---
## Recommended Approach: B (Responsive Scale + Weight-Based Hierarchy)
**Why:**
- Solves the real problem (hierarchy, not just size)
- Effort-to-impact ratio is excellent
- Works across all devices without bloat
- Uses tools already in the design system
- Reviewable component-by-component (low risk of breaking things)
**Concrete next step:**
1. Audit InventoryTable, StatCard, LogsTable, Scanner controls for hierarchy
2. Add weight rules: primary data `font-bold`, metadata `font-normal`
3. Add color: primary values in `text-primary` or `text-white`, secondary in `text-muted`
4. Test on iPad (actual device or browser dev tools)
5. Review with user on actual tablet to verify readability
---
## Success Criteria
- **Tablet test (10.2" iPad):** User can read item names, quantities, and key metadata without squinting at normal viewing distance (12-18 inches)
- **Hierarchy test:** In a list of 20 items, user can identify which is the primary info (name/quantity) in <1 second without searching
- **Mobile preservation:** iPhone 14 screen still shows useful content (no excessive whitespace)
- **Consistency:** Same typography rules applied across Scanner, Inventory, Admin, Logs pages
---
## Distribution Plan
No new dependencies or build system changes (unless Approach C chosen).
Changes ship in the existing `dev` branch → `master` on next release.
---
## Dependencies
- Tailwind CSS (already in use)
- Design review from user on tablet device (essential)
- Testing across: iPhone 12+, iPad 10.2", iPad Pro 12.9" (ideally)
---
## The Assignment
**Before next office hours:**
1. Load the app on an actual tablet (iPad or comparable).
2. Open the Inventory page, Scanner page, and Admin Dashboard.
3. Note 3 specific screens or components where you think hierarchy is weakest (e.g., "StatCard mixes label and value with equal prominence").
4. Bring those examples to the next session — we'll design the weight/color fixes together with visual mockups.
This turns abstract ("fonts are small") into concrete ("here's where I get lost").
---
## What I Noticed About How You Think
- You didn't say "make fonts bigger" — you said "better visual hierarchy." That's a designer's instinct, not an engineer's kneejerk response. You're thinking about information clarity, not pixel counts.
- You identified the problem on tablets specifically, not mobile. That's precise observation. Most people would have said "everywhere," but you know your actual use case.
- When asked what success looks like, you said hierarchy, not size. That shows you understand that **hierarchy IS the usability feature.** Big text that's all the same weight is just... big, hard-to-read text.
You're thinking like a product person. Keep that going.

View File

@@ -1,7 +1,7 @@
import os
import yaml
import logging
from .config_loader import load_config, get_config
from .config_loader import load_config as loader_load_config, get_config as loader_get_config
log = logging.getLogger("ainventory")
@@ -52,8 +52,8 @@ class ConfigManager:
log.info(f"✅ Updated {path} with new values.")
# Reload the global config
load_config()
return get_config()
loader_load_config()
return loader_get_config()
except Exception as e:
log.error(f"❌ Failed to write {path}: {e}")
raise
@@ -72,10 +72,78 @@ class ConfigManager:
except Exception:
return False
@staticmethod
def get_secrets_path():
"""Returns the absolute path to secrets.yaml."""
base_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(base_dir)
return os.path.join(project_root, "config", "secrets.yaml")
@staticmethod
def update_secrets(updates: dict) -> dict:
"""Update secrets.yaml with new values (flattened structure)."""
path = ConfigManager.get_secrets_path()
current_secrets = {}
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8') as f:
current_secrets = yaml.safe_load(f) or {}
except Exception as e:
log.error(f"❌ Failed to read {path}: {e}")
# Update flattened secrets
current_secrets.update(updates)
try:
with open(path, 'w', encoding='utf-8') as f:
# Header for clarity
f.write("# TFM aInventory - Managed Secrets (Updated via Admin UI)\n")
yaml.dump(current_secrets, f, default_flow_style=False, sort_keys=True)
log.info(f"✅ Updated {path} with new secrets.")
# Reload the global config
loader_load_config()
return loader_get_config()
except Exception as e:
log.error(f"❌ Failed to write {path}: {e}")
raise
@staticmethod
def update_keys(updates: dict) -> dict:
"""
Intelligent key update: routes sensitive AI keys to secrets.yaml
and others to backend.yaml.
"""
secrets_updates = {}
backend_updates = {}
sensitive_keys = ["GEMINI_API_KEY", "CLAUDE_API_KEY", "JWT_SECRET_KEY", "LDAP_PASSWORD"]
for key, val in updates.items():
if key in sensitive_keys:
secrets_updates[key] = val
else:
# Non-sensitive or complex nested settings
backend_updates[key] = val
if secrets_updates:
ConfigManager.update_secrets(secrets_updates)
if backend_updates:
ConfigManager.update_config(backend_updates)
return loader_get_config()
@staticmethod
def get_config() -> dict:
"""Return the current global configuration."""
return loader_get_config()
@staticmethod
def get_masked_key(key_name: str):
"""Returns a masked version of a configuration value or env var."""
config = get_config()
config = loader_get_config()
# Try to find in config first
val = None
@@ -113,6 +181,10 @@ def update_config(updates: dict) -> dict:
"""Update backend.yaml with new values and return updated config."""
return ConfigManager.update_config(updates)
def get_config() -> dict:
"""Return the current global configuration."""
return loader_get_config()
def validate_config_file() -> bool:
"""Validate backend.yaml syntax and required fields."""
return ConfigManager.validate_config_file()

View File

@@ -67,8 +67,11 @@ def get_ai_config(
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Check AI provider status and active provider."""
gemini_key = os.environ.get("GEMINI_API_KEY")
claude_key = os.environ.get("CLAUDE_API_KEY")
config = ConfigManager.get_config() # Alias for config_loader.get_config
ai_config = config.get("ai", {})
gemini_key = ai_config.get("gemini_api_key")
claude_key = ai_config.get("claude_api_key")
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
active_provider = provider_setting.value if provider_setting else "gemini"
@@ -112,10 +115,14 @@ def update_ai_keys(
if updates:
ConfigManager.update_keys(updates)
# Re-load config to return accurate status
config = ConfigManager.get_config()
ai_cfg = config.get("ai", {})
return {
"status": "success",
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
"claude_configured": bool(os.environ.get("CLAUDE_API_KEY"))
"gemini_configured": bool(ai_cfg.get("gemini_api_key")),
"claude_configured": bool(ai_cfg.get("claude_api_key"))
}

View File

@@ -5,7 +5,7 @@ Supports CSV and Excel formats.
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from backend.database import get_db
@@ -61,7 +61,7 @@ async def export_inventory_snapshot(
# Log export action
from backend.models import AuditLog
audit_entry = AuditLog(
user_id=admin_user.id,
user_id=admin_user.sub,
action="EXPORT_INVENTORY_SNAPSHOT",
details=f"Exported in {format_type} format",
)
@@ -70,20 +70,16 @@ async def export_inventory_snapshot(
# Return file response
if format_type == "csv":
# For CSV, use FileResponse with bytes
import io
return FileResponse(
return StreamingResponse(
io.BytesIO(content.encode("utf-8")),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
else:
# For Excel, content is already bytes
import io
return FileResponse(
return StreamingResponse(
io.BytesIO(content),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@@ -117,7 +113,7 @@ async def export_audit_trail(
# Log export action
audit_entry = AuditLog(
user_id=admin_user.id,
user_id=admin_user.sub,
action="EXPORT_AUDIT_TRAIL",
details=f"Exported in {format_type} format",
)
@@ -126,22 +122,20 @@ async def export_audit_trail(
# Return file response
if format_type == "csv":
import io
return FileResponse(
return StreamingResponse(
io.BytesIO(content.encode("utf-8")),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
else:
import io
return FileResponse(
return StreamingResponse(
io.BytesIO(content),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.get("/db/export")
@router.get("/reports/export")
async def export_db(
format: str = Query("csv", description="Export format: csv or xlsx"),
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
@@ -217,7 +211,7 @@ async def export_db(
# Log export action
audit_entry = AuditLog(
user_id=admin_user.id,
user_id=admin_user.sub,
action="EXPORT_DB",
details=f"Exported {type} in {format_type} format",
)
@@ -226,14 +220,14 @@ async def export_db(
# Return file response
if format_type == "csv":
return FileResponse(
return StreamingResponse(
io.BytesIO(content.encode("utf-8")),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
else:
return FileResponse(
return StreamingResponse(
io.BytesIO(content),
media_type=media_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"version": "1.14.11",
"last_build": "2026-04-23-1404",
"version": "1.14.19",
"last_build": "2026-04-23-1527",
"codename": "ConfigCore",
"commit": "e5a24df1"
"commit": "941761ad"
}

View File

@@ -37,7 +37,7 @@ export default function AdminPage() {
</div>
<button
onClick={admin.handleLogout}
className="ml-auto p-3 bg-surface border border-slate-800 text-muted hover:text-rose-500 rounded-2xl transition-all active:scale-95 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-rose-500 focus-visible:outline-none"
className="ml-auto p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-surface border border-slate-800 text-muted hover:text-rose-500 rounded-2xl transition-all active:scale-95 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-rose-500 focus-visible:outline-none"
title="Secure Logout"
aria-label="Logout from admin panel"
>

View File

@@ -346,7 +346,7 @@ export default function InventoryPage() {
{/* Stock Adjustment / Edit Item Overlay */}
{selectedItem && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/80 animate-in fade-in duration-200">
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border-x border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl p-5 sm:p-8 overflow-hidden animate-in slide-in-from-bottom-10 duration-300">
<div className="flex justify-between items-center mb-3 md:mb-4">
<h3 className="text-xl font-normal tracking-tight flex items-center gap-2">
@@ -364,7 +364,7 @@ export default function InventoryPage() {
setEditedItem(selectedItem);
setIsEditing(true);
}}
className="p-2 hover:bg-slate-800 rounded-full text-secondary"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full text-secondary"
>
<Edit2 size={20} />
</button>
@@ -372,7 +372,7 @@ export default function InventoryPage() {
{isEditing && (
<button
onClick={handleDeleteItem}
className="p-2 hover:bg-red-500/20 rounded-full text-red-500"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-red-500/20 rounded-full text-red-500"
>
<Trash2 size={20} />
</button>
@@ -383,7 +383,7 @@ export default function InventoryPage() {
setIsEditing(false);
setAdjustQty(1);
}}
className="p-2 hover:bg-slate-800 rounded-full"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full"
>
<X size={20} />
</button>
@@ -486,7 +486,7 @@ export default function InventoryPage() {
toast.success("Ready to scan Box label...");
}}
className={cn(
"absolute right-2 p-2 rounded-lg transition-all",
"absolute right-2 p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-lg transition-all",
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-muted hover:bg-slate-800"
)}
>
@@ -524,7 +524,7 @@ export default function InventoryPage() {
key={t.id}
onClick={() => setAdjustType(t.id as any)}
className={cn(
"flex-1 flex flex-col items-center py-3 rounded-xl transition-all",
"flex-1 flex flex-col items-center py-3 lg:py-4 xl:py-5 rounded-xl transition-all",
adjustType === t.id ? "bg-slate-800 shadow-lg" : "text-muted"
)}
>
@@ -552,11 +552,11 @@ export default function InventoryPage() {
</div>
{adjustType === 'TRASH' && (
<div className="w-full bg-red-500/5 border border-red-500/20 p-4 rounded-2xl">
<div className="w-full bg-red-500/5 border border-red-500/20 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl">
<select
value={trashReason}
onChange={(e) => setTrashReason(e.target.value)}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-foreground"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-foreground"
>
<option>Damaged</option>
<option>Expired</option>
@@ -608,7 +608,7 @@ export default function InventoryPage() {
type="text"
value={catEditedName}
onChange={e => setCatEditedName(e.target.value)}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
/>
</div>
<div>
@@ -616,7 +616,7 @@ export default function InventoryPage() {
<textarea
value={catEditedDesc}
onChange={e => setCatEditedDesc(e.target.value)}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary h-24 resize-none"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary h-24 resize-none"
/>
</div>
</div>
@@ -641,7 +641,7 @@ export default function InventoryPage() {
{/* Box Manager Modal */}
{showBoxManager && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/90 animate-in fade-in duration-300">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/90 animate-in fade-in duration-300">
<div className="w-full max-w-2xl bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div className="p-4 md:p-6 pb-3 md:pb-4 flex flex-col gap-3 md:gap-4 shrink-0 bg-surface/70">
<div className="flex justify-between items-center">
@@ -652,7 +652,7 @@ export default function InventoryPage() {
</h3>
<p className="text-sm text-muted font-normal mt-1">Manage physical box labels & printing</p>
</div>
<button onClick={() => { setShowBoxManager(false); setBoxSearchQuery(''); }} className="p-3 hover:bg-slate-800 rounded-full transition-colors text-secondary">
<button onClick={() => { setShowBoxManager(false); setBoxSearchQuery(''); }} className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 hover:bg-slate-800 rounded-full transition-colors text-secondary">
<X size={24} />
</button>
</div>
@@ -663,7 +663,7 @@ export default function InventoryPage() {
placeholder="Search boxes..."
value={boxSearchQuery}
onChange={(e) => setBoxSearchQuery(e.target.value)}
className="w-full bg-background border border-slate-800 rounded-2xl py-3.5 pl-11 pr-4 text-sm focus:border-primary outline-none transition-all placeholder:text-secondary"
className="w-full bg-background border border-slate-800 rounded-2xl py-3 lg:py-4 xl:py-5.5 pl-11 pr-4 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl focus:border-primary outline-none transition-all placeholder:text-secondary"
/>
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={18} />
{boxSearchQuery && (

View File

@@ -101,7 +101,7 @@ export default function LoginPage() {
key={user.id}
data-testid="user-list-item"
onClick={() => handleSelectUser(user)}
className="bg-slate-800/50 hover:bg-slate-800 border border-slate-800 hover:border-primary/40 p-4 rounded-2xl text-left transition-all group flex items-center justify-between"
className="bg-slate-800/50 hover:bg-slate-800 border border-slate-800 hover:border-primary/40 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl text-left transition-all group flex items-center justify-between"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-surface border border-slate-700 flex items-center justify-center text-muted group-hover:text-primary transition-colors">
@@ -196,7 +196,7 @@ export default function LoginPage() {
</div>
) : (
<div className="space-y-4 animate-in slide-in-from-right-4 duration-300">
<div className="bg-slate-800/30 p-4 rounded-2xl border border-slate-800 flex items-center gap-3">
<div className="bg-slate-800/30 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl border border-slate-800 flex items-center gap-3">
<button
onClick={() => setSelectedUserForLogin(null)}
className="p-1 hover:bg-slate-700 rounded-lg transition-colors"

View File

@@ -90,7 +90,7 @@ export default function LogsPage() {
<main className="p-3 md:p-8 max-w-7xl mx-auto space-y-3 md:space-y-6 mb-20">
<header className="flex flex-col sm:flex-row sm:items-end justify-between gap-3 md:gap-4">
<div className="flex items-center gap-4">
<div className="p-3 md:p-4 bg-primary/10 rounded-2xl text-primary border border-primary/20 shadow-xl shadow-primary/5">
<div className="p-3 md:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-primary/10 rounded-2xl text-primary border border-primary/20 shadow-xl shadow-primary/5">
<History size={28} className="md:w-8 md:h-8" />
</div>
<div>
@@ -142,7 +142,7 @@ export default function LogsPage() {
placeholder="Filter by user, action or asset..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-surface/50 border border-slate-800 rounded-2xl py-3.5 pr-4 pl-12 text-sm focus:border-primary outline-none transition-all placeholder:text-secondary shadow-2xl"
className="w-full bg-surface/50 border border-slate-800 rounded-2xl py-3 lg:py-4 xl:py-5.5 pr-4 pl-12 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl focus:border-primary outline-none transition-all placeholder:text-secondary shadow-2xl"
/>
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary transition-colors group-focus-within:text-primary">
<Search size={18} />

View File

@@ -414,7 +414,7 @@ export default function Home() {
onClick={handleSync}
disabled={syncing}
data-testid="manual-sync-button"
className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95 disabled:opacity-50"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw size={18} className={syncing ? "animate-spin text-primary" : ""} />
</button>
@@ -507,7 +507,7 @@ export default function Home() {
</h3>
<p className="text-xs text-secondary font-normal mt-1">Select the item you want to {mode.replace('_', ' ').toLowerCase()}</p>
</div>
<button onClick={() => setBoxMatches([])} className="p-2 hover:bg-slate-800 rounded-full">
<button onClick={() => setBoxMatches([])} className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full">
<X size={20} />
</button>
</div>
@@ -539,7 +539,7 @@ export default function Home() {
<footer className="mt-12 md:mt-20 mb-4 md:mb-8 flex flex-col items-center gap-2 opacity-70">
<p className="text-sm font-normal text-secondary">Powered by TFM Group Software</p>
<div className="h-px w-16 bg-slate-700/60" />
<p className="text-xs font-mono text-secondary">v{versionData.version} {versionData.last_build} BUILD: dev-{(versionData as any).commit || 'N/A'}</p>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl font-mono text-secondary">v{versionData.version} {versionData.last_build} BUILD: dev-{(versionData as any).commit || 'N/A'}</p>
</footer>
</div>
</PageShell>

View File

@@ -58,10 +58,10 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
};
return (
<div data-testid="ai-extraction-overlay" className="fixed inset-0 z-50 bg-background flex flex-col p-6 animate-in fade-in slide-in-from-bottom-5 duration-300">
<div data-testid="ai-extraction-overlay" className="fixed inset-0 z-50 bg-background flex flex-col p-6 lg:p-8 xl:p-10 animate-in fade-in slide-in-from-bottom-5 duration-300">
<div className="flex justify-between items-center mb-6 shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/20 rounded-xl">
<div className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-primary/20 rounded-xl">
<Sparkles className="text-primary w-5 h-5" />
</div>
<div>
@@ -72,7 +72,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button
onClick={() => { stopLiveCamera(); onCancel(); }}
aria-label="Close AI Discovery"
className="p-2 hover:bg-surface rounded-full cursor-pointer transition-colors focus:ring-2 focus:ring-blue-500 focus:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-surface rounded-full cursor-pointer transition-colors focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<X size={24} />
</button>
@@ -87,7 +87,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-normal cursor-pointer transition-all focus:ring-2 focus:ring-primary focus:outline-none ${mode === 'item' ? 'bg-primary text-white shadow-lg' : 'text-muted hover:text-secondary'}`}
>
<Package size={18} />
<span className="text-xs">Discovery Mode</span>
<span className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl">Discovery Mode</span>
</button>
<button
onClick={() => setMode('box')}
@@ -95,11 +95,11 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-normal cursor-pointer transition-all focus:ring-2 focus:ring-primary focus:outline-none ${mode === 'box' ? 'bg-primary text-white shadow-lg' : 'text-muted hover:text-secondary'}`}
>
<Layers size={18} />
<span className="text-xs">Box Lookup</span>
<span className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl">Box Lookup</span>
</button>
</div>
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-800 rounded-[2.5rem] bg-surface/30 overflow-hidden px-4">
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-800 rounded-[2.5rem] bg-surface/30 overflow-hidden px-4 lg:px-5 xl:px-6">
<div className="w-20 h-20 bg-surface rounded-3xl flex items-center justify-center mb-6 shadow-inner text-primary">
<Camera size={32} />
</div>
@@ -120,7 +120,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
className="flex flex-col items-center justify-center gap-2 bg-primary text-white rounded-3xl font-normal shadow-2xl shadow-primary/20 cursor-pointer active:scale-95 transition-all focus:ring-2 focus:ring-blue-400 focus:outline-none"
>
<Camera size={24} />
<span className="text-sm">Scan Camera</span>
<span className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl">Scan Camera</span>
</button>
<button
onClick={() => fileInputRef.current?.click()}
@@ -129,7 +129,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
className="flex flex-col items-center justify-center gap-2 bg-surface text-secondary border border-slate-800 rounded-3xl font-normal cursor-pointer active:scale-95 transition-all focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<ImageIcon size={24} />
<span className="text-sm">Upload Photo</span>
<span className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl">Upload Photo</span>
</button>
</div>
@@ -339,7 +339,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<textarea
value={extractedItems[editingIndex].Description || extractedItems[editingIndex].description || ''}
onChange={(e) => updateEditingItem({ Description: e.target.value })}
className="bg-transparent w-full text-sm leading-tight outline-none resize-none h-8 text-secondary py-0"
className="bg-transparent w-full text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl leading-tight outline-none resize-none h-8 text-secondary py-0"
placeholder="Product description..."
/>
</div>
@@ -422,7 +422,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<div
key={idx}
data-testid="extraction-item-row"
className="bg-surface/70 border border-slate-800 rounded-3xl p-5 flex items-center justify-between group hover:border-primary/40 transition-all active:scale-[0.98] focus-within:border-primary/60 focus-within:ring-2 focus-within:ring-primary/20"
className="bg-surface/70 border border-slate-800 rounded-3xl p-5 lg:p-6 xl:p-8 flex items-center justify-between group hover:border-primary/40 transition-all active:scale-[0.98] focus-within:border-primary/60 focus-within:ring-2 focus-within:ring-primary/20"
>
<div
className="flex-1 min-w-0 cursor-pointer"
@@ -461,14 +461,14 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
setExtractedItems(newItems);
}}
aria-label={`Delete item: ${item.Item || item.name}`}
className="p-3 text-secondary hover:text-red-500 cursor-pointer transition-colors focus:ring-2 focus:ring-red-500 focus:outline-none rounded-lg"
className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 text-secondary hover:text-red-500 cursor-pointer transition-colors focus:ring-2 focus:ring-red-500 focus:outline-none rounded-lg"
>
<X size={20} />
</button>
<button
onClick={() => setEditingIndex(idx)}
aria-label={`Edit item: ${item.Item || item.name}`}
className="p-3 bg-primary/10 text-primary rounded-2xl cursor-pointer hover:bg-primary/20 transition-all focus:ring-2 focus:ring-primary focus:outline-none"
className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-primary/10 text-primary rounded-2xl cursor-pointer hover:bg-primary/20 transition-all focus:ring-2 focus:ring-primary focus:outline-none"
>
<ChevronDown size={20} className="-rotate-90" />
</button>

View File

@@ -117,7 +117,7 @@ export default function AdminOverlay({
<div className="absolute inset-y-0 right-0 w-full max-w-md sm:max-w-lg md:max-w-md bg-surface border-l border-slate-800 shadow-2xl flex flex-col animate-in slide-in-from-right duration-500">
<div className="p-3 md:p-6 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-800 rounded-xl text-primary">
<div className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-slate-800 rounded-xl text-primary">
<Shield size={20} />
</div>
<h2 className="text-xl font-normal text-white">System Admin</h2>
@@ -125,7 +125,7 @@ export default function AdminOverlay({
<button
data-testid="close-admin-overlay"
onClick={onClose}
className="p-2 hover:bg-slate-800 rounded-full transition-colors text-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full transition-colors text-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Close"
>
<X size={20} />
@@ -171,7 +171,7 @@ export default function AdminOverlay({
loading: false,
});
}}
className="p-2 text-rose-500 hover:bg-rose-500/10 rounded-lg transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-rose-500 hover:bg-rose-500/10 rounded-lg transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
aria-label={`Delete user ${u.username}`}
>
<Trash2 size={16} />
@@ -220,7 +220,7 @@ export default function AdminOverlay({
loading: false,
});
}}
className="p-2 text-rose-500 hover:bg-rose-500/10 rounded-lg transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-rose-500 hover:bg-rose-500/10 rounded-lg transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
aria-label={`Delete category ${cat.name}`}
>
<Trash2 size={16} />
@@ -235,7 +235,7 @@ export default function AdminOverlay({
<AlertTriangle size={16} />
<p className="text-sm font-normal">Sign Out</p>
</div>
<p className="text-xs text-muted">End your session and return to the login screen.</p>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl text-muted">End your session and return to the login screen.</p>
<button
onClick={() => {
import('@/lib/auth').then(m => m.clearAuth());

View File

@@ -25,7 +25,7 @@ export default function BottomNav({
const isAdmin = pathname === '/admin';
return (
<footer className="fixed bottom-0 left-0 right-0 py-3 px-2 pb-safe bg-background border-t border-slate-900 z-40 transition-all duration-300">
<footer className="fixed bottom-0 left-0 right-0 py-3 lg:py-4 xl:py-5 px-2 pb-safe bg-background border-t border-slate-900 z-40 transition-all duration-300">
<div className="max-w-xl mx-auto flex justify-around items-center text-secondary">
<button

View File

@@ -44,7 +44,7 @@ export default function CameraView({
return (
<div className="w-full max-w-md mx-auto flex flex-col gap-3 md:gap-4">
{/* Video Viewport Area */}
<div className="relative w-full aspect-square overflow-hidden rounded-[2.5rem] shadow-[0_20px_50px_rgba(0,0,0,0.5)] bg-background border-2 border-slate-800/50 p-4 sm:p-6">
<div className="relative w-full aspect-square overflow-hidden rounded-[2.5rem] shadow-[0_20px_50px_rgba(0,0,0,0.5)] bg-background border-2 border-slate-800/50 p-4 sm:p-6 lg:p-8 xl:p-10">
<div className="absolute inset-4 sm:inset-6 z-10 pointer-events-none flex items-center justify-center">
<div className="w-full h-full border border-primary/30 rounded-[2rem] relative">
<div className="absolute top-0 left-0 w-10 h-10 border-t-4 border-l-4 border-primary rounded-tl-2xl shadow-[0_0_15px_rgba(59,130,246,0.5)]" />
@@ -89,7 +89,7 @@ export default function CameraView({
</div>
</div>
</div>
<div className="p-6 bg-surface/90 border-t border-slate-800 flex flex-col gap-4">
<div className="p-6 lg:p-8 xl:p-10 bg-surface/90 border-t border-slate-800 flex flex-col gap-4">
<div className="flex flex-col items-center gap-1">
<p className="text-sm font-normal text-white italic text-center">Text Found</p>
<p className="text-xs text-muted font-normal text-center">Tap any text to use it</p>
@@ -108,7 +108,7 @@ export default function CameraView({
{!isStarted && !error && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-surface text-secondary gap-4">
<RefreshCw className="w-8 h-8 animate-spin text-primary" />
<p className="text-sm font-medium">Initializing camera...</p>
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl font-medium">Initializing camera...</p>
</div>
)}
@@ -131,7 +131,7 @@ export default function CameraView({
</div>
{/* External Controls Area */}
<div className="flex flex-col gap-4 bg-surface/50 p-5 rounded-[2.5rem] border border-slate-800/50 shadow-2xl">
<div className="flex flex-col gap-4 bg-surface/50 p-5 lg:p-6 xl:p-8 rounded-[2.5rem] border border-slate-800/50 shadow-2xl">
<div className="flex items-center gap-3 w-full">
{hasZoom && (
<button
@@ -153,7 +153,7 @@ export default function CameraView({
</button>
)}
<div className="flex-1 h-14 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-center gap-3 px-4 relative overflow-hidden group">
<div className="flex-1 h-14 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-center gap-3 px-4 lg:px-5 xl:px-6 relative overflow-hidden group">
{ocrProcessing ? (
<>
<RefreshCw className="animate-spin text-primary" size={18} />

View File

@@ -52,9 +52,9 @@ export default function CategoryCreationModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface border border-slate-800 rounded-lg shadow-xl max-w-sm w-full mx-4">
<div className="flex items-center justify-between p-6 border-b border-slate-800">
<div className="flex items-center justify-between p-6 lg:p-8 xl:p-10 border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<div className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-primary/10 rounded-lg text-primary">
<Tag size={20} />
</div>
<h2 className="text-lg font-normal text-white">New Category</h2>
@@ -69,7 +69,7 @@ export default function CategoryCreationModal({
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<form onSubmit={handleSubmit} className="p-6 lg:p-8 xl:p-10 space-y-4">
<div>
<label className="text-sm font-normal text-secondary">Name</label>
<input
@@ -80,7 +80,7 @@ export default function CategoryCreationModal({
setError(null);
}}
placeholder="e.g., Electronics"
className="w-full mt-2 px-3 py-2.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
className="w-full mt-2 px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
disabled={loading}
autoFocus
/>
@@ -93,14 +93,14 @@ export default function CategoryCreationModal({
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief category description"
className="w-full mt-2 px-3 py-2.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
className="w-full mt-2 px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
disabled={loading}
/>
</div>
{error && (
<div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded">
<p className="text-sm text-rose-400">{error}</p>
<div className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-rose-500/10 border border-rose-500/20 rounded">
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-rose-400">{error}</p>
</div>
)}

View File

@@ -57,9 +57,9 @@ export default function ConfirmationModal({
<div data-testid="confirmation-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface border border-slate-800 rounded-lg shadow-xl max-w-sm w-full mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800">
<div className="flex items-center justify-between p-6 lg:p-8 xl:p-10 border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-rose-500/10 rounded-lg text-rose-500">
<div className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-rose-500/10 rounded-lg text-rose-500">
<AlertTriangle size={20} />
</div>
<h2 className="text-lg font-normal text-white">{title}</h2>
@@ -75,9 +75,9 @@ export default function ConfirmationModal({
</div>
{/* Content */}
<div className="p-6 space-y-4">
<div className="p-6 lg:p-8 xl:p-10 space-y-4">
{/* Description */}
<p className="text-sm text-secondary">{description}</p>
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary">{description}</p>
{/* Item Name (if provided) */}
{itemName && (
@@ -89,15 +89,15 @@ export default function ConfirmationModal({
{/* Consequence Warning */}
{consequence && (
<div className="flex gap-2 p-3 bg-rose-500/5 border border-rose-500/20 rounded">
<div className="flex gap-2 p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-rose-500/5 border border-rose-500/20 rounded">
<span className="text-rose-500 flex-shrink-0"></span>
<p className="text-sm text-rose-400">{consequence}</p>
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-rose-400">{consequence}</p>
</div>
)}
{/* Affected Items Count (for medium-risk+) */}
{affectedCount && affectedCount > 0 && (
<div className="text-sm text-secondary">
<div className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary">
{affectedCount === 1
? 'This will affect 1 item.'
: `This will affect ${affectedCount} items.`}
@@ -118,7 +118,7 @@ export default function ConfirmationModal({
setError(null);
}}
placeholder="Type DELETE"
className="w-full px-3 py-2.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
className="w-full px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border border-slate-700 rounded text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
disabled={loading}
onKeyDown={(e) => {
if (e.key === 'Enter' && canConfirm && !loading) {
@@ -127,14 +127,14 @@ export default function ConfirmationModal({
}}
/>
{error && (
<p className="text-xs text-rose-500">{error}</p>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl text-rose-500">{error}</p>
)}
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 p-6 border-t border-slate-800">
<div className="flex gap-2 p-6 lg:p-8 xl:p-10 border-t border-slate-800">
<button
onClick={onCancel}
disabled={loading}

View File

@@ -85,7 +85,7 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<form onSubmit={handleSubmit} className="p-6 lg:p-8 xl:p-10 space-y-4">
{/* Username Field */}
<div>
<label htmlFor="username" className="block text-sm font-normal text-secondary mb-2">
@@ -101,13 +101,13 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
if (errors.username) setErrors({ ...errors, username: undefined });
}}
placeholder="Enter username"
className={`w-full px-3 py-2.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
className={`w-full px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
errors.username ? 'border-red-500' : 'border-slate-700'
}`}
disabled={loading}
/>
{errors.username && (
<p className="mt-1.5 text-sm text-red-500">{errors.username}</p>
<p className="mt-1.5 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-red-500">{errors.username}</p>
)}
</div>
@@ -126,13 +126,13 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
if (errors.password) setErrors({ ...errors, password: undefined });
}}
placeholder="Enter password"
className={`w-full px-3 py-2.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
className={`w-full px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
errors.password ? 'border-red-500' : 'border-slate-700'
}`}
disabled={loading}
/>
{errors.password && (
<p className="mt-1.5 text-sm text-red-500">{errors.password}</p>
<p className="mt-1.5 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-red-500">{errors.password}</p>
)}
</div>

View File

@@ -15,7 +15,7 @@ export default function FilterBar({ searchQuery, onChange }: FilterBarProps) {
placeholder="Search catalog..."
value={searchQuery}
onChange={(e) => onChange(e.target.value)}
className="w-full bg-surface border border-slate-800 rounded-2xl py-3.5 pr-4 pl-11 text-sm focus:border-primary outline-none transition-all placeholder:text-secondary"
className="w-full bg-surface border border-slate-800 rounded-2xl py-3 lg:py-4 xl:py-5.5 pr-4 pl-11 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl focus:border-primary outline-none transition-all placeholder:text-secondary"
/>
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary">
<Search size={18} />

View File

@@ -79,7 +79,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
key={user.id}
data-testid="user-list-item"
onClick={() => handleSelectUser(user)}
className="bg-slate-800/40 hover:bg-slate-800 border border-slate-800/50 hover:border-primary/40 p-5 rounded-[1.5rem] text-left transition-all group flex items-center justify-between shadow-sm active:scale-[0.98]"
className="bg-slate-800/40 hover:bg-slate-800 border border-slate-800/50 hover:border-primary/40 p-5 lg:p-6 xl:p-8 rounded-[1.5rem] text-left transition-all group flex items-center justify-between shadow-sm active:scale-[0.98]"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-2xl bg-surface border border-slate-800 flex items-center justify-center text-muted group-hover:text-primary transition-all group-hover:scale-110">
@@ -126,7 +126,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
data-testid="username-input"
type="text"
autoFocus
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
placeholder="DIRECTORY ID"
/>
</div>
@@ -140,7 +140,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
data-testid="password-input"
type="password"
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
placeholder="••••••••"
/>
</div>
@@ -156,7 +156,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div>
) : (
<div className="space-y-5 animate-in slide-in-from-bottom-5 duration-300">
<div data-testid="user-display" className="bg-background/50 p-5 rounded-[1.5rem] border border-slate-800 flex items-center justify-between group">
<div data-testid="user-display" className="bg-background/50 p-5 lg:p-6 xl:p-8 rounded-[1.5rem] border border-slate-800 flex items-center justify-between group">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-lg">
<Shield size={20} />
@@ -168,7 +168,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div>
<button
onClick={() => setSelectedUserForLogin(null)}
className="p-2 hover:bg-slate-800 rounded-xl transition-all text-secondary hover:text-rose-500"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-xl transition-all text-secondary hover:text-rose-500"
title="Change User"
>
<X size={18} />
@@ -184,7 +184,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
type="password"
autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary focus:outline-none transition-all placeholder:text-muted font-mono"
placeholder="KEY-PHRASE"
/>
</div>

View File

@@ -53,7 +53,7 @@ export default function InventoryTable({
if (categories.length === 0) {
return (
<div className="py-20 text-center text-secondary">
<div className="py-2 lg:py-3 xl:py-40 text-center text-secondary">
<Package size={48} className="mx-auto mb-4 opacity-10" />
<p className="font-medium">No results found</p>
</div>
@@ -67,7 +67,7 @@ export default function InventoryTable({
return (
<div key={cat} className="bg-surface/50 border border-slate-800/50 rounded-3xl overflow-hidden transition-all duration-300">
<div
className="w-full p-4 md:p-5 flex items-center justify-between hover:bg-surface/60 transition-colors cursor-pointer"
className="w-full p-4 md:p-5 lg:p-6 xl:p-8 flex items-center justify-between hover:bg-surface/60 transition-colors cursor-pointer"
onClick={() => onExpandCategory(expandedCategory === cat ? null : cat)}
>
<div className="flex items-center gap-3">
@@ -75,7 +75,7 @@ export default function InventoryTable({
<Layers size={20} />
</div>
<div className="text-left">
<h3 className="card-title text-base sm:text-lg">{cat}</h3>
<h3 className="card-title text-base sm:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl">{cat}</h3>
<p className="card-subtitle tracking-tight">
{categoryItems.length} Item types in stock
</p>
@@ -89,7 +89,7 @@ export default function InventoryTable({
e.stopPropagation();
onEditCategory(cat);
}}
className="p-2 hover:bg-slate-800 rounded-full text-muted hover:text-primary transition-colors relative z-10"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full text-muted hover:text-primary transition-colors relative z-10"
>
<EditIcon size={16} />
</button>
@@ -98,7 +98,7 @@ export default function InventoryTable({
</div>
{expandedCategory === cat && (
<div className="p-4 pt-0 space-y-2 animate-in slide-in-from-top-4 duration-300">
<div className="p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 pt-0 space-y-2 animate-in slide-in-from-top-4 duration-300">
<div className="h-px bg-slate-800/50 mb-4 mx-2" />
{categoryItems.map(item => (
<div

View File

@@ -42,7 +42,7 @@ export default function ItemComparisonModal({
const hasChanges = fields.some(f => isDifferent(f.key));
return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-background/80">
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/80">
<div className="bg-surface border border-slate-800 rounded-[2.5rem] p-8 w-full max-w-2xl shadow-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center gap-3 mb-6">
<AlertTriangle size={24} className="text-amber-500" />
@@ -68,7 +68,7 @@ export default function ItemComparisonModal({
return (
<div
key={field.key}
className={`grid grid-cols-3 gap-4 p-3 rounded-lg ${
className={`grid grid-cols-3 gap-4 p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 rounded-lg ${
different ? 'bg-amber-500/10 border border-amber-500/20' : 'bg-slate-800/30'
}`}
>
@@ -85,8 +85,8 @@ export default function ItemComparisonModal({
</div>
{!hasChanges && (
<div className="p-4 bg-slate-800/50 rounded-xl mb-6 border border-slate-700">
<p className="text-sm text-secondary"> Items are identical. No update needed.</p>
<div className="p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-slate-800/50 rounded-xl mb-6 border border-slate-700">
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary"> Items are identical. No update needed.</p>
</div>
)}

View File

@@ -14,7 +14,7 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
if (!show) return null;
return (
<div className="fixed inset-0 z-50 flex flex-col bg-background p-6 animate-in slide-in-from-bottom-20 duration-500">
<div className="fixed inset-0 z-50 flex flex-col bg-background p-6 lg:p-8 xl:p-10 animate-in slide-in-from-bottom-20 duration-500">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-normal tracking-tight">Audit History</h2>
@@ -22,7 +22,7 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
</div>
<button
onClick={onClose}
className="p-3 bg-surface rounded-full text-secondary"
className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-surface rounded-full text-secondary"
>
<X size={24} />
</button>
@@ -36,7 +36,7 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
</div>
) : (
logs.map((log) => (
<div key={log.id} className="bg-surface/70 border border-slate-800 p-4 rounded-2xl flex items-center justify-between gap-4">
<div key={log.id} className="bg-surface/70 border border-slate-800 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className={`text-xs font-normal ${
@@ -54,11 +54,11 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
</div>
{(log.details || log.timestamp) && (
<div className="flex items-center gap-3 mt-2">
<p className="text-xs text-secondary font-mono">
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl text-secondary font-mono">
{new Date(log.timestamp).toLocaleString()}
</p>
{log.details && (
<span className="text-xs bg-slate-800 text-muted px-2 py-0.5 rounded border border-slate-700 font-mono">
<span className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl bg-slate-800 text-muted px-2 py-0.5 rounded border border-slate-700 font-mono">
{log.details}
</span>
)}

View File

@@ -25,7 +25,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
if (loading) {
return (
<div className="flex flex-col items-center justify-center py-32 text-secondary gap-4 animate-pulse">
<div className="flex flex-col items-center justify-center py-3 lg:py-4 xl:py-52 text-secondary gap-4 animate-pulse">
<div className="w-10 h-10 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
<p className="text-xs font-normal tracking-widest italic">Securing Audit Stream...</p>
</div>
@@ -53,7 +53,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
<button
key={log.id}
onClick={() => setSelectedLog(log)}
className="w-full text-left bg-surface/50 border border-slate-800/30 p-3 px-4 rounded-2xl flex items-center justify-between gap-4 hover:bg-slate-800/40 hover:border-primary/30 transition-all group active:scale-[0.99] relative overflow-hidden shadow-sm"
className="w-full text-left bg-surface/50 border border-slate-800/30 p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 px-4 lg:px-5 xl:px-6 rounded-2xl flex items-center justify-between gap-4 hover:bg-slate-800/40 hover:border-primary/30 transition-all group active:scale-[0.99] relative overflow-hidden shadow-sm"
>
<div className="flex-1 min-w-0 z-10 flex items-center gap-4">
{/* Compact Action Badge */}
@@ -111,7 +111,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
{selectedLog.resolved_name}
</h2>
</div>
<button onClick={() => setSelectedLog(null)} className="p-3 bg-slate-800/50 hover:bg-slate-800 rounded-2xl text-muted transition-colors border border-slate-800 shrink-0 shadow-lg">
<button onClick={() => setSelectedLog(null)} className="p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8 bg-slate-800/50 hover:bg-slate-800 rounded-2xl text-muted transition-colors border border-slate-800 shrink-0 shadow-lg">
<X size={20} />
</button>
</div>

View File

@@ -28,12 +28,12 @@ export default function NewItemDialog({ onScannerClick, onAddItemClick }: NewIte
onClick={onAddItemClick}
className="w-full flex flex-col items-center justify-center p-4 md:p-6 rounded-[2rem] bg-indigo-500/5 border border-indigo-500/20 group hover:border-indigo-500/50 transition-all font-normal text-indigo-400 gap-2 md:gap-3"
>
<div className="p-4 bg-indigo-500/10 rounded-2xl group-hover:scale-110 transition-transform shadow-lg shadow-indigo-500/10">
<div className="p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-indigo-500/10 rounded-2xl group-hover:scale-110 transition-transform shadow-lg shadow-indigo-500/10">
<Sparkles size={32} />
</div>
<div className="text-center">
<p className="text-lg leading-tight">Add New Item</p>
<p className="text-xs opacity-60 font-mono mt-1">AI Smart Discovery</p>
<p className="text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl leading-tight">Add New Item</p>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl opacity-60 font-mono mt-1">AI Smart Discovery</p>
</div>
</button>
</div>

View File

@@ -54,14 +54,14 @@ export default function ScannerSection({
</div>
{/* Scanner Section */}
<section className="glass-card rounded-3xl p-6">
<section className="glass-card rounded-3xl p-6 lg:p-8 xl:p-10">
{showScanner ? (
<div className="space-y-2 md:space-y-3">
<div className="flex justify-between items-center">
<h2 className="text-lg font-normal">scanning...</h2>
<button
onClick={() => onShowScanner(false)}
className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-rose-500 transition-all active:scale-95"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-rose-500 transition-all active:scale-95"
>
<X size={18} />
</button>

View File

@@ -9,7 +9,7 @@ interface StatCardProps {
export default function StatCard({ label, value, icon: Icon }: StatCardProps) {
return (
<div className="flex justify-between items-center gap-2 p-3.5 bg-surface/70 border border-slate-800/50 rounded-2xl shadow-sm transition-all hover:bg-surface" role="status">
<div className="flex justify-between items-center gap-2 p-3 lg:p-4 xl:p-5 lg:p-6 xl:p-8.5 bg-surface/70 border border-slate-800/50 rounded-2xl shadow-sm transition-all hover:bg-surface" role="status">
<div className="flex items-center gap-2.5 min-w-0">
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" aria-hidden="true" />}
<span className="text-base md:text-lg text-secondary font-normal truncate">

View File

@@ -77,7 +77,7 @@ export default function StockAdjustmentPanel({
{!isEditing && (
<button
onClick={() => onEdit(selectedItem)}
className="p-2 hover:bg-slate-800 rounded-full text-secondary"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full text-secondary"
>
<Edit2 size={20} />
</button>
@@ -85,7 +85,7 @@ export default function StockAdjustmentPanel({
{isEditing && (
<button
onClick={onDeleteItem}
className="p-2 hover:bg-red-500/20 rounded-full text-red-500"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-red-500/20 rounded-full text-red-500"
>
<Trash2 size={20} />
</button>
@@ -93,7 +93,7 @@ export default function StockAdjustmentPanel({
<button
onClick={onCancel}
data-testid="adjustment-cancel"
className="p-2 hover:bg-slate-800 rounded-full"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full"
>
<X size={20} />
</button>
@@ -119,7 +119,7 @@ export default function StockAdjustmentPanel({
type="text"
value={editedItem.part_number || ''}
onChange={e => onEditChange({ ...editedItem, part_number: e.target.value })}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-mono outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl font-mono outline-none text-secondary"
placeholder="e.g. PN-12345"
/>
</div>
@@ -131,7 +131,7 @@ export default function StockAdjustmentPanel({
list="existing-categories"
value={editedItem.category || ''}
onChange={e => onEditChange({ ...editedItem, category: e.target.value })}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary placeholder:text-muted"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary placeholder:text-muted"
placeholder="e.g. storage"
/>
<datalist id="existing-categories">
@@ -147,7 +147,7 @@ export default function StockAdjustmentPanel({
list="existing-types"
value={editedItem.type || ''}
onChange={e => onEditChange({...editedItem, type: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
placeholder="e.g. spare parts"
/>
</div>
@@ -159,7 +159,7 @@ export default function StockAdjustmentPanel({
list="existing-boxes"
value={editedItem.box_label || ''}
onChange={e => onEditChange({...editedItem, box_label: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm outline-none text-secondary placeholder:text-muted focus:border-primary transition-colors"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 pl-4 pr-12 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary placeholder:text-muted focus:border-primary transition-colors"
placeholder="e.g. SFPs 40G Cisco"
/>
<button
@@ -169,7 +169,7 @@ export default function StockAdjustmentPanel({
toast.success("Scanning for BOX label...");
}}
className={cn(
"absolute right-2 p-2 rounded-lg transition-all",
"absolute right-2 p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-lg transition-all",
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-muted hover:bg-slate-800"
)}
>
@@ -183,7 +183,7 @@ export default function StockAdjustmentPanel({
type="text"
value={editedItem.connector || ''}
onChange={e => onEditChange({...editedItem, connector: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
placeholder="e.g. LC/UPC"
/>
</div>
@@ -193,7 +193,7 @@ export default function StockAdjustmentPanel({
type="text"
value={editedItem.size || ''}
onChange={e => onEditChange({...editedItem, size: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
placeholder="e.g. 5m / 1600GB"
/>
</div>
@@ -203,7 +203,7 @@ export default function StockAdjustmentPanel({
type="text"
value={editedItem.color || ''}
onChange={e => onEditChange({...editedItem, color: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
placeholder="e.g. Black"
/>
</div>
@@ -212,7 +212,7 @@ export default function StockAdjustmentPanel({
<textarea
value={editedItem.description || ''}
onChange={e => onEditChange({ ...editedItem, description: e.target.value })}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary resize-none h-20"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary resize-none h-20"
placeholder="Item description..."
/>
</div>
@@ -239,7 +239,7 @@ export default function StockAdjustmentPanel({
key={t.id}
onClick={() => onTypeChange(t.id as 'ADD' | 'REMOVE' | 'TRASH')}
className={cn(
"flex-1 flex flex-col items-center py-3 rounded-xl transition-all",
"flex-1 flex flex-col items-center py-3 lg:py-4 xl:py-5 rounded-xl transition-all",
adjustType === t.id ? "bg-slate-800 shadow-lg" : "text-muted"
)}
>
@@ -270,7 +270,7 @@ export default function StockAdjustmentPanel({
</div>
{adjustType === 'TRASH' && (
<div className="w-full bg-red-500/5 border border-red-500/20 p-4 rounded-2xl animate-in shake duration-500">
<div className="w-full bg-red-500/5 border border-red-500/20 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl animate-in shake duration-500">
<div className="flex items-center gap-2 mb-3">
<AlertTriangle size={16} className="text-red-500" />
<span className="text-sm font-normal text-red-400">Waste Declaration</span>
@@ -278,7 +278,7 @@ export default function StockAdjustmentPanel({
<select
value={trashReason}
onChange={(e) => onReasonChange(e.target.value)}
className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-secondary"
className="w-full bg-background border border-slate-800 rounded-xl py-3 lg:py-4 xl:py-5 px-4 lg:px-5 xl:px-6 text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl outline-none text-secondary"
>
<option>Damaged</option>
<option>Expired</option>

View File

@@ -49,7 +49,7 @@ export default function AiManager({
data-testid="provider-option"
onClick={() => handleUpdateAiProvider(p.id)}
className={cn(
"p-4 rounded-2xl border transition-all text-left flex items-center justify-between group",
"p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl border transition-all text-left flex items-center justify-between group",
p.active
? "bg-primary border-primary shadow-xl shadow-primary/10"
: "bg-background/60 border-slate-800 hover:border-primary/40"

View File

@@ -42,7 +42,7 @@ export default function CategoryManager({
<div data-testid="category-list" className="grid sm:grid-cols-2 lg:grid-cols-4 gap-2.5">
{categories.map(cat => (
<div key={cat.id} data-testid="category-item" className="p-4 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
<div key={cat.id} data-testid="category-item" className="p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
<div className="min-w-0 pr-4">
<p className="card-title group-hover:text-primary transition-colors">{cat.name}</p>
<p className="card-subtitle">{cat.description || 'General storage'}</p>
@@ -53,14 +53,14 @@ export default function CategoryManager({
setEditingCategory(cat);
setEditCatForm({ name: cat.name, description: cat.description || '' });
}}
className="p-2 text-secondary hover:text-white hover:bg-slate-800 rounded-lg transition-all"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-secondary hover:text-white hover:bg-slate-800 rounded-lg transition-all"
>
<Edit2 size={14} />
</button>
<button
onClick={() => onDeleteCategory(cat.id, cat.name)}
data-testid="delete-category-button"
className="p-2 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-lg transition-all"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-lg transition-all"
>
<Trash2 size={14} />
</button>

View File

@@ -145,7 +145,7 @@ export default function DatabaseManager({
</div>
<button
onClick={() => onRestore(bak.filename)}
className="p-2 text-secondary hover:text-primary hover:bg-primary/5 rounded-xl transition-all opacity-0 group-hover/item:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-secondary hover:text-primary hover:bg-primary/5 rounded-xl transition-all opacity-0 group-hover/item:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label={`Restore backup ${bak.filename}`}
>
<RotateCcw size={16} />

View File

@@ -1,136 +1,119 @@
"use client";
import { useState } from "react";
import { FileDown, Loader2 } from "lucide-react";
import { FileDown, Loader2, FileSpreadsheet, FileText } from "lucide-react";
import { useExport } from "@/hooks/useExport";
import { Toast } from "@/components/Toast";
import { toast } from "react-hot-toast";
export function ExportPanel() {
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
try {
setSuccessMessage(null);
await exportSnapshot(format);
const formatName = format === "csv" ? "CSV" : "Excel";
setSuccessMessage(`Inventory snapshot exported as ${formatName}`);
setTimeout(() => setSuccessMessage(null), 4000);
toast.success(`Inventory snapshot exported as ${formatName}`);
} catch (err) {
// Error handled by useExport hook
// Error handled by hook
}
};
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
try {
setSuccessMessage(null);
await exportAuditTrail(format);
const formatName = format === "csv" ? "CSV" : "Excel";
setSuccessMessage(`Audit trail exported as ${formatName}`);
setTimeout(() => setSuccessMessage(null), 4000);
toast.success(`Audit trail exported as ${formatName}`);
} catch (err) {
// Error handled by useExport hook
// Error handled by hook
}
};
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-200 bg-white p-6">
<div className="mb-6 flex items-center gap-3">
<div className="rounded-lg bg-primary/10 p-4 border border-primary/20">
<FileDown className="text-primary" size={24} />
</div>
<div>
<h2 className="text-2xl font-normal">Export & Reports</h2>
<p className="text-xs text-slate-500">
Download inventory snapshots and audit trails in CSV or Excel formats
<div data-testid="admin-tab-exports" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 flex flex-col shadow-2xl transition-all h-full">
<div className="flex items-center gap-3 mb-4 md:mb-6">
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
<FileDown size={20} />
</div>
<div>
<h2 className="text-xl font-normal text-white tracking-tight">Export & Reports</h2>
<p className="text-[10px] md:text-xs text-muted font-normal tracking-tight">Download system snapshots and action logs</p>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
{/* Inventory Snapshot Section */}
<div className="p-4 bg-background/40 border border-slate-800/40 rounded-2xl hover:border-primary/20 transition-all group flex flex-col justify-between">
<div className="mb-4">
<h3 className="text-sm font-normal text-secondary flex items-center gap-2">
<FileSpreadsheet size={14} className="text-primary" />
Inventory Snapshot
</h3>
<p className="text-xs text-muted mt-1 leading-relaxed">
Export current inventory state with all item details and locations.
</p>
</div>
</div>
{/* Inventory Snapshot Section */}
<div className="mb-8 space-y-3">
<h3 className="font-normal text-slate-700">Inventory Snapshot</h3>
<p className="text-sm text-slate-500">
Export current inventory state with all item details
</p>
<div className="flex flex-col gap-2 sm:flex-row">
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleExportSnapshot("csv")}
disabled={isLoading}
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center justify-center gap-2 disabled:opacity-50"
aria-label="Export inventory snapshot as CSV"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
CSV
</button>
<button
onClick={() => handleExportSnapshot("xlsx")}
disabled={isLoading}
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center justify-center gap-2 disabled:opacity-50"
aria-label="Export inventory snapshot as Excel"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
Excel
</button>
</div>
</div>
{/* Audit Trail Section */}
<div className="space-y-3">
<h3 className="font-normal text-slate-700">Audit Trail</h3>
<p className="text-sm text-slate-500">
Export complete audit log of all system actions and changes
</p>
<div className="flex flex-col gap-2 sm:flex-row">
<div className="p-4 bg-background/40 border border-slate-800/40 rounded-2xl hover:border-primary/20 transition-all group flex flex-col justify-between">
<div className="mb-4">
<h3 className="text-sm font-normal text-secondary flex items-center gap-2">
<FileText size={14} className="text-indigo-400" />
Audit Trail
</h3>
<p className="text-xs text-muted mt-1 leading-relaxed">
Complete history of system actions, changes, and user operations.
</p>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleExportAuditTrail("csv")}
disabled={isLoading}
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center justify-center gap-2 disabled:opacity-50"
aria-label="Export audit trail as CSV"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
CSV
</button>
<button
onClick={() => handleExportAuditTrail("xlsx")}
disabled={isLoading}
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center justify-center gap-2 disabled:opacity-50"
aria-label="Export audit trail as Excel"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
Excel
</button>
</div>
</div>
</div>
{/* Status Messages */}
{error && (
<Toast
type="error"
message={`Export failed: ${error}`}
onClose={() => {}}
/>
)}
{successMessage && (
<Toast type="success" message={successMessage} onClose={() => {}} />
<div className="mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-[10px] flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
<span className="font-normal truncate">Error: {error}</span>
</div>
)}
</div>
);

View File

@@ -63,7 +63,7 @@ export default function IdentityManager({
setEditingUser(user);
setEditUserForm({ username: user.username, password: '', role: user.role });
}}
className="p-2 text-secondary hover:text-primary hover:bg-primary/5 rounded-xl transition-all focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-secondary hover:text-primary hover:bg-primary/5 rounded-xl transition-all focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label={`Edit user ${user.username}`}
>
<Edit2 size={16} />
@@ -72,7 +72,7 @@ export default function IdentityManager({
<button
onClick={() => onDeleteUser(user.id, user.username)}
data-testid="delete-user-button"
className="p-2 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
aria-label={`Delete user ${user.username}`}
>
<Trash2 size={16} />
@@ -91,7 +91,7 @@ export default function IdentityManager({
<h3 className="text-xl font-normal text-white tracking-tight">Edit Identity</h3>
<button
onClick={() => setEditingUser(null)}
className="p-2 text-muted hover:text-white transition-colors focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded"
className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 text-muted hover:text-white transition-colors focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded"
aria-label="Close edit dialog"
>
<X size={20} />

View File

@@ -28,7 +28,7 @@ export default function LdapManager({
</div>
</div>
<div className="flex items-center justify-between p-4 bg-background/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-primary/30">
<div className="flex items-center justify-between p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-primary/30">
<div className="flex items-center gap-3">
<div className={cn(
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
@@ -115,7 +115,7 @@ export default function LdapManager({
</div>
</div>
<div className="flex items-center justify-between p-4 bg-background/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-primary/30">
<div className="flex items-center justify-between p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-primary/30">
<div className="flex items-center gap-3">
<div className={cn(
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",

View File

@@ -50,7 +50,7 @@ export function useExport(): UseExportReturn {
try {
const response = await axiosInstance.get(
`/admin/db/export?format=${format}&type=inventory`,
`/admin/reports/export?format=${format}&type=inventory`,
{
responseType: "blob"
}
@@ -83,7 +83,7 @@ export function useExport(): UseExportReturn {
try {
const response = await axiosInstance.get(
`/admin/db/export?format=${format}&type=audit`,
`/admin/reports/export?format=${format}&type=audit`,
{
responseType: "blob"
}