Compare commits

...

34 Commits

Author SHA1 Message Date
a6d78c4083 Merge branch 'dev' 2026-04-23 15:36:20 +03:00
6e88ea9107 Build [v1.14.20] 2026-04-23 15:36:20 +03:00
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
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
46 changed files with 491 additions and 276 deletions

View File

@@ -1,3 +1,3 @@
830554 836417
830555 836418
830569 836432

View File

@@ -17,7 +17,15 @@ https://:8919 {
} }
# Next.js HMR Support (WebSocket) # 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-Proto https
header_up X-Forwarded-Host {host} header_up X-Forwarded-Host {host}
} }
@@ -37,7 +45,7 @@ https://:8918 {
} }
reverse_proxy http://localhost:8916 { 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-Proto https
header_up X-Forwarded-Host {host} 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

@@ -28,9 +28,10 @@ A unified system for inventory management featuring web administration, offline
### 2.3 Operations & Tooling ### 2.3 Operations & Tooling
- **PWA**: `next-pwa` (Service Workers + Manifest) - **PWA**: `next-pwa` (Service Workers + Manifest)
- **HTTPS Proxy**: Caddy (Port 8909) - **HTTPS Proxy**: Caddy (Ports 8918/8919)
- **Containerization**: Docker & Docker Compose - **Containerization**: Docker & Docker Compose
- **Deployment**: `deploy.sh` (Docker) or `start_server.sh` (Standalone) - **Deployment**: `deploy.py` (Docker) or `run_standalone.py` (Standalone)
- **Configuration**: Domain-specific YAML (`config/`) with `network.yaml` as SSOT.
--- ---
@@ -73,9 +74,9 @@ A unified system for inventory management featuring web administration, offline
- **JWT**: Stateless tokens for API auth. - **JWT**: Stateless tokens for API auth.
- **LDAP**: Primary source of truth for users in enterprise mode. - **LDAP**: Primary source of truth for users in enterprise mode.
- **Password Caching**: Encrypted local cache for offline authentication. - **Password Caching**: Encrypted local cache for offline authentication.
- **CORS**: Restricted origins in production via `config/backend.yaml`. - **CORS**: Restricted origins in production via `config/network.yaml` (SSOT).
--- ---
**Last Updated**: 2026-04-23 **Last Updated**: 2026-04-23
**Version**: 1.14.7 **Version**: 1.14.19

View File

@@ -1,7 +1,7 @@
import os import os
import yaml import yaml
import logging 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") log = logging.getLogger("ainventory")
@@ -52,8 +52,8 @@ class ConfigManager:
log.info(f"✅ Updated {path} with new values.") log.info(f"✅ Updated {path} with new values.")
# Reload the global config # Reload the global config
load_config() loader_load_config()
return get_config() return loader_get_config()
except Exception as e: except Exception as e:
log.error(f"❌ Failed to write {path}: {e}") log.error(f"❌ Failed to write {path}: {e}")
raise raise
@@ -103,8 +103,8 @@ class ConfigManager:
log.info(f"✅ Updated {path} with new secrets.") log.info(f"✅ Updated {path} with new secrets.")
# Reload the global config # Reload the global config
load_config() loader_load_config()
return get_config() return loader_get_config()
except Exception as e: except Exception as e:
log.error(f"❌ Failed to write {path}: {e}") log.error(f"❌ Failed to write {path}: {e}")
raise raise
@@ -133,17 +133,17 @@ class ConfigManager:
if backend_updates: if backend_updates:
ConfigManager.update_config(backend_updates) ConfigManager.update_config(backend_updates)
return get_config() return loader_get_config()
@staticmethod @staticmethod
def get_config() -> dict: def get_config() -> dict:
"""Return the current global configuration.""" """Return the current global configuration."""
return get_config() return loader_get_config()
@staticmethod @staticmethod
def get_masked_key(key_name: str): def get_masked_key(key_name: str):
"""Returns a masked version of a configuration value or env var.""" """Returns a masked version of a configuration value or env var."""
config = get_config() config = loader_get_config()
# Try to find in config first # Try to find in config first
val = None val = None
@@ -183,7 +183,7 @@ def update_config(updates: dict) -> dict:
def get_config() -> dict: def get_config() -> dict:
"""Return the current global configuration.""" """Return the current global configuration."""
return ConfigManager.get_config() return loader_get_config()
def validate_config_file() -> bool: def validate_config_file() -> bool:
"""Validate backend.yaml syntax and required fields.""" """Validate backend.yaml syntax and required fields."""

View File

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

@@ -41,9 +41,10 @@ A unified inventory management system that eliminates manual data entry through
### Phase 7: Config Consolidation (COMPLETED ✓) ### Phase 7: Config Consolidation (COMPLETED ✓)
- **Centralization**: All config in `config/` folder (YAML format). - **Centralization**: All config in `config/` folder (YAML format).
- **Automation**: Bash scripts converted to Python (`scripts/`). - **Automation**: Bash scripts converted to Python (`scripts/`).
- **Structure**: Clear separation of `backend.yaml`, `frontend.yaml`, `network.yaml`, `docker.yaml`, `secrets.yaml`. - **SSOT Architecture**: `network.yaml` established as Master IP/Port authority.
- **Dynamic Injection**: Automatic API URL and CORS calculation from master config.
- **D-06 Load Order**: Env Vars > YAML > Defaults. - **D-06 Load Order**: Env Vars > YAML > Defaults.
- **Deprecation**: `inventory.env` completely removed. - **Deprecation**: `inventory.env` and legacy `.sh` scripts completely removed.
### Phase 8: Hardening & Release (PLANNED) ### Phase 8: Hardening & Release (PLANNED)
- Stability monitoring, final UX refinements, production-ready runbook. - Stability monitoring, final UX refinements, production-ready runbook.

View File

@@ -2,53 +2,53 @@
**Active AI:** Claude Haiku 4.5 (Claude Code) **Active AI:** Claude Haiku 4.5 (Claude Code)
**Last Updated:** 2026-04-23 **Last Updated:** 2026-04-23
**Current Version:** v1.14.7 **Current Version:** v1.14.19
**Status**: ✅ PHASE 7 COMPLETE **Status**: ✅ PHASE 7 COMPLETE | 🟢 READY FOR PHASE 8
--- ---
## SESSION 41 EXECUTION — Phase 7 Execution & Consolidation ## SESSION 41 EXECUTION SUMMARY — Infrastructure & Export Fixes
### Work Completed This Session ### 1. Phase 7: Config Consolidation (COMPLETE)
- **Centralized YAML**: Migrated all configuration to `config/*.yaml`.
- **SSOT Architecture**: Established `network.yaml` as the Master Source of Truth for Server IP and Port management.
- **Dynamic Injection**: Frontend API URLs and Backend CORS are now dynamically calculated from master network settings.
- **Python Tooling**: All legacy `.sh` scripts converted to robust, secure Python scripts in `scripts/`.
- **Enhanced Launcher**: `run_standalone.py` now supports `start`, `stop`, `restart`, `status`, and Caddy SSL proxying.
**1. Phase 7 Execution (Config Consolidation):** ### 2. Admin & Security Improvements
- ✅ Wave 1: Created `config/` structure, 5 schema examples, 4 actual config files, and 155-line `config/README.md`. - **AI Key Detection**: Fixed backend logic to correctly report AI key status from YAML/Secrets.
- ✅ Wave 2 (Backend): Refactored `backend/config_loader.py` for YAML and D-06 load order. Updated `config_manager.py`, `main.py`, and `entrypoint.sh`. - **Secure Saving**: Implemented `ConfigManager.update_keys()` to route sensitive data correctly to `secrets.yaml`.
- ✅ Wave 2 (Scripts): Converted `deploy.sh`, `run_standalone.sh`, `install_service.sh`, and `export_prod.sh` to secure Python scripts with YAML parsing. - **Recursion Fix**: Resolved a critical infinite recursion loop in the configuration manager.
- ✅ Wave 3 (Docker & Docs): Updated `docker-compose.yml` (volume mounts), `Dockerfile`, `.gitignore`, `DEPLOYMENT.md`, and `README.md`.
- ✅ Post-Wave: Refactored legacy `load_dotenv()` in `ai_vision.py`, `check_models.py`, `gemini.py`, and `claude.py`.
- ✅ Cleanup: Deleted all deprecated bash scripts and legacy `.env` files. Updated `scripts/save_version.py` for new infrastructure.
**2. Versioning & State Sync:** ### 3. Export System Overhaul
- ✅ Incremented version to `v1.14.7` across all SSOT files (`VERSION.json`, `frontend/VERSION.json`, `PROJECT_ARCHITECTURE.md`, `dev_docs/PLAN.md`). - **Data Integrity**: Fixed a routing collision that caused binary SQLite downloads instead of CSV data.
- ✅ Updated roadmap in `dev_docs/PLAN.md` marking Phase 7 as COMPLETED. - **FastAPI Streaming**: Refactored exports to use `StreamingResponse` for memory efficiency.
- ✅ Verified all files pass syntax checks (Python/Bash). - **Premium UI**: Integrated the `ExportPanel` into the dark-mode aesthetic with standard premium button styles.
- **WebSocket Fix**: Improved Caddy configuration for Next.js HMR stability.
### Phase 7 Artifact Status ### 4. Cleanup & Preparation
- **Plans**: `.planning/phases/07-config-consolidation/07-01-PLAN.md` ✓ COMPLETED - ✅ Deleted all deprecated `.sh` and `.env` files.
- **Plans**: `.planning/phases/07-config-consolidation/07-02-PLAN.md` ✓ COMPLETED - ✅ Rebuilt virtual environment (`venv/`) and verified dependencies (added `openpyxl`).
- **Plans**: `.planning/phases/07-config-consolidation/07-03-PLAN.md` ✓ COMPLETED - ✅ Incremented system version to **v1.14.19**.
- **Plans**: `.planning/phases/07-config-consolidation/07-04-PLAN.md` ✓ COMPLETED
- **Summaries**: `.planning/phases/07-config-consolidation/07-01,02,03,04-SUMMARY.md` ✓ Created
- **Context**: `.planning/phases/07-config-consolidation/07-CONTEXT.md` ✓ Archived
--- ---
## NEXT STEPS ## NEXT STEPS (Phase 8: Hardening & Release)
1. **Start Phase 8 (Hardening & Release)**: 1. **End-to-End Testing**:
- Perform end-to-end testing of the new Python deployment scripts. - Verify data consistency in multi-page Excel exports.
- Verify Docker deployment with `python3 scripts/deploy.py production`. - Stress test the new Python-based deployment scripts.
- Perform final UX refinements and accessibility audits.
- Prepare production-ready runbook for v1.15.0 stable release.
2. **Stability Monitoring**: 2. **UX Refinement**:
- Check logs for any configuration parsing warnings. - Perform a final accessibility audit on the new Admin UI components.
- Ensure all environment variable overrides work as expected (D-06). - Ensure consistent loading states across all Admin panels.
3. **Cleanup**: 3. **Production Preparation**:
- Archive Phase 7 planning artifacts to `dev_docs/ARCHIVE_LOGS.md` or similar. - Finalize the production runbook.
- Prepare for the v1.15.0 "Stable" release milestone.
--- ---
Phase 7 fully implemented, verified, and cleaned up. Ready for the next phase. All tasks for this session are implemented, verified, and committed.
✓ Services are currently RUNNING in the background.

View File

@@ -1,6 +1,6 @@
{ {
"version": "1.14.12", "version": "1.14.20",
"last_build": "2026-04-23-1445", "last_build": "2026-04-23-1536",
"codename": "ConfigCore", "codename": "ConfigCore",
"commit": "c14a28b0" "commit": "b0ac7426"
} }

View File

@@ -37,7 +37,7 @@ export default function AdminPage() {
</div> </div>
<button <button
onClick={admin.handleLogout} 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" title="Secure Logout"
aria-label="Logout from admin panel" aria-label="Logout from admin panel"
> >

View File

@@ -346,7 +346,7 @@ export default function InventoryPage() {
{/* Stock Adjustment / Edit Item Overlay */} {/* Stock Adjustment / Edit Item Overlay */}
{selectedItem && ( {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="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"> <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"> <h3 className="text-xl font-normal tracking-tight flex items-center gap-2">
@@ -364,7 +364,7 @@ export default function InventoryPage() {
setEditedItem(selectedItem); setEditedItem(selectedItem);
setIsEditing(true); 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} /> <Edit2 size={20} />
</button> </button>
@@ -372,7 +372,7 @@ export default function InventoryPage() {
{isEditing && ( {isEditing && (
<button <button
onClick={handleDeleteItem} 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} /> <Trash2 size={20} />
</button> </button>
@@ -383,7 +383,7 @@ export default function InventoryPage() {
setIsEditing(false); setIsEditing(false);
setAdjustQty(1); 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} /> <X size={20} />
</button> </button>
@@ -486,7 +486,7 @@ export default function InventoryPage() {
toast.success("Ready to scan Box label..."); toast.success("Ready to scan Box label...");
}} }}
className={cn( 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" 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} key={t.id}
onClick={() => setAdjustType(t.id as any)} onClick={() => setAdjustType(t.id as any)}
className={cn( 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" adjustType === t.id ? "bg-slate-800 shadow-lg" : "text-muted"
)} )}
> >
@@ -552,11 +552,11 @@ export default function InventoryPage() {
</div> </div>
{adjustType === 'TRASH' && ( {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 <select
value={trashReason} value={trashReason}
onChange={(e) => setTrashReason(e.target.value)} 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>Damaged</option>
<option>Expired</option> <option>Expired</option>
@@ -608,7 +608,7 @@ export default function InventoryPage() {
type="text" type="text"
value={catEditedName} value={catEditedName}
onChange={e => setCatEditedName(e.target.value)} 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>
<div> <div>
@@ -616,7 +616,7 @@ export default function InventoryPage() {
<textarea <textarea
value={catEditedDesc} value={catEditedDesc}
onChange={e => setCatEditedDesc(e.target.value)} 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>
</div> </div>
@@ -641,7 +641,7 @@ export default function InventoryPage() {
{/* Box Manager Modal */} {/* Box Manager Modal */}
{showBoxManager && ( {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="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="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"> <div className="flex justify-between items-center">
@@ -652,7 +652,7 @@ export default function InventoryPage() {
</h3> </h3>
<p className="text-sm text-muted font-normal mt-1">Manage physical box labels & printing</p> <p className="text-sm text-muted font-normal mt-1">Manage physical box labels & printing</p>
</div> </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} /> <X size={24} />
</button> </button>
</div> </div>
@@ -663,7 +663,7 @@ export default function InventoryPage() {
placeholder="Search boxes..." placeholder="Search boxes..."
value={boxSearchQuery} value={boxSearchQuery}
onChange={(e) => setBoxSearchQuery(e.target.value)} 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} /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={18} />
{boxSearchQuery && ( {boxSearchQuery && (

View File

@@ -101,7 +101,7 @@ export default function LoginPage() {
key={user.id} key={user.id}
data-testid="user-list-item" data-testid="user-list-item"
onClick={() => handleSelectUser(user)} 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="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"> <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>
) : ( ) : (
<div className="space-y-4 animate-in slide-in-from-right-4 duration-300"> <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 <button
onClick={() => setSelectedUserForLogin(null)} onClick={() => setSelectedUserForLogin(null)}
className="p-1 hover:bg-slate-700 rounded-lg transition-colors" 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"> <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"> <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="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" /> <History size={28} className="md:w-8 md:h-8" />
</div> </div>
<div> <div>
@@ -142,7 +142,7 @@ export default function LogsPage() {
placeholder="Filter by user, action or asset..." placeholder="Filter by user, action or asset..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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"> <div className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary transition-colors group-focus-within:text-primary">
<Search size={18} /> <Search size={18} />

View File

@@ -414,7 +414,7 @@ export default function Home() {
onClick={handleSync} onClick={handleSync}
disabled={syncing} disabled={syncing}
data-testid="manual-sync-button" 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" : ""} /> <RefreshCw size={18} className={syncing ? "animate-spin text-primary" : ""} />
</button> </button>
@@ -507,7 +507,7 @@ export default function Home() {
</h3> </h3>
<p className="text-xs text-secondary font-normal mt-1">Select the item you want to {mode.replace('_', ' ').toLowerCase()}</p> <p className="text-xs text-secondary font-normal mt-1">Select the item you want to {mode.replace('_', ' ').toLowerCase()}</p>
</div> </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} /> <X size={20} />
</button> </button>
</div> </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"> <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> <p className="text-sm font-normal text-secondary">Powered by TFM Group Software</p>
<div className="h-px w-16 bg-slate-700/60" /> <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> </footer>
</div> </div>
</PageShell> </PageShell>

View File

@@ -58,10 +58,10 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
}; };
return ( 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 justify-between items-center mb-6 shrink-0">
<div className="flex items-center gap-3"> <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" /> <Sparkles className="text-primary w-5 h-5" />
</div> </div>
<div> <div>
@@ -72,7 +72,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button <button
onClick={() => { stopLiveCamera(); onCancel(); }} onClick={() => { stopLiveCamera(); onCancel(); }}
aria-label="Close AI Discovery" 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} /> <X size={24} />
</button> </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'}`} 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} /> <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>
<button <button
onClick={() => setMode('box')} 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'}`} 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} /> <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> </button>
</div> </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"> <div className="w-20 h-20 bg-surface rounded-3xl flex items-center justify-center mb-6 shadow-inner text-primary">
<Camera size={32} /> <Camera size={32} />
</div> </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" 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} /> <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>
<button <button
onClick={() => fileInputRef.current?.click()} 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" 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} /> <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> </button>
</div> </div>
@@ -339,7 +339,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<textarea <textarea
value={extractedItems[editingIndex].Description || extractedItems[editingIndex].description || ''} value={extractedItems[editingIndex].Description || extractedItems[editingIndex].description || ''}
onChange={(e) => updateEditingItem({ Description: e.target.value })} 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..." placeholder="Product description..."
/> />
</div> </div>
@@ -422,7 +422,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<div <div
key={idx} key={idx}
data-testid="extraction-item-row" 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 <div
className="flex-1 min-w-0 cursor-pointer" className="flex-1 min-w-0 cursor-pointer"
@@ -461,14 +461,14 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
setExtractedItems(newItems); setExtractedItems(newItems);
}} }}
aria-label={`Delete item: ${item.Item || item.name}`} 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} /> <X size={20} />
</button> </button>
<button <button
onClick={() => setEditingIndex(idx)} onClick={() => setEditingIndex(idx)}
aria-label={`Edit item: ${item.Item || item.name}`} 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" /> <ChevronDown size={20} className="-rotate-90" />
</button> </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="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="p-3 md:p-6 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-3"> <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} /> <Shield size={20} />
</div> </div>
<h2 className="text-xl font-normal text-white">System Admin</h2> <h2 className="text-xl font-normal text-white">System Admin</h2>
@@ -125,7 +125,7 @@ export default function AdminOverlay({
<button <button
data-testid="close-admin-overlay" data-testid="close-admin-overlay"
onClick={onClose} 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" aria-label="Close"
> >
<X size={20} /> <X size={20} />
@@ -171,7 +171,7 @@ export default function AdminOverlay({
loading: false, 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}`} aria-label={`Delete user ${u.username}`}
> >
<Trash2 size={16} /> <Trash2 size={16} />
@@ -220,7 +220,7 @@ export default function AdminOverlay({
loading: false, 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}`} aria-label={`Delete category ${cat.name}`}
> >
<Trash2 size={16} /> <Trash2 size={16} />
@@ -235,7 +235,7 @@ export default function AdminOverlay({
<AlertTriangle size={16} /> <AlertTriangle size={16} />
<p className="text-sm font-normal">Sign Out</p> <p className="text-sm font-normal">Sign Out</p>
</div> </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 <button
onClick={() => { onClick={() => {
import('@/lib/auth').then(m => m.clearAuth()); import('@/lib/auth').then(m => m.clearAuth());

View File

@@ -25,7 +25,7 @@ export default function BottomNav({
const isAdmin = pathname === '/admin'; const isAdmin = pathname === '/admin';
return ( 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"> <div className="max-w-xl mx-auto flex justify-around items-center text-secondary">
<button <button

View File

@@ -44,7 +44,7 @@ export default function CameraView({
return ( return (
<div className="w-full max-w-md mx-auto flex flex-col gap-3 md:gap-4"> <div className="w-full max-w-md mx-auto flex flex-col gap-3 md:gap-4">
{/* Video Viewport Area */} {/* 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="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="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)]" /> <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>
</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"> <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-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> <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 && ( {!isStarted && !error && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-surface text-secondary gap-4"> <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" /> <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> </div>
)} )}
@@ -131,7 +131,7 @@ export default function CameraView({
</div> </div>
{/* External Controls Area */} {/* 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"> <div className="flex items-center gap-3 w-full">
{hasZoom && ( {hasZoom && (
<button <button
@@ -153,7 +153,7 @@ export default function CameraView({
</button> </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 ? ( {ocrProcessing ? (
<> <>
<RefreshCw className="animate-spin text-primary" size={18} /> <RefreshCw className="animate-spin text-primary" size={18} />

View File

@@ -52,9 +52,9 @@ export default function CategoryCreationModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <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="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="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} /> <Tag size={20} />
</div> </div>
<h2 className="text-lg font-normal text-white">New Category</h2> <h2 className="text-lg font-normal text-white">New Category</h2>
@@ -69,7 +69,7 @@ export default function CategoryCreationModal({
</button> </button>
</div> </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> <div>
<label className="text-sm font-normal text-secondary">Name</label> <label className="text-sm font-normal text-secondary">Name</label>
<input <input
@@ -80,7 +80,7 @@ export default function CategoryCreationModal({
setError(null); setError(null);
}} }}
placeholder="e.g., Electronics" 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} disabled={loading}
autoFocus autoFocus
/> />
@@ -93,14 +93,14 @@ export default function CategoryCreationModal({
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Brief category description" 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} disabled={loading}
/> />
</div> </div>
{error && ( {error && (
<div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded"> <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 text-rose-400">{error}</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">{error}</p>
</div> </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 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"> <div className="bg-surface border border-slate-800 rounded-lg shadow-xl max-w-sm w-full mx-4">
{/* Header */} {/* 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="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} /> <AlertTriangle size={20} />
</div> </div>
<h2 className="text-lg font-normal text-white">{title}</h2> <h2 className="text-lg font-normal text-white">{title}</h2>
@@ -75,9 +75,9 @@ export default function ConfirmationModal({
</div> </div>
{/* Content */} {/* Content */}
<div className="p-6 space-y-4"> <div className="p-6 lg:p-8 xl:p-10 space-y-4">
{/* Description */} {/* 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) */} {/* Item Name (if provided) */}
{itemName && ( {itemName && (
@@ -89,15 +89,15 @@ export default function ConfirmationModal({
{/* Consequence Warning */} {/* Consequence Warning */}
{consequence && ( {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> <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> </div>
)} )}
{/* Affected Items Count (for medium-risk+) */} {/* Affected Items Count (for medium-risk+) */}
{affectedCount && affectedCount > 0 && ( {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 {affectedCount === 1
? 'This will affect 1 item.' ? 'This will affect 1 item.'
: `This will affect ${affectedCount} items.`} : `This will affect ${affectedCount} items.`}
@@ -118,7 +118,7 @@ export default function ConfirmationModal({
setError(null); setError(null);
}} }}
placeholder="Type DELETE" 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} disabled={loading}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && canConfirm && !loading) { if (e.key === 'Enter' && canConfirm && !loading) {
@@ -127,14 +127,14 @@ export default function ConfirmationModal({
}} }}
/> />
{error && ( {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>
)} )}
</div> </div>
{/* Actions */} {/* 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 <button
onClick={onCancel} onClick={onCancel}
disabled={loading} disabled={loading}

View File

@@ -85,7 +85,7 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
</div> </div>
{/* Form */} {/* 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 */} {/* Username Field */}
<div> <div>
<label htmlFor="username" className="block text-sm font-normal text-secondary mb-2"> <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 }); if (errors.username) setErrors({ ...errors, username: undefined });
}} }}
placeholder="Enter username" 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' errors.username ? 'border-red-500' : 'border-slate-700'
}`} }`}
disabled={loading} disabled={loading}
/> />
{errors.username && ( {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> </div>
@@ -126,13 +126,13 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
if (errors.password) setErrors({ ...errors, password: undefined }); if (errors.password) setErrors({ ...errors, password: undefined });
}} }}
placeholder="Enter password" 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' errors.password ? 'border-red-500' : 'border-slate-700'
}`} }`}
disabled={loading} disabled={loading}
/> />
{errors.password && ( {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> </div>

View File

@@ -15,7 +15,7 @@ export default function FilterBar({ searchQuery, onChange }: FilterBarProps) {
placeholder="Search catalog..." placeholder="Search catalog..."
value={searchQuery} value={searchQuery}
onChange={(e) => onChange(e.target.value)} 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"> <div className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary">
<Search size={18} /> <Search size={18} />

View File

@@ -79,7 +79,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
key={user.id} key={user.id}
data-testid="user-list-item" data-testid="user-list-item"
onClick={() => handleSelectUser(user)} 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="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"> <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" data-testid="username-input"
type="text" type="text"
autoFocus 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" placeholder="DIRECTORY ID"
/> />
</div> </div>
@@ -140,7 +140,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
data-testid="password-input" data-testid="password-input"
type="password" type="password"
onKeyDown={(e) => e.key === 'Enter' && handleLogin()} 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="••••••••" placeholder="••••••••"
/> />
</div> </div>
@@ -156,7 +156,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div> </div>
) : ( ) : (
<div className="space-y-5 animate-in slide-in-from-bottom-5 duration-300"> <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="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"> <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} /> <Shield size={20} />
@@ -168,7 +168,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div> </div>
<button <button
onClick={() => setSelectedUserForLogin(null)} 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" title="Change User"
> >
<X size={18} /> <X size={18} />
@@ -184,7 +184,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
type="password" type="password"
autoFocus autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleLogin()} 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" placeholder="KEY-PHRASE"
/> />
</div> </div>

View File

@@ -53,7 +53,7 @@ export default function InventoryTable({
if (categories.length === 0) { if (categories.length === 0) {
return ( 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" /> <Package size={48} className="mx-auto mb-4 opacity-10" />
<p className="font-medium">No results found</p> <p className="font-medium">No results found</p>
</div> </div>
@@ -67,7 +67,7 @@ export default function InventoryTable({
return ( return (
<div key={cat} className="bg-surface/50 border border-slate-800/50 rounded-3xl overflow-hidden transition-all duration-300"> <div key={cat} className="bg-surface/50 border border-slate-800/50 rounded-3xl overflow-hidden transition-all duration-300">
<div <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)} onClick={() => onExpandCategory(expandedCategory === cat ? null : cat)}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -75,7 +75,7 @@ export default function InventoryTable({
<Layers size={20} /> <Layers size={20} />
</div> </div>
<div className="text-left"> <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"> <p className="card-subtitle tracking-tight">
{categoryItems.length} Item types in stock {categoryItems.length} Item types in stock
</p> </p>
@@ -89,7 +89,7 @@ export default function InventoryTable({
e.stopPropagation(); e.stopPropagation();
onEditCategory(cat); 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} /> <EditIcon size={16} />
</button> </button>
@@ -98,7 +98,7 @@ export default function InventoryTable({
</div> </div>
{expandedCategory === cat && ( {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" /> <div className="h-px bg-slate-800/50 mb-4 mx-2" />
{categoryItems.map(item => ( {categoryItems.map(item => (
<div <div

View File

@@ -42,7 +42,7 @@ export default function ItemComparisonModal({
const hasChanges = fields.some(f => isDifferent(f.key)); const hasChanges = fields.some(f => isDifferent(f.key));
return ( 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="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"> <div className="flex items-center gap-3 mb-6">
<AlertTriangle size={24} className="text-amber-500" /> <AlertTriangle size={24} className="text-amber-500" />
@@ -68,7 +68,7 @@ export default function ItemComparisonModal({
return ( return (
<div <div
key={field.key} 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' different ? 'bg-amber-500/10 border border-amber-500/20' : 'bg-slate-800/30'
}`} }`}
> >
@@ -85,8 +85,8 @@ export default function ItemComparisonModal({
</div> </div>
{!hasChanges && ( {!hasChanges && (
<div className="p-4 bg-slate-800/50 rounded-xl mb-6 border border-slate-700"> <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 text-secondary"> Items are identical. No update needed.</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"> Items are identical. No update needed.</p>
</div> </div>
)} )}

View File

@@ -14,7 +14,7 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
if (!show) return null; if (!show) return null;
return ( 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 className="flex justify-between items-center mb-8">
<div> <div>
<h2 className="text-2xl font-normal tracking-tight">Audit History</h2> <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> </div>
<button <button
onClick={onClose} 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} /> <X size={24} />
</button> </button>
@@ -36,7 +36,7 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
</div> </div>
) : ( ) : (
logs.map((log) => ( 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-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<p className={`text-xs font-normal ${ <p className={`text-xs font-normal ${
@@ -54,11 +54,11 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
</div> </div>
{(log.details || log.timestamp) && ( {(log.details || log.timestamp) && (
<div className="flex items-center gap-3 mt-2"> <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()} {new Date(log.timestamp).toLocaleString()}
</p> </p>
{log.details && ( {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} {log.details}
</span> </span>
)} )}

View File

@@ -25,7 +25,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
if (loading) { if (loading) {
return ( 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" /> <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> <p className="text-xs font-normal tracking-widest italic">Securing Audit Stream...</p>
</div> </div>
@@ -53,7 +53,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
<button <button
key={log.id} key={log.id}
onClick={() => setSelectedLog(log)} 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"> <div className="flex-1 min-w-0 z-10 flex items-center gap-4">
{/* Compact Action Badge */} {/* Compact Action Badge */}
@@ -111,7 +111,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
{selectedLog.resolved_name} {selectedLog.resolved_name}
</h2> </h2>
</div> </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} /> <X size={20} />
</button> </button>
</div> </div>

View File

@@ -28,12 +28,12 @@ export default function NewItemDialog({ onScannerClick, onAddItemClick }: NewIte
onClick={onAddItemClick} 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" 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} /> <Sparkles size={32} />
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-lg leading-tight">Add New Item</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 opacity-60 font-mono mt-1">AI Smart Discovery</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> </div>
</button> </button>
</div> </div>

View File

@@ -54,14 +54,14 @@ export default function ScannerSection({
</div> </div>
{/* Scanner Section */} {/* 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 ? ( {showScanner ? (
<div className="space-y-2 md:space-y-3"> <div className="space-y-2 md:space-y-3">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-normal">scanning...</h2> <h2 className="text-lg font-normal">scanning...</h2>
<button <button
onClick={() => onShowScanner(false)} 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} /> <X size={18} />
</button> </button>

View File

@@ -9,7 +9,7 @@ interface StatCardProps {
export default function StatCard({ label, value, icon: Icon }: StatCardProps) { export default function StatCard({ label, value, icon: Icon }: StatCardProps) {
return ( 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"> <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" />} {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"> <span className="text-base md:text-lg text-secondary font-normal truncate">

View File

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

View File

@@ -49,7 +49,7 @@ export default function AiManager({
data-testid="provider-option" data-testid="provider-option"
onClick={() => handleUpdateAiProvider(p.id)} onClick={() => handleUpdateAiProvider(p.id)}
className={cn( 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 p.active
? "bg-primary border-primary shadow-xl shadow-primary/10" ? "bg-primary border-primary shadow-xl shadow-primary/10"
: "bg-background/60 border-slate-800 hover:border-primary/40" : "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"> <div data-testid="category-list" className="grid sm:grid-cols-2 lg:grid-cols-4 gap-2.5">
{categories.map(cat => ( {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"> <div className="min-w-0 pr-4">
<p className="card-title group-hover:text-primary transition-colors">{cat.name}</p> <p className="card-title group-hover:text-primary transition-colors">{cat.name}</p>
<p className="card-subtitle">{cat.description || 'General storage'}</p> <p className="card-subtitle">{cat.description || 'General storage'}</p>
@@ -53,14 +53,14 @@ export default function CategoryManager({
setEditingCategory(cat); setEditingCategory(cat);
setEditCatForm({ name: cat.name, description: cat.description || '' }); 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} /> <Edit2 size={14} />
</button> </button>
<button <button
onClick={() => onDeleteCategory(cat.id, cat.name)} onClick={() => onDeleteCategory(cat.id, cat.name)}
data-testid="delete-category-button" 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} /> <Trash2 size={14} />
</button> </button>

View File

@@ -145,7 +145,7 @@ export default function DatabaseManager({
</div> </div>
<button <button
onClick={() => onRestore(bak.filename)} 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}`} aria-label={`Restore backup ${bak.filename}`}
> >
<RotateCcw size={16} /> <RotateCcw size={16} />

View File

@@ -1,136 +1,119 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { FileDown, Loader2 } from "lucide-react"; import { FileDown, Loader2, FileSpreadsheet, FileText } from "lucide-react";
import { useExport } from "@/hooks/useExport"; import { useExport } from "@/hooks/useExport";
import { Toast } from "@/components/Toast"; import { toast } from "react-hot-toast";
export function ExportPanel() { export function ExportPanel() {
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport(); const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const handleExportSnapshot = async (format: "csv" | "xlsx") => { const handleExportSnapshot = async (format: "csv" | "xlsx") => {
try { try {
setSuccessMessage(null);
await exportSnapshot(format); await exportSnapshot(format);
const formatName = format === "csv" ? "CSV" : "Excel"; const formatName = format === "csv" ? "CSV" : "Excel";
setSuccessMessage(`Inventory snapshot exported as ${formatName}`); toast.success(`Inventory snapshot exported as ${formatName}`);
setTimeout(() => setSuccessMessage(null), 4000);
} catch (err) { } catch (err) {
// Error handled by useExport hook // Error handled by hook
} }
}; };
const handleExportAuditTrail = async (format: "csv" | "xlsx") => { const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
try { try {
setSuccessMessage(null);
await exportAuditTrail(format); await exportAuditTrail(format);
const formatName = format === "csv" ? "CSV" : "Excel"; const formatName = format === "csv" ? "CSV" : "Excel";
setSuccessMessage(`Audit trail exported as ${formatName}`); toast.success(`Audit trail exported as ${formatName}`);
setTimeout(() => setSuccessMessage(null), 4000);
} catch (err) { } catch (err) {
// Error handled by useExport hook // Error handled by hook
} }
}; };
return ( return (
<div className="space-y-6"> <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="rounded-lg border border-slate-200 bg-white p-6"> <div className="flex items-center gap-3 mb-4 md:mb-6">
<div className="mb-6 flex items-center gap-3"> <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">
<div className="rounded-lg bg-primary/10 p-4 border border-primary/20"> <FileDown size={20} />
<FileDown className="text-primary" size={24} /> </div>
</div> <div>
<div> <h2 className="text-xl font-normal text-white tracking-tight">Export & Reports</h2>
<h2 className="text-2xl font-normal">Export & Reports</h2> <p className="text-[10px] md:text-xs text-muted font-normal tracking-tight">Download system snapshots and action logs</p>
<p className="text-xs text-slate-500"> </div>
Download inventory snapshots and audit trails in CSV or Excel formats </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> </p>
</div> </div>
</div>
<div className="grid grid-cols-2 gap-2">
{/* 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">
<button <button
onClick={() => handleExportSnapshot("csv")} onClick={() => handleExportSnapshot("csv")}
disabled={isLoading} 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" aria-label="Export inventory snapshot as CSV"
> >
{isLoading ? ( {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
<Loader2 size={16} className="animate-spin" /> CSV
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
</button> </button>
<button <button
onClick={() => handleExportSnapshot("xlsx")} onClick={() => handleExportSnapshot("xlsx")}
disabled={isLoading} 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" aria-label="Export inventory snapshot as Excel"
> >
{isLoading ? ( {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
<Loader2 size={16} className="animate-spin" /> Excel
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
</button> </button>
</div> </div>
</div> </div>
{/* Audit Trail Section */} {/* Audit Trail Section */}
<div className="space-y-3"> <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">
<h3 className="font-normal text-slate-700">Audit Trail</h3> <div className="mb-4">
<p className="text-sm text-slate-500"> <h3 className="text-sm font-normal text-secondary flex items-center gap-2">
Export complete audit log of all system actions and changes <FileText size={14} className="text-indigo-400" />
</p> Audit Trail
<div className="flex flex-col gap-2 sm:flex-row"> </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 <button
onClick={() => handleExportAuditTrail("csv")} onClick={() => handleExportAuditTrail("csv")}
disabled={isLoading} 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" aria-label="Export audit trail as CSV"
> >
{isLoading ? ( {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
<Loader2 size={16} className="animate-spin" /> CSV
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
</button> </button>
<button <button
onClick={() => handleExportAuditTrail("xlsx")} onClick={() => handleExportAuditTrail("xlsx")}
disabled={isLoading} 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" aria-label="Export audit trail as Excel"
> >
{isLoading ? ( {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
<Loader2 size={16} className="animate-spin" /> Excel
) : (
<FileDown size={16} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Status Messages */}
{error && ( {error && (
<Toast <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">
type="error" <div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
message={`Export failed: ${error}`} <span className="font-normal truncate">Error: {error}</span>
onClose={() => {}} </div>
/>
)}
{successMessage && (
<Toast type="success" message={successMessage} onClose={() => {}} />
)} )}
</div> </div>
); );

View File

@@ -63,7 +63,7 @@ export default function IdentityManager({
setEditingUser(user); setEditingUser(user);
setEditUserForm({ username: user.username, password: '', role: user.role }); 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}`} aria-label={`Edit user ${user.username}`}
> >
<Edit2 size={16} /> <Edit2 size={16} />
@@ -72,7 +72,7 @@ export default function IdentityManager({
<button <button
onClick={() => onDeleteUser(user.id, user.username)} onClick={() => onDeleteUser(user.id, user.username)}
data-testid="delete-user-button" 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}`} aria-label={`Delete user ${user.username}`}
> >
<Trash2 size={16} /> <Trash2 size={16} />
@@ -91,7 +91,7 @@ export default function IdentityManager({
<h3 className="text-xl font-normal text-white tracking-tight">Edit Identity</h3> <h3 className="text-xl font-normal text-white tracking-tight">Edit Identity</h3>
<button <button
onClick={() => setEditingUser(null)} 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" aria-label="Close edit dialog"
> >
<X size={20} /> <X size={20} />

View File

@@ -28,7 +28,7 @@ export default function LdapManager({
</div> </div>
</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="flex items-center gap-3">
<div className={cn( <div className={cn(
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner", "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> </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="flex items-center gap-3">
<div className={cn( <div className={cn(
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner", "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 { try {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`/admin/db/export?format=${format}&type=inventory`, `/admin/reports/export?format=${format}&type=inventory`,
{ {
responseType: "blob" responseType: "blob"
} }
@@ -83,7 +83,7 @@ export function useExport(): UseExportReturn {
try { try {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`/admin/db/export?format=${format}&type=audit`, `/admin/reports/export?format=${format}&type=audit`,
{ {
responseType: "blob" responseType: "blob"
} }