Compare commits

..

29 Commits

Author SHA1 Message Date
c15a80a53e Build [v1.14.26] 2026-04-25 12:25:17 +03:00
701e30fb27 Build [v1.14.25] 2026-04-25 12:21:30 +03:00
b74d1172cb bede 2026-04-25 12:13:58 +03:00
382245369b Merge master into dev and resolve conflicts (prefer master) 2026-04-25 12:05:27 +03:00
da06282d48 Resolve merge conflicts and update session state for v1.14.23 2026-04-23 18:24:21 +03:00
c8214ff086 build: sync root version to v1.14.22 2026-04-23 16:38:10 +03:00
7f5cfe637a chore: cleanup audit source file 2026-04-23 16:36:56 +03:00
ac62330fd9 docs: update session state for milestone completion 2026-04-23 16:30:41 +03:00
41a1f7d548 chore: archive v1.14.22 milestone 2026-04-23 16:26:27 +03:00
d9b60b6b89 Merge branch 'dev' 2026-04-23 15:59:17 +03:00
a6d78c4083 Merge branch 'dev' 2026-04-23 15:36:20 +03:00
b4df7201f5 Merge branch 'dev' 2026-04-23 15:27:27 +03:00
20ace033a2 Merge branch 'dev' 2026-04-23 15:24:27 +03:00
34ae803c7a Merge branch 'dev' 2026-04-23 15:19:55 +03:00
fb2947cfd0 Merge branch 'dev' 2026-04-23 15:18:14 +03:00
5ed8c6c7db Merge branch 'dev' 2026-04-23 15:14:59 +03:00
14ffecafdd Merge branch 'dev' 2026-04-23 15:08:46 +03:00
2ce0f84cd3 Merge branch 'dev' 2026-04-23 14:51:52 +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
66 changed files with 1465 additions and 986 deletions

View File

@@ -0,0 +1,29 @@
# Requirements Archive: v1.14.22
**Milestone:** Config Consolidation & Infrastructure
**Status:** 100% COMPLETE
---
## 1. Functional Requirements (Archived)
- [x] **R-01: Centralized Configuration**: All environment settings must live in the `config/` directory.
- [x] **R-02: Secret Separation**: Production secrets must be stored in `secrets.yaml` and excluded from Git.
- [x] **R-03: Multi-Domain Schema**: Config must be split into `backend`, `frontend`, `network`, and `docker` domains.
- [x] **R-04: Python Tooling**: All deployment and management scripts must be implemented in Python with consistent logging.
- [x] **R-05: Dynamic Topology**: The system must automatically calculate API endpoints from the `server_ip` defined in `network.yaml`.
- [x] **R-06: Unified Auth Mgmt**: Admin UI must save LDAP settings directly to the YAML configuration.
---
## 2. Technical Quality Requirements
- [x] **Q-01: Type Compliance**: No bold/medium font weights in the UI.
- [x] **Q-02: Copywriting**: Contextual CTA labels for all destructive or safety-critical actions.
- [x] **Q-03: Backward Compatibility**: Legacy `inventory.env` support maintained during the transition (deprecated).
- [x] **Q-04: Security**: Masked secrets in logs and subprocess injection protection.
---
## 3. Requirement Outcomes
- **Validated**: YAML configuration significantly improved developer experience and deployment reliability.
- **Adjusted**: Initially planned to keep `inventory.env` as primary; adjusted to deprecate it immediately in favor of YAML.
- **Dropped**: Scale testing with 10k items (Deferred to Phase 8).

View File

@@ -0,0 +1,38 @@
# Milestone Archive: v1.14.22 — Config Consolidation & Infrastructure
**Shipped:** 2026-04-23
**Git Tag:** v1.14.22
**Audit Rating:** 10/10
---
## 1. Executive Summary
Milestone v1.14.22 focused on the transition from legacy environment-variable-based configuration to a centralized, domain-driven YAML architecture. It established a Single Source of Truth (SSOT) for network topology and replaced all legacy shell scripts with a unified Python management suite.
---
## 2. Phase Breakdown (Phase 7)
### Phase 7: Config Consolidation
- **Plan 01: Folder Structure**: Created `config/` with YAML schemas and examples.
- **Plan 02: Backend Refactor**: Migrated `config_loader.py` and `config_manager.py` to YAML.
- **Plan 03: Script Unification**: Converted `deploy`, `run_standalone`, `install_service`, and `export` to Python.
- **Plan 04: Docker Integration**: Integrated volume-mounted YAML configs and updated entrypoints.
- **Hardening Pass**: Fixed typography (NO BOLD), contextual CTAs, and Port 80 SSL conflicts.
---
## 3. Key Accomplishments
-**YAML-First Architecture**: Load order enforced (Env > YAML > Secrets > Defaults).
-**Python Tooling Suite**: 100% removal of legacy `.sh` management scripts.
-**Dynamic Client Discovery**: Frontend automatically finds backend via `network.json` sync.
-**Premium UI Polish**: Full compliance with AI_RULES.md typography and copywriting standards.
-**Infrastructure Stability**: Robust Caddy standalone proxy with SSL.
---
## 4. Statistics
- **Phases**: 1
- **Tasks Completed**: 19
- **Files Modified**: 45+
- **Commit Range**: 778d671a..e5bb14d9

View File

@@ -0,0 +1,136 @@
---
wave: 1
depends_on: ["07-config-consolidation"]
files_modified: [
"frontend/tailwind.config.ts",
"frontend/styles/globals.css",
"frontend/components/ui/StatCard.tsx",
"frontend/components/ui/Button.tsx",
"frontend/components/ui/Input.tsx",
"frontend/app/layout.tsx",
"frontend/app/page.tsx",
"AI_RULES.md"
]
autonomous: true
---
# Phase 7.1: Frontend UI/UX Overhaul (Industrial Precision) - Implementation Plan
**Objective:** Implement the "Industrial Precision" design system from `DESIGN.md` across all frontend pages, ensuring high-contrast signaling, sharp corners (0px radius), and a technical, utilitarian aesthetic while maintaining compliance with mandatory typography rules.
**Success Criteria:**
- Tailwind configuration updated with the new "Industrial Precision" color palette.
- Global styles updated to enforce 0px border radius (sharp corners) for all components.
- Typography updated to use "Space Grotesk" as the primary font.
- Visual hierarchy achieved through size, color, and spacing (preserving the "NO BOLD" and "NO UPPERCASE" rules from AI_RULES.md).
- StatCards, Buttons, and Inputs reflect the new brutalist/industrial aesthetic.
- All pages (Dashboard, Inventory, Admin, Logs) updated to use the new container and margin system.
---
## Task 1: Update Tailwind Configuration [COMPLETED]
**Read First:**
- `DESIGN.md` for color palette and spacing units.
- `frontend/tailwind.config.ts` for current configuration.
**Action:**
1. Update `tailwind.config.ts` with the "Industrial Precision" colors:
- Primary: `#F58618` (Caution Orange)
- Background: `#131313`
- Surfaces: `#0A0A0A`, `#121212`, `#1A1A1A`
2. Add "Space Grotesk" to the fontFamily configuration.
3. Configure the spacing unit (4px) and 12-column grid.
**Acceptance Criteria:**
- `tailwind.config.ts` includes the new color palette.
- `Space Grotesk` is defined as the default sans font.
- Spacing units are aligned with `DESIGN.md`.
---
## Task 2: Implement Global Sharp Edges & Borders
**Read First:**
- `DESIGN.md` (Elevation & Depth, Shapes).
- `frontend/styles/globals.css`.
**Action:**
1. Update `globals.css` to set `border-radius: 0` globally or through Tailwind layer overrides.
2. Implement the "Bold Borders" style: 1px or 2px solid borders for levels 1 and 2.
3. Remove all shadows and blurs, replacing them with tonal layers as per `DESIGN.md`.
**Acceptance Criteria:**
- UI components have sharp 90-degree corners.
- Shadows are removed from the entire application.
- Borders use the specified technical colors (`#222222`, `#F58618`).
---
## Task 3: Component-Level Overhaul (Buttons & Inputs)
**Read First:**
- `DESIGN.md` (Components section).
- `frontend/components/ui/Button.tsx` (if it exists, otherwise standard elements).
- `frontend/components/ui/Input.tsx`.
**Action:**
1. Update Button styles: Solid `#F58618` for primary, transparent with 1px border for secondary.
2. Update Input styles: Darker background (`#000000`), bottom-only or full 1px border.
3. Ensure no rounded corners and no bold text.
**Acceptance Criteria:**
- Buttons and Inputs match the "Industrial Precision" visual style.
- Active/Focus states use the Caution Orange border.
---
## Task 4: StatCard & Data Table Refinement
**Read First:**
- `DESIGN.md` (StatCards, Data Tables).
- `AI_RULES.md` (StatCard Typography Rule).
- `frontend/components/ui/StatCard.tsx`.
**Action:**
1. Refine StatCards to use high-contrast blocks and tight internal padding.
2. Ensure numeric values match label size (as per AI_RULES.md) but use color for emphasis.
3. Update Data Tables: remove zebra-striping, use 1px horizontal dividers, apply technical headers.
**Acceptance Criteria:**
- StatCards provide high "glanceability" without overwhelming font sizes.
- Data Tables reflect the "glass cockpit" philosophy with high information density.
---
## Task 5: Layout & Viewport Adjustments
**Read First:**
- `DESIGN.md` (Layout & Spacing).
- `frontend/app/layout.tsx`.
- `frontend/app/page.tsx` and major route pages.
**Action:**
1. Set the primary viewport margins (24px-32px).
2. Ensure main containers use `max-w-7xl` (consistent with AI_RULES.md).
3. Update page headers to match the unified icon box + title standard.
**Acceptance Criteria:**
- Spacing is compressed internally but framed by generous outer margins.
- All pages feel consistent with the "high-end command center" aesthetic.
---
## Task 6: Final Audit & Rule Alignment
**Read First:**
- `AI_RULES.md` and `GEMINI.md`.
**Action:**
1. Audit the new UI for any accidental BOLD or UPPERCASE text.
2. Re-verify contrast ratios (7:1 target for status-critical text).
3. Ensure "Lucide Icons" are used exclusively.
**Acceptance Criteria:**
- 100% compliance with AI_RULES.md.
- Visual style perfectly aligns with `DESIGN.md` spirit.

View File

@@ -1,3 +1,2 @@
5813 21336
5814 21347
5831

View File

@@ -15,7 +15,7 @@ This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCH
## 2. ENGINEERING & OPERATIONAL LAWS ## 2. ENGINEERING & OPERATIONAL LAWS
- **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. - **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately.
- **GIT PROTOCOL**: Use system `git`. Never push or use `--force` unless explicitly asked. - **GIT PROTOCOL**: Use system `git`. Never push or use `--force` unless explicitly asked. Never push to master unless user say so. The maik work is done in "dev" git branch, and in other branches only user say so explicitly.
- **VERSIONING**: Update `VERSION.json` on every commit using `scripts/save_version.py`. - **VERSIONING**: Update `VERSION.json` on every commit using `scripts/save_version.py`.
- **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, `DEPLOYMENT.md`, and `dev_docs/PLAN.md`. - **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, `DEPLOYMENT.md`, and `dev_docs/PLAN.md`.
- **CODE QUALITY**: Files under 300 lines, complexity < 10, strict Single Responsibility Principle. - **CODE QUALITY**: Files under 300 lines, complexity < 10, strict Single Responsibility Principle.
@@ -27,6 +27,7 @@ This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCH
- **NO BOLD FONTS**: Use `font-normal` throughout. Hierarchy via size and color only. - **NO BOLD FONTS**: Use `font-normal` throughout. Hierarchy via size and color only.
- **NO `tracking-widest`**. - **NO `tracking-widest`**.
- **StatCard Typography**: Numeric values in StatCards MUST match the exact text size of their corresponding labels (e.g., `text-base md:text-lg`) to avoid overwhelming the density of the component. - **StatCard Typography**: Numeric values in StatCards MUST match the exact text size of their corresponding labels (e.g., `text-base md:text-lg`) to avoid overwhelming the density of the component.
- **Shapes**: All components MUST use 0px border radius (sharp corners) and NO box-shadows. Depth via tonal layers and borders only.
- **Layout**: Main pages MUST use `max-w-7xl`. - **Layout**: Main pages MUST use `max-w-7xl`.
- **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-normal`). - **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-normal`).
- **Iconography**: Use **Lucide Icons** exclusively. - **Iconography**: Use **Lucide Icons** exclusively.

View File

@@ -15,18 +15,7 @@ Do not proceed with any task until you have analyzed these three files.
DO NOT, EVER, USE "uppercase" or "toUpper" in any UI/UX context, nowhere. This SHOULD NOT be used in UI/UX anywhere, no text should be displayed in CAPITAL LETTERS! But do not replace "toUpper" with "toLower" or "uppercase" with "lowercase" either, leave text in UI/UX as is. DO NOT, EVER, USE "uppercase" or "toUpper" in any UI/UX context, nowhere. This SHOULD NOT be used in UI/UX anywhere, no text should be displayed in CAPITAL LETTERS! But do not replace "toUpper" with "toLower" or "uppercase" with "lowercase" either, leave text in UI/UX as is.
# Project Context
## Project-Specific Guidelines ## Project-Specific Guidelines
- Use TypeScript strict mode - Use TypeScript strict mode
- All API endpoints must have tests - All API endpoints must have tests
## Design System
This project uses a design system defined in `DESIGN.md` at the project root.
Always refer to this file when generating or modifying any UI component.
- Use only colors, fonts, and spacing values defined in DESIGN.md.
- Do not invent new values or use defaults from any framework.
- Match component states (hover, focus, active, disabled) to the patterns in DESIGN.md.
- Follow the typographic scale and weight assignments in DESIGN.md.

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.

0
DESIGN.2.md Normal file
View File

178
DESIGN.md
View File

@@ -1,145 +1,149 @@
--- ---
name: Obsidian & Amber name: Industrial Precision
colors: colors:
surface: '#0c1322' surface: '#131313'
surface-dim: '#0c1322' surface-dim: '#131313'
surface-bright: '#323949' surface-bright: '#3a3939'
surface-container-lowest: '#070e1d' surface-container-lowest: '#0e0e0e'
surface-container-low: '#141b2b' surface-container-low: '#1c1b1b'
surface-container: '#191f2f' surface-container: '#201f1f'
surface-container-high: '#232a3a' surface-container-high: '#2a2a2a'
surface-container-highest: '#2e3545' surface-container-highest: '#353534'
on-surface: '#dce2f7' on-surface: '#e5e2e1'
on-surface-variant: '#d8c3ad' on-surface-variant: '#ddc1af'
inverse-surface: '#dce2f7' inverse-surface: '#e5e2e1'
inverse-on-surface: '#293040' inverse-on-surface: '#313030'
outline: '#a08e7a' outline: '#a48c7c'
outline-variant: '#534434' outline-variant: '#564335'
surface-tint: '#ffb95f' surface-tint: '#ffb781'
primary: '#ffc174' primary: '#ffb781'
on-primary: '#472a00' on-primary: '#4e2600'
primary-container: '#f59e0b' primary-container: '#f58618'
on-primary-container: '#613b00' on-primary-container: '#5b2d00'
inverse-primary: '#855300' inverse-primary: '#924c00'
secondary: '#ffb599' secondary: '#c8c6c5'
on-secondary: '#5a1c00' on-secondary: '#303030'
secondary-container: '#f66018' secondary-container: '#474746'
on-secondary-container: '#4f1700' on-secondary-container: '#b7b5b4'
tertiary: '#56e5a9' tertiary: '#00e639'
on-tertiary: '#003824' on-tertiary: '#003907'
tertiary-container: '#30c88f' tertiary-container: '#00bd2d'
on-tertiary-container: '#004e34' on-tertiary-container: '#00440a'
error: '#ffb4ab' error: '#ffb4ab'
on-error: '#690005' on-error: '#690005'
error-container: '#93000a' error-container: '#93000a'
on-error-container: '#ffdad6' on-error-container: '#ffdad6'
primary-fixed: '#ffddb8' primary-fixed: '#ffdcc4'
primary-fixed-dim: '#ffb95f' primary-fixed-dim: '#ffb781'
on-primary-fixed: '#2a1700' on-primary-fixed: '#2f1400'
on-primary-fixed-variant: '#653e00' on-primary-fixed-variant: '#6f3800'
secondary-fixed: '#ffdbce' secondary-fixed: '#e5e2e1'
secondary-fixed-dim: '#ffb599' secondary-fixed-dim: '#c8c6c5'
on-secondary-fixed: '#370e00' on-secondary-fixed: '#1b1c1c'
on-secondary-fixed-variant: '#7f2b00' on-secondary-fixed-variant: '#474746'
tertiary-fixed: '#6ffbbe' tertiary-fixed: '#72ff70'
tertiary-fixed-dim: '#4edea3' tertiary-fixed-dim: '#00e639'
on-tertiary-fixed: '#002113' on-tertiary-fixed: '#002203'
on-tertiary-fixed-variant: '#005236' on-tertiary-fixed-variant: '#00530e'
background: '#0c1322' background: '#131313'
on-background: '#dce2f7' on-background: '#e5e2e1'
surface-variant: '#2e3545' surface-variant: '#353534'
typography: typography:
headline-lg: headline-lg:
fontFamily: Space Grotesk fontFamily: Space Grotesk
fontSize: 32px fontSize: 48px
fontWeight: '700' fontWeight: '700'
lineHeight: '1.2' lineHeight: '1.1'
letterSpacing: -0.02em letterSpacing: -0.02em
headline-md: headline-md:
fontFamily: Space Grotesk
fontSize: 32px
fontWeight: '600'
lineHeight: '1.2'
letterSpacing: -0.01em
headline-sm:
fontFamily: Space Grotesk fontFamily: Space Grotesk
fontSize: 24px fontSize: 24px
fontWeight: '600' fontWeight: '600'
lineHeight: '1.2' lineHeight: '1.2'
body-lg: body-lg:
fontFamily: Inter fontFamily: Space Grotesk
fontSize: 16px fontSize: 18px
fontWeight: '400' fontWeight: '400'
lineHeight: '1.5' lineHeight: '1.5'
body-md: body-md:
fontFamily: Inter fontFamily: Space Grotesk
fontSize: 14px fontSize: 16px
fontWeight: '400' fontWeight: '400'
lineHeight: '1.5' lineHeight: '1.5'
data-lg: label-md:
fontFamily: Space Grotesk fontFamily: Space Grotesk
fontSize: 18px fontSize: 14px
fontWeight: '500' fontWeight: '500'
lineHeight: '1.2' lineHeight: '1'
letterSpacing: 0.05em letterSpacing: 0.05em
data-sm: mono-data:
fontFamily: Space Grotesk fontFamily: Space Grotesk
fontSize: 12px fontSize: 14px
fontWeight: '500'
lineHeight: '1.2'
letterSpacing: 0.1em
label-caps:
fontFamily: Space Grotesk
fontSize: 11px
fontWeight: '700' fontWeight: '700'
lineHeight: '1' lineHeight: '1'
letterSpacing: 0.15em letterSpacing: 0.02em
spacing: spacing:
unit: 4px unit: 4px
gutter: 16px gutter: 16px
margin: 24px margin: 32px
container-max: 1440px container-gap: 24px
stack-sm: 8px
stack-md: 16px
--- ---
## Brand & Style ## Brand & Style
This design system is engineered for high-stakes datacenter environments where clarity and rapid data ingestion are critical. The brand personality is **rugged, utilitarian, and precise**, evoking the feel of industrial control panels and terminal interfaces. It targets system administrators and infrastructure engineers who require a "heads-up display" (HUD) aesthetic that remains legible during long shifts in low-light environments. This design system is engineered for the high-stakes environment of a datacenter, where speed of comprehension and operational uptime are paramount. The brand personality is technical, authoritative, and unapologetically utilitarian. It draws heavily from **Minimalist Industrialism** and **Brutalism**, stripping away decorative flourishes in favor of structural clarity and high-contrast signaling.
The design style is **Industrial Brutalism**. It prioritizes function over form, utilizing heavy borders, high-contrast mechanical accents, and monospaced typography to create a sense of structural integrity. Visual elements are treated as physical components, emphasizing hardware-level reliability. The emotional response is one of controlled intensity—evoking the feeling of a high-end command center. It prioritizes "glanceability," ensuring that critical status changes are immediately perceptible against a void-like backdrop. The aesthetic mimics physical hardware interfaces: rack-mounted servers, heavy-duty machinery, and tactical displays.
## Colors ## Colors
The palette is anchored by **Deep Obsidian (#111827)** to minimize eye strain and light bleed in dark server rooms. **Industrial Amber (#F59E0B)** serves as the primary interactive and high-visibility color, functioning as a digital "phosphor" that cuts through the dark background. The palette is anchored by a deep charcoal and true black foundation to minimize eye strain in low-light environments and maximize contrast.
**Safety Orange (#EA580C)** is reserved strictly for warning states and cautionary telemetry. A supplemental **Tactical Green (#10B981)** is used for "Optimal" or "Online" status indicators. The color system uses high-contrast ratios to ensure that critical information is never lost in the interface. - **Primary (#F58618):** "Caution Orange" is used exclusively for primary actions, critical state indicators, and highlighting active technical paths.
- **Surface Palette:** Backgrounds utilize a tiered black system: `#0A0A0A` for the base, `#121212` for containers, and `#1A1A1A` for hover states.
- **Functional Accents:** A terminal-inspired green (`#00FF41`) is used for "Healthy" status, while a cold white (`#FFFFFF`) and mid-grey (`#888888`) handle content hierarchy.
- **Contrast:** Maintain a minimum 7:1 contrast ratio for all status-critical text against the dark backgrounds.
## Typography ## Typography
While the user requested "Roboto Mono," this design system utilizes **Space Grotesk** for technical data and headlines to provide a modern, geometric "tech-brutalist" edge, paired with **Inter** for dense body text to maintain maximum legibility. The typography utilizes **Space Grotesk** across all levels to maintain a technical, geometric rigour. Its idiosyncratic letterforms provide a futuristic "code-like" feel while remaining highly legible at small sizes.
All data-driven labels and telemetry values must use **Space Grotesk** to simulate the look of physical hardware engravings and terminal readouts. Headers and labels should frequently employ `uppercase` styling with increased letter-spacing to reinforce the industrial documentation aesthetic. - **Headlines:** Use tight tracking and heavy weights to create a sense of structural density.
- **Labels:** Small caps or all-caps are preferred for metadata and technical specs to differentiate them from prose.
- **Numeric Data:** Ensure tabular figures are used for metrics, alignment of IP addresses, and throughput values to facilitate easy vertical scanning of logs and tables.
## Layout & Spacing ## Layout & Spacing
This design system employs a **Fluid-Grid hybrid** model. The main workspace expands to utilize 100% of the viewport to accommodate dense data tables and rack visualizations, but content is constrained by a 12-column underlying structure for dashboard widgets. This design system employs a **fixed grid** model for primary dashboards to ensure telemetry data remains in a predictable location. A 12-column system is used for high-level layouts, but interior modules utilize a strict 4px baseline grid.
The spacing rhythm is strictly based on a **4px baseline grid**. Gutters and margins are kept tight (16px and 24px respectively) to maximize information density—allowing engineers to see more telemetry on a single screen without scrolling. Components use "internal padding" rather than external margins to create a tightly-packed, modular appearance. Spacing is compressed to increase information density, reflecting the "glass cockpit" philosophy where more data is preferable to excessive whitespace. Use heavy 24px-32px margins for the primary viewport to frame the content, but keep internal component padding tight (8px-12px) to maximize screen real estate for logs and graphs.
## Elevation & Depth ## Elevation & Depth
Depth is conveyed through **Tonal Layering and Bold Borders** rather than traditional soft shadows. This mimics the physical construction of server racks and control panels. Depth is conveyed through **Bold Borders** and **Tonal Layers** rather than shadows. In a datacenter environment, shadows can muddy the interface; instead, we use high-contrast outlines to define hierarchy.
1. **Base Level:** Deep Obsidian (#111827) representing the "chassis." - **Level 0 (Base):** `#0A0A0A`
2. **Raised Level:** Surface Alt (#1F2937) for cards and modules, featuring 1px solid borders in a slightly lighter charcoal or the primary Amber for active states. - **Level 1 (Cards/Panels):** `#121212` with a 1px solid border of `#222222`.
3. **Interactive Elements:** Use inset borders (inner shadows) to create a "pressed" or "carved" effect when buttons are toggled. - **Level 2 (Modals/Popovers):** `#1A1A1A` with a 2px solid border of the Primary Orange or a high-visibility grey.
- **Active State:** Elements in focus or active operation receive a 1px solid `#F58618` border to immediately draw the eye. No blurs or gradients are permitted.
Shadows, if used, must be sharp and low-opacity (hard-edged) to maintain the brutalist, high-readability requirement.
## Shapes ## Shapes
The shape language is strictly **Sharp (0px)**. All containers, buttons, and input fields must have 90-degree corners. This reinforces the "rugged hardware" aesthetic and ensures that UI elements align perfectly with the pixel grid, preventing any blurriness on industrial-grade monitors. The shape language is strictly **Sharp (0px)**. 90-degree angles reinforce the industrial, hardware-centric aesthetic. This lack of rounding suggests precision and eliminates the "friendly" softness typical of consumer SaaS. Every button, input, and container must maintain a hard edge to align with the rigid grid of a server rack.
Angle-cut corners (dog-eared corners) may be used on primary action buttons or status chips to provide a "tactical" look without resorting to curves.
## Components ## Components
- **Buttons:** High-contrast blocks. Primary buttons use a solid Amber background with black text. Secondary buttons use an Amber 1px border with no fill. - **Buttons:** Rectangular with no radius. Primary buttons are solid `#F58618` with black text. Secondary buttons are transparent with a 1px white or grey border.
- **Input Fields:** Deep charcoal fills with an Amber bottom-border only. Labels are placed above the field in `label-caps` style. - **Input Fields:** Darker than the container background (`#000000`). Use a bottom-only border or a full 1px border that changes to the primary orange on focus.
- **Data Tables:** Zebra-striping is forbidden. Instead, use thin 1px horizontal dividers. Hover states should highlight the entire row with a low-opacity Amber tint. - **Status Chips:** High-contrast blocks. "Success" is a solid green block with black text; "Alert" is a solid orange block. No rounded corners.
- **Status Chips:** Rectangular boxes with no roundedness. Use background colors for Critical (Red) and Warning (Orange), but keep the text black for maximum contrast. - **Data Tables:** Zebra-striping is prohibited. Use 1px horizontal dividers only. Header cells must be all-caps Space Grotesk with a subtle background tint.
- **Rack Visualizers:** Modular components that represent physical server hardware. Use high-contrast outlines to distinguish between individual units (U). - **Telemetry Charts:** Lines should be 2px thick with no smoothing (stair-step/linear interpolation only) to represent raw data accuracy.
- **Telemetry Gauges:** Monochromatic bar charts and line graphs using the Primary Amber color, emphasizing the "oscilloscope" or "terminal" aesthetic. - **Log Viewers:** Use the `mono-data` type style. Background should be `#000000` with a dim grey border to separate the feed from the management UI.

View File

@@ -14,14 +14,3 @@ Be concise! Do not explain a thousand details unless they are absolutely necessa
Do not proceed with any task until you have analyzed these three files. Do not proceed with any task until you have analyzed these three files.
DO NOT, EVER, USE "uppercase" or "toUpper" in any UI/UX context, nowhere. This SHOULD NOT be used in UI/UX anywhere, no text should be displayed in CAPITAL LETTERS! But do not replace "toUpper" with "toLower" or "uppercase" with "lowercase" either, leave text in UI/UX as is. DO NOT, EVER, USE "uppercase" or "toUpper" in any UI/UX context, nowhere. This SHOULD NOT be used in UI/UX anywhere, no text should be displayed in CAPITAL LETTERS! But do not replace "toUpper" with "toLower" or "uppercase" with "lowercase" either, leave text in UI/UX as is.
# Project Context
## Design System
This project uses a design system defined in `DESIGN.md` at the project root.
Always refer to this file when generating or modifying any UI component.
- Use only colors, fonts, and spacing values defined in DESIGN.md.
- Do not invent new values or use defaults from any framework.
- Match component states (hover, focus, active, disabled) to the patterns in DESIGN.md.
- Follow the typographic scale and weight assignments in DESIGN.md.

View File

@@ -20,7 +20,7 @@ A unified system for inventory management featuring web administration, offline
### 2.2 Frontend (Web & PWA) ### 2.2 Frontend (Web & PWA)
- **Architecture**: Next.js 15+ (App Router, TypeScript Strict) - **Architecture**: Next.js 15+ (App Router, TypeScript Strict)
- **Styling**: Tailwind CSS v3.4 (Standard typography, normal weight only) - **Styling**: Tailwind CSS v3.4 (Industrial Precision, Space Grotesk, normal weight only)
- **Icons**: Lucide Icons (exclusive) - **Icons**: Lucide Icons (exclusive)
- **Offline Persistence**: Dexie.js (IndexedDB) - **Offline Persistence**: Dexie.js (IndexedDB)
- **Scanner**: `html5-qrcode` (Client-side, offline) - **Scanner**: `html5-qrcode` (Client-side, offline)
@@ -55,23 +55,18 @@ A unified system for inventory management featuring web administration, offline
--- ---
## 4. Design & Mobile Constraints (Industrial Brutalism) ## 4. Design & Mobile Constraints
### 4.1 Spacing & Layout ### 4.1 Spacing & Layout
- **Container**: `max-w-7xl` for main pages. - **Container**: `max-w-7xl` for main pages.
- **Responsive Spacing**: `p-4` (mobile) → `p-8` (desktop) with `space-y-4``space-y-6`. - **Responsive Spacing**: `space-y-3` (mobile) → `space-y-6` (desktop).
- **Shapes**: **Strictly Sharp (0px)** corners globally. No rounded borders. - **Padding**: `p-4` (mobile) → `p-8` (desktop).
- **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-normal`). - **Height**: Avoid `min-h-screen` on mobile to prevent viewport overflow; use `md:min-h-screen`.
### 4.2 Typography & Colors ### 4.2 Typography
- **Palette (Obsidian & Amber)**: - **Readability**: Standard camel/Title case.
- Background: Deep Obsidian (`#0c1322`) - **NO UPPERCASE**: Strictly forbidden in UI text.
- Primary: Industrial Amber (`#ffc174`) - **NO BOLD**: Use `font-normal`. Hierarchy via size (`text-sm` vs `text-3xl`) and color (`text-slate-500` vs `text-white`).
- Borders: Tactical Charcoal (`#a08e7a`)
- **Fonts**: **Space Grotesk** (Headlines/Data) & **Inter** (Body).
- **NO UPPERCASE**: Strictly forbidden in all UI contexts.
- **NO BOLD**: Strictly forbidden. Use `font-normal` throughout. Hierarchy via size and color only.
- **StatCards**: Numeric values must match label text size (e.g., `text-base md:text-lg`).
--- ---

View File

@@ -56,5 +56,5 @@ To generate a clean production package:
--- ---
**Last Updated**: 2026-05-15 **Last Updated**: 2026-04-25
**Version**: 1.15.0 (Phase 7) **Version**: 1.14.25 (Phase 7.1)

View File

@@ -1,5 +1,5 @@
{ {
"version": "1.14.7", "version": "1.14.22",
"lastUpdated": "2026-04-23", "lastUpdated": "2026-04-23",
"phase": "Phase 7 Complete - Config Consolidation" "phase": "Milestone 7 Shipped - Config Consolidation"
} }

Binary file not shown.

View File

@@ -21,6 +21,13 @@ A unified inventory management system that eliminates manual data entry through
## 3. Active Roadmap ## 3. Active Roadmap
### Phase 7.1: Frontend UI/UX Overhaul (Industrial Precision) [ACTIVE]
- **Design System Integration**: Apply the "Industrial Precision" system from `DESIGN.md`.
- **Component Refactoring**: Update all UI components to use sharp corners (0px) and bold borders.
- **Color Palette Update**: Implement the high-contrast Caution Orange and tiered black surface system.
- **Typography Alignment**: Apply Space Grotesk while strictly adhering to the "NO BOLD" and "NO UPPERCASE" rules from AI_RULES.md.
- **Glanceability Optimization**: Increase information density in StatCards and Data Tables.
### Phase 8: Hardening & Release (PLANNED) ### Phase 8: Hardening & Release (PLANNED)
- **Production Runbook**: Finalized guide for production deployment. - **Production Runbook**: Finalized guide for production deployment.
- **Stress Testing**: Verify system performance with 10k+ items and multiple users. - **Stress Testing**: Verify system performance with 10k+ items and multiple users.
@@ -42,7 +49,12 @@ A unified inventory management system that eliminates manual data entry through
## 5. Design & Operational Constraints ## 5. Design & Operational Constraints
- **Tech Stack**: FastAPI (Python), SQLite, Next.js (TypeScript), Tailwind CSS. - **Tech Stack**: FastAPI (Python), SQLite, Next.js (TypeScript), Tailwind CSS.
<<<<<<< HEAD
- **UI Fidelity**: Premium density, Lucide icons, **NO UPPERCASE**, **NO BOLD FONTS**.
- **Security**: Mandatory auth, immutable audit logs.
=======
- **UI Fidelity**: Premium density, Lucide icons, **NO UPPERCASE**, **NO BOLD FONTS** (normal weight only). - **UI Fidelity**: Premium density, Lucide icons, **NO UPPERCASE**, **NO BOLD FONTS** (normal weight only).
- **StatCard Rule**: Numeric values MUST match label text size for balanced density.
- **Database**: Single-instance SQLite (WAL mode). Multi-instance is v3+. - **Database**: Single-instance SQLite (WAL mode). Multi-instance is v3+.
- **Security**: Mandatory auth, no bypasses, immutable audit logs. - **Security**: Mandatory auth, no bypasses, immutable audit logs.
- **AI**: Multi-provider resilience (Gemini primary, Claude fallback). - **AI**: Multi-provider resilience (Gemini primary, Claude fallback).
@@ -53,6 +65,7 @@ A unified inventory management system that eliminates manual data entry through
- **Reset Planning (2026-04-22)**: Refocused on v2 stable release, deferred complex analytics to v3. - **Reset Planning (2026-04-22)**: Refocused on v2 stable release, deferred complex analytics to v3.
- **Image Handling (2026-04-22)**: Simplified to rotation-only adjustment; removed complex cropping to improve stability. - **Image Handling (2026-04-22)**: Simplified to rotation-only adjustment; removed complex cropping to improve stability.
- **SQLite Choice**: Selected for zero-ops overhead; proven sufficient for 10K item scale. - **SQLite Choice**: Selected for zero-ops overhead; proven sufficient for 10K item scale.
>>>>>>> dev
--- ---

View File

@@ -1,34 +1,29 @@
# CURRENT AI WORKING SESSION — HANDOVER # CURRENT AI WORKING SESSION — HANDOVER
**Active AI:** Gemini CLI **Active AI:** Gemini Executor
**Last Updated:** 2026-04-25 **Last Updated:** 2026-04-25
**Current Version:** v1.14.23 **Current Version:** v1.14.25
**Status**: ✅ DESIGN SYSTEM INTEGRATED | 🟢 READY FOR HARDENING **Status**: 🟢 PHASE 7.1 TASK 1 COMPLETE
--- ---
## SESSION 43 EXECUTION SUMMARY — Design System Integration & UI/UX Overhaul ## SESSION EXECUTION SUMMARY — Task 1: Tailwind Update
### 1. "Obsidian & Amber" Palette (COMPLETE) ### 1. Tailwind Config (COMPLETE)
- **Foundation**: Integrated `DESIGN.md` colors into `tailwind.config.ts` and `globals.css`. - Updated `frontend/tailwind.config.ts` with "Industrial Precision" palette.
- **Refactor**: Replaced legacy `slate`, `indigo`, `blue` classes with system `primary`, `secondary`, `surface`, and `outline` tokens. - Added `Space Grotesk` font family.
- **Aesthetic**: Established "Industrial Brutalism" with Deep Obsidian backgrounds and Industrial Amber highlights. - Aligned font sizes with `DESIGN.md` (14px, 16px, 18px, 24px, 32px, 48px).
- Defined tiered surface system: `#0A0A0A`, `#121212`, `#1A1A1A`.
### 2. High-Fidelity Typography (COMPLETE) ### 2. Documentation Update
- **Fonts**: Switched to **Space Grotesk** for data/headlines and **Inter** for body text. - Updated `PROJECT_ARCHITECTURE.md` with new styling specs.
- **Compliance**: Enforced `font-normal` globally; eliminated all `bold`, `semibold`, and `medium` instances. - Updated `README.md` version and date.
- **Strict Rules**: Removed all `uppercase` and `italic` classes from the UI to comply with `AI_RULES.md`. - Marked Task 1 as completed in `.planning/phases/07.1-frontend-overhaul/PLAN.md`.
### 3. Structural & Layout Consistency
- **Shapes**: Enforced **strictly sharp (0px)** corners globally via `globals.css`.
- **Headers**: Standardized all page headers to the **Unified Header** pattern: `p-4` Icon box (`bg-primary/10`) + `text-3xl font-normal` Title.
- **Containers**: Verified and enforced `max-w-7xl` containers with responsive `p-4` to `p-8` padding.
--- ---
## NEXT STEPS (Phase 8: Hardening & Release) ## NEXT STEPS (Phase 7.1: Frontend UI/UX Overhaul)
1. **Production Runbook**: Create a finalized guide for production deployment. 1. **Task 2: Implement Global Sharp Edges & Borders**: Update `globals.css` for 0px radius and 1px/2px borders.
2. **Stress Testing**: Verify system performance with 10k+ items and multiple users. 2. **Task 3: Component-Level Overhaul**: Update Buttons and Inputs.
3. **E2E Validation**: Run full suite of Playwright tests to ensure design changes didn't break accessibility or interaction targets. 3. **Task 4: StatCard & Data Table Refinement**.
4. **Final Polish**: Verify that `StatCard` values match label sizes consistently in all viewports.

View File

@@ -1,6 +1,6 @@
{ {
"version": "1.14.24", "version": "1.14.26",
"last_build": "2026-04-25-1204", "last_build": "2026-04-25-1225",
"codename": "ConfigCore", "codename": "ConfigCore",
"commit": "33c1555d" "commit": "701e30fb"
} }

View File

@@ -18,7 +18,7 @@ export default function AdminPage() {
return ( return (
<PageShell> <PageShell>
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary animate-spin" /> <div className="w-12 h-12 border-4 border-indigo-500/20 border-t-indigo-500 rounded-full animate-spin" />
</div> </div>
</PageShell> </PageShell>
); );
@@ -26,18 +26,18 @@ export default function AdminPage() {
return ( return (
<PageShell> <PageShell>
<main data-testid="admin-page" className="p-4 md:p-8 max-w-7xl mx-auto space-y-4 md:space-y-6 mb-20"> <main data-testid="admin-page" className="p-3 md:p-8 max-w-7xl mx-auto space-y-3 md:space-y-6 mb-20">
<header className="flex items-center gap-4 mb-4 md:mb-6"> <header className="flex items-center gap-4 mb-4 md:mb-6">
<div className="p-4 bg-primary/10 text-primary border border-primary/20 shadow-xl shadow-primary/5"> <div className="p-3 md:p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20 shadow-xl shadow-indigo-500/5">
<Shield size={32} /> <Shield size={28} className="md:w-8 md:h-8" />
</div> </div>
<div> <div>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-foreground leading-tight">Admin Control</h1> <h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">Admin Control</h1>
<p className="text-[10px] md:text-xs text-secondary font-normal tracking-tight mt-0.5">System Configuration & Security</p> <p className="text-[10px] md:text-xs text-muted font-normal tracking-tight mt-0.5">System Configuration & Security</p>
</div> </div>
<button <button
onClick={admin.handleLogout} onClick={admin.handleLogout}
className="ml-auto p-3 bg-surface border border-outline-variant text-secondary hover:text-error transition-all active:scale-95 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-error 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

@@ -1,70 +1,89 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400&family=Space+Grotesk:wght@400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root { @layer base {
/* Industrial Brutalism Palette - Obsidian & Amber */ :root {
--background: #0c1322; --background: #131313;
--foreground: #dce2f7; --foreground: #FFFFFF;
--primary: #F58618;
--surface: #0c1322; --border: #222222;
--surface-container: #191f2f; --muted: #888888;
--surface-bright: #323949; }
--primary: #ffc174;
--primary-foreground: #472a00;
--secondary: #ffb599;
--secondary-foreground: #5a1c00;
--success: #10B981;
--warning: #f59e0b;
--error: #ffb4ab;
--outline: #a08e7a;
--outline-variant: #534434;
--font-data: 'Space Grotesk', sans-serif; * {
--font-body: 'Inter', sans-serif; @apply border-border;
border-radius: 0 !important;
box-shadow: none !important;
}
body {
@apply bg-background text-foreground antialiased font-normal;
font-family: 'Space Grotesk', sans-serif;
letter-spacing: -0.01em;
}
/* Strict "NO BOLD" enforcement from AI_RULES.md */
*, *::before, *::after {
font-weight: 400 !important;
}
/* No uppercase/italics as per AI_RULES.md */
*, *::before, *::after {
text-transform: none !important;
font-style: normal !important;
}
h1, h2, h3, h4, h5, h6 {
@apply tracking-tighter;
}
} }
* { @layer components {
border-radius: 0 !important; /* Level 1 (Cards/Panels): #121212 with a 1px solid border of #222222 */
.level-1 {
@apply bg-[#121212] border border-[#222222];
}
/* Level 2 (Modals/Popovers): #1A1A1A with a 2px solid border of the Primary Orange */
.level-2 {
@apply bg-[#1A1A1A] border-2 border-primary;
}
/* Active State */
.active-border {
@apply border border-primary;
}
/* High-contrast status blocks */
.status-success {
@apply bg-[#00FF41] text-black px-2 py-0.5;
}
.status-alert {
@apply bg-[#F58618] text-black px-2 py-0.5;
}
} }
body { /* Custom Industrial Scrollbar Styling */
color: var(--foreground);
background-color: var(--background);
font-family: var(--font-body);
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6, [data-font="space-grotesk"] {
font-family: var(--font-data);
font-weight: 400;
}
/* Custom Scrollbar Styling - Industrial */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--background); background: #0A0A0A;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--outline-variant); background: #222222;
border: 1px solid var(--outline); border-radius: 0;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--outline); background: #F58618;
} }
@layer utilities { @layer utilities {
@@ -77,18 +96,20 @@ h1, h2, h3, h4, h5, h6, [data-font="space-grotesk"] {
} }
.card-title { .card-title {
@apply text-base font-normal text-foreground; @apply text-base font-normal tracking-tight;
} }
.card-subtitle { .card-subtitle {
@apply text-xs font-normal text-secondary mt-0.5; @apply text-xs font-normal text-muted-foreground tracking-tight mt-0.5;
} }
.industrial-border { .mono-data {
@apply border border-outline/30; @apply font-normal tracking-normal;
font-variant-numeric: tabular-nums;
} }
} }
/* Safe Area Insets for Modern Mobile Devices */
.pb-safe { .pb-safe {
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
@@ -97,6 +118,7 @@ h1, h2, h3, h4, h5, h6, [data-font="space-grotesk"] {
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
} }
/* Accessibility: Respect user's reduced motion preference */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *,
*::before, *::before,
@@ -105,4 +127,10 @@ h1, h2, h3, h4, h5, h6, [data-font="space-grotesk"] {
animation-iteration-count: 1 !important; animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important; transition-duration: 0.01ms !important;
} }
.animate-scan-fast,
.animate-spin-slow,
.animate-pulse {
animation: none !important;
}
} }

View File

@@ -266,7 +266,7 @@ export default function InventoryPage() {
return ( return (
<PageShell> <PageShell>
<div className="p-4 md:p-8 max-w-7xl mx-auto space-y-4 md:space-y-6"> <div className="p-3 md:p-8 max-w-7xl mx-auto space-y-3 md:space-y-4">
<datalist id="existing-types"> <datalist id="existing-types">
{existingTypes.map(t => <option key={t} value={t} />)} {existingTypes.map(t => <option key={t} value={t} />)}
</datalist> </datalist>
@@ -275,23 +275,23 @@ export default function InventoryPage() {
</datalist> </datalist>
<header className="flex items-center gap-4 mb-4 md:mb-6"> <header className="flex items-center gap-4 mb-4 md:mb-6">
<div className="p-4 bg-primary/10 text-primary border border-primary/20 shadow-xl shadow-primary/5"> <div className="p-3 md:p-4 bg-primary/10 rounded-3xl text-primary border border-primary/20 shadow-xl shadow-primary/5">
<Package size={32} /> <Package size={28} className="md:w-8 md:h-8" />
</div> </div>
<div> <div>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-foreground leading-tight">Inventory Catalog</h1> <h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">Inventory Catalog</h1>
<p className="text-xs md:text-sm text-secondary font-normal mt-1">Enterprise Stock Overview</p> <p className="text-xs md:text-sm text-secondary font-normal mt-1 tracking-widest">Enterprise Stock Overview</p>
</div> </div>
<button <button
onClick={() => setShowSearchModal(true)} onClick={() => setShowSearchModal(true)}
className="ml-auto p-3 bg-surface border border-outline/30 text-secondary hover:text-primary transition-all active:scale-95 shadow-xl" className="ml-auto p-3 bg-surface border border-slate-800 text-secondary rounded-2xl hover:text-primary transition-all active:scale-95 shadow-xl"
title="Search inventory" title="Search inventory"
> >
<Search size={20} /> <Search size={20} />
</button> </button>
<button <button
onClick={() => setShowBoxManager(true)} onClick={() => setShowBoxManager(true)}
className="p-3 bg-surface border border-outline/30 text-secondary hover:text-primary transition-all active:scale-95 shadow-xl" className="p-3 bg-surface border border-slate-800 text-secondary rounded-2xl hover:text-primary transition-all active:scale-95 shadow-xl"
title="Manage Boxes" title="Manage Boxes"
> >
<Layout size={20} /> <Layout size={20} />
@@ -346,13 +346,13 @@ 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-outline/30 ] sm:] 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">
{isEditing ? "Edit Item" : selectedItem.name} {isEditing ? "Edit Item" : selectedItem.name}
{!isEditing && ( {!isEditing && (
<span className="text-xs bg-surface-container text-secondary px-3 py-1 font-normal tracking-tight"> <span className="text-xs bg-slate-800 text-secondary px-3 py-1 rounded-lg font-normal tracking-tight">
In Stock: {selectedItem.quantity} In Stock: {selectedItem.quantity}
</span> </span>
)} )}
@@ -364,7 +364,7 @@ export default function InventoryPage() {
setEditedItem(selectedItem); setEditedItem(selectedItem);
setIsEditing(true); setIsEditing(true);
}} }}
className="p-2 hover:bg-surface-container 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-error/20 text-error" 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-surface-container" 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>
@@ -398,7 +398,7 @@ export default function InventoryPage() {
type="text" type="text"
value={editedItem.name || ''} value={editedItem.name || ''}
onChange={e => setEditedItem({...editedItem, name: e.target.value})} onChange={e => setEditedItem({...editedItem, name: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-muted"
/> />
</div> </div>
<div> <div>
@@ -407,7 +407,7 @@ export default function InventoryPage() {
type="text" type="text"
value={editedItem.part_number || ''} value={editedItem.part_number || ''}
onChange={e => setEditedItem({...editedItem, part_number: e.target.value})} onChange={e => setEditedItem({...editedItem, part_number: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-muted"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-2 md:gap-3"> <div className="grid grid-cols-2 gap-2 md:gap-3">
@@ -418,7 +418,7 @@ export default function InventoryPage() {
list="existing-categories" list="existing-categories"
value={editedItem.category || ''} value={editedItem.category || ''}
onChange={e => setEditedItem({ ...editedItem, category: e.target.value })} onChange={e => setEditedItem({ ...editedItem, category: e.target.value })}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary placeholder:text-muted"
placeholder="e.g. storage" placeholder="e.g. storage"
/> />
<datalist id="existing-categories"> <datalist id="existing-categories">
@@ -434,7 +434,7 @@ export default function InventoryPage() {
list="existing-types" list="existing-types"
value={editedItem.type || ''} value={editedItem.type || ''}
onChange={e => setEditedItem({...editedItem, type: e.target.value})} onChange={e => setEditedItem({...editedItem, type: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary"
/> />
</div> </div>
<div> <div>
@@ -443,7 +443,7 @@ export default function InventoryPage() {
type="text" type="text"
value={editedItem.connector || ''} value={editedItem.connector || ''}
onChange={e => setEditedItem({...editedItem, connector: e.target.value})} onChange={e => setEditedItem({...editedItem, connector: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary"
placeholder="e.g. LC/UPC" placeholder="e.g. LC/UPC"
/> />
</div> </div>
@@ -453,7 +453,7 @@ export default function InventoryPage() {
type="text" type="text"
value={editedItem.size || ''} value={editedItem.size || ''}
onChange={e => setEditedItem({...editedItem, size: e.target.value})} onChange={e => setEditedItem({...editedItem, size: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary"
placeholder="e.g. 1.6TB" placeholder="e.g. 1.6TB"
/> />
</div> </div>
@@ -463,7 +463,7 @@ export default function InventoryPage() {
type="text" type="text"
value={editedItem.color || ''} value={editedItem.color || ''}
onChange={e => setEditedItem({...editedItem, color: e.target.value})} onChange={e => setEditedItem({...editedItem, color: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary"
placeholder="e.g. Blue" placeholder="e.g. Blue"
/> />
</div> </div>
@@ -475,7 +475,7 @@ export default function InventoryPage() {
list="existing-boxes" list="existing-boxes"
value={editedItem.box_label || ''} value={editedItem.box_label || ''}
onChange={e => setEditedItem({...editedItem, box_label: e.target.value})} onChange={e => setEditedItem({...editedItem, box_label: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 pl-4 pr-12 text-sm font-normal outline-none text-secondary placeholder:text-secondary focus:border-primary transition-colors" className="w-full bg-background border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm font-normal outline-none text-secondary placeholder:text-muted focus:border-primary transition-colors"
placeholder="e.g. Box 5" placeholder="e.g. Box 5"
/> />
<button <button
@@ -486,8 +486,8 @@ 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 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-foreground animate-pulse" : "text-secondary hover:bg-surface-container" fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-muted hover:bg-slate-800"
)} )}
> >
<Camera size={18} /> <Camera size={18} />
@@ -500,7 +500,7 @@ export default function InventoryPage() {
<textarea <textarea
value={editedItem.specs || ''} value={editedItem.specs || ''}
onChange={e => setEditedItem({...editedItem, specs: e.target.value})} onChange={e => setEditedItem({...editedItem, specs: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary h-20 resize-none" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary h-20 resize-none"
/> />
</div> </div>
<div> <div>
@@ -508,24 +508,24 @@ export default function InventoryPage() {
<textarea <textarea
value={editedItem.ocr_text || ''} value={editedItem.ocr_text || ''}
onChange={e => setEditedItem({...editedItem, ocr_text: e.target.value})} onChange={e => setEditedItem({...editedItem, ocr_text: e.target.value})}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-primary/80 h-16 resize-none" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-primary/80 h-16 resize-none"
/> />
</div> </div>
</div> </div>
) : ( ) : (
<> <>
<div className="flex p-1 bg-background mb-4 md:mb-6"> <div className="flex p-1 bg-background rounded-2xl mb-4 md:mb-6">
{[ {[
{ id: 'ADD', label: 'Buy More', icon: Plus, color: 'text-primary' }, { id: 'ADD', label: 'Buy More', icon: Plus, color: 'text-primary' },
{ id: 'REMOVE', label: 'Subtract', icon: Minus, color: 'text-warning' }, { id: 'REMOVE', label: 'Subtract', icon: Minus, color: 'text-amber-500' },
{ id: 'TRASH', label: 'Discard', icon: Trash2, color: 'text-error' } { id: 'TRASH', label: 'Discard', icon: Trash2, color: 'text-red-500' }
].map((t) => ( ].map((t) => (
<button <button
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 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-surface-container shadow-lg" : "text-secondary" adjustType === t.id ? "bg-slate-800 shadow-lg" : "text-muted"
)} )}
> >
<t.icon size={20} className={adjustType === t.id ? t.color : ""} /> <t.icon size={20} className={adjustType === t.id ? t.color : ""} />
@@ -538,25 +538,25 @@ export default function InventoryPage() {
<div className="flex items-center gap-3 md:gap-4"> <div className="flex items-center gap-3 md:gap-4">
<button <button
onClick={() => setAdjustQty(Math.max(1, adjustQty - 1))} onClick={() => setAdjustQty(Math.max(1, adjustQty - 1))}
className="w-12 h-12 border border-outline/30 flex items-center justify-center hover:bg-surface-container transition-colors" className="w-12 h-12 rounded-full border border-slate-800 flex items-center justify-center hover:bg-slate-800 transition-colors"
> >
<Minus size={24} /> <Minus size={24} />
</button> </button>
<span className="text-5xl font-normal tabular-nums">{adjustQty}</span> <span className="text-5xl font-normal tabular-nums">{adjustQty}</span>
<button <button
onClick={() => setAdjustQty(adjustQty + 1)} onClick={() => setAdjustQty(adjustQty + 1)}
className="w-12 h-12 border border-outline/30 flex items-center justify-center hover:bg-surface-container transition-colors" className="w-12 h-12 rounded-full border border-slate-800 flex items-center justify-center hover:bg-slate-800 transition-colors"
> >
<Plus size={24} /> <Plus size={24} />
</button> </button>
</div> </div>
{adjustType === 'TRASH' && ( {adjustType === 'TRASH' && (
<div className="w-full bg-error/5 border border-error/20 p-4"> <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-outline/30 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>
@@ -572,11 +572,11 @@ export default function InventoryPage() {
<button <button
onClick={isEditing ? handleUpdateItem : handleAdjustStock} onClick={isEditing ? handleUpdateItem : handleAdjustStock}
className={cn( className={cn(
"w-full py-4 sm:py-5 ] font-normal text-lg transition-all active:scale-[0.98] shadow-xl", "w-full py-4 sm:py-5 rounded-[1.8rem] font-normal text-lg transition-all active:scale-[0.98] shadow-xl",
isEditing ? "bg-white text-background shadow-white/10" : ( isEditing ? "bg-white text-slate-950 shadow-white/10" : (
adjustType === 'ADD' ? "bg-primary text-foreground shadow-primary/20" : adjustType === 'ADD' ? "bg-primary text-white shadow-primary/20" :
adjustType === 'REMOVE' ? "bg-warning text-foreground shadow-warning/20" : adjustType === 'REMOVE' ? "bg-amber-600 text-white shadow-amber-600/20" :
"bg-error text-foreground shadow-error/20" "bg-red-600 text-white shadow-red-600/20"
) )
)} )}
> >
@@ -593,10 +593,10 @@ export default function InventoryPage() {
{/* Category Edit Overlay */} {/* Category Edit Overlay */}
{editingCategory && ( {editingCategory && (
<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 bg-background/90 animate-in fade-in duration-300">
<div className="bg-surface border border-outline/30 p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in zoom-in-95 duration-200"> <div className="bg-surface border border-slate-800 rounded-3xl p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in zoom-in-95 duration-200">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-xl font-normal">Edit Category</h2> <h2 className="text-xl font-normal">Edit Category</h2>
<button onClick={() => setEditingCategory(null)} className="p-2 hover:bg-surface-container transition-colors text-secondary"> <button onClick={() => setEditingCategory(null)} className="p-2 hover:bg-slate-800 rounded-full transition-colors text-secondary">
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
@@ -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-outline/30 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,14 +616,14 @@ 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-outline/30 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>
<button <button
onClick={handleUpdateCategory} onClick={handleUpdateCategory}
className="w-full bg-primary text-primary-foreground font-normal py-4 shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all" className="w-full bg-primary text-white font-normal py-4 rounded-2xl shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all"
> >
Update Category Update Category
</button> </button>
@@ -641,8 +641,8 @@ 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-outline/30 ] 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">
<div> <div>
@@ -650,9 +650,9 @@ export default function InventoryPage() {
<Package className="text-primary" size={28} /> <Package className="text-primary" size={28} />
Box Inventory Box Inventory
</h3> </h3>
<p className="text-sm text-secondary 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-surface-container 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,13 +663,13 @@ 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-outline/30 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 && (
<button <button
onClick={() => setBoxSearchQuery('')} onClick={() => setBoxSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 hover:bg-surface-container text-secondary" className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 hover:bg-slate-800 rounded-lg text-muted"
> >
<X size={14} /> <X size={14} />
</button> </button>
@@ -689,21 +689,21 @@ export default function InventoryPage() {
{getFilteredBoxes(existingBoxes).map(box => { {getFilteredBoxes(existingBoxes).map(box => {
const itemCount = inventory.filter(i => i.box_label === box).length; const itemCount = inventory.filter(i => i.box_label === box).length;
return ( return (
<div key={box} className="bg-background/50 border border-outline/30 p-3 md:p-4 flex flex-col gap-2 md:gap-3 group hover:border-primary/40 transition-all"> <div key={box} className="bg-background/50 border border-slate-800/60 p-3 md:p-4 rounded-3xl flex flex-col gap-2 md:gap-3 group hover:border-primary/40 transition-all">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="text-lg font-normal text-foreground truncate">{box}</h4> <h4 className="text-lg font-normal text-white truncate">{box}</h4>
<p className="text-xs text-secondary font-normal mt-0.5">{itemCount} items linked</p> <p className="text-xs text-muted font-normal mt-0.5">{itemCount} items linked</p>
</div> </div>
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<button <button
onClick={() => setSelectedBoxLabel(box)} onClick={() => setSelectedBoxLabel(box)}
className="flex-1 py-3 bg-primary text-primary-foreground text-xs font-normal flex items-center justify-center gap-2 hover:bg-primary/90 transition-colors shadow-lg shadow-primary/10 active:scale-95" className="flex-1 py-3 bg-primary text-white text-xs font-normal rounded-xl flex items-center justify-center gap-2 hover:bg-blue-500 transition-colors shadow-lg shadow-primary/10 active:scale-95"
> >
<Printer size={14} /> Print Label <Printer size={14} /> Print Label
</button> </button>
<button <button
onClick={() => { setSearchQuery(box); setShowBoxManager(false); setBoxSearchQuery(''); }} onClick={() => { setSearchQuery(box); setShowBoxManager(false); setBoxSearchQuery(''); }}
className="px-4 py-3 bg-surface border border-outline/30 text-secondary text-xs font-normal hover:bg-surface-container active:scale-95" className="px-4 py-3 bg-surface border border-slate-800 text-secondary text-xs font-normal rounded-xl hover:bg-slate-800 active:scale-95"
> >
View View
</button> </button>
@@ -715,8 +715,8 @@ export default function InventoryPage() {
)} )}
</div> </div>
<div className="p-4 md:p-6 py-3 md:py-4 bg-background/50 border-t border-outline/30 text-center flex items-center justify-center gap-2"> <div className="p-4 md:p-6 py-3 md:py-4 bg-background/50 border-t border-slate-800 text-center flex items-center justify-center gap-2">
<div className="w-1 h-1 bg-primary animate-pulse" /> <div className="w-1 h-1 rounded-full bg-primary animate-pulse" />
<p className="text-xs text-secondary font-normal font-mono">TFM aInventory Box management mode</p> <p className="text-xs text-secondary font-normal font-mono">TFM aInventory Box management mode</p>
</div> </div>
</div> </div>
@@ -728,7 +728,7 @@ export default function InventoryPage() {
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/95 animate-in zoom-in-95 duration-200"> <div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/95 animate-in zoom-in-95 duration-200">
<div className="w-full max-w-md flex flex-col gap-3 md:gap-4"> <div className="w-full max-w-md flex flex-col gap-3 md:gap-4">
<div id="print-label-area" className="w-full bg-white p-4 md:p-6 shadow-2xl flex flex-col items-center gap-3 md:gap-4"> <div id="print-label-area" className="w-full bg-white p-4 md:p-6 rounded-lg shadow-2xl flex flex-col items-center gap-3 md:gap-4">
<h2 className="text-2xl font-normal text-black tracking-tighter text-center"> <h2 className="text-2xl font-normal text-black tracking-tighter text-center">
{selectedBoxLabel} {selectedBoxLabel}
</h2> </h2>
@@ -743,7 +743,7 @@ export default function InventoryPage() {
<div className="flex flex-col gap-2 md:gap-3 no-print"> <div className="flex flex-col gap-2 md:gap-3 no-print">
<button <button
onClick={() => window.print()} onClick={() => window.print()}
className="w-full py-5 bg-primary text-primary-foreground ] font-normal text-lg flex items-center justify-center gap-3 shadow-2xl shadow-primary/40 active:scale-95 transition-all" className="w-full py-5 bg-primary text-white rounded-[2rem] font-normal text-lg flex items-center justify-center gap-3 shadow-2xl shadow-primary/40 active:scale-95 transition-all"
> >
<Printer size={20} /> Print to Dymo/Brother <Printer size={20} /> Print to Dymo/Brother
</button> </button>
@@ -772,14 +772,14 @@ export default function InventoryPage() {
img.src = "data:image/svg+xml;base64," + btoa(svgData); img.src = "data:image/svg+xml;base64," + btoa(svgData);
} }
}} }}
className="w-full py-4 bg-surface border border-outline/30 text-foreground ] font-normal text-sm flex items-center justify-center gap-2 active:scale-95 transition-all" className="w-full py-4 bg-surface border border-slate-800 text-white rounded-[2rem] font-normal text-sm flex items-center justify-center gap-2 active:scale-95 transition-all"
> >
<Download size={16} /> Save for Mobile App <Download size={16} /> Save for Mobile App
</button> </button>
<button <button
onClick={() => setSelectedBoxLabel(null)} onClick={() => setSelectedBoxLabel(null)}
className="w-full py-4 text-secondary font-normal text-xs" className="w-full py-4 text-muted font-normal text-xs"
> >
Close Label Preview Close Label Preview
</button> </button>
@@ -828,8 +828,3 @@ export default function InventoryPage() {
</PageShell> </PageShell>
); );
} }
lose}
/>
</PageShell>
);
}

View File

@@ -110,10 +110,18 @@ export default function CreateItemPage() {
return ( return (
<div <div
className={`flex items-center gap-2 text-xs font-normal ${ isActive ? 'text-primary' : isCompleted ? 'text-secondary' : 'text-secondary' }`} className={`flex items-center gap-2 text-xs font-normal ${
isActive ? 'text-primary' : isCompleted ? 'text-slate-400' : 'text-slate-500'
}`}
> >
<div <div
className={`w-6 h-6 flex items-center justify-center ${ isActive ? 'bg-primary text-foreground' : isCompleted ? 'bg-surface-bright text-foreground' : 'bg-surface-bright' }`} className={`w-6 h-6 rounded-full flex items-center justify-center ${
isActive
? 'bg-primary text-white'
: isCompleted
? 'bg-slate-400 text-white'
: 'bg-slate-700'
}`}
> >
{stepNum} {stepNum}
</div> </div>
@@ -123,24 +131,24 @@ export default function CreateItemPage() {
}; };
return ( return (
<div className="min-h-screen bg-background text-foreground p-4"> <div className="min-h-screen bg-slate-950 text-white p-4">
{/* Header */} {/* Header */}
<div className="max-w-2xl mx-auto mb-6"> <div className="max-w-2xl mx-auto mb-6">
<button <button
onClick={() => router.back()} onClick={() => router.back()}
className="flex items-center gap-2 text-sm text-secondary hover:text-foreground transition-colors mb-6" className="flex items-center gap-2 text-sm text-slate-400 hover:text-white transition-colors mb-6"
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
Back Back
</button> </button>
<div className="flex items-center gap-4 mb-8"> <div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-primary/10 border border-primary/20"> <div className="p-3 bg-primary/10 rounded-lg border border-primary/20">
<Upload size={24} className="text-primary" /> <Upload size={24} className="text-primary" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-normal">Create New Item</h1> <h1 className="text-2xl font-normal">Create New Item</h1>
<p className="text-xs text-secondary mt-1"> <p className="text-xs text-slate-500 mt-1">
Step {step === 'details' ? 1 : step === 'photo' ? 2 : step === 'preview' ? 3 : 4} of 4 Step {step === 'details' ? 1 : step === 'photo' ? 2 : step === 'preview' ? 3 : 4} of 4
</p> </p>
</div> </div>
@@ -159,7 +167,7 @@ export default function CreateItemPage() {
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
{/* Details Step */} {/* Details Step */}
{step === 'details' && ( {step === 'details' && (
<div className="bg-surface-container border border-outline/30 p-6"> <div className="bg-slate-900 rounded-lg border border-slate-800 p-6">
<h2 className="text-lg font-normal mb-6">Item Details</h2> <h2 className="text-lg font-normal mb-6">Item Details</h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -171,7 +179,7 @@ export default function CreateItemPage() {
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ name: e.target.value })} onChange={(e) => setFormData({ name: e.target.value })}
placeholder="Enter item name" placeholder="Enter item name"
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground placeholder-secondary focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
@@ -182,7 +190,7 @@ export default function CreateItemPage() {
<select <select
value={formData.category} value={formData.category}
onChange={(e) => setFormData({ category: e.target.value })} onChange={(e) => setFormData({ category: e.target.value })}
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
> >
<option value="">Select a category</option> <option value="">Select a category</option>
@@ -202,7 +210,7 @@ export default function CreateItemPage() {
value={formData.item_type} value={formData.item_type}
onChange={(e) => setFormData({ item_type: e.target.value })} onChange={(e) => setFormData({ item_type: e.target.value })}
placeholder="e.g., Component, Part, Equipment" placeholder="e.g., Component, Part, Equipment"
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground placeholder-secondary focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
@@ -215,7 +223,7 @@ export default function CreateItemPage() {
min="1" min="1"
value={formData.quantity} value={formData.quantity}
onChange={(e) => setFormData({ quantity: parseInt(e.target.value) || 1 })} onChange={(e) => setFormData({ quantity: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
@@ -228,7 +236,7 @@ export default function CreateItemPage() {
value={formData.part_number || ''} value={formData.part_number || ''}
onChange={(e) => setFormData({ part_number: e.target.value })} onChange={(e) => setFormData({ part_number: e.target.value })}
placeholder="e.g., PN-12345" placeholder="e.g., PN-12345"
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground placeholder-secondary focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
@@ -241,14 +249,14 @@ export default function CreateItemPage() {
value={formData.barcode || ''} value={formData.barcode || ''}
onChange={(e) => setFormData({ barcode: e.target.value })} onChange={(e) => setFormData({ barcode: e.target.value })}
placeholder="e.g., 1234567890" placeholder="e.g., 1234567890"
className="w-full px-3 py-2 bg-surface-container border border-outline-variant text-foreground placeholder-secondary focus:border-primary focus:outline-none" className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:border-primary focus:outline-none"
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="p-3 bg-error/10 border border-error/30 text-error text-sm"> <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{error} {error}
</div> </div>
)} )}
@@ -257,7 +265,7 @@ export default function CreateItemPage() {
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
onClick={() => router.back()} onClick={() => router.back()}
className="flex-1 px-4 py-2 border border-outline-variant text-foreground hover:border-outline-variant transition-colors font-normal" className="flex-1 px-4 py-2 border border-slate-700 text-slate-300 rounded-lg hover:border-slate-600 transition-colors font-normal"
disabled={isLoading} disabled={isLoading}
> >
Discard Draft Discard Draft
@@ -265,7 +273,7 @@ export default function CreateItemPage() {
<button <button
onClick={handleDetailsSubmit} onClick={handleDetailsSubmit}
disabled={isLoading} disabled={isLoading}
className="flex-1 px-4 py-2 bg-primary text-foreground hover:bg-primary/90 transition-colors font-normal flex items-center justify-center gap-2" className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors font-normal flex items-center justify-center gap-2"
> >
{isLoading ? <Loader2 size={16} className="animate-spin" /> : <ChevronRight size={16} />} {isLoading ? <Loader2 size={16} className="animate-spin" /> : <ChevronRight size={16} />}
{isLoading ? 'Creating...' : 'Next: Upload Photo'} {isLoading ? 'Creating...' : 'Next: Upload Photo'}
@@ -277,9 +285,9 @@ export default function CreateItemPage() {
{/* Photo Upload Step */} {/* Photo Upload Step */}
{step === 'photo' && ( {step === 'photo' && (
<div className="bg-surface-container border border-outline/30 p-6"> <div className="bg-slate-900 rounded-lg border border-slate-800 p-6">
<h2 className="text-lg font-normal mb-4">Upload Item Photo</h2> <h2 className="text-lg font-normal mb-4">Upload Item Photo</h2>
<p className="text-sm text-secondary mb-6"> <p className="text-sm text-slate-400 mb-6">
Take a photo or upload an image. You can crop it manually on the next step. Take a photo or upload an image. You can crop it manually on the next step.
</p> </p>
@@ -296,7 +304,7 @@ export default function CreateItemPage() {
</div> </div>
{photoError && ( {photoError && (
<div className="p-3 bg-error/10 border border-error/30 text-error text-sm mb-6"> <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm mb-6">
{photoError} {photoError}
</div> </div>
)} )}
@@ -305,14 +313,14 @@ export default function CreateItemPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={prevStep} onClick={prevStep}
className="flex-1 px-4 py-2 border border-outline-variant text-foreground hover:border-outline transition-colors font-normal" className="flex-1 px-4 py-2 border border-slate-700 text-slate-300 rounded-lg hover:border-slate-600 transition-colors font-normal"
> >
Back Back
</button> </button>
<button <button
onClick={nextStep} onClick={nextStep}
disabled={!uploadedPhoto} disabled={!uploadedPhoto}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-surface-bright disabled:text-secondary transition-colors font-normal flex items-center justify-center gap-2" className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 disabled:bg-slate-700 disabled:text-slate-500 transition-colors font-normal flex items-center justify-center gap-2"
> >
<ChevronRight size={16} /> <ChevronRight size={16} />
Next: Crop & Preview Next: Crop & Preview
@@ -323,13 +331,13 @@ export default function CreateItemPage() {
{/* Preview Step (Crop) */} {/* Preview Step (Crop) */}
{step === 'preview' && uploadedPhoto && ( {step === 'preview' && uploadedPhoto && (
<div className="bg-surface-container border border-outline/30 p-6"> <div className="bg-slate-900 rounded-lg border border-slate-800 p-6">
<h2 className="text-lg font-normal mb-4">Crop & Preview</h2> <h2 className="text-lg font-normal mb-4">Crop & Preview</h2>
<p className="text-sm text-secondary mb-6"> <p className="text-sm text-slate-400 mb-6">
Adjust the crop area or use the full photo. Manual crop handles are visible. Adjust the crop area or use the full photo. Manual crop handles are visible.
</p> </p>
<div className="mb-6 bg-background p-4 border border-outline/20"> <div className="mb-6 bg-slate-800 rounded-lg p-4">
<ManualCropUI <ManualCropUI
imageUrl={uploadedPhoto.full_url} imageUrl={uploadedPhoto.full_url}
onCropChange={setCropBounds} onCropChange={setCropBounds}
@@ -338,7 +346,7 @@ export default function CreateItemPage() {
</div> </div>
{/* Use Full Photo Toggle */} {/* Use Full Photo Toggle */}
<div className="flex items-center gap-3 mb-6 p-3 bg-surface-bright border border-outline/20"> <div className="flex items-center gap-3 mb-6 p-3 bg-slate-800 rounded-lg">
<input <input
type="checkbox" type="checkbox"
id="use-full-photo" id="use-full-photo"
@@ -349,9 +357,9 @@ export default function CreateItemPage() {
setCropBounds(null); setCropBounds(null);
} }
}} }}
className="w-4 h-4 border-outline-variant accent-primary" className="w-4 h-4 rounded border-slate-600 accent-primary"
/> />
<label htmlFor="use-full-photo" className="text-sm font-normal text-foreground"> <label htmlFor="use-full-photo" className="text-sm font-normal text-slate-300">
Use full photo (skip cropping) Use full photo (skip cropping)
</label> </label>
</div> </div>
@@ -360,13 +368,13 @@ export default function CreateItemPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={prevStep} onClick={prevStep}
className="flex-1 px-4 py-2 border border-outline-variant text-foreground hover:border-outline transition-colors font-normal" className="flex-1 px-4 py-2 border border-slate-700 text-slate-300 rounded-lg hover:border-slate-600 transition-colors font-normal"
> >
Back Back
</button> </button>
<button <button
onClick={nextStep} onClick={nextStep}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors font-normal flex items-center justify-center gap-2" className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors font-normal flex items-center justify-center gap-2"
> >
<ChevronRight size={16} /> <ChevronRight size={16} />
Next: Confirm Next: Confirm
@@ -377,31 +385,31 @@ export default function CreateItemPage() {
{/* Confirm Step */} {/* Confirm Step */}
{step === 'confirm' && ( {step === 'confirm' && (
<div className="bg-surface-container border border-outline/30 p-6"> <div className="bg-slate-900 rounded-lg border border-slate-800 p-6">
<h2 className="text-lg font-normal mb-6">Confirm & Save</h2> <h2 className="text-lg font-normal mb-6">Confirm & Save</h2>
{/* Item Summary */} {/* Item Summary */}
<div className="bg-surface-bright p-4 mb-6 space-y-2 border border-outline/20"> <div className="bg-slate-800 rounded-lg p-4 mb-6 space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-secondary">Name:</span> <span className="text-slate-400">Name:</span>
<span className="font-normal text-foreground">{formData.name}</span> <span className="font-normal">{formData.name}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-secondary">Category:</span> <span className="text-slate-400">Category:</span>
<span className="font-normal text-foreground">{formData.category}</span> <span className="font-normal">{formData.category}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-secondary">Type:</span> <span className="text-slate-400">Type:</span>
<span className="font-normal text-foreground">{formData.item_type}</span> <span className="font-normal">{formData.item_type}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-secondary">Quantity:</span> <span className="text-slate-400">Quantity:</span>
<span className="font-normal text-foreground">{formData.quantity}</span> <span className="font-normal">{formData.quantity}</span>
</div> </div>
{uploadedPhoto && ( {uploadedPhoto && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-secondary">Photo:</span> <span className="text-slate-400">Photo:</span>
<span className="font-normal text-success">Uploaded</span> <span className="font-normal text-green-400">Uploaded</span>
</div> </div>
)} )}
</div> </div>
@@ -409,14 +417,12 @@ export default function CreateItemPage() {
{/* Photo Thumbnail */} {/* Photo Thumbnail */}
{uploadedPhoto && ( {uploadedPhoto && (
<div className="mb-6"> <div className="mb-6">
<p className="text-sm text-secondary mb-2">Photo Preview</p> <p className="text-sm text-slate-400 mb-2">Photo Preview</p>
<div className="border border-outline/30 overflow-hidden"> <img
<img src={uploadedPhoto.thumbnail_url}
src={uploadedPhoto.thumbnail_url} alt="Item"
alt="Item" className="w-full h-48 object-cover rounded-lg"
className="w-full h-48 object-cover" />
/>
</div>
</div> </div>
)} )}
@@ -424,13 +430,13 @@ export default function CreateItemPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={prevStep} onClick={prevStep}
className="flex-1 px-4 py-2 border border-outline-variant text-foreground hover:border-outline transition-colors font-normal" className="flex-1 px-4 py-2 border border-slate-700 text-slate-300 rounded-lg hover:border-slate-600 transition-colors font-normal"
> >
Back Back
</button> </button>
<button <button
onClick={handleConfirm} onClick={handleConfirm}
className="flex-1 px-4 py-2 bg-success text-foreground hover:bg-success/80 transition-colors font-normal flex items-center justify-center gap-2 shadow-lg shadow-success/20" className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-normal flex items-center justify-center gap-2"
> >
Save & Close Save & Close
</button> </button>

View File

@@ -82,13 +82,13 @@ export default function LoginPage() {
<div data-testid="identity-check-overlay" className="bg-background flex items-center justify-center p-4 md:min-h-screen"> <div data-testid="identity-check-overlay" className="bg-background flex items-center justify-center p-4 md:min-h-screen">
<Toaster position="top-center" /> <Toaster position="top-center" />
<div className="bg-surface border border-outline/30 p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in fade-in zoom-in duration-500"> <div className="bg-surface border border-slate-800 rounded-3xl p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in fade-in zoom-in duration-500">
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<div className="w-16 h-16 bg-primary/10 text-primary flex items-center justify-center mx-auto mb-4 border border-primary/20"> <div className="w-16 h-16 bg-primary/10 text-primary rounded-2xl flex items-center justify-center mx-auto mb-4 border border-primary/20">
<User size={32} /> <User size={32} />
</div> </div>
<h2 className="text-2xl font-normal text-foreground tracking-tight">Identity Check</h2> <h2 className="text-2xl font-normal text-white tracking-tight">Identity Check</h2>
<p className="text-secondary text-sm">Select operator profile or use direct login</p> <p className="text-muted text-sm">Select operator profile or use direct login</p>
</div> </div>
<div data-testid="user-list" className="grid gap-2 md:gap-3"> <div data-testid="user-list" className="grid gap-2 md:gap-3">
@@ -101,15 +101,15 @@ 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-surface-container/50 hover:bg-surface-container border border-outline/30 hover:border-primary/40 p-4 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 bg-surface border border-outline-variant flex items-center justify-center text-secondary 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">
{user.role === 'admin' ? <Shield size={14} /> : <User size={14} />} {user.role === 'admin' ? <Shield size={14} /> : <User size={14} />}
</div> </div>
<div> <div>
<p className="text-foreground font-normal text-sm">{user.username}</p> <p className="text-white font-normal text-sm">{user.username}</p>
<p className="text-xs text-secondary font-normal mt-1">{user.role}</p> <p className="text-xs text-muted font-normal mt-1">{user.role}</p>
</div> </div>
</div> </div>
<ChevronRight size={16} className="text-secondary group-hover:text-primary transition-colors" /> <ChevronRight size={16} className="text-secondary group-hover:text-primary transition-colors" />
@@ -117,7 +117,7 @@ export default function LoginPage() {
))} ))}
</> </>
) : ( ) : (
<div className="text-center p-4 text-secondary text-xs font-normal animate-pulse"> <div className="text-center p-4 text-muted text-xs font-normal animate-pulse">
Connectivity issues? Use manual login below. Connectivity issues? Use manual login below.
</div> </div>
)} )}
@@ -126,7 +126,7 @@ export default function LoginPage() {
<button <button
data-testid="ldap-login-tab" data-testid="ldap-login-tab"
onClick={() => setIsEnterprise(true)} onClick={() => setIsEnterprise(true)}
className="flex items-center justify-center gap-2 py-4 border border-dashed border-outline-variant text-secondary hover:text-foreground hover:border-outline/30 transition-all font-normal text-xs" className="flex items-center justify-center gap-2 py-4 rounded-2xl border border-dashed border-slate-700 text-muted hover:text-white hover:border-slate-500 transition-all font-normal text-xs"
> >
<Lock size={12} /> <Lock size={12} />
Enterprise Enterprise
@@ -137,7 +137,7 @@ export default function LoginPage() {
setSelectedUserForLogin({ username: '' }); setSelectedUserForLogin({ username: '' });
// We use an empty username object to trigger the manual input view // We use an empty username object to trigger the manual input view
}} }}
className="flex items-center justify-center gap-2 py-4 border border-primary/20 bg-primary/5 text-primary hover:bg-primary/10 transition-all font-normal text-xs" className="flex items-center justify-center gap-2 py-4 rounded-2xl border border-primary/20 bg-primary/5 text-primary hover:bg-primary/10 transition-all font-normal text-xs"
> >
<User size={12} /> <User size={12} />
Manual Login Manual Login
@@ -147,7 +147,7 @@ export default function LoginPage() {
) : isEnterprise ? ( ) : isEnterprise ? (
<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="flex justify-between items-center px-1"> <div className="flex justify-between items-center px-1">
<p className="text-xs font-normal text-secondary">Enterprise Account</p> <p className="text-xs font-normal text-muted">Enterprise Account</p>
<button <button
onClick={() => setIsEnterprise(false)} onClick={() => setIsEnterprise(false)}
className="text-xs font-normal text-primary hover:underline" className="text-xs font-normal text-primary hover:underline"
@@ -157,30 +157,30 @@ export default function LoginPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-normal text-secondary px-1">Username</label> <label className="text-xs font-normal text-muted px-1">Username</label>
<div className="relative"> <div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={16} /> <User className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input <input
ref={enterpriseUserRef} ref={enterpriseUserRef}
data-testid="username-input" data-testid="username-input"
type="text" type="text"
autoFocus autoFocus
className="w-full bg-surface-container/50 border border-outline/30 focus:border-primary py-4 pl-12 pr-4 text-foreground focus:outline-none transition-all placeholder:text-surface-bright font-mono" className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
placeholder="e.g. jsmith" placeholder="e.g. jsmith"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-normal text-secondary px-1">Password</label> <label className="text-xs font-normal text-muted px-1">Password</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={16} /> <Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input <input
ref={enterprisePassRef} ref={enterprisePassRef}
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-surface-container/50 border border-outline/30 focus:border-primary py-4 pl-12 pr-4 text-foreground/50 focus:text-foreground focus:outline-none transition-all placeholder:text-surface-bright font-mono" className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
placeholder="Enter password" placeholder="Enter password"
/> />
</div> </div>
@@ -189,52 +189,52 @@ export default function LoginPage() {
<button <button
data-testid="login-submit" data-testid="login-submit"
onClick={handleLogin} onClick={handleLogin}
className="w-full bg-primary text-foreground font-normal py-4 shadow-xl shadow-primary/20 active:scale-95 transition-all" className="w-full bg-primary text-white font-normal py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
> >
Sign In Sign In
</button> </button>
</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-surface-container/30 p-4 border border-outline/30 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-surface-bright transition-colors" className="p-1 hover:bg-slate-700 rounded-lg transition-colors"
> >
<X size={16} className="text-secondary" /> <X size={16} className="text-secondary" />
</button> </button>
<div> <div>
<p className="text-sm font-normal text-secondary">Logging in as</p> <p className="text-sm font-normal text-muted">Logging in as</p>
<p className="text-foreground font-normal">{selectedUserForLogin.username || "Manual Input"}</p> <p className="text-white font-normal">{selectedUserForLogin.username || "Manual Input"}</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-normal text-secondary px-1">Username</label> <label className="text-xs font-normal text-muted px-1">Username</label>
<div className="relative"> <div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={16} /> <User className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input <input
type="text" type="text"
autoFocus autoFocus
value={selectedUserForLogin.username || ""} value={selectedUserForLogin.username || ""}
onChange={(e) => setSelectedUserForLogin({...selectedUserForLogin, username: e.target.value})} onChange={(e) => setSelectedUserForLogin({...selectedUserForLogin, username: e.target.value})}
className="w-full bg-surface-container/50 border border-outline/30 focus:border-primary py-4 pl-12 pr-4 text-foreground focus:outline-none transition-all placeholder:text-surface-bright font-mono" className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
placeholder="Admin" placeholder="Admin"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-normal text-secondary px-1">Password</label> <label className="text-xs font-normal text-muted px-1">Password</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-secondary" size={16} /> <Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input <input
ref={localPassRef} ref={localPassRef}
data-testid="local-password-input" data-testid="local-password-input"
type="password" type="password"
autoFocus={!!selectedUserForLogin.username} autoFocus={!!selectedUserForLogin.username}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()} onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-surface-container/50 border border-outline/30 focus:border-primary py-4 pl-12 pr-4 text-foreground/50 focus:text-foreground focus:outline-none transition-all placeholder:text-surface-bright font-mono" className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
placeholder="Enter password" placeholder="Enter password"
/> />
</div> </div>
@@ -243,7 +243,7 @@ export default function LoginPage() {
<button <button
data-testid="local-login-submit" data-testid="local-login-submit"
onClick={handleLogin} onClick={handleLogin}
className="w-full bg-primary text-foreground font-normal py-4 shadow-xl shadow-primary/20 active:scale-95 transition-all" className="w-full bg-primary text-white font-normal py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
> >
Verify Identity Verify Identity
</button> </button>
@@ -253,7 +253,7 @@ export default function LoginPage() {
{/* Progress bar hint */} {/* Progress bar hint */}
{users.length === 0 && !selectedUserForLogin && !isEnterprise && ( {users.length === 0 && !selectedUserForLogin && !isEnterprise && (
<div className="w-full bg-surface-container h-1 overflow-hidden"> <div className="w-full bg-slate-800 h-1 rounded-full overflow-hidden">
<div className="bg-primary h-full animate-progress-fast shadow-[0_0_8px_rgba(var(--primary-rgb),0.5)]"></div> <div className="bg-primary h-full animate-progress-fast shadow-[0_0_8px_rgba(var(--primary-rgb),0.5)]"></div>
</div> </div>
)} )}

View File

@@ -87,15 +87,15 @@ export default function LogsPage() {
return ( return (
<PageShell> <PageShell>
<main className="p-4 md:p-8 max-w-7xl mx-auto space-y-4 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-4 bg-primary/10 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={32} /> <History size={28} className="md:w-8 md:h-8" />
</div> </div>
<div> <div>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-foreground leading-tight">Operations Audit</h1> <h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">Operations Audit</h1>
<p className="text-xs md:text-sm text-secondary font-normal mt-0.5">Real-time Intervention Tracking</p> <p className="text-xs md:text-sm text-secondary font-normal tracking-widest mt-0.5">Real-time Intervention Tracking</p>
</div> </div>
</div> </div>
@@ -103,7 +103,7 @@ export default function LogsPage() {
<button <button
onClick={loadData} onClick={loadData}
disabled={loading} disabled={loading}
className="flex items-center gap-2 px-5 py-2.5 bg-surface border border-outline/30 text-secondary hover:text-foreground text-xs font-normal transition-all active:scale-95 disabled:opacity-50 shadow-xl" className="flex items-center gap-2 px-5 py-2.5 bg-surface border border-slate-800 text-secondary hover:text-white rounded-2xl text-xs font-normal transition-all active:scale-95 disabled:opacity-50 shadow-xl"
> >
<RefreshCw size={14} className={cn(loading && "animate-spin")} /> <RefreshCw size={14} className={cn(loading && "animate-spin")} />
Sync Logs Sync Logs
@@ -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-outline/30 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} />
@@ -153,10 +153,10 @@ export default function LogsPage() {
<button <button
onClick={() => setFilterAction('ALL')} onClick={() => setFilterAction('ALL')}
className={cn( className={cn(
"px-5 py-2 text-xs font-normal transition-all whitespace-nowrap border ", "px-5 py-2 rounded-full text-xs font-normal transition-all whitespace-nowrap border tracking-widest",
filterAction === 'ALL' filterAction === 'ALL'
? "bg-white text-background border-white shadow-xl shadow-white/10" ? "bg-white text-slate-950 border-white shadow-xl shadow-white/10"
: "bg-surface/70 text-secondary border-outline/30 hover:border-outline-variant" : "bg-surface/70 text-secondary border-slate-800 hover:border-slate-700"
)} )}
> >
All Streams All Streams
@@ -166,10 +166,10 @@ export default function LogsPage() {
key={f} key={f}
onClick={() => setFilterAction(f)} onClick={() => setFilterAction(f)}
className={cn( className={cn(
"px-5 py-2 text-xs font-normal transition-all whitespace-nowrap border ", "px-5 py-2 rounded-full text-xs font-normal transition-all whitespace-nowrap border tracking-widest",
filterAction === f filterAction === f
? "bg-primary text-foreground border-primary shadow-xl shadow-primary/10" ? "bg-primary text-white border-primary shadow-xl shadow-primary/10"
: "bg-surface/70 text-secondary border-outline/30 hover:border-outline-variant" : "bg-surface/70 text-secondary border-slate-800 hover:border-slate-700"
)} )}
> >
{f} {f}

View File

@@ -360,7 +360,7 @@ export default function Home() {
return ( return (
<PageShell> <PageShell>
<div className="p-4 md:p-8 overflow-x-hidden w-full max-w-7xl mx-auto"> <div className="p-3 md:p-8 overflow-x-hidden w-full max-w-7xl mx-auto">
{/* Search datalists for autocomplete */} {/* Search datalists for autocomplete */}
<datalist id="existing-types"> <datalist id="existing-types">
{existingTypes.map(t => <option key={t} value={t} />)} {existingTypes.map(t => <option key={t} value={t} />)}
@@ -370,30 +370,34 @@ export default function Home() {
</datalist> </datalist>
{/* Header */} {/* Header */}
<header className="flex flex-col sm:flex-row justify-between sm:items-center gap-4 mb-4 md:mb-6 w-full"> <header className="flex flex-col sm:flex-row justify-between sm:items-center gap-3 md:gap-4 mb-3 md:mb-4 w-full px-1">
<div className="flex items-center gap-4"> <div className="flex items-center gap-2 md:gap-3">
<div className="p-4 bg-primary/10 border border-primary/20 shadow-xl shadow-primary/5"> <div className="p-1">
<Package size={32} className="text-primary" /> <img
src="/logo.png"
alt="TFM aInventory"
className="h-8 md:h-10 object-contain drop-shadow-xl rounded-lg"
/>
</div> </div>
<div> <div>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-foreground leading-tight">TFM aInventory</h1> <h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">TFM aInventory</h1>
<p className="text-xs md:text-sm text-secondary font-normal tracking-tight mt-1 leading-relaxed">Check-in, Check-out & Trash Operations</p> <p className="text-xs md:text-sm text-secondary font-normal tracking-tight mt-1 leading-relaxed">Check-in, Check-out & Trash Operations</p>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between sm:justify-end gap-2 md:gap-3 bg-surface/50 sm:bg-transparent px-3 py-2 sm:p-0 border border-outline/30/50 sm:border-none"> <div className="flex flex-wrap items-center justify-between sm:justify-end gap-2 md:gap-3 bg-surface/50 sm:bg-transparent px-3 py-2 sm:p-0 rounded-2xl border border-slate-800/50 sm:border-none">
<div className="flex flex-wrap items-center gap-2 md:gap-3"> <div className="flex flex-wrap items-center gap-2 md:gap-3">
{isScannerReady && ( {isScannerReady && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-success shadow-[0_0_8px_rgba(34,197,94,0.6)]" /> <div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
<span className="text-xs font-normal text-success/90 whitespace-nowrap"> <span className="text-xs font-normal text-green-500/90 whitespace-nowrap">
Scanner: OK Scanner: OK
</span> </span>
</div> </div>
)} )}
<div className="flex items-center gap-2" data-testid={!isOnline ? "offline-indicator" : undefined}> <div className="flex items-center gap-2" data-testid={!isOnline ? "offline-indicator" : undefined}>
<div className={`w-1.5 h-1.5 ${isOnline ? 'bg-success animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-error shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} /> <div className={`w-1.5 h-1.5 rounded-full ${isOnline ? 'bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} />
<span className={`text-xs font-normal whitespace-nowrap ${isOnline ? 'text-success/90' : 'text-error/90'}`} data-testid={!isOnline ? "offline-sync-indicator" : undefined}> <span className={`text-xs font-normal whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`} data-testid={!isOnline ? "offline-sync-indicator" : undefined}>
Sync: {isOnline ? 'Active' : 'Offline'} Sync: {isOnline ? 'Active' : 'Offline'}
</span> </span>
</div> </div>
@@ -401,7 +405,7 @@ export default function Home() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setShowSearch(true)} onClick={() => setShowSearch(true)}
className="p-2.5 bg-surface border border-outline/30 text-secondary hover:text-foreground transition-all active:scale-95" className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95"
title="Search (Ctrl+K)" title="Search (Ctrl+K)"
> >
<Search size={18} /> <Search size={18} />
@@ -410,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-outline/30 text-secondary hover:text-foreground 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>
@@ -494,7 +498,7 @@ export default function Home() {
{/* Box Contents Selection Modal */} {/* Box Contents Selection Modal */}
{boxMatches.length > 0 && !selectedItem && ( {boxMatches.length > 0 && !selectedItem && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center 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-4 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border border-outline/30 ] shadow-2xl p-4 md:p-6 overflow-hidden animate-in slide-in-from-bottom-10 duration-300 flex flex-col max-h-[85vh]"> <div className="w-full max-w-lg bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl p-4 md:p-6 overflow-hidden animate-in slide-in-from-bottom-10 duration-300 flex flex-col max-h-[85vh]">
<div className="flex justify-between items-center mb-3 md:mb-4 shrink-0"> <div className="flex justify-between items-center mb-3 md:mb-4 shrink-0">
<div> <div>
<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">
@@ -503,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-surface-container"> <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>
@@ -516,10 +520,10 @@ export default function Home() {
setBoxMatches([]); setBoxMatches([]);
setAdjustType(mode === 'CHECK_IN' ? 'ADD' : 'REMOVE'); setAdjustType(mode === 'CHECK_IN' ? 'ADD' : 'REMOVE');
}} }}
className="w-full text-left bg-background/50 hover:bg-surface-container border border-outline/50 p-3 md:p-4 flex items-center gap-2 md:gap-3 transition-all active:scale-[0.98] group" className="w-full text-left bg-background/50 hover:bg-slate-800 border border-slate-800/80 p-3 md:p-4 rounded-2xl flex items-center gap-2 md:gap-3 transition-all active:scale-[0.98] group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-normal text-foreground">{item.name}</p> <p className="text-sm font-normal text-white">{item.name}</p>
<p className="text-xs text-secondary font-normal mt-1">Part #: {item.part_number || 'N/A'}</p> <p className="text-xs text-secondary font-normal mt-1">Part #: {item.part_number || 'N/A'}</p>
</div> </div>
<ChevronRight size={18} className="text-secondary group-hover:text-primary" /> <ChevronRight size={18} className="text-secondary group-hover:text-primary" />
@@ -534,8 +538,8 @@ export default function Home() {
{/* Footer Branding */} {/* Footer Branding */}
<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-outline/30" /> <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,21 +58,21 @@ 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"> <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>
<h2 className="text-lg font-normal">AI Discovery</h2> <h2 className="text-lg font-normal">AI Discovery</h2>
<p className="text-xs text-secondary font-normal">Powered by Gemini 2.0 Flash</p> <p className="text-xs text-muted font-normal">Powered by Gemini 2.0 Flash</p>
</div> </div>
</div> </div>
<button <button
onClick={() => { stopLiveCamera(); onCancel(); }} onClick={() => { stopLiveCamera(); onCancel(); }}
aria-label="Close AI Discovery" aria-label="Close AI Discovery"
className="p-2 hover:bg-surface cursor-pointer transition-colors focus:ring-2 focus:ring-primary 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>
@@ -80,33 +80,33 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
{!image && !isLive ? ( {!image && !isLive ? (
<div className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0"> <div className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0">
<div data-testid="multi-item-toggle" className="flex bg-surface/70 p-1.5 border border-outline/30/50 shrink-0"> <div data-testid="multi-item-toggle" className="flex bg-surface/70 p-1.5 rounded-2xl border border-slate-800/50 shrink-0">
<button <button
onClick={() => setMode('item')} onClick={() => setMode('item')}
aria-label="Select Discovery Mode" aria-label="Select Discovery Mode"
className={`flex-1 flex items-center justify-center gap-2 py-3 font-normal cursor-pointer transition-all focus:ring-2 focus:ring-primary focus:outline-none ${mode === 'item' ? 'bg-primary text-foreground shadow-lg' : 'text-secondary 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')}
aria-label="Select Box Lookup mode" aria-label="Select Box Lookup mode"
className={`flex-1 flex items-center justify-center gap-2 py-3 font-normal cursor-pointer transition-all focus:ring-2 focus:ring-primary focus:outline-none ${mode === 'box' ? 'bg-primary text-foreground shadow-lg' : 'text-secondary 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-outline/30 ] 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 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>
<p className="text-secondary mb-2 text-center font-normal"> <p className="text-secondary mb-2 text-center font-normal">
{mode === 'box' ? 'Deep Box Analysis' : 'Multi-Item Extraction'} {mode === 'box' ? 'Deep Box Analysis' : 'Multi-Item Extraction'}
</p> </p>
<p className="text-xs text-secondary px-8 text-center leading-relaxed font-normal"> <p className="text-xs text-muted px-8 text-center leading-relaxed font-normal">
{mode === 'box' {mode === 'box'
? 'Scan container labels to identify storage locations' ? 'Scan container labels to identify storage locations'
: 'Identify multiple technical items from a single photo or label'} : 'Identify multiple technical items from a single photo or label'}
@@ -117,19 +117,19 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button <button
onClick={startLiveCamera} onClick={startLiveCamera}
aria-label="Start camera scan" aria-label="Start camera scan"
className="flex flex-col items-center justify-center gap-2 bg-primary text-foreground font-normal shadow-2xl shadow-primary/20 cursor-pointer active:scale-95 transition-all focus:ring-2 focus:ring-primary 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()}
data-testid="manual-entry-tab" data-testid="manual-entry-tab"
aria-label="Upload photo from device" aria-label="Upload photo from device"
className="flex flex-col items-center justify-center gap-2 bg-surface text-secondary border border-outline/30 font-normal cursor-pointer active:scale-95 transition-all focus:ring-2 focus:ring-primary 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>
@@ -138,7 +138,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
) : isLive ? ( ) : isLive ? (
// LIVE VIEWFINDER MODE // LIVE VIEWFINDER MODE
<div data-testid="wizard-step wizard-step-capture" className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0 overflow-hidden"> <div data-testid="wizard-step wizard-step-capture" className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0 overflow-hidden">
<div data-testid="capture-camera" className="relative flex-1 min-h-0 ] overflow-hidden border-4 border-outline/30 shadow-2xl bg-black"> <div data-testid="capture-camera" className="relative flex-1 min-h-0 rounded-[2.5rem] overflow-hidden border-4 border-slate-900 shadow-2xl bg-black">
<video <video
ref={videoRef} ref={videoRef}
autoPlay autoPlay
@@ -149,18 +149,18 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<canvas ref={canvasRef} className="hidden" /> <canvas ref={canvasRef} className="hidden" />
<div className="absolute inset-0 border-[20px] border-black/20 pointer-events-none"> <div className="absolute inset-0 border-[20px] border-black/20 pointer-events-none">
<div className="w-full h-full border border-primary/30 relative"> <div className="w-full h-full border border-primary/30 rounded-2xl relative">
{/* Corner Markers */} {/* Corner Markers */}
<div className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-primary" /> <div className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-primary rounded-tl-xl" />
<div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-primary" /> <div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-primary rounded-tr-xl" />
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-primary" /> <div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-primary rounded-bl-xl" />
<div className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-primary" /> <div className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-primary rounded-br-xl" />
</div> </div>
</div> </div>
<div className="absolute top-6 left-1/2 -translate-x-1/2 bg-black/60 px-4 py-1.5 border border-white/10"> <div className="absolute top-6 left-1/2 -translate-x-1/2 bg-black/60 px-4 py-1.5 rounded-full border border-white/10">
<span className="text-xs font-normal text-foreground flex items-center gap-2"> <span className="text-xs font-normal text-white flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-error animate-pulse" /> <div className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse" />
Live Viewfinder Live Viewfinder
</span> </span>
</div> </div>
@@ -170,7 +170,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button <button
onClick={stopLiveCamera} onClick={stopLiveCamera}
aria-label="Cancel camera scan" aria-label="Cancel camera scan"
className="px-6 py-4 bg-surface border border-outline/30 text-secondary font-normal cursor-pointer transition-all active:scale-95 focus:ring-2 focus:ring-primary focus:outline-none" className="px-6 py-4 bg-surface border border-slate-800 text-secondary rounded-2xl font-normal cursor-pointer transition-all active:scale-95 focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
Cancel Cancel
</button> </button>
@@ -178,10 +178,10 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
onClick={captureSnapshot} onClick={captureSnapshot}
data-testid="capture-button" data-testid="capture-button"
aria-label="Capture image from camera" aria-label="Capture image from camera"
className="flex-1 py-4 bg-white text-background font-normal text-lg shadow-xl shadow-white/10 cursor-pointer flex items-center justify-center gap-3 active:scale-95 hover:bg-foreground transition-all focus:ring-2 focus:ring-offset-2 focus:ring-offset-background focus:ring-white focus:outline-none" className="flex-1 py-4 bg-white text-slate-950 rounded-2xl font-normal text-lg shadow-xl shadow-white/10 cursor-pointer flex items-center justify-center gap-3 active:scale-95 hover:bg-slate-200 transition-all focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 focus:ring-white focus:outline-none"
> >
<div className="w-5 h-5 border-2 border-background flex items-center justify-center"> <div className="w-5 h-5 rounded-full border-2 border-slate-950 flex items-center justify-center">
<div className="w-2.5 h-2.5 bg-background" /> <div className="w-2.5 h-2.5 rounded-full bg-background" />
</div> </div>
Capture Image Capture Image
</button> </button>
@@ -189,7 +189,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div> </div>
) : extractedItems.length === 0 ? ( ) : extractedItems.length === 0 ? (
<div data-testid="wizard-step" className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0 overflow-hidden"> <div data-testid="wizard-step" className="flex-1 flex flex-col gap-3 md:gap-4 min-h-0 overflow-hidden">
<div className="relative flex-1 min-h-0 ] overflow-hidden border-4 border-outline/30 shadow-2xl bg-surface"> <div className="relative flex-1 min-h-0 rounded-[2.5rem] overflow-hidden border-4 border-slate-900 shadow-2xl bg-surface">
<img src={image || undefined} className="w-full h-full object-contain" alt="Captured label" /> <img src={image || undefined} className="w-full h-full object-contain" alt="Captured label" />
<div className="absolute inset-0 bg-black/30 pointer-events-none" /> <div className="absolute inset-0 bg-black/30 pointer-events-none" />
@@ -199,7 +199,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<RefreshCw className="w-12 h-12 text-primary animate-spin" /> <RefreshCw className="w-12 h-12 text-primary animate-spin" />
<Sparkles className="absolute -top-2 -right-2 text-amber-400 w-6 h-6 animate-pulse" /> <Sparkles className="absolute -top-2 -right-2 text-amber-400 w-6 h-6 animate-pulse" />
</div> </div>
<p className="text-foreground font-normal tracking-tight text-sm">Gemini is processing...</p> <p className="text-white font-normal tracking-tight text-sm">Gemini is processing...</p>
</div> </div>
)} )}
</div> </div>
@@ -210,7 +210,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
disabled={uploading} disabled={uploading}
data-testid="retake-button" data-testid="retake-button"
aria-label="Retake photo" aria-label="Retake photo"
className="px-6 py-4 bg-surface border border-outline/30 text-secondary font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-primary focus:outline-none" className="px-6 py-4 bg-surface border border-slate-800 text-secondary rounded-2xl font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
Retake Retake
</button> </button>
@@ -218,7 +218,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
onClick={processImage} onClick={processImage}
disabled={uploading} disabled={uploading}
aria-label="Extract data from image" aria-label="Extract data from image"
className="flex-1 py-4 bg-primary text-foreground font-normal text-lg shadow-xl shadow-primary/30 cursor-pointer flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-primary transition-all focus:ring-2 focus:ring-primary focus:outline-none" className="flex-1 py-4 bg-primary text-white rounded-2xl font-normal text-lg shadow-xl shadow-primary/30 cursor-pointer flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-500 transition-all focus:ring-2 focus:ring-blue-400 focus:outline-none"
> >
{uploading ? "Analyzing..." : "Extract Data"} {uploading ? "Analyzing..." : "Extract Data"}
{!uploading && <Check size={20} />} {!uploading && <Check size={20} />}
@@ -228,11 +228,11 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
) : editingIndex !== null ? ( ) : editingIndex !== null ? (
<div data-testid="extraction-results-form" className="flex-1 flex flex-col gap-3 md:gap-4 overflow-hidden"> <div data-testid="extraction-results-form" className="flex-1 flex flex-col gap-3 md:gap-4 overflow-hidden">
<div data-testid="ai-onboarding-wizard" className="flex items-center justify-between"> <div data-testid="ai-onboarding-wizard" className="flex items-center justify-between">
<span className="text-xs text-secondary font-normal">Item Details (<span data-testid="current-step">{editingIndex + 1}</span>/<span data-testid="total-steps">{extractedItems.length}</span>)</span> <span className="text-xs text-muted font-normal">Item Details (<span data-testid="current-step">{editingIndex + 1}</span>/<span data-testid="total-steps">{extractedItems.length}</span>)</span>
<button <button
onClick={() => setEditingIndex(null)} onClick={() => setEditingIndex(null)}
aria-label="Back to item list" aria-label="Back to item list"
className="text-xs text-primary font-normal px-3 py-1 bg-primary/10 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none" className="text-xs text-primary font-normal px-3 py-1 bg-primary/10 rounded-full cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none"
> >
Back to List Back to List
</button> </button>
@@ -240,7 +240,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
{/* Image Preview Section */} {/* Image Preview Section */}
{image && ( {image && (
<div className="bg-surface/70 border border-outline/30/50 overflow-hidden"> <div className="bg-surface/70 border border-slate-800/50 rounded-2xl overflow-hidden">
<div className="relative h-40 md:h-48 bg-black/20 flex items-center justify-center"> <div className="relative h-40 md:h-48 bg-black/20 flex items-center justify-center">
<img <img
src={image} src={image}
@@ -249,16 +249,16 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
/> />
</div> </div>
<div className="p-3 space-y-2"> <div className="p-3 space-y-2">
<p className="text-xs text-secondary font-normal">Extracted Photo</p> <p className="text-xs text-muted font-normal">Extracted Photo</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
id="save-photo-check" id="save-photo-check"
checked={!extractedItems[editingIndex]._skipPhoto} checked={!extractedItems[editingIndex]._skipPhoto}
onChange={(e) => updateEditingItem({ _skipPhoto: !e.target.checked })} onChange={(e) => updateEditingItem({ _skipPhoto: !e.target.checked })}
className="w-4 h-4 border-outline-variant accent-primary cursor-pointer" className="w-4 h-4 rounded border-slate-600 accent-primary cursor-pointer"
/> />
<label htmlFor="save-photo-check" className="text-xs text-foreground font-normal cursor-pointer"> <label htmlFor="save-photo-check" className="text-xs text-slate-300 font-normal cursor-pointer">
Save this photo with the item Save this photo with the item
</label> </label>
</div> </div>
@@ -267,19 +267,19 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
)} )}
<div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide"> <div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
<div data-testid="extracted-name" className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div data-testid="extracted-name" className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Name</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Name</label>
<textarea <textarea
value={extractedItems[editingIndex].Item || extractedItems[editingIndex].name || ''} value={extractedItems[editingIndex].Item || extractedItems[editingIndex].name || ''}
onChange={(e) => updateEditingItem({ Item: e.target.value })} onChange={(e) => updateEditingItem({ Item: e.target.value })}
className="bg-transparent w-full text-lg font-normal outline-none text-foreground placeholder:text-secondary resize-none h-8 leading-tight selection:bg-primary/30 py-0" className="bg-transparent w-full text-lg font-normal outline-none text-white placeholder:text-muted resize-none h-8 leading-tight selection:bg-primary/30 py-0"
placeholder="SSD, SFP, Cable..." placeholder="SSD, SFP, Cable..."
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div data-testid="extracted-category" className="bg-surface py-3 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div data-testid="extracted-category" className="bg-surface py-3 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Category</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Category</label>
<input <input
type="text" type="text"
list="onboarding-categories" list="onboarding-categories"
@@ -294,8 +294,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
))} ))}
</datalist> </datalist>
</div> </div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Type</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Type</label>
<input <input
value={extractedItems[editingIndex].Type || extractedItems[editingIndex].type || ''} value={extractedItems[editingIndex].Type || extractedItems[editingIndex].type || ''}
list="onboarding-types" list="onboarding-types"
@@ -310,8 +310,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="bg-surface py-3 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-3 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Color</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Color</label>
<input <input
value={extractedItems[editingIndex].Color || extractedItems[editingIndex].color || ''} value={extractedItems[editingIndex].Color || extractedItems[editingIndex].color || ''}
onChange={(e) => updateEditingItem({ Color: e.target.value })} onChange={(e) => updateEditingItem({ Color: e.target.value })}
@@ -319,8 +319,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
placeholder="e.g. Aqua" placeholder="e.g. Aqua"
/> />
</div> </div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Box / Container</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Box / Container</label>
<input <input
value={extractedItems[editingIndex].box_label || ''} value={extractedItems[editingIndex].box_label || ''}
list="onboarding-boxes" list="onboarding-boxes"
@@ -334,19 +334,19 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div> </div>
</div> </div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Description</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Description</label>
<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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="bg-surface py-3 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-3 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Connector</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Connector</label>
<input <input
value={extractedItems[editingIndex].Connector || extractedItems[editingIndex].connector || ''} value={extractedItems[editingIndex].Connector || extractedItems[editingIndex].connector || ''}
onChange={(e) => updateEditingItem({ Connector: e.target.value })} onChange={(e) => updateEditingItem({ Connector: e.target.value })}
@@ -354,8 +354,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
placeholder="e.g. LC/UPC" placeholder="e.g. LC/UPC"
/> />
</div> </div>
<div className="bg-surface px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Size / Length</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Size / Length</label>
<input <input
value={extractedItems[editingIndex].Size || extractedItems[editingIndex].size || ''} value={extractedItems[editingIndex].Size || extractedItems[editingIndex].size || ''}
onChange={(e) => updateEditingItem({ Size: e.target.value })} onChange={(e) => updateEditingItem({ Size: e.target.value })}
@@ -365,8 +365,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div> </div>
</div> </div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">OCR Matching Key</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">OCR Matching Key</label>
<textarea <textarea
value={extractedItems[editingIndex].OCR || extractedItems[editingIndex].ocr_text || ''} value={extractedItems[editingIndex].OCR || extractedItems[editingIndex].ocr_text || ''}
onChange={(e) => updateEditingItem({ OCR: e.target.value })} onChange={(e) => updateEditingItem({ OCR: e.target.value })}
@@ -376,8 +376,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Part Number</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Part Number</label>
<input <input
value={extractedItems[editingIndex].PartNr || extractedItems[editingIndex].part_number || ''} value={extractedItems[editingIndex].PartNr || extractedItems[editingIndex].part_number || ''}
onChange={(e) => updateEditingItem({ PartNr: e.target.value })} onChange={(e) => updateEditingItem({ PartNr: e.target.value })}
@@ -385,8 +385,8 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
placeholder="ID code..." placeholder="ID code..."
/> />
</div> </div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tighter">Initial Qty</label> <label className="text-xs text-muted font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tighter">Initial Qty</label>
<input <input
type="number" type="number"
value={extractedItems[editingIndex].quantity || 1} value={extractedItems[editingIndex].quantity || 1}
@@ -402,7 +402,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
onClick={() => confirmSingleItem(editingIndex)} onClick={() => confirmSingleItem(editingIndex)}
data-testid="confirm-extraction" data-testid="confirm-extraction"
aria-label="Add item to catalog" aria-label="Add item to catalog"
className="py-5 bg-primary text-foreground ] font-normal text-lg shadow-2xl shadow-primary/20 cursor-pointer active:scale-95 transition-all hover:bg-primary focus:ring-2 focus:ring-primary focus:outline-none" className="py-5 bg-primary text-white rounded-[1.8rem] font-normal text-lg shadow-2xl shadow-primary/20 cursor-pointer active:scale-95 transition-all hover:bg-blue-500 focus:ring-2 focus:ring-blue-400 focus:outline-none"
> >
Add to Catalog Add to Catalog
</button> </button>
@@ -412,7 +412,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<div data-testid="manual-entry-form" className="flex-1 flex flex-col gap-6 overflow-hidden"> <div data-testid="manual-entry-form" className="flex-1 flex flex-col gap-6 overflow-hidden">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-normal text-secondary">Discovery Dashboard</h3> <h3 className="text-sm font-normal text-secondary">Discovery Dashboard</h3>
<span data-testid="wizard-progress" className="text-xs bg-primary/20 text-primary px-3 py-1 font-normal"> <span data-testid="wizard-progress" className="text-xs bg-primary/20 text-primary px-3 py-1 rounded-full font-normal">
{extractedItems.length} items found {extractedItems.length} items found
</span> </span>
</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-outline/30 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"
@@ -438,16 +438,16 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
aria-label={`Edit item: ${item.Item || item.name}`} aria-label={`Edit item: ${item.Item || item.name}`}
> >
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 bg-primary/10 flex items-center justify-center text-primary"> <div className="w-8 h-8 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
<Package size={16} /> <Package size={16} />
</div> </div>
<h4 className="font-normal text-secondary truncate">{item.Item || item.name || "Unknown Item"}</h4> <h4 className="font-normal text-secondary truncate">{item.Item || item.name || "Unknown Item"}</h4>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<span className="text-xs font-normal px-2 py-0.5 bg-surface-container text-secondary"> <span className="text-xs font-normal px-2 py-0.5 bg-slate-800 text-secondary rounded-md">
{item.Type || item.type || "Generic"} {item.Type || item.type || "Generic"}
</span> </span>
<span className="text-xs font-normal px-2 py-0.5 bg-surface-container text-secondary"> <span className="text-xs font-normal px-2 py-0.5 bg-slate-800 text-muted rounded-md">
{item.PartNr || item.part_number || "No P/N"} {item.PartNr || item.part_number || "No P/N"}
</span> </span>
</div> </div>
@@ -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-error cursor-pointer transition-colors focus:ring-2 focus:ring-error focus:outline-none" 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 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>
@@ -481,7 +481,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button <button
onClick={confirmAllItems} onClick={confirmAllItems}
aria-label={`Add ${extractedItems.length} items to catalog`} aria-label={`Add ${extractedItems.length} items to catalog`}
className="py-5 bg-white text-background ] font-normal text-lg shadow-2xl shadow-white/10 cursor-pointer active:scale-95 transition-all hover:bg-foreground focus:ring-2 focus:ring-offset-2 focus:ring-offset-background focus:ring-white focus:outline-none" className="py-5 bg-white text-slate-950 rounded-[1.8rem] font-normal text-lg shadow-2xl shadow-white/10 cursor-pointer active:scale-95 transition-all hover:bg-slate-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 focus:ring-white focus:outline-none"
> >
Add {extractedItems.length} items to catalog Add {extractedItems.length} items to catalog
</button> </button>
@@ -492,7 +492,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
}} }}
data-testid="reject-extraction" data-testid="reject-extraction"
aria-label="Discard discovery session" aria-label="Discard discovery session"
className="py-3 text-xs text-secondary font-normal cursor-pointer hover:text-secondary transition-colors focus:ring-2 focus:ring-secondary focus:outline-none px-2" className="py-3 text-xs text-secondary font-normal cursor-pointer hover:text-secondary transition-colors focus:ring-2 focus:ring-slate-500 focus:outline-none rounded px-2"
> >
Discard Discovery Discard Discovery
</button> </button>

View File

@@ -114,18 +114,18 @@ export default function AdminOverlay({
dangerLevel={confirmState.affectedCount && confirmState.affectedCount > 10 ? 'high' : 'medium'} dangerLevel={confirmState.affectedCount && confirmState.affectedCount > 10 ? 'high' : 'medium'}
/> />
<div data-testid="admin-overlay" className="fixed inset-0 z-50 bg-background/60 animate-in fade-in duration-300"> <div data-testid="admin-overlay" className="fixed inset-0 z-50 bg-background/60 animate-in fade-in duration-300">
<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-outline/30 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-outline/30 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-surface-container 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-foreground">System Admin</h2> <h2 className="text-xl font-normal text-white">System Admin</h2>
</div> </div>
<button <button
data-testid="close-admin-overlay" data-testid="close-admin-overlay"
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-surface-container transition-colors text-secondary 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} />
@@ -137,11 +137,11 @@ export default function AdminOverlay({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-normal text-secondary">User Management</h3> <h3 className="text-sm font-normal text-secondary">User Management</h3>
<p className="text-xs text-secondary mt-0.5">Create accounts and control access</p> <p className="text-xs text-muted mt-0.5">Create accounts and control access</p>
</div> </div>
<button <button
onClick={() => setShowCreateUserModal(true)} onClick={() => setShowCreateUserModal(true)}
className="flex items-center gap-1 text-xs font-normal text-primary hover:text-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none px-1" className="flex items-center gap-1 text-xs font-normal text-primary hover:text-blue-400 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded px-1"
> >
<UserPlus size={12} /> Add User <UserPlus size={12} /> Add User
</button> </button>
@@ -149,14 +149,14 @@ export default function AdminOverlay({
<div className="grid gap-1.5 md:gap-2"> <div className="grid gap-1.5 md:gap-2">
{users.map(u => ( {users.map(u => (
<div key={u.id} className="bg-surface-container/40 border border-outline/30 p-3 md:p-4 flex items-center justify-between"> <div key={u.id} className="bg-slate-800/40 border border-slate-800 p-3 md:p-4 rounded-2xl flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-3"> <div className="flex items-center gap-2 md:gap-3">
<div className={`p-2 ${u.role === 'admin' ? "bg-primary/20 text-primary" : "bg-surface-bright text-secondary"}`}> <div className={`p-2 rounded-lg ${u.role === 'admin' ? "bg-primary/20 text-primary" : "bg-slate-700 text-secondary"}`}>
<User size={16} /> <User size={16} />
</div> </div>
<div> <div>
<p className="text-sm font-normal text-foreground">{u.username}</p> <p className="text-sm font-normal text-white">{u.username}</p>
<p className="text-xs font-normal text-secondary">{u.role}</p> <p className="text-xs font-normal text-muted">{u.role}</p>
</div> </div>
</div> </div>
@@ -171,7 +171,7 @@ export default function AdminOverlay({
loading: false, loading: false,
}); });
}} }}
className="p-2 text-error hover:bg-error/10 transition-all focus-visible:ring-2 focus-visible:ring-error 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} />
@@ -186,11 +186,11 @@ export default function AdminOverlay({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-normal text-secondary">Category Groups</h3> <h3 className="text-sm font-normal text-secondary">Category Groups</h3>
<p className="text-xs text-secondary mt-0.5">Organize items by type or location</p> <p className="text-xs text-muted mt-0.5">Organize items by type or location</p>
</div> </div>
<button <button
onClick={() => setShowCreateCategoryModal(true)} onClick={() => setShowCreateCategoryModal(true)}
className="flex items-center gap-1 text-xs font-normal text-primary hover:text-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none px-1" className="flex items-center gap-1 text-xs font-normal text-primary hover:text-blue-400 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded px-1"
> >
<Plus size={12} /> Add Category <Plus size={12} /> Add Category
</button> </button>
@@ -198,14 +198,14 @@ export default function AdminOverlay({
<div className="grid gap-1.5 md:gap-2"> <div className="grid gap-1.5 md:gap-2">
{categories.map(cat => ( {categories.map(cat => (
<div key={cat.id} className="bg-surface-container/40 border border-outline/30 p-3 md:p-4 flex items-center justify-between"> <div key={cat.id} className="bg-slate-800/40 border border-slate-800 p-3 md:p-4 rounded-2xl flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-3"> <div className="flex items-center gap-2 md:gap-3">
<div className="p-2 bg-primary/10 text-primary"> <div className="p-2 bg-primary/10 rounded-lg text-primary">
<Layers size={16} /> <Layers size={16} />
</div> </div>
<div> <div>
<p className="text-sm font-normal text-foreground">{cat.name}</p> <p className="text-sm font-normal text-white">{cat.name}</p>
<p className="text-xs text-secondary font-mono">{cat.description || 'No description'}</p> <p className="text-xs text-muted font-mono">{cat.description || 'No description'}</p>
</div> </div>
</div> </div>
@@ -220,7 +220,7 @@ export default function AdminOverlay({
loading: false, loading: false,
}); });
}} }}
className="p-2 text-error hover:bg-error/10 transition-all focus-visible:ring-2 focus-visible:ring-error 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} />
@@ -230,18 +230,18 @@ export default function AdminOverlay({
</div> </div>
</section> </section>
<section className="p-4 md:p-6 bg-surface-container/30 border border-outline/30 space-y-2 md:space-y-3"> <section className="p-4 md:p-6 bg-slate-800/30 rounded-3xl border border-slate-800 space-y-2 md:space-y-3">
<div className="flex items-center gap-2 text-error"> <div className="flex items-center gap-2 text-rose-500">
<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-secondary">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());
window.location.href = '/login'; window.location.href = '/login';
}} }}
className="w-full bg-error/10 hover:bg-error/20 text-error border border-error/20 font-normal py-3 transition-all flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-error focus-visible:outline-none" className="w-full bg-rose-500/10 hover:bg-rose-500/20 text-rose-500 border border-rose-500/20 font-normal py-3 rounded-xl transition-all flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
> >
<LogOut size={16} /> Sign Out <LogOut size={16} /> Sign Out
</button> </button>

View File

@@ -25,13 +25,13 @@ 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-outline/30 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
onClick={() => router.push('/')} onClick={() => router.push('/')}
aria-label="Go to Home" aria-label="Go to Home"
className={cn("flex flex-col items-center gap-1 px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isHome && "text-primary")} className={cn("flex flex-col items-center gap-1 rounded px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isHome && "text-primary")}
> >
<Smartphone size={20} /> <Smartphone size={20} />
<span className="text-xs font-normal transition-all">Home</span> <span className="text-xs font-normal transition-all">Home</span>
@@ -41,7 +41,7 @@ export default function BottomNav({
<button <button
onClick={() => router.push('/inventory')} onClick={() => router.push('/inventory')}
aria-label="Go to Inventory" aria-label="Go to Inventory"
className={cn("flex flex-col items-center gap-1 px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isInventory && "text-primary")} className={cn("flex flex-col items-center gap-1 rounded px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isInventory && "text-primary")}
> >
<Package size={20} /> <Package size={20} />
<span className="text-xs font-normal transition-all">Inventory</span> <span className="text-xs font-normal transition-all">Inventory</span>
@@ -51,7 +51,7 @@ export default function BottomNav({
<button <button
onClick={() => router.push('/logs')} onClick={() => router.push('/logs')}
aria-label="Go to Logs" aria-label="Go to Logs"
className={cn("flex flex-col items-center gap-1 px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isLogs && "text-primary")} className={cn("flex flex-col items-center gap-1 rounded px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isLogs && "text-primary")}
> >
<History size={20} /> <History size={20} />
<span className="text-xs font-normal transition-all">Logs</span> <span className="text-xs font-normal transition-all">Logs</span>
@@ -63,7 +63,7 @@ export default function BottomNav({
data-testid="admin-button" data-testid="admin-button"
onClick={() => router.push('/admin')} onClick={() => router.push('/admin')}
aria-label="Go to Admin" aria-label="Go to Admin"
className={cn("flex flex-col items-center gap-1 px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isAdmin && "text-primary")} className={cn("flex flex-col items-center gap-1 rounded px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isAdmin && "text-primary")}
> >
<Settings size={20} /> <Settings size={20} />
<span className="text-xs font-normal transition-all">Admin</span> <span className="text-xs font-normal transition-all">Admin</span>
@@ -79,7 +79,7 @@ export default function BottomNav({
} }
}} }}
aria-label="Logout" aria-label="Logout"
className="flex flex-col items-center gap-1 text-error hover:text-error/80 cursor-pointer transition-colors px-2 py-1 focus:ring-2 focus:ring-error focus:outline-none" className="flex flex-col items-center gap-1 text-rose-500 hover:text-rose-400 cursor-pointer transition-colors rounded px-2 py-1 focus:ring-2 focus:ring-rose-500 focus:outline-none"
> >
<LogOut size={20} /> <LogOut size={20} />
<span className="text-xs font-normal transition-all">Logout</span> <span className="text-xs font-normal transition-all">Logout</span>

View File

@@ -44,13 +44,13 @@ 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 ] shadow-[0_20px_50px_rgba(0,0,0,0.5)] bg-background border-2 border-outline/20 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 ] 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 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)]" />
<div className="absolute top-0 right-0 w-10 h-10 border-t-4 border-r-4 border-primary shadow-[0_0_15px_rgba(59,130,246,0.5)]" /> <div className="absolute top-0 right-0 w-10 h-10 border-t-4 border-r-4 border-primary rounded-tr-2xl shadow-[0_0_15px_rgba(59,130,246,0.5)]" />
<div className="absolute bottom-0 left-0 w-10 h-10 border-b-4 border-l-4 border-primary shadow-[0_0_15px_rgba(59,130,246,0.5)]" /> <div className="absolute bottom-0 left-0 w-10 h-10 border-b-4 border-l-4 border-primary rounded-bl-2xl shadow-[0_0_15px_rgba(59,130,246,0.5)]" />
<div className="absolute bottom-0 right-0 w-10 h-10 border-b-4 border-r-4 border-primary shadow-[0_0_15px_rgba(59,130,246,0.5)]" /> <div className="absolute bottom-0 right-0 w-10 h-10 border-b-4 border-r-4 border-primary rounded-br-2xl shadow-[0_0_15px_rgba(59,130,246,0.5)]" />
{isStarted && !paused && !isSelecting && ( {isStarted && !paused && !isSelecting && (
<div className="absolute top-0 left-0 right-0 h-1 bg-primary animate-scan-fast" /> <div className="absolute top-0 left-0 right-0 h-1 bg-primary animate-scan-fast" />
@@ -77,7 +77,7 @@ export default function CameraView({
key={i} key={i}
onClick={() => onWordSelect(w.text)} onClick={() => onWordSelect(w.text)}
aria-label={`Select text: ${w.text}`} aria-label={`Select text: ${w.text}`}
className="absolute border border-primary bg-primary/20 active:bg-primary/50 cursor-pointer transition-colors pointer-events-auto focus:ring-2 focus:ring-primary focus:outline-none" className="absolute border border-primary bg-primary/20 rounded-sm active:bg-primary/50 cursor-pointer transition-colors pointer-events-auto focus:ring-2 focus:ring-primary focus:outline-none"
style={{ style={{
left: `${(w.bbox.x0 / 1600) * 100}%`, left: `${(w.bbox.x0 / 1600) * 100}%`,
top: `${(w.bbox.y0 / (1600 * (9 / 16))) * 100}%`, top: `${(w.bbox.y0 / (1600 * (9 / 16))) * 100}%`,
@@ -89,17 +89,17 @@ export default function CameraView({
</div> </div>
</div> </div>
</div> </div>
<div className="p-6 bg-surface/90 border-t border-outline/30 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-foreground text-center">Text Found</p> <p className="text-sm font-normal text-white italic text-center">Text Found</p>
<p className="text-xs text-secondary 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>
</div> </div>
<button <button
onClick={onCancelSelection} onClick={onCancelSelection}
aria-label="Cancel text selection" aria-label="Cancel text selection"
className="w-full py-4 bg-surface-container hover:bg-surface-bright text-foreground font-normal text-xs cursor-pointer transition-all active:scale-95 border border-outline-variant focus:ring-2 focus:ring-primary focus:outline-none" className="w-full py-4 bg-slate-800 hover:bg-slate-700 text-white rounded-2xl font-normal text-xs cursor-pointer transition-all active:scale-95 border border-slate-700 focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
Cancel Close OCR Preview
</button> </button>
</div> </div>
</div> </div>
@@ -114,15 +114,15 @@ export default function CameraView({
{error && ( {error && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-surface text-secondary px-8 text-center gap-4"> <div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-surface text-secondary px-8 text-center gap-4">
<XCircle className="w-10 h-10 text-error" /> <XCircle className="w-10 h-10 text-red-500" />
<div> <div>
<p className="font-normal text-foreground">Camera Error</p> <p className="font-normal text-white">Camera Error</p>
<p className="text-xs text-secondary mt-1">{error}</p> <p className="text-xs text-secondary mt-1">{error}</p>
</div> </div>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
aria-label="Reload page and try again" aria-label="Reload page and try again"
className="mt-4 px-6 py-2 bg-surface-container text-sm font-normal cursor-pointer hover:bg-surface-bright transition-colors focus:ring-2 focus:ring-primary focus:outline-none" className="mt-4 px-6 py-2 bg-slate-800 rounded-full text-sm font-normal cursor-pointer hover:bg-slate-700 transition-colors focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
Try Again Try Again
</button> </button>
@@ -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 ] border border-outline/20 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
@@ -146,14 +146,14 @@ export default function CameraView({
}} }}
data-testid="zoom-control" data-testid="zoom-control"
aria-label={`Zoom ${zoom.toFixed(1)}x`} aria-label={`Zoom ${zoom.toFixed(1)}x`}
className="h-14 px-5 bg-surface-container/80 hover:bg-surface-bright border border-outline-variant text-foreground flex flex-col items-center justify-center shadow-lg cursor-pointer transition-all active:scale-95 shrink-0 focus:ring-2 focus:ring-primary focus:outline-none" className="h-14 px-5 bg-slate-800/80 hover:bg-slate-700 border border-slate-700 text-white rounded-2xl flex flex-col items-center justify-center shadow-lg cursor-pointer transition-all active:scale-95 shrink-0 focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
<span className="text-xs font-normal tabular-nums">{zoom.toFixed(1)}x</span> <span className="text-xs font-normal tabular-nums">{zoom.toFixed(1)}x</span>
<span className="text-xs text-primary font-normal tracking-tighter">Zoom</span> <span className="text-xs text-primary font-normal tracking-tighter">Zoom</span>
</button> </button>
)} )}
<div className="flex-1 h-14 bg-background/40 border border-outline/20 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} />
@@ -166,7 +166,7 @@ export default function CameraView({
size={18} size={18}
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs text-secondary font-normal leading-none">Smart Scan</span> <span className="text-xs text-muted font-normal leading-none">Smart Scan</span>
<span className="text-xs font-normal text-primary leading-tight tabular-nums"> <span className="text-xs font-normal text-primary leading-tight tabular-nums">
{countdown === 0 ? 'Scanning' : `${countdown}s`} {countdown === 0 ? 'Scanning' : `${countdown}s`}
</span> </span>
@@ -183,7 +183,7 @@ export default function CameraView({
</div> </div>
<div className="w-full flex justify-center items-center gap-2"> <div className="w-full flex justify-center items-center gap-2">
<div className="w-1 h-1 bg-success animate-pulse" /> <div className="w-1 h-1 rounded-full bg-green-500 animate-pulse" />
<p className="text-xs text-secondary font-normal"> <p className="text-xs text-secondary font-normal">
Scanner active · Use zoom or tap scan Scanner active · Use zoom or tap scan
</p> </p>

View File

@@ -51,25 +51,25 @@ 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-outline/30 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-outline/30"> <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 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-foreground">New Category</h2> <h2 className="text-lg font-normal text-white">New Category</h2>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
disabled={loading} disabled={loading}
className="p-1 text-secondary hover:text-secondary focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none disabled:opacity-50" className="p-1 text-muted hover:text-secondary rounded focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none disabled:opacity-50"
aria-label="Close" aria-label="Close"
> >
<X size={20} /> <X size={20} />
</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-surface-container border border-outline-variant text-foreground placeholder:text-secondary 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-surface-container border border-outline-variant text-foreground placeholder:text-secondary 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-error/10 border border-error/20"> <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>
)} )}
@@ -109,14 +109,14 @@ export default function CategoryCreationModal({
type="button" type="button"
onClick={onClose} onClick={onClose}
disabled={loading} disabled={loading}
className="flex-1 px-4 py-2.5 text-secondary bg-surface-container border border-outline-variant font-normal hover:bg-surface-bright transition-colors disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="flex-1 px-4 py-2.5 text-secondary bg-slate-800 border border-slate-700 rounded font-normal hover:bg-slate-700 transition-colors disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading || !name.trim()} disabled={loading || !name.trim()}
className="flex-1 px-4 py-2.5 bg-primary text-foreground font-normal hover:bg-primary transition-colors disabled:opacity-50 flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-container focus-visible:ring-primary focus-visible:outline-none" className="flex-1 px-4 py-2.5 bg-primary text-white rounded font-normal hover:bg-blue-600 transition-colors disabled:opacity-50 flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 focus-visible:ring-primary focus-visible:outline-none"
> >
{loading ? ( {loading ? (
<> <>

View File

@@ -55,19 +55,19 @@ export default function ConfirmationModal({
return ( return (
<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-outline/30 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-outline/30"> <div className="flex items-center justify-between p-6 border-b border-slate-800">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-error/10 text-error"> <div className="p-2 bg-rose-500/10 rounded-lg text-rose-500">
<AlertTriangle size={20} /> <AlertTriangle size={20} />
</div> </div>
<h2 className="text-lg font-normal text-foreground">{title}</h2> <h2 className="text-lg font-normal text-white">{title}</h2>
</div> </div>
<button <button
onClick={onCancel} onClick={onCancel}
disabled={loading} disabled={loading}
className="p-1 text-secondary hover:text-secondary cursor-pointer focus:ring-2 focus:ring-primary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 text-muted hover:text-secondary rounded cursor-pointer focus:ring-2 focus:ring-primary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Close modal" aria-label="Close modal"
> >
<X size={20} /> <X size={20} />
@@ -81,16 +81,16 @@ export default function ConfirmationModal({
{/* Item Name (if provided) */} {/* Item Name (if provided) */}
{itemName && ( {itemName && (
<div className="bg-surface-container/40 border border-outline/30 p-3"> <div className="bg-slate-800/40 border border-slate-800 rounded p-3">
<p className="text-xs text-secondary font-normal">Item</p> <p className="text-xs text-muted font-normal">Item</p>
<p className="text-sm font-mono text-foreground mt-1">{itemName}</p> <p className="text-sm font-mono text-white mt-1">{itemName}</p>
</div> </div>
)} )}
{/* Consequence Warning */} {/* Consequence Warning */}
{consequence && ( {consequence && (
<div className="flex gap-2 p-3 bg-error/5 border border-error/20"> <div className="flex gap-2 p-3 bg-rose-500/5 border border-rose-500/20 rounded">
<span className="text-error 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 text-rose-400">{consequence}</p>
</div> </div>
)} )}
@@ -106,7 +106,7 @@ export default function ConfirmationModal({
{/* High-Risk Warning & Confirmation Input */} {/* High-Risk Warning & Confirmation Input */}
{requiresTextConfirm && ( {requiresTextConfirm && (
<div className="space-y-3 pt-2 border-t border-outline/30"> <div className="space-y-3 pt-2 border-t border-slate-800">
<p className="text-sm font-normal text-rose-400"> <p className="text-sm font-normal text-rose-400">
Type "Delete" to confirm this high-risk action Type "Delete" to confirm this high-risk action
</p> </p>
@@ -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-surface-container border border-outline-variant text-foreground placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50" 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"
disabled={loading} disabled={loading}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && canConfirm && !loading) { if (e.key === 'Enter' && canConfirm && !loading) {
@@ -127,18 +127,18 @@ export default function ConfirmationModal({
}} }}
/> />
{error && ( {error && (
<p className="text-xs text-error">{error}</p> <p className="text-xs text-rose-500">{error}</p>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Actions */} {/* Actions */}
<div className="flex gap-2 p-6 border-t border-outline/30"> <div className="flex gap-2 p-6 border-t border-slate-800">
<button <button
onClick={onCancel} onClick={onCancel}
disabled={loading} disabled={loading}
className="flex-1 px-4 py-2.5 text-secondary bg-surface-container border border-outline-variant font-normal hover:bg-surface-bright cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-primary focus:outline-none" className="flex-1 px-4 py-2.5 text-secondary bg-slate-800 border border-slate-700 rounded font-normal hover:bg-slate-700 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-primary focus:outline-none"
> >
Keep Item Keep Item
</button> </button>
@@ -146,7 +146,7 @@ export default function ConfirmationModal({
onClick={handleConfirm} onClick={handleConfirm}
data-testid="confirm-action" data-testid="confirm-action"
disabled={loading || !canConfirm} disabled={loading || !canConfirm}
className="flex-1 px-4 py-2.5 bg-rose-600 text-foreground font-normal hover:bg-rose-700 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 focus:ring-2 focus:ring-offset-2 focus:ring-offset-surface-container focus:ring-rose-600 focus:outline-none" className="flex-1 px-4 py-2.5 bg-rose-600 text-white rounded font-normal hover:bg-rose-700 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900 focus:ring-rose-600 focus:outline-none"
> >
{loading ? ( {loading ? (
<> <>

View File

@@ -71,13 +71,13 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
return ( return (
<div data-testid="create-user-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div data-testid="create-user-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface border border-outline/30 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-outline/30"> <div className="flex items-center justify-between p-6 border-b border-slate-800">
<h2 className="text-lg font-normal text-foreground">Create User</h2> <h2 className="text-lg font-normal text-white">Create User</h2>
<button <button
onClick={onClose} onClick={onClose}
className="p-1 text-secondary hover:text-secondary focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="p-1 text-muted hover:text-secondary rounded focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Close" aria-label="Close"
> >
<X size={20} /> <X size={20} />
@@ -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,11 +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-surface-container border text-foreground placeholder:text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${ errors.username ? 'border-error' : 'border-outline-variant' }`} className={`w-full px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
errors.username ? 'border-red-500' : 'border-slate-700'
}`}
disabled={loading} disabled={loading}
/> />
{errors.username && ( {errors.username && (
<p className="mt-1.5 text-sm text-error">{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>
@@ -124,11 +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-surface-container border text-foreground placeholder:text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${ errors.password ? 'border-error' : 'border-outline-variant' }`} className={`w-full px-3 lg:px-4 xl:px-5 py-2 lg:py-3 xl:py-4.5 bg-slate-800 border rounded text-white placeholder:text-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
errors.password ? 'border-red-500' : 'border-slate-700'
}`}
disabled={loading} disabled={loading}
/> />
{errors.password && ( {errors.password && (
<p className="mt-1.5 text-sm text-error">{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>
@@ -138,7 +142,7 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
type="button" type="button"
onClick={onClose} onClick={onClose}
disabled={loading} disabled={loading}
className="flex-1 px-4 py-2.5 text-secondary bg-surface-container border border-outline-variant font-normal hover:bg-surface-bright transition-colors disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="flex-1 px-4 py-2.5 text-secondary bg-slate-800 border border-slate-700 rounded font-normal hover:bg-slate-700 transition-colors disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
> >
Cancel Cancel
</button> </button>
@@ -146,7 +150,7 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
type="submit" type="submit"
data-testid="create-user-submit" data-testid="create-user-submit"
disabled={!isFormValid || loading} disabled={!isFormValid || loading}
className="flex-1 px-4 py-2.5 bg-primary text-primary-foreground font-normal hover:bg-primary transition-colors disabled:opacity-50 flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-container focus-visible:ring-primary focus-visible:outline-none" className="flex-1 px-4 py-2.5 bg-primary text-primary-foreground rounded font-normal hover:bg-blue-600 transition-colors disabled:opacity-50 flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 focus-visible:ring-primary focus-visible:outline-none"
> >
{loading ? ( {loading ? (
<> <>

View File

@@ -283,13 +283,13 @@ export function DebugRotationPanel({
return ( return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-surface-container border border-outline-variant shadow-2xl max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col"> <div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col">
{/* Header */} {/* Header */}
<div className="p-3 border-b border-outline-variant flex items-center justify-between bg-surface-container flex-shrink-0"> <div className="p-3 border-b border-slate-700 flex items-center justify-between bg-slate-800 flex-shrink-0">
<h2 className="text-lg font-normal text-foreground">Debug Rotation & Crop</h2> <h2 className="text-lg font-normal text-white">Debug Rotation & Crop</h2>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-surface-bright text-gray-400 hover:text-foreground transition" className="p-2 hover:bg-slate-700 rounded text-gray-400 hover:text-white transition"
> >
<X size={20} /> <X size={20} />
</button> </button>
@@ -297,11 +297,11 @@ export function DebugRotationPanel({
<div className="flex gap-4 p-4 flex-1 overflow-hidden"> <div className="flex gap-4 p-4 flex-1 overflow-hidden">
{/* Left: Controls */} {/* Left: Controls */}
<div className="w-48 bg-surface-container p-3 space-y-4 flex-shrink-0 overflow-y-auto"> <div className="w-48 bg-slate-800 p-3 rounded space-y-4 flex-shrink-0 overflow-y-auto">
{/* Rotation Slider */} {/* Rotation Slider */}
<div> <div>
<label className="block text-sm font-normal text-foreground mb-2"> <label className="block text-sm font-normal text-white mb-2">
Rotation: <span className="text-warning">{rotation}°</span> Rotation: <span className="text-yellow-400">{rotation}°</span>
</label> </label>
<input <input
type="range" type="range"
@@ -315,13 +315,17 @@ export function DebugRotationPanel({
{/* Quick Presets */} {/* Quick Presets */}
<div> <div>
<label className="block text-sm font-normal text-foreground mb-2">Quick Angles</label> <label className="block text-sm font-normal text-white mb-2">Quick Angles</label>
<div className="grid grid-cols-2 gap-1"> <div className="grid grid-cols-2 gap-1">
{commonAngles.map((angle) => ( {commonAngles.map((angle) => (
<button <button
key={angle.value} key={angle.value}
onClick={() => setRotation(angle.value)} onClick={() => setRotation(angle.value)}
className={`py-1 px-2 text-xs font-normal transition ${ rotation === angle.value ? 'bg-warning text-foreground' : 'bg-surface-bright text-gray-300 hover:bg-outline-variant' }`} className={`py-1 px-2 rounded text-xs font-normal transition ${
rotation === angle.value
? 'bg-yellow-600 text-white'
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
}`}
> >
{angle.label} {angle.label}
</button> </button>
@@ -333,15 +337,15 @@ export function DebugRotationPanel({
<button <button
onClick={() => setAdjustedCropBounds(null)} onClick={() => setAdjustedCropBounds(null)}
disabled={!adjustedCropBounds} disabled={!adjustedCropBounds}
className="w-full py-2 px-3 text-sm font-normal bg-surface-bright text-gray-300 hover:bg-outline-variant disabled:opacity-50 disabled:cursor-not-allowed transition" className="w-full py-2 px-3 rounded text-sm font-normal bg-slate-700 text-gray-300 hover:bg-slate-600 disabled:opacity-50 disabled:cursor-not-allowed transition"
> >
Reset Crop Box Reset Crop Box
</button> </button>
{/* Original Values */} {/* Original Values */}
{originalRotation !== undefined && ( {originalRotation !== undefined && (
<div className="bg-surface-bright p-3 text-xs space-y-1"> <div className="bg-slate-700 p-3 rounded text-xs space-y-1">
<h3 className="font-normal text-foreground">Original AI Values</h3> <h3 className="font-normal text-white">Original AI Values</h3>
<div className="text-gray-300 font-mono text-xs"> <div className="text-gray-300 font-mono text-xs">
<div>Rotation: {originalRotation}°</div> <div>Rotation: {originalRotation}°</div>
{originalCropBounds && ( {originalCropBounds && (
@@ -361,12 +365,12 @@ export function DebugRotationPanel({
{/* Right: Canvas + Log */} {/* Right: Canvas + Log */}
<div className="flex-1 flex flex-col gap-3 min-w-0 overflow-hidden"> <div className="flex-1 flex flex-col gap-3 min-w-0 overflow-hidden">
{/* Canvas - takes most space */} {/* Canvas - takes most space */}
<div className="flex-1 flex flex-col bg-surface-container p-3 border border-outline-variant overflow-hidden"> <div className="flex-1 flex flex-col bg-slate-800 p-3 rounded border border-slate-700 overflow-hidden">
<h3 className="text-xs font-normal text-foreground mb-2">Preview (Green = Crop, Orange = Rotation)</h3> <h3 className="text-xs font-normal text-white mb-2">Preview (Green = Crop, Orange = Rotation)</h3>
<div className="flex-1 flex items-center justify-center bg-black/70 border border-outline-variant overflow-auto"> <div className="flex-1 flex items-center justify-center bg-black/70 rounded border border-slate-600 overflow-auto">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="cursor-move" className="rounded cursor-move"
onMouseDown={handleCanvasMouseDown} onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove} onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp} onMouseUp={handleCanvasMouseUp}
@@ -376,8 +380,8 @@ export function DebugRotationPanel({
</div> </div>
{/* Log - single line */} {/* Log - single line */}
<div className="bg-surface-container p-2 border border-outline-variant flex-shrink-0"> <div className="bg-slate-800 p-2 rounded border border-slate-700 flex-shrink-0">
<div className="bg-black text-success p-2 font-mono text-xs"> <div className="bg-black text-green-400 p-2 rounded font-mono text-xs">
{log} {log}
</div> </div>
</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-outline/30 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

@@ -61,14 +61,14 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
return ( return (
<div data-testid="identity-check-overlay" className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/90 animate-in fade-in duration-500"> <div data-testid="identity-check-overlay" className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/90 animate-in fade-in duration-500">
<div className="bg-surface border border-outline/30 ] p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in zoom-in-95 duration-300 relative overflow-hidden"> <div className="bg-surface border border-slate-800 rounded-[2.5rem] p-4 md:p-6 max-w-sm w-full shadow-2xl space-y-3 md:space-y-4 animate-in zoom-in-95 duration-300 relative overflow-hidden">
<div className="text-center space-y-3"> <div className="text-center space-y-3">
<div className="w-20 h-20 bg-primary/10 text-primary ] flex items-center justify-center mx-auto mb-6 border border-primary/20 shadow-xl shadow-primary/5"> <div className="w-20 h-20 bg-primary/10 text-primary rounded-[2rem] flex items-center justify-center mx-auto mb-6 border border-primary/20 shadow-xl shadow-primary/5">
<Shield size={40} /> <Shield size={40} className="italic" />
</div> </div>
<h2 className="text-3xl font-normal text-foreground tracking-tight">Protocol Access</h2> <h2 className="text-3xl font-normal text-white tracking-tight">Protocol Access</h2>
<p className="text-secondary text-xs font-normal">Select operator profile to initialize</p> <p className="text-muted text-xs font-normal">Select operator profile to initialize</p>
</div> </div>
<div data-testid="user-list" className="grid gap-3.5"> <div data-testid="user-list" className="grid gap-3.5">
@@ -79,10 +79,10 @@ 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-surface-container/40 hover:bg-surface-container border border-outline/30/50 hover:border-primary/40 p-5 ] 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 bg-surface border border-outline/30 flex items-center justify-center text-secondary 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">
{user.role === 'admin' ? <Shield size={18} /> : <User size={18} />} {user.role === 'admin' ? <Shield size={18} /> : <User size={18} />}
</div> </div>
<div> <div>
@@ -90,7 +90,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<p className="text-xs text-secondary font-normal mt-0.5">{user.role}</p> <p className="text-xs text-secondary font-normal mt-0.5">{user.role}</p>
</div> </div>
</div> </div>
<div className="p-1.5 bg-surface/70 text-secondary group-hover:text-primary group-hover:bg-primary/10 transition-all"> <div className="p-1.5 rounded-full bg-surface/70 text-muted group-hover:text-primary group-hover:bg-primary/10 transition-all">
<ChevronRight size={14} /> <ChevronRight size={14} />
</div> </div>
</button> </button>
@@ -98,7 +98,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<div className="pt-4"> <div className="pt-4">
<button <button
onClick={() => setIsEnterprise(true)} onClick={() => setIsEnterprise(true)}
className="w-full flex items-center justify-center gap-3 py-5 ] border border-dashed border-outline/30 text-secondary hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-all font-normal text-xs" className="w-full flex items-center justify-center gap-3 py-5 rounded-[1.5rem] border border-dashed border-slate-800 text-secondary hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-all font-normal text-xs"
> >
<Lock size={14} /> <Lock size={14} />
Enterprise Directory Enterprise Directory
@@ -108,8 +108,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
) : isEnterprise ? ( ) : isEnterprise ? (
<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 className="flex justify-between items-center px-1"> <div className="flex justify-between items-center px-1">
<p className="text-xs font-normal text-secondary">LDAP Authentication</p> <p className="text-xs font-normal text-secondary italic">LDAP Authentication</p>
<button <button
data-testid="local-login-tab" data-testid="local-login-tab"
onClick={() => setIsEnterprise(false)} onClick={() => setIsEnterprise(false)}
@@ -127,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-outline/30/80 focus:border-primary/50 focus:bg-background ] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-secondary 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>
@@ -141,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-outline/30/80 focus:border-primary/50 focus:bg-background ] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-secondary 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>
@@ -150,26 +149,26 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<button <button
data-testid="login-submit" data-testid="login-submit"
onClick={handleLogin} onClick={handleLogin}
className="w-full bg-primary text-foreground font-normal py-5 ] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20" className="w-full bg-primary text-white font-normal py-5 rounded-[1.5rem] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20"
> >
Initialize Session Initialize Session
</button> </button>
</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 ] border border-outline/30 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 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} />
</div> </div>
<div> <div>
<p className="text-xs font-normal text-secondary">Active Profile</p> <p className="text-xs font-normal text-secondary">Active Profile</p>
<p className="text-foreground font-normal tracking-tight">{selectedUserForLogin.username}</p> <p className="text-white font-normal tracking-tight">{selectedUserForLogin.username}</p>
</div> </div>
</div> </div>
<button <button
onClick={() => setSelectedUserForLogin(null)} onClick={() => setSelectedUserForLogin(null)}
className="p-2 hover:bg-surface-container transition-all text-secondary hover:text-error" 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} />
@@ -185,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-outline/30/80 focus:border-primary/50 focus:bg-background ] py-4.5 pl-14 pr-5 text-sm text-secondary focus:outline-none transition-all placeholder:text-secondary 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>
@@ -194,7 +193,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<button <button
data-testid="local-login-submit" data-testid="local-login-submit"
onClick={handleLogin} onClick={handleLogin}
className="w-full bg-primary text-foreground font-normal py-5 ] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20" className="w-full bg-primary text-white font-normal py-5 rounded-[1.5rem] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20"
> >
Unlock Account Unlock Account
</button> </button>
@@ -203,7 +202,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div> </div>
{users.length === 0 && ( {users.length === 0 && (
<div className="text-center p-8 text-secondary animate-pulse font-normal text-xs"> <div className="text-center p-8 text-muted animate-pulse font-normal text-xs">
Synchronizing User Tokens... Synchronizing User Tokens...
</div> </div>
)} )}

View File

@@ -200,13 +200,13 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
return ( return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-background border border-outline/30 shadow-2xl max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col"> <div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col">
{/* Header */} {/* Header */}
<div className="p-4 border-b border-outline/30 flex items-center justify-between bg-surface-container"> <div className="p-4 border-b border-slate-700 flex items-center justify-between bg-slate-800">
<h2 className="text-xl font-normal text-foreground">Rotate Image</h2> <h2 className="text-xl font-normal text-white">Rotate Image</h2>
<button <button
onClick={onCancel} onClick={onCancel}
className="p-2 hover:bg-surface-bright text-secondary hover:text-foreground transition" className="p-2 hover:bg-slate-700 rounded text-gray-400 hover:text-white transition"
> >
<X size={20} /> <X size={20} />
</button> </button>
@@ -214,11 +214,11 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
<div className="flex gap-4 p-4 flex-1 overflow-hidden"> <div className="flex gap-4 p-4 flex-1 overflow-hidden">
{/* Left: Controls */} {/* Left: Controls */}
<div className="w-56 bg-surface-container p-4 space-y-4 flex-shrink-0 overflow-y-auto"> <div className="w-56 bg-slate-800 p-4 rounded space-y-4 flex-shrink-0 overflow-y-auto">
{/* Rotation */} {/* Rotation */}
<div> <div>
<label className="block text-sm font-normal text-foreground mb-2"> <label className="block text-sm font-normal text-white mb-2">
Rotation: <span className="text-warning">{rotation}°</span> Rotation: <span className="text-yellow-400">{rotation}°</span>
</label> </label>
<input <input
type="range" type="range"
@@ -232,8 +232,8 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
{/* Zoom */} {/* Zoom */}
<div> <div>
<label className="block text-sm font-normal text-foreground mb-2"> <label className="block text-sm font-normal text-white mb-2">
Zoom: <span className="text-warning">{zoom.toFixed(2)}x</span> Zoom: <span className="text-yellow-400">{zoom.toFixed(2)}x</span>
</label> </label>
<input <input
type="range" type="range"
@@ -246,10 +246,30 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
/> />
</div> </div>
{/* Aspect Ratio - DISABLED (cropping not implemented) */}
{/* <div>
<label className="block text-sm font-normal text-white mb-2">Aspect Ratio</label>
<div className="space-y-2">
{aspectRatios.map((ratio) => (
<button
key={ratio.label}
onClick={() => setAspectRatio(ratio.value)}
className={`w-full py-2 px-3 rounded text-sm font-normal transition ${
aspectRatio === ratio.value
? 'bg-yellow-600 text-white'
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
}`}
>
{ratio.label}
</button>
))}
</div>
</div> */}
{/* Reset */} {/* Reset */}
<button <button
onClick={handleReset} onClick={handleReset}
className="w-full py-2 px-3 text-sm font-normal bg-surface-bright text-foreground hover:bg-surface-container transition flex items-center justify-center gap-2" className="w-full py-2 px-3 rounded text-sm font-normal bg-slate-700 text-gray-300 hover:bg-slate-600 transition flex items-center justify-center gap-2"
> >
<RotateCw size={16} /> <RotateCw size={16} />
Reset Reset
@@ -257,9 +277,9 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
</div> </div>
{/* Center: Canvas */} {/* Center: Canvas */}
<div className="flex-1 flex flex-col bg-surface-container p-4 border border-outline/30 overflow-hidden"> <div className="flex-1 flex flex-col bg-slate-800 p-4 rounded border border-slate-700 overflow-hidden">
<h3 className="text-sm font-normal text-foreground mb-2">Image Preview</h3> <h3 className="text-sm font-normal text-white mb-2">Image Preview</h3>
<div className="flex-1 flex items-center justify-center bg-black/50 border border-outline/50 overflow-hidden"> <div className="flex-1 flex items-center justify-center bg-black/50 rounded border border-slate-600 overflow-hidden">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
onWheel={handleWheel} onWheel={handleWheel}
@@ -269,13 +289,13 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
className="max-w-full max-h-full cursor-grab active:cursor-grabbing touch-none" className="max-w-full max-h-full cursor-grab active:cursor-grabbing touch-none"
/> />
</div> </div>
<p className="text-xs text-secondary mt-2">Scroll to zoom | Drag to pan | Rotate with slider</p> <p className="text-xs text-gray-400 mt-2">Scroll to zoom | Drag to pan | Rotate with slider</p>
</div> </div>
</div> </div>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t border-outline/30 bg-surface-container flex items-center justify-between"> <div className="p-4 border-t border-slate-700 bg-slate-800 flex items-center justify-between">
<label className="flex items-center gap-2 text-sm font-normal text-foreground"> <label className="flex items-center gap-2 text-sm font-normal text-white">
<input <input
type="checkbox" type="checkbox"
checked={useImage} checked={useImage}
@@ -287,13 +307,13 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-sm font-normal bg-surface-bright text-foreground hover:bg-surface-container transition" className="px-4 py-2 rounded text-sm font-normal bg-slate-700 text-gray-300 hover:bg-slate-600 transition"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleConfirm} onClick={handleConfirm}
className="px-4 py-2 text-sm font-normal bg-warning text-primary-foreground hover:bg-warning/80 transition" className="px-4 py-2 rounded text-sm font-normal bg-yellow-600 text-white hover:bg-yellow-700 transition"
> >
Confirm Confirm
</button> </button>

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-normal">No results found</p> <p className="font-normal">No results found</p>
</div> </div>
@@ -65,17 +65,17 @@ export default function InventoryTable({
{categories.map(cat => { {categories.map(cat => {
const categoryItems = items.filter(i => i.category === cat); const categoryItems = items.filter(i => i.category === cat);
return ( return (
<div key={cat} className="bg-surface/50 border border-outline/20 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">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary transition-colors"> <div className="w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center text-primary transition-colors">
<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-surface-container text-secondary 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,12 +98,12 @@ 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-outline/20 mb-4 mx-2" /> <div className="h-px bg-slate-800/50 mb-4 mx-2" />
{categoryItems.map(item => ( {categoryItems.map(item => (
<div <div
key={item.id} key={item.id}
className="bg-background/40 border border-outline/20 p-4 flex items-center justify-between hover:border-primary/40 transition-all active:scale-[0.98]" className="bg-background/40 border border-slate-800/50 p-4 rounded-2xl flex items-center justify-between hover:border-primary/40 transition-all active:scale-[0.98]"
> >
<div <div
onClick={() => handleItemClick(item)} onClick={() => handleItemClick(item)}
@@ -115,7 +115,7 @@ export default function InventoryTable({
e.stopPropagation(); e.stopPropagation();
setSelectedPhotoItem(item); setSelectedPhotoItem(item);
}} }}
className="w-12 h-12 shrink-0 border border-outline/30 overflow-hidden cursor-pointer hover:border-primary transition-colors active:scale-95" className="w-12 h-12 rounded-xl shrink-0 border-2 border-slate-300 overflow-hidden cursor-pointer hover:border-primary transition-colors active:scale-95"
> >
<img <img
src={buildPhotoUrl(backendUrl, item.photo_path || item.image_url || '')} src={buildPhotoUrl(backendUrl, item.photo_path || item.image_url || '')}
@@ -125,17 +125,17 @@ export default function InventoryTable({
/> />
</div> </div>
) : ( ) : (
<div className="w-12 h-12 bg-success/10 flex items-center justify-center text-success shrink-0"> <div className="w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center text-green-500 shrink-0">
<Package size={14} /> <Package size={14} />
</div> </div>
)} )}
<div className="truncate"> <div className="truncate">
<h4 className="card-title truncate">{item.name}</h4> <h4 className="card-title truncate">{item.name}</h4>
{item.photo_path || item.image_url ? ( {item.photo_path || item.image_url ? (
<p className="card-subtitle mt-0 opacity-80 text-xs">Tap photo for details</p> <p className="card-subtitle mt-0 lowercase opacity-80 text-xs">Tap photo for details</p>
) : ( ) : (
<> <>
<p className="card-subtitle mt-0 opacity-80 truncate">{item.specs}</p> <p className="card-subtitle mt-0 lowercase opacity-80 truncate">{item.specs}</p>
<p className="card-subtitle mt-0 text-xs">No photo</p> <p className="card-subtitle mt-0 text-xs">No photo</p>
</> </>
)} )}
@@ -144,11 +144,11 @@ export default function InventoryTable({
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<span className={cn( <span className={cn(
"text-lg font-normal", "text-lg font-normal",
item.quantity <= item.min_quantity ? "text-warning" : "text-primary" item.quantity <= item.min_quantity ? "text-amber-500" : "text-primary"
)}> )}>
{item.quantity} {item.quantity}
</span> </span>
<p className="text-sm text-secondary font-normal tracking-tight">Stock</p> <p className="text-sm text-muted font-normal tracking-tight">Stock</p>
</div> </div>
</div> </div>
))} ))}

View File

@@ -42,20 +42,20 @@ 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-outline/30 ] 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-warning" /> <AlertTriangle size={24} className="text-amber-500" />
<div> <div>
<h3 className="text-xl font-normal text-foreground">Part Number Already Exists</h3> <h3 className="text-xl font-normal text-white">Part Number Already Exists</h3>
<p className="text-xs text-secondary mt-1">Compare the versions below</p> <p className="text-xs text-secondary mt-1">Compare the versions below</p>
</div> </div>
</div> </div>
{/* Comparison Table */} {/* Comparison Table */}
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
<div className="grid grid-cols-3 gap-4 mb-4 pb-4 border-b border-outline/30"> <div className="grid grid-cols-3 gap-4 mb-4 pb-4 border-b border-slate-800">
<div className="text-xs font-normal text-secondary">Field</div> <div className="text-xs font-normal text-muted">Field</div>
<div className="text-xs font-normal text-secondary">Current (ID: {existingItem?.id})</div> <div className="text-xs font-normal text-secondary">Current (ID: {existingItem?.id})</div>
<div className="text-xs font-normal text-primary">New (Import)</div> <div className="text-xs font-normal text-primary">New (Import)</div>
</div> </div>
@@ -68,7 +68,9 @@ export default function ItemComparisonModal({
return ( return (
<div <div
key={field.key} key={field.key}
className={`grid grid-cols-3 gap-4 p-3 ${ different ? 'bg-warning/10 border border-warning/20' : 'bg-surface-container/30' }`} 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'
}`}
> >
<div className="text-sm font-normal text-secondary">{field.label}</div> <div className="text-sm font-normal text-secondary">{field.label}</div>
<div className={`text-sm font-mono ${different ? 'text-amber-200' : 'text-secondary'}`}> <div className={`text-sm font-mono ${different ? 'text-amber-200' : 'text-secondary'}`}>
@@ -83,8 +85,8 @@ export default function ItemComparisonModal({
</div> </div>
{!hasChanges && ( {!hasChanges && (
<div className="p-4 bg-surface-container/50 mb-6 border border-outline-variant"> <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>
)} )}
@@ -94,7 +96,7 @@ export default function ItemComparisonModal({
onClick={onSkip} onClick={onSkip}
disabled={loading} disabled={loading}
aria-label="Skip this item comparison" aria-label="Skip this item comparison"
className="flex-1 flex items-center justify-center gap-2 py-4 bg-surface-container hover:bg-surface-bright text-secondary text-sm font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed border border-outline-variant focus:ring-2 focus:ring-primary focus:outline-none" className="flex-1 flex items-center justify-center gap-2 py-4 bg-slate-800 hover:bg-slate-700 text-secondary rounded-2xl text-sm font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed border border-slate-700 focus:ring-2 focus:ring-blue-500 focus:outline-none"
> >
<SkipForward size={16} /> Skip <SkipForward size={16} /> Skip
</button> </button>
@@ -103,7 +105,7 @@ export default function ItemComparisonModal({
onClick={onUpdate} onClick={onUpdate}
disabled={loading} disabled={loading}
aria-label="Update item with new data" aria-label="Update item with new data"
className="flex-1 flex items-center justify-center gap-2 py-4 bg-primary hover:bg-primary text-foreground text-sm font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-primary/20 focus:ring-2 focus:ring-primary focus:outline-none" className="flex-1 flex items-center justify-center gap-2 py-4 bg-primary hover:bg-blue-600 text-white rounded-2xl text-sm font-normal cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-primary/20 focus:ring-2 focus:ring-blue-400 focus:outline-none"
> >
{loading ? ( {loading ? (
<> <>

View File

@@ -69,15 +69,15 @@ export default function ItemDetailModal({
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-surface border border-outline/30 max-w-2xl w-full max-h-[90vh] overflow-y-auto"> <div className="bg-surface border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */} {/* Header */}
<div className="sticky top-0 bg-surface border-b border-outline/30/50 p-4 md:p-6 flex items-center justify-between"> <div className="sticky top-0 bg-surface border-b border-slate-800/50 p-4 md:p-6 flex items-center justify-between">
<h2 className="text-xl md:text-2xl font-normal text-foreground truncate">{item.name}</h2> <h2 className="text-xl md:text-2xl font-normal text-white truncate">{item.name}</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentPhoto && ( {currentPhoto && (
<button <button
onClick={() => setShowDebugPanel(true)} onClick={() => setShowDebugPanel(true)}
className="p-2 hover:bg-yellow-900/50 text-warning hover:text-yellow-300 transition-colors" className="p-2 hover:bg-yellow-900/50 rounded-full text-yellow-400 hover:text-yellow-300 transition-colors"
title="Debug rotation & crop" title="Debug rotation & crop"
aria-label="Debug panel" aria-label="Debug panel"
> >
@@ -86,7 +86,7 @@ export default function ItemDetailModal({
)} )}
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-surface-container text-secondary hover:text-foreground transition-colors" className="p-2 hover:bg-slate-800 rounded-full text-muted hover:text-white transition-colors"
aria-label="Close modal" aria-label="Close modal"
> >
<X size={20} /> <X size={20} />
@@ -99,32 +99,32 @@ export default function ItemDetailModal({
{/* Item Details */} {/* Item Details */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<p className="text-xs text-secondary mb-1">Category</p> <p className="text-xs text-muted mb-1">Category</p>
<p className="text-sm text-foreground">{item.category}</p> <p className="text-sm text-white">{item.category}</p>
</div> </div>
<div> <div>
<p className="text-xs text-secondary mb-1">Type</p> <p className="text-xs text-muted mb-1">Type</p>
<p className="text-sm text-foreground">{item.type || 'N/A'}</p> <p className="text-sm text-white">{item.type || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-xs text-secondary mb-1">Quantity</p> <p className="text-xs text-muted mb-1">Quantity</p>
<p className="text-sm text-foreground">{item.quantity}</p> <p className="text-sm text-white">{item.quantity}</p>
</div> </div>
<div> <div>
<p className="text-xs text-secondary mb-1">Part Number</p> <p className="text-xs text-muted mb-1">Part Number</p>
<p className="text-sm text-foreground">{item.part_number || 'N/A'}</p> <p className="text-sm text-white">{item.part_number || 'N/A'}</p>
</div> </div>
{item.barcode && ( {item.barcode && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs text-secondary mb-1">Barcode</p> <p className="text-xs text-muted mb-1">Barcode</p>
<p className="text-sm text-foreground font-mono">{item.barcode}</p> <p className="text-sm text-white font-mono">{item.barcode}</p>
</div> </div>
)} )}
</div> </div>
{/* Photo Section */} {/* Photo Section */}
<div className="border-t border-outline/30/50 pt-6"> <div className="border-t border-slate-800/50 pt-6">
<h3 className="text-lg font-normal text-foreground mb-4 flex items-center gap-2"> <h3 className="text-lg font-normal text-white mb-4 flex items-center gap-2">
<Camera size={18} className="text-primary" /> <Camera size={18} className="text-primary" />
Photo Photo
</h3> </h3>
@@ -133,7 +133,7 @@ export default function ItemDetailModal({
<> <>
{currentPhoto ? ( {currentPhoto ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="relative bg-surface-container/50 overflow-hidden border border-outline/30/50 aspect-video max-h-96"> <div className="relative bg-slate-900/50 rounded-2xl overflow-hidden border border-slate-800/50 aspect-video max-h-96">
<img <img
src={buildPhotoUrl(backendUrl, currentPhoto.full_url)} src={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
alt={item.name} alt={item.name}
@@ -143,25 +143,25 @@ export default function ItemDetailModal({
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setShowPhotoUpload(true)} onClick={() => setShowPhotoUpload(true)}
className="flex-1 px-4 py-2 bg-primary/20 border border-primary/50 text-primary hover:bg-primary/30 transition-colors font-normal text-sm" className="flex-1 px-4 py-2 bg-primary/20 border border-primary/50 text-primary rounded-xl hover:bg-primary/30 transition-colors font-normal text-sm"
> >
Replace Photo Replace Photo
</button> </button>
<button <button
onClick={handleDeletePhoto} onClick={handleDeletePhoto}
disabled={isDeleting} disabled={isDeleting}
className="px-4 py-2 bg-error/20 border border-error/50 text-rose-400 hover:bg-error/30 disabled:opacity-50 transition-colors font-normal text-sm" className="px-4 py-2 bg-rose-500/20 border border-rose-500/50 text-rose-400 rounded-xl hover:bg-rose-500/30 disabled:opacity-50 transition-colors font-normal text-sm"
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</div> </div>
</div> </div>
) : ( ) : (
<div className="bg-surface-container/30 border border-outline/30/50 p-8 text-center"> <div className="bg-slate-900/30 border border-slate-800/50 rounded-2xl p-8 text-center">
<p className="text-secondary text-sm mb-4">No photo uploaded</p> <p className="text-muted text-sm mb-4">No photo uploaded</p>
<button <button
onClick={() => setShowPhotoUpload(true)} onClick={() => setShowPhotoUpload(true)}
className="px-4 py-2 bg-primary/20 border border-primary/50 text-primary hover:bg-primary/30 transition-colors font-normal text-sm" className="px-4 py-2 bg-primary/20 border border-primary/50 text-primary rounded-xl hover:bg-primary/30 transition-colors font-normal text-sm"
> >
Upload Photo Upload Photo
</button> </button>
@@ -169,12 +169,12 @@ export default function ItemDetailModal({
)} )}
</> </>
) : ( ) : (
<div ref={photoUploadRef} className="bg-surface-container/30 border border-outline/30/50 p-4 md:p-6"> <div ref={photoUploadRef} className="bg-slate-900/30 border border-slate-800/50 rounded-2xl p-4 md:p-6">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h4 className="font-normal text-foreground">Upload New Photo</h4> <h4 className="font-normal text-white">Upload New Photo</h4>
<button <button
onClick={() => setShowPhotoUpload(false)} onClick={() => setShowPhotoUpload(false)}
className="p-1 hover:bg-surface-container text-secondary hover:text-foreground transition-colors" className="p-1 hover:bg-slate-800 rounded-lg text-muted hover:text-white transition-colors"
aria-label="Cancel upload" aria-label="Cancel upload"
> >
<X size={16} /> <X size={16} />

View File

@@ -115,7 +115,7 @@ export default function ItemPhotoUpload({
<button <button
onClick={triggerFileInput} onClick={triggerFileInput}
disabled={isLoading} disabled={isLoading}
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-primary text-foreground font-normal text-base transition-colors hover:bg-primary/90 disabled:opacity-60 disabled:cursor-not-allowed" className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-primary text-white rounded-lg font-normal text-base transition-colors hover:bg-primary/90 disabled:opacity-60 disabled:cursor-not-allowed"
aria-label="Upload photo" aria-label="Upload photo"
> >
{isLoading ? ( {isLoading ? (
@@ -130,7 +130,7 @@ export default function ItemPhotoUpload({
<button <button
onClick={triggerCameraInput} onClick={triggerCameraInput}
disabled={isLoading} disabled={isLoading}
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-surface-bright text-foreground font-normal text-base transition-colors hover:bg-outline-variant disabled:opacity-60 disabled:cursor-not-allowed" className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-slate-700 text-white rounded-lg font-normal text-base transition-colors hover:bg-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
aria-label="Capture photo with camera" aria-label="Capture photo with camera"
> >
{isLoading ? ( {isLoading ? (
@@ -144,11 +144,11 @@ export default function ItemPhotoUpload({
{/* Status messages */} {/* Status messages */}
{isLoading && ( {isLoading && (
<div className="text-sm text-secondary">Uploading...</div> <div className="text-sm text-slate-400">Uploading...</div>
)} )}
{localError && ( {localError && (
<div className="text-sm text-error">{localError}</div> <div className="text-sm text-rose-500">{localError}</div>
)} )}
</div> </div>
); );

View File

@@ -14,15 +14,15 @@ 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>
<p className="text-xs text-secondary">Live transaction log from cloud</p> <p className="text-xs text-muted">Live transaction log from cloud</p>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="p-3 bg-surface 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,13 +36,15 @@ 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-outline/30 p-4 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 ${ log.action.includes('CHECK_IN') ? "text-success" : (log.action.includes('TRASH') ? "text-error" : "text-warning") }`}> <p className={`text-xs font-normal ${
log.action.includes('CHECK_IN') ? "text-green-500" : (log.action.includes('TRASH') ? "text-rose-500" : "text-amber-500")
}`}>
{log.action} {log.action}
</p> </p>
<span className="text-xs font-normal text-secondary">by</span> <span className="text-xs font-normal text-muted">by</span>
<span className="text-xs font-normal text-secondary">{log.username || 'System'}</span> <span className="text-xs font-normal text-secondary">{log.username || 'System'}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -52,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-surface-container text-secondary px-2 py-0.5 border border-outline-variant 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>
)} )}
@@ -64,7 +66,9 @@ export default function LogsOverlay({ show, onClose, logs, inventory }: LogsOver
)} )}
</div> </div>
<div className="text-right"> <div className="text-right">
<p className={`text-lg font-normal ${ log.quantity_change > 0 ? "text-success" : "text-rose-400" }`}> <p className={`text-lg font-normal ${
log.quantity_change > 0 ? "text-green-400" : "text-rose-400"
}`}>
{log.quantity_change > 0 ? '+' : ''}{log.quantity_change} {log.quantity_change > 0 ? '+' : ''}{log.quantity_change}
</p> </p>
</div> </div>

View File

@@ -25,17 +25,17 @@ 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 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">Securing Audit Stream...</p> <p className="text-xs font-normal tracking-widest italic">Securing Audit Stream...</p>
</div> </div>
); );
} }
if (logs.length === 0) { if (logs.length === 0) {
return ( return (
<div className="bg-surface/20 border border-outline/20 border-dashed ] py-20 flex flex-col items-center justify-center text-center gap-3 md:gap-4"> <div className="bg-surface/20 border border-slate-800/50 border-dashed rounded-[2.5rem] py-20 flex flex-col items-center justify-center text-center gap-3 md:gap-4">
<div className="w-16 h-16 bg-surface flex items-center justify-center text-secondary border border-outline/30"> <div className="w-16 h-16 bg-surface rounded-2xl flex items-center justify-center text-slate-700 border border-slate-800">
<ArrowDownCircle size={32} /> <ArrowDownCircle size={32} />
</div> </div>
<div> <div>
@@ -53,17 +53,17 @@ 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-outline/30 p-3 px-4 flex items-center justify-between gap-4 hover:bg-surface-container/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 */}
<div className={cn( <div className={cn(
"text-[10px] font-normal px-3 py-1.5 border min-w-[85px] text-center tracking-tight", "text-[10px] font-normal px-3 py-1.5 rounded-lg border min-w-[85px] text-center tracking-tight",
log.action.includes('CHECK_IN') ? "bg-success/10 text-success border-success/20" : log.action.includes('CHECK_IN') ? "bg-green-500/10 text-green-500 border-green-500/20" :
(log.action.includes('TRASH') ? "bg-error/10 text-error border-error/20" : (log.action.includes('TRASH') ? "bg-rose-500/10 text-rose-500 border-rose-500/20" :
(log.action.includes('DB') ? "bg-primary/10 text-primary border-primary/20" : (log.action.includes('DB') ? "bg-sky-500/10 text-sky-400 border-sky-500/20" :
(log.action.includes('DELETE') ? "bg-error/10 text-error border-error/30" : (log.action.includes('DELETE') ? "bg-red-500/10 text-red-500 border-red-500/30" :
(log.action.includes('CREATE') ? "bg-primary/10 text-primary border-primary/20" : "bg-primary/10 text-primary border-primary/20")))) (log.action.includes('CREATE') ? "bg-indigo-500/10 text-indigo-400 border-indigo-500/20" : "bg-primary/10 text-primary border-primary/20"))))
)}> )}>
{log.action.replace('_', ' ')} {log.action.replace('_', ' ')}
</div> </div>
@@ -74,7 +74,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
</h3> </h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="card-subtitle mt-0 shrink-0">{log.username || 'System'}</span> <span className="card-subtitle mt-0 shrink-0">{log.username || 'System'}</span>
<span className="w-1 h-1 bg-outline/30 shrink-0 mt-1" /> <span className="w-1 h-1 rounded-full bg-slate-800 shrink-0 mt-1" />
<span className="card-subtitle mt-0 lowercase opacity-80 truncate"> <span className="card-subtitle mt-0 lowercase opacity-80 truncate">
{new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · {new Date(log.timestamp).toLocaleDateString()} {new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · {new Date(log.timestamp).toLocaleDateString()}
</span> </span>
@@ -85,7 +85,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
<div className="shrink-0 flex items-center gap-3 z-10"> <div className="shrink-0 flex items-center gap-3 z-10">
<div className={cn( <div className={cn(
"text-lg font-normal tabular-nums min-w-[35px] text-right", "text-lg font-normal tabular-nums min-w-[35px] text-right",
(log.quantity_change || 0) > 0 ? "text-success" : ((log.quantity_change || 0) < 0 ? "text-error" : "text-primary/50") (log.quantity_change || 0) > 0 ? "text-green-500" : ((log.quantity_change || 0) < 0 ? "text-rose-500" : "text-primary/50")
)}> )}>
{log.quantity_change ? (log.quantity_change > 0 ? `+${log.quantity_change}` : log.quantity_change) : (log.action.includes('DB') ? 'SYS' : '±')} {log.quantity_change ? (log.quantity_change > 0 ? `+${log.quantity_change}` : log.quantity_change) : (log.action.includes('DB') ? 'SYS' : '±')}
</div> </div>
@@ -97,35 +97,35 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
{/* Selected Log Modal */} {/* Selected Log Modal */}
{selectedLog && ( {selectedLog && (
<div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/90 animate-in fade-in duration-300"> <div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/90 animate-in fade-in duration-300">
<div className="bg-surface border-t sm:border border-outline/30 ] sm:] p-4 md:p-6 max-w-lg w-full shadow-2xl space-y-3 md:space-y-4 animate-in slide-in-from-bottom-10 duration-300 overflow-hidden"> <div className="bg-surface border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-[3rem] p-4 md:p-6 max-w-lg w-full shadow-2xl space-y-3 md:space-y-4 animate-in slide-in-from-bottom-10 duration-300 overflow-hidden">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="space-y-1 pr-4"> <div className="space-y-1 pr-4">
<div className={cn( <div className={cn(
"text-xs font-normal px-4 py-1.5 border inline-block ", "text-xs font-normal px-4 py-1.5 rounded-full border inline-block tracking-widest",
selectedLog.action.includes('CHECK_IN') ? "bg-success/10 text-success border-success/30" : selectedLog.action.includes('CHECK_IN') ? "bg-green-500/10 text-green-500 border-green-500/30" :
(selectedLog.action.includes('TRASH') ? "bg-error/10 text-error border-error/30" : "bg-primary/10 text-primary border-primary/30") (selectedLog.action.includes('TRASH') ? "bg-rose-500/10 text-rose-500 border-rose-500/30" : "bg-primary/10 text-primary border-primary/30")
)}> )}>
{selectedLog.action} {selectedLog.action}
</div> </div>
<h2 className="text-2xl font-normal text-foreground tracking-tight pt-2 leading-tight"> <h2 className="text-2xl font-normal text-white tracking-tight pt-2 leading-tight">
{selectedLog.resolved_name} {selectedLog.resolved_name}
</h2> </h2>
</div> </div>
<button onClick={() => setSelectedLog(null)} className="p-3 bg-surface-container hover:bg-surface-bright text-secondary transition-colors border border-outline/30 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>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-1 bg-background/50 p-4 border border-outline/20"> <div className="space-y-1 bg-background/50 p-4 rounded-2xl border border-slate-800/50">
<p className="text-[11px] font-normal text-secondary">Protocol Operator</p> <p className="text-[11px] font-normal text-muted tracking-widest">Protocol Operator</p>
<p className="text-sm font-normal text-secondary">{selectedLog.username || 'Automated Process'}</p> <p className="text-sm font-normal text-secondary">{selectedLog.username || 'Automated Process'}</p>
</div> </div>
<div className="space-y-1 bg-background/50 p-4 border border-outline/20"> <div className="space-y-1 bg-background/50 p-4 rounded-2xl border border-slate-800/50">
<p className="text-[11px] font-normal text-secondary">Quantity Delta</p> <p className="text-[11px] font-normal text-muted tracking-widest">Quantity Delta</p>
<p className={cn( <p className={cn(
"text-xl font-normal tabular-nums", "text-xl font-normal tabular-nums",
(selectedLog.quantity_change || 0) > 0 ? "text-success" : "text-error" (selectedLog.quantity_change || 0) > 0 ? "text-green-500" : "text-rose-500"
)}> )}>
{selectedLog.quantity_change {selectedLog.quantity_change
? `${selectedLog.quantity_change > 0 ? '+' : ''}${selectedLog.quantity_change}` ? `${selectedLog.quantity_change > 0 ? '+' : ''}${selectedLog.quantity_change}`
@@ -136,8 +136,8 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
<div className="space-y-5"> <div className="space-y-5">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[11px] font-normal text-secondary ml-1">Universal Timestamp</p> <p className="text-[11px] font-normal text-muted tracking-widest ml-1">Universal Timestamp</p>
<p className="text-xs font-normal text-secondary bg-background/30 p-4 border border-outline/20 tabular-nums"> <p className="text-xs font-normal text-secondary bg-background/30 p-4 rounded-2xl border border-slate-800/30 tabular-nums">
{new Date(selectedLog.timestamp).toLocaleString(undefined, { dateStyle: 'full', timeStyle: 'medium' })} {new Date(selectedLog.timestamp).toLocaleString(undefined, { dateStyle: 'full', timeStyle: 'medium' })}
</p> </p>
</div> </div>
@@ -148,15 +148,15 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
return ( return (
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-px flex-1 bg-outline/20" /> <div className="h-px flex-1 bg-slate-800/50" />
<p className="text-[8px] font-normal text-secondary tracking-[0.2em]">Snapshot Record</p> <p className="text-[8px] font-normal text-slate-700 tracking-[0.2em]">Snapshot Record</p>
<div className="h-px flex-1 bg-outline/20" /> <div className="h-px flex-1 bg-slate-800/50" />
</div> </div>
<div className="grid grid-cols-2 gap-2.5"> <div className="grid grid-cols-2 gap-2.5">
{Object.entries(snap).map(([key, val]) => ( {Object.entries(snap).map(([key, val]) => (
(val && key !== 'image_url' && key !== 'id') ? ( (val && key !== 'image_url' && key !== 'id') ? (
<div key={key} className="bg-background/20 p-3 border border-outline/20"> <div key={key} className="bg-background/20 p-3 rounded-xl border border-slate-800/20">
<p className="text-[10px] font-normal text-secondary mb-1 tracking-tight opacity-70">{key.replace('_', ' ')}</p> <p className="text-[10px] font-normal text-muted mb-1 tracking-tight opacity-70">{key.replace('_', ' ')}</p>
<p className="text-xs font-normal text-secondary truncate" title={String(val)}>{String(val)}</p> <p className="text-xs font-normal text-secondary truncate" title={String(val)}>{String(val)}</p>
</div> </div>
) : null ) : null
@@ -169,8 +169,8 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
{selectedLog.details && ( {selectedLog.details && (
<div className="space-y-1.5"> <div className="space-y-1.5">
<p className="text-[11px] font-normal text-secondary ml-1">Intervention Details</p> <p className="text-[11px] font-normal text-muted tracking-widest ml-1">Intervention Details</p>
<div className="bg-primary/5 text-primary/80 p-5 border border-primary/10 text-sm font-normal leading-relaxed shadow-inner"> <div className="bg-primary/5 text-primary/80 p-5 rounded-3xl border border-primary/10 text-sm font-normal leading-relaxed italic shadow-inner">
"{selectedLog.details}" "{selectedLog.details}"
</div> </div>
</div> </div>
@@ -179,7 +179,7 @@ export default function LogsTable({ logs, loading }: LogsTableProps) {
<button <button
onClick={() => setSelectedLog(null)} onClick={() => setSelectedLog(null)}
className="w-full bg-surface-container hover:bg-surface-bright text-foreground font-normal py-4.5 transition-all active:scale-95 border border-outline/30 shadow-xl" className="w-full bg-slate-800 hover:bg-slate-700 text-white font-normal py-4.5 rounded-2xl transition-all active:scale-95 border border-slate-700 shadow-xl"
> >
Close Audit Insight Close Audit Insight
</button> </button>

View File

@@ -67,9 +67,9 @@ export default function ManualCropUI({
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center p-8 bg-surface-container"> <div className="flex items-center justify-center p-8 bg-slate-900 rounded-lg">
<div className="text-center"> <div className="text-center">
<p className="text-error">{error}</p> <p className="text-rose-500">{error}</p>
</div> </div>
</div> </div>
); );
@@ -79,7 +79,7 @@ export default function ManualCropUI({
if (!actualDimensions) { if (!actualDimensions) {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="relative bg-surface-container overflow-hidden border border-outline/30"> <div className="relative bg-slate-900 rounded-lg overflow-hidden border border-slate-800">
<img <img
ref={imageRef} ref={imageRef}
src={imageUrl} src={imageUrl}
@@ -90,7 +90,7 @@ export default function ManualCropUI({
style={{ visibility: 'hidden' }} style={{ visibility: 'hidden' }}
/> />
</div> </div>
<div className="text-sm text-secondary text-center">Loading image...</div> <div className="text-sm text-slate-400 text-center">Loading image...</div>
</div> </div>
); );
} }
@@ -198,7 +198,7 @@ export default function ManualCropUI({
{/* Image container with crop preview */} {/* Image container with crop preview */}
<div <div
ref={containerRef} ref={containerRef}
className="relative bg-background overflow-hidden border border-outline/30" className="relative bg-slate-900 rounded-lg overflow-hidden border border-slate-800"
style={{ style={{
aspectRatio: `${actualDimensions.width} / ${actualDimensions.height}`, aspectRatio: `${actualDimensions.width} / ${actualDimensions.height}`,
maxWidth: '100%', maxWidth: '100%',
@@ -256,7 +256,7 @@ export default function ManualCropUI({
{/* Crop bounding box */} {/* Crop bounding box */}
<div <div
className="absolute border-2 border-primary" className="absolute border-2 border-cyan-400"
style={{ style={{
left: `${crop.x * scale}px`, left: `${crop.x * scale}px`,
top: `${crop.y * scale}px`, top: `${crop.y * scale}px`,
@@ -271,7 +271,9 @@ export default function ManualCropUI({
key={handleType} key={handleType}
onMouseDown={handleMouseDown(handleType)} onMouseDown={handleMouseDown(handleType)}
onTouchStart={handleTouchStart(handleType)} onTouchStart={handleTouchStart(handleType)}
className={`absolute w-${HANDLE_SIZE} h-${HANDLE_SIZE} bg-primary border-2 border-white shadow-lg hover:scale-125 transition-transform cursor-grab active:cursor-grabbing ${ isDragging ? 'scale-125' : '' }`} className={`absolute w-${HANDLE_SIZE} h-${HANDLE_SIZE} bg-cyan-400 rounded-full border-2 border-white shadow-lg hover:scale-125 transition-transform cursor-grab active:cursor-grabbing ${
isDragging ? 'scale-125' : ''
}`}
style={{ style={{
...getHandlePosition(handleType), ...getHandlePosition(handleType),
width: `${HANDLE_SIZE}px`, width: `${HANDLE_SIZE}px`,
@@ -289,14 +291,14 @@ export default function ManualCropUI({
{crop && ( {crop && (
<button <button
onClick={() => resetCrop()} onClick={() => resetCrop()}
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-surface-bright text-foreground font-normal text-base transition-colors hover:bg-surface-container" className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-slate-700 text-white rounded-lg font-normal text-base transition-colors hover:bg-slate-600"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
<span>Use Full Photo</span> <span>Use Full Photo</span>
</button> </button>
)} )}
{!crop && ( {!crop && (
<div className="text-sm text-secondary text-center flex-1 py-2.5"> <div className="text-sm text-slate-400 text-center flex-1 py-2.5">
Drag handles to adjust crop area Drag handles to adjust crop area
</div> </div>
)} )}
@@ -304,7 +306,7 @@ export default function ManualCropUI({
{/* Crop bounds display (debug) */} {/* Crop bounds display (debug) */}
{crop && ( {crop && (
<div className="text-xs text-secondary text-center"> <div className="text-xs text-slate-500 text-center">
Crop: {Math.round(crop.x)}, {Math.round(crop.y)} | Size: {Math.round(crop.width)} x{' '} Crop: {Math.round(crop.x)}, {Math.round(crop.y)} | Size: {Math.round(crop.width)} x{' '}
{Math.round(crop.height)} {Math.round(crop.height)}
</div> </div>

View File

@@ -18,7 +18,7 @@ export default function NewItemDialog({ onScannerClick, onAddItemClick }: NewIte
<div className="flex flex-col items-center py-3 md:py-4 text-center gap-3 md:gap-4"> <div className="flex flex-col items-center py-3 md:py-4 text-center gap-3 md:gap-4">
<button <button
onClick={onScannerClick} onClick={onScannerClick}
className="w-24 h-24 bg-primary/20 hover:bg-primary/30 border-2 border-primary border-dashed flex items-center justify-center group transition-all" className="w-24 h-24 rounded-full bg-primary/20 hover:bg-primary/30 border-2 border-primary border-dashed flex items-center justify-center group transition-all"
> >
<Smartphone className="w-10 h-10 text-primary group-hover:scale-110 transition-transform" /> <Smartphone className="w-10 h-10 text-primary group-hover:scale-110 transition-transform" />
</button> </button>
@@ -26,14 +26,14 @@ export default function NewItemDialog({ onScannerClick, onAddItemClick }: NewIte
<div className="w-full flex justify-center"> <div className="w-full flex justify-center">
<button <button
onClick={onAddItemClick} onClick={onAddItemClick}
className="w-full flex flex-col items-center justify-center p-4 md:p-6 ] bg-primary/5 border border-primary/20 group hover:border-primary/50 transition-all font-normal text-primary 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-primary/10 group-hover:scale-110 transition-transform shadow-lg shadow-primary/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

@@ -34,18 +34,18 @@ export default function PhotoModal({
aria-label={`Photo viewer for ${title}`} aria-label={`Photo viewer for ${title}`}
> >
<div <div
className="bg-surface border border-outline/30 max-w-2xl w-full max-h-[90vh] overflow-auto flex flex-col" className="bg-surface border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-auto flex flex-col"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="sticky top-0 bg-surface border-b border-outline/30/50 p-4 md:p-6 flex items-center justify-between"> <div className="sticky top-0 bg-surface border-b border-slate-800/50 p-4 md:p-6 flex items-center justify-between">
<h2 className="text-xl md:text-2xl font-normal text-foreground truncate">{title}</h2> <h2 className="text-xl md:text-2xl font-normal text-white truncate">{title}</h2>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-surface-container text-secondary hover:text-foreground transition-colors" className="p-2 hover:bg-slate-800 rounded-full text-muted hover:text-white transition-colors"
aria-label="Close modal" aria-label="Close modal"
> >
<X size={20} className="text-error" /> <X size={20} className="text-rose-500" />
</button> </button>
</div> </div>
@@ -54,7 +54,7 @@ export default function PhotoModal({
<img <img
src={photoUrl} src={photoUrl}
alt={title} alt={title}
className="max-w-full max-h-[calc(90vh-120px)] object-contain" className="max-w-full max-h-[calc(90vh-120px)] object-contain rounded-2xl"
loading="lazy" loading="lazy"
/> />
</div> </div>

View File

@@ -32,7 +32,7 @@ export default function ScannerSection({
return ( return (
<div className="w-full px-1 space-y-3 md:space-y-4"> <div className="w-full px-1 space-y-3 md:space-y-4">
{/* Mode Switcher */} {/* Mode Switcher */}
<div className="flex p-1.5 bg-surface shadow-inner w-full gap-1"> <div className="flex p-1.5 bg-surface rounded-2xl shadow-inner w-full gap-1">
{[ {[
{ id: 'CHECK_IN', label: 'Check In', icon: ArrowDownCircle }, { id: 'CHECK_IN', label: 'Check In', icon: ArrowDownCircle },
{ id: 'CHECK_OUT', label: 'Check Out', icon: ArrowUpCircle }, { id: 'CHECK_OUT', label: 'Check Out', icon: ArrowUpCircle },
@@ -43,8 +43,8 @@ export default function ScannerSection({
data-testid={m.id === 'CHECK_IN' ? 'operation-checkin' : m.id === 'CHECK_OUT' ? 'operation-checkout' : undefined} data-testid={m.id === 'CHECK_IN' ? 'operation-checkin' : m.id === 'CHECK_OUT' ? 'operation-checkout' : undefined}
onClick={() => onModeChange(m.id)} onClick={() => onModeChange(m.id)}
className={cn( className={cn(
"flex-1 py-3.5 text-xs sm:text-sm font-normal transition-all flex items-center justify-center gap-3", "flex-1 py-3.5 rounded-xl text-xs sm:text-sm font-normal transition-all flex items-center justify-center gap-3",
mode === m.id ? "bg-surface-container text-primary shadow-lg ring-1 ring-primary/20" : "text-secondary hover:text-foreground" mode === m.id ? "bg-slate-800 text-primary shadow-lg ring-1 ring-primary/20" : "text-muted hover:text-secondary"
)} )}
> >
<m.icon size={18} className={mode === m.id ? "scale-110 transition-transform" : ""} /> <m.icon size={18} className={mode === m.id ? "scale-110 transition-transform" : ""} />
@@ -54,14 +54,14 @@ export default function ScannerSection({
</div> </div>
{/* Scanner Section */} {/* Scanner Section */}
<section className="glass-card 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-outline/30 text-secondary hover:text-error 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

@@ -13,22 +13,22 @@ export function SearchErrorModal({ isOpen, error, onRetry, onSkip, canRetry = tr
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-background p-6 max-w-md shadow-xl border border-outline/30"> <div className="bg-white rounded-lg p-6 max-w-md shadow-xl">
<h2 className="text-xl font-normal mb-2 text-error">Search failed</h2> <h2 className="text-xl font-normal mb-2 text-rose-500">Search failed</h2>
<p className="text-sm text-secondary mb-4">{error || 'Unable to retrieve specs from the web'}</p> <p className="text-sm text-slate-600 mb-4">{error || 'Unable to retrieve specs from the web'}</p>
<div className="flex gap-3"> <div className="flex gap-3">
{canRetry && ( {canRetry && (
<button <button
onClick={onRetry} onClick={onRetry}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground font-normal text-sm hover:bg-primary/90" className="flex-1 px-4 py-2 bg-primary text-white rounded font-normal text-sm hover:bg-primary/90"
> >
Retry Retry
</button> </button>
)} )}
<button <button
onClick={onSkip} onClick={onSkip}
className="flex-1 px-4 py-2 border border-outline/30 font-normal text-sm hover:bg-surface-container text-foreground" className="flex-1 px-4 py-2 border border-slate-300 rounded font-normal text-sm hover:bg-slate-50"
> >
Skip Skip
</button> </button>

View File

@@ -35,12 +35,12 @@ export function SearchLoadingModal({ isOpen, onTimeout, maxSeconds = 30 }: Searc
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white p-6 max-w-md shadow-xl"> <div className="bg-white rounded-lg p-6 max-w-md shadow-xl">
<h2 className="text-xl font-normal mb-4">Searching for specs...</h2> <h2 className="text-xl font-normal mb-4">Searching for specs...</h2>
<p className="text-sm text-outline-variant mb-4">This may take up to {maxSeconds} seconds</p> <p className="text-sm text-slate-600 mb-4">This may take up to {maxSeconds} seconds</p>
<div className="mb-4"> <div className="mb-4">
<div className="relative h-2 bg-foreground overflow-hidden"> <div className="relative h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-primary transition-all duration-1000" className="h-full bg-primary transition-all duration-1000"
style={{ width: `${(seconds / maxSeconds) * 100}%` }} style={{ width: `${(seconds / maxSeconds) * 100}%` }}

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-outline/30 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">
@@ -17,7 +17,7 @@ export default function StatCard({ label, value, icon: Icon }: StatCardProps) {
</span> </span>
</div> </div>
<span className="text-base md:text-lg font-normal text-foreground whitespace-nowrap tabular-nums"> <span className="text-base md:text-lg font-normal text-white whitespace-nowrap tabular-nums">
{value} {value}
</span> </span>
</div> </div>

View File

@@ -63,12 +63,12 @@ export default function StockAdjustmentPanel({
return ( return (
<div data-testid="stock-adjustment-form" className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200"> <div data-testid="stock-adjustment-form" className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border border-outline/30 ] shadow-2xl p-4 md:p-6 overflow-hidden animate-in slide-in-from-bottom-10 duration-300"> <div className="w-full max-w-lg bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl p-4 md:p-6 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">
<span data-testid="adjustment-item-name">{isEditing ? "Edit Metadata" : selectedItem.name}</span> <span data-testid="adjustment-item-name">{isEditing ? "Edit Metadata" : selectedItem.name}</span>
{!isEditing && ( {!isEditing && (
<span data-testid="current-quantity" className="text-[11px] bg-surface-container text-secondary px-2 py-0.5 font-normal tracking-tight shadow-sm border border-outline-variant/50"> <span data-testid="current-quantity" className="text-[11px] bg-slate-800 text-secondary px-2 py-0.5 rounded-md font-normal tracking-tight shadow-sm border border-slate-700/50">
In Stock: {selectedItem.quantity} In Stock: {selectedItem.quantity}
</span> </span>
)} )}
@@ -77,7 +77,7 @@ export default function StockAdjustmentPanel({
{!isEditing && ( {!isEditing && (
<button <button
onClick={() => onEdit(selectedItem)} onClick={() => onEdit(selectedItem)}
className="p-2 hover:bg-surface-container 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-error/20 text-error" 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-surface-container" 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>
@@ -103,12 +103,12 @@ export default function StockAdjustmentPanel({
{isEditing ? ( {isEditing ? (
<div className="space-y-2 md:space-y-3 mb-4 md:mb-6"> <div className="space-y-2 md:space-y-3 mb-4 md:mb-6">
<div> <div>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Name</label> <label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Name</label>
<textarea <textarea
value={editedItem.name || ''} value={editedItem.name || ''}
onChange={(e) => onEditChange({ ...editedItem, name: e.target.value })} onChange={(e) => onEditChange({ ...editedItem, name: e.target.value })}
className="bg-transparent w-full text-lg font-normal outline-none text-foreground placeholder:text-secondary resize-none h-8 leading-tight selection:bg-primary/30 py-0" className="bg-transparent w-full text-lg font-normal outline-none text-white placeholder:text-muted resize-none h-8 leading-tight selection:bg-primary/30 py-0"
placeholder="SSD, SFP, Cable..." placeholder="SSD, SFP, Cable..."
/> />
</div> </div>
@@ -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-outline/30 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-outline/30 py-3 px-4 text-sm outline-none text-secondary placeholder: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: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-outline/30 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-outline/30 py-3 pl-4 pr-12 text-sm outline-none text-secondary placeholder:text-secondary 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,8 +169,8 @@ 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 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-foreground animate-pulse" : "text-secondary hover:bg-surface-container" fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-muted hover:bg-slate-800"
)} )}
> >
<Camera size={18} /> <Camera size={18} />
@@ -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-outline/30 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-outline/30 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-outline/30 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,16 +212,16 @@ 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-outline/30 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>
<div className="bg-surface py-2.5 px-4 ] border border-outline/30 focus-within:border-primary/50 transition-colors group"> <div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item ID or Code</label> <label className="text-xs text-secondary font-normal mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item ID or Code</label>
<textarea <textarea
value={editedItem.ocr_text || ''} value={editedItem.ocr_text || ''}
onChange={e => onEditChange({ ...editedItem, ocr_text: e.target.value })} onChange={e => onEditChange({ ...editedItem, ocr_text: e.target.value })}
className="w-full bg-background border border-outline/30 py-3 px-4 text-sm font-normal outline-none text-secondary resize-none h-12" className="w-full bg-background border border-slate-800 rounded-xl py-3 px-4 text-sm font-normal outline-none text-secondary resize-none h-12"
placeholder="e.g., SKU-12345 or barcode text..." placeholder="e.g., SKU-12345 or barcode text..."
/> />
</div> </div>
@@ -229,18 +229,18 @@ export default function StockAdjustmentPanel({
</div> </div>
) : ( ) : (
<> <>
<div className="flex p-1 bg-background mb-4 md:mb-6"> <div className="flex p-1 bg-background rounded-2xl mb-4 md:mb-6">
{[ {[
{ id: 'ADD', label: 'Buy More', icon: Plus, color: 'text-primary' }, { id: 'ADD', label: 'Buy More', icon: Plus, color: 'text-primary' },
{ id: 'REMOVE', label: 'Subtract', icon: Minus, color: 'text-warning' }, { id: 'REMOVE', label: 'Subtract', icon: Minus, color: 'text-amber-500' },
{ id: 'TRASH', label: 'Discard', icon: Trash2, color: 'text-error' } { id: 'TRASH', label: 'Discard', icon: Trash2, color: 'text-red-500' }
].map((t) => ( ].map((t) => (
<button <button
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 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-surface-container shadow-lg" : "text-secondary" adjustType === t.id ? "bg-slate-800 shadow-lg" : "text-muted"
)} )}
> >
<t.icon size={20} className={adjustType === t.id ? t.color : ""} /> <t.icon size={20} className={adjustType === t.id ? t.color : ""} />
@@ -253,7 +253,7 @@ export default function StockAdjustmentPanel({
<div className="flex items-center gap-2 md:gap-4"> <div className="flex items-center gap-2 md:gap-4">
<button <button
onClick={() => onQuantityChange(Math.max(1, adjustQty - 1))} onClick={() => onQuantityChange(Math.max(1, adjustQty - 1))}
className="w-12 h-12 border border-outline/30 flex items-center justify-center text-secondary active:bg-surface-container" className="w-12 h-12 rounded-full border border-slate-800 flex items-center justify-center text-secondary active:bg-slate-800"
> >
<Minus size={24} /> <Minus size={24} />
</button> </button>
@@ -263,22 +263,22 @@ export default function StockAdjustmentPanel({
</div> </div>
<button <button
onClick={() => onQuantityChange(adjustQty + 1)} onClick={() => onQuantityChange(adjustQty + 1)}
className="w-12 h-12 border border-outline/30 flex items-center justify-center text-secondary active:bg-surface-container" className="w-12 h-12 rounded-full border border-slate-800 flex items-center justify-center text-secondary active:bg-slate-800"
> >
<Plus size={24} /> <Plus size={24} />
</button> </button>
</div> </div>
{adjustType === 'TRASH' && ( {adjustType === 'TRASH' && (
<div className="w-full bg-error/5 border border-error/20 p-4 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-error" /> <AlertTriangle size={16} className="text-red-500" />
<span className="text-sm font-normal text-error">Waste Declaration</span> <span className="text-sm font-normal text-red-400">Waste Declaration</span>
</div> </div>
<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-outline/30 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>
@@ -296,11 +296,11 @@ export default function StockAdjustmentPanel({
onClick={isEditing ? onUpdateItem : onAdjustStock} onClick={isEditing ? onUpdateItem : onAdjustStock}
data-testid="adjustment-submit" data-testid="adjustment-submit"
className={cn( className={cn(
"w-full py-5 ] font-normal text-lg transition-all active:scale-[0.98] shadow-2xl", "w-full py-5 rounded-[1.8rem] font-normal text-lg transition-all active:scale-[0.98] shadow-2xl",
isEditing ? "bg-foreground text-surface-container" : ( isEditing ? "bg-slate-100 text-slate-900" : (
adjustType === 'ADD' ? "bg-primary shadow-primary/20 text-foreground" : adjustType === 'ADD' ? "bg-primary shadow-primary/20 text-white" :
adjustType === 'REMOVE' ? "bg-warning shadow-warning/20 text-foreground" : adjustType === 'REMOVE' ? "bg-amber-600 shadow-amber-500/20 text-white" :
"bg-error shadow-error/20 text-foreground" "bg-red-600 shadow-red-500/20 text-white"
) )
)} )}
> >

View File

@@ -16,15 +16,15 @@ export function Toast({ type, message, onClose, duration = 3000 }: ToastProps) {
}, [onClose, duration]); }, [onClose, duration]);
const bgColor = { const bgColor = {
success: 'bg-success', success: 'bg-green-500',
error: 'bg-error', error: 'bg-red-500',
warning: 'bg-warning', warning: 'bg-yellow-500',
info: 'bg-primary', info: 'bg-blue-500',
}[type]; }[type];
return ( return (
<div <div
className={`fixed bottom-4 right-4 ${bgColor} text-foreground px-4 py-2 shadow-lg z-50 animate-fade-in`} className={`fixed bottom-4 right-4 ${bgColor} text-white px-4 py-2 rounded shadow-lg z-50 animate-fade-in`}
role="alert" role="alert"
aria-live="polite" aria-live="polite"
> >

View File

@@ -32,13 +32,13 @@ export default function AiManager({
onUpdatePrompt onUpdatePrompt
}: AiManagerProps) { }: AiManagerProps) {
return ( return (
<section data-testid="ai-config" className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 shadow-2xl space-y-4 transition-all group/ai"> <section data-testid="ai-config" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl space-y-4 transition-all group/ai">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Brain size={20} /> <Brain size={20} />
</div> </div>
<h2 className="text-xl font-normal text-foreground tracking-tight">AI Intelligence</h2> <h2 className="text-xl font-normal text-white tracking-tight">AI Intelligence</h2>
</div> </div>
</div> </div>
@@ -49,33 +49,33 @@ 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 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-outline/30 hover:border-primary/40" : "bg-background/60 border-slate-800 hover:border-primary/40"
)} )}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={cn( <div className={cn(
"w-8 h-8 flex items-center justify-center transition-colors", "w-8 h-8 rounded-lg flex items-center justify-center transition-colors",
p.active ? "bg-white/20 text-foreground" : "bg-primary/10 text-primary group-hover:bg-primary/20" p.active ? "bg-white/20 text-white" : "bg-primary/10 text-primary group-hover:bg-primary/20"
)}> )}>
{p.id === 'gemini' ? <Cpu size={16} /> : <Zap size={16} />} {p.id === 'gemini' ? <Cpu size={16} /> : <Zap size={16} />}
</div> </div>
<div> <div>
<p className={cn("text-xs font-normal tracking-tight", p.active ? "text-foreground" : "text-secondary")}>{p.name}</p> <p className={cn("text-xs font-normal tracking-tight", p.active ? "text-white" : "text-secondary")}>{p.name}</p>
<p className={cn( <p className={cn(
"text-xs font-normal mt-1 px-0.5 ", "text-xs font-normal mt-1 px-0.5 rounded",
p.active p.active
? (p.configured ? "text-emerald-200" : "text-rose-100") ? (p.configured ? "text-emerald-200" : "text-rose-100")
: (p.configured ? "text-emerald-500" : "text-error") : (p.configured ? "text-emerald-500" : "text-rose-500")
)}> )}>
{p.configured ? "● Key Configured" : "○ Missing Api Key"} {p.configured ? "● Key Configured" : "○ Missing Api Key"}
</p> </p>
</div> </div>
</div> </div>
{p.active && ( {p.active && (
<div className="bg-white/20 px-2.5 py-1 text-xs font-normal text-foreground tracking-tight"> <div className="bg-white/20 px-2.5 py-1 rounded-full text-xs font-normal text-white tracking-tight">
Active Active
</div> </div>
)} )}
@@ -83,17 +83,17 @@ export default function AiManager({
))} ))}
</div> </div>
<div className="bg-primary/5 border border-primary/10 ] p-5 space-y-3"> <div className="bg-primary/5 border border-primary/10 rounded-[2rem] p-5 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 text-primary"><Lock size={14} /></div> <div className="p-2 bg-primary/10 rounded-lg text-primary"><Lock size={14} /></div>
<h3 className="text-sm font-normal text-secondary tracking-tight">Provider Access Keys</h3> <h3 className="text-sm font-normal text-secondary tracking-tight">Provider Access Keys</h3>
</div> </div>
<button <button
onClick={onSaveAiKeys} onClick={onSaveAiKeys}
disabled={isSavingKeys} disabled={isSavingKeys}
data-testid="save-settings-button" data-testid="save-settings-button"
className="px-5 py-2 bg-primary hover:bg-primary text-foreground text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center gap-2" className="px-5 py-2 bg-primary hover:bg-primary 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 gap-2"
> >
{isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />} {isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />}
{isSavingKeys ? "Storing..." : "Store API Keys"} {isSavingKeys ? "Storing..." : "Store API Keys"}
@@ -110,16 +110,16 @@ export default function AiManager({
value={aiKeys.gemini} value={aiKeys.gemini}
onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})} onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})}
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."} placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."}
className="flex-1 bg-background/80 border border-outline/30 py-1.5 px-4 text-xs text-foreground outline-none focus:border-primary/50 transition-all placeholder:text-secondary" className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-1.5 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
/> />
<button <button
onClick={() => onTestAiKey('gemini')} onClick={() => onTestAiKey('gemini')}
disabled={isTestingKeys.gemini} disabled={isTestingKeys.gemini}
className={cn( className={cn(
"px-3 py-1.5 text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5", "px-3 py-1.5 rounded-xl text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5",
isTestingKeys.gemini isTestingKeys.gemini
? "bg-surface-container border-outline-variant text-secondary cursor-wait" ? "bg-slate-800 border-slate-700 text-muted cursor-wait"
: "bg-primary border-primary text-foreground hover:bg-primary" : "bg-primary border-primary text-white hover:bg-primary"
)} )}
> >
{isTestingKeys.gemini ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />} {isTestingKeys.gemini ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
@@ -136,16 +136,16 @@ export default function AiManager({
value={aiKeys.claude} value={aiKeys.claude}
onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})} onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})}
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."} placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."}
className="flex-1 bg-background/80 border border-outline/30 py-1.5 px-4 text-xs text-foreground outline-none focus:border-primary/50 transition-all placeholder:text-secondary" className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-1.5 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
/> />
<button <button
onClick={() => onTestAiKey('claude')} onClick={() => onTestAiKey('claude')}
disabled={isTestingKeys.claude} disabled={isTestingKeys.claude}
className={cn( className={cn(
"px-3 py-1.5 text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5", "px-3 py-1.5 rounded-xl text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5",
isTestingKeys.claude isTestingKeys.claude
? "bg-surface-container border-outline-variant text-secondary cursor-wait" ? "bg-slate-800 border-slate-700 text-muted cursor-wait"
: "bg-primary border-primary text-foreground hover:bg-primary" : "bg-primary border-primary text-white hover:bg-primary"
)} )}
> >
{isTestingKeys.claude ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />} {isTestingKeys.claude ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
@@ -163,7 +163,7 @@ export default function AiManager({
<button <button
onClick={onUpdatePrompt} onClick={onUpdatePrompt}
disabled={isSavingPrompt} disabled={isSavingPrompt}
className="px-5 py-1.5 bg-primary hover:bg-primary text-foreground text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center gap-2" className="px-5 py-1.5 bg-primary hover:bg-primary 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 gap-2"
> >
{isSavingPrompt ? <RotateCcw size={14} className="animate-spin" /> : <FileText size={14} />} {isSavingPrompt ? <RotateCcw size={14} className="animate-spin" /> : <FileText size={14} />}
{isSavingPrompt ? "Saving..." : "Save Prompt"} {isSavingPrompt ? "Saving..." : "Save Prompt"}
@@ -172,11 +172,11 @@ export default function AiManager({
<textarea <textarea
value={aiPrompt} value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)} onChange={(e) => setAiPrompt(e.target.value)}
className="w-full bg-background/80 border border-outline/30 p-4 text-xs font-mono font-normal text-secondary leading-relaxed outline-none focus:border-purple-500/50 transition-all min-h-[200px] custom-scrollbar shadow-inner" className="w-full bg-background/80 border border-slate-800 rounded-2xl p-4 text-xs font-mono font-normal text-secondary leading-relaxed outline-none focus:border-purple-500/50 transition-all min-h-[200px] custom-scrollbar shadow-inner"
/> />
</div> </div>
<div className="bg-primary/5 border border-purple-500/10 p-4 flex gap-3 items-start"> <div className="bg-primary/5 border border-purple-500/10 rounded-2xl p-4 flex gap-3 items-start">
<div className="p-2 bg-primary/10 text-purple-400 shrink-0"><Shield size={14} /></div> <div className="p-2 bg-primary/10 rounded-lg text-purple-400 shrink-0"><Shield size={14} /></div>
<p className="text-xs font-normal text-secondary leading-relaxed"> <p className="text-xs font-normal text-secondary leading-relaxed">
This prompt instructs the Vision AI core on label interpretation. Ensure it defines explicit mapping for technical attributes like <span className="text-purple-400">Item</span>, <span className="text-purple-400">Type</span>, and <span className="text-purple-400">Part Number</span> to avoid extraction null-pointers and ensure inventory data integrity. This prompt instructs the Vision AI core on label interpretation. Ensure it defines explicit mapping for technical attributes like <span className="text-purple-400">Item</span>, <span className="text-purple-400">Type</span>, and <span className="text-purple-400">Part Number</span> to avoid extraction null-pointers and ensure inventory data integrity.
</p> </p>

View File

@@ -23,18 +23,18 @@ export default function CategoryManager({
onUpdateCategorySubmit onUpdateCategorySubmit
}: CategoryManagerProps) { }: CategoryManagerProps) {
return ( return (
<section className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 shadow-2xl space-y-4 transition-all group/categories"> <section className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl space-y-4 transition-all group/categories">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Layers size={20} /> <Layers size={20} />
</div> </div>
<h2 className="text-xl font-normal text-foreground tracking-tight">Category Groups</h2> <h2 className="text-xl font-normal text-white tracking-tight">Category Groups</h2>
</div> </div>
<button <button
onClick={onAddCategory} onClick={onAddCategory}
data-testid="add-category-button" data-testid="add-category-button"
className="flex items-center gap-2 bg-primary hover:bg-primary text-foreground px-5 py-2 text-sm font-normal transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight" className="flex items-center gap-2 bg-primary hover:bg-primary text-white px-5 py-2 rounded-xl text-sm font-normal transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight"
> >
<Plus size={14} /> New Group <Plus size={14} /> New Group
</button> </button>
@@ -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-outline/30/50 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-foreground hover:bg-surface-container 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-error hover:bg-error/5 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>
@@ -72,15 +72,15 @@ export default function CategoryManager({
{/* Edit Category Modal */} {/* Edit Category Modal */}
{editingCategory && ( {editingCategory && (
<div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/90 animate-in fade-in duration-300"> <div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-background/90 animate-in fade-in duration-300">
<div className="w-full max-w-lg bg-surface border-x border-t sm:border border-outline/30 ] sm: p-6 shadow-2xl space-y-3 overflow-hidden animate-in slide-in-from-bottom-10"> <div className="w-full max-w-lg bg-surface border-x border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-3xl p-6 shadow-2xl space-y-3 overflow-hidden animate-in slide-in-from-bottom-10">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Edit2 size={20} /> <Edit2 size={20} />
</div> </div>
<h3 className="text-xl font-normal text-foreground tracking-tight">Modify Group</h3> <h3 className="text-xl font-normal text-white tracking-tight">Modify Group</h3>
</div> </div>
<button onClick={() => setEditingCategory(null)} className="p-2 hover:bg-surface-container text-secondary transition-colors border border-outline/30"> <button onClick={() => setEditingCategory(null)} className="p-2 hover:bg-slate-800 rounded-2xl text-muted transition-colors border border-slate-800">
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
@@ -92,7 +92,7 @@ export default function CategoryManager({
type="text" type="text"
value={editCatForm.name} value={editCatForm.name}
onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})} onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})}
className="w-full bg-background border border-outline/30 py-1.5 px-4 text-sm text-foreground focus:border-primary outline-none transition-all" className="w-full bg-background border border-slate-800 rounded-2xl py-1.5 px-4 text-sm text-white focus:border-primary outline-none transition-all"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -100,10 +100,10 @@ export default function CategoryManager({
<textarea <textarea
value={editCatForm.description} value={editCatForm.description}
onChange={(e) => setEditCatForm({...editCatForm, description: e.target.value})} onChange={(e) => setEditCatForm({...editCatForm, description: e.target.value})}
className="w-full bg-background border border-outline/30 py-1.5 px-4 text-sm text-foreground focus:border-primary outline-none transition-all h-24 resize-none" className="w-full bg-background border border-slate-800 rounded-2xl py-1.5 px-4 text-sm text-white focus:border-primary outline-none transition-all h-24 resize-none"
/> />
</div> </div>
<button data-testid="add-category-submit" onClick={onUpdateCategorySubmit} className="w-full bg-primary hover:bg-primary text-foreground font-normal py-2.5 shadow-xl shadow-primary/20 active:scale-95 transition-all text-sm tracking-tight"> <button data-testid="add-category-submit" onClick={onUpdateCategorySubmit} className="w-full bg-primary hover:bg-primary text-white font-normal py-2.5 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all text-sm tracking-tight">
Update Asset Group Update Asset Group
</button> </button>
</div> </div>

View File

@@ -35,24 +35,24 @@ export default function DatabaseManager({
return ( return (
<div className="space-y-3 md:space-y-4 h-full"> <div className="space-y-3 md:space-y-4 h-full">
<div data-testid="database-info" className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 shadow-2xl overflow-hidden relative group transition-all"> <div data-testid="database-info" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl overflow-hidden relative group transition-all">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Database size={20} /> <Database size={20} />
</div> </div>
<h2 className="text-xl font-normal text-foreground tracking-tight">System Integrity</h2> <h2 className="text-xl font-normal text-white tracking-tight">System Integrity</h2>
</div> </div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1 sm:pr-4"> <div className="space-y-1 sm:pr-4">
<p className="text-xs font-normal text-secondary tracking-tight">Database Health</p> <p className="text-xs font-normal text-muted tracking-tight">Database Health</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 bg-success animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.3)]" /> <div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.3)]" />
<span className="text-sm font-normal text-secondary">Operational</span> <span className="text-sm font-normal text-secondary">Operational</span>
</div> </div>
</div> </div>
<div className="sm:pl-4 sm:border-l border-outline/30"> <div className="sm:pl-4 sm:border-l border-slate-800">
<p className="text-xs font-normal text-secondary tracking-tight">Last Backup</p> <p className="text-xs font-normal text-muted tracking-tight">Last Backup</p>
<p className="text-sm font-normal text-secondary tabular-nums">{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}</p> <p className="text-sm font-normal text-secondary tabular-nums">{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}</p>
</div> </div>
<div className="ml-auto"> <div className="ml-auto">
@@ -60,7 +60,7 @@ export default function DatabaseManager({
onClick={onCreateBackup} onClick={onCreateBackup}
disabled={isBackingUp} disabled={isBackingUp}
data-testid="backup-button" data-testid="backup-button"
className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-primary text-foreground text-sm font-normal transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-primary/10 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-normal transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-primary/10 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Create database backup" aria-label="Create database backup"
> >
{isBackingUp ? <RotateCcw size={14} className="animate-spin" /> : <Database size={14} />} {isBackingUp ? <RotateCcw size={14} className="animate-spin" /> : <Database size={14} />}
@@ -70,32 +70,32 @@ export default function DatabaseManager({
</div> </div>
</div> </div>
<div className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 shadow-2xl space-y-4"> <div className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<History size={20} /> <History size={20} />
</div> </div>
<div> <div>
<h3 className="text-lg font-normal text-foreground tracking-tight">Storage Policy</h3> <h3 className="text-lg font-normal text-white tracking-tight">Storage Policy</h3>
<p className="text-sm text-secondary font-normal tracking-tight">Retention & Maintenance</p> <p className="text-sm text-secondary font-normal tracking-tight">Retention & Maintenance</p>
</div> </div>
</div> </div>
<div className="grid sm:grid-cols-3 gap-3"> <div className="grid sm:grid-cols-3 gap-3">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1 flex items-center gap-2"> <label className="text-sm font-normal text-muted tracking-tight ml-1 flex items-center gap-2">
<History size={10} /> Max Backups <History size={10} /> Max Backups
</label> </label>
<input <input
type="number" type="number"
value={dbSettings.retention_count} value={dbSettings.retention_count}
onChange={(e) => onUpdateDbSettings({...dbSettings, retention_count: parseInt(e.target.value)})} onChange={(e) => onUpdateDbSettings({...dbSettings, retention_count: parseInt(e.target.value)})}
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums" className="w-full bg-background/80 border border-slate-800 rounded-xl py-1.5 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1 flex items-center gap-2"> <label className="text-sm font-normal text-muted tracking-tight ml-1 flex items-center gap-2">
<Clock size={10} /> Schedule (Hour) <Clock size={10} /> Schedule (Hour)
</label> </label>
<input <input
@@ -103,11 +103,11 @@ export default function DatabaseManager({
min="0" max="23" min="0" max="23"
value={dbSettings.schedule_hour} value={dbSettings.schedule_hour}
onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_hour: parseInt(e.target.value)})} onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_hour: parseInt(e.target.value)})}
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums" className="w-full bg-background/80 border border-slate-800 rounded-xl py-1.5 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1 flex items-center gap-2"> <label className="text-sm font-normal text-muted tracking-tight ml-1 flex items-center gap-2">
<Zap size={10} /> Frequency (Days) <Zap size={10} /> Frequency (Days)
</label> </label>
<input <input
@@ -115,7 +115,7 @@ export default function DatabaseManager({
min="1" min="1"
value={dbSettings.schedule_freq_days} value={dbSettings.schedule_freq_days}
onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_freq_days: parseInt(e.target.value)})} onChange={(e) => onUpdateDbSettings({...dbSettings, schedule_freq_days: parseInt(e.target.value)})}
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums" className="w-full bg-background/80 border border-slate-800 rounded-xl py-1.5 px-4 text-xs font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none transition-all tabular-nums"
/> />
</div> </div>
</div> </div>
@@ -123,17 +123,17 @@ export default function DatabaseManager({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<label className="text-sm font-normal text-secondary tracking-tight">Recovery Points</label> <label className="text-sm font-normal text-muted tracking-tight">Recovery Points</label>
<div className="text-xs font-normal text-primary tracking-tight bg-primary/5 px-3 py-1 border border-primary/10"> <div className="text-xs font-normal text-primary tracking-tight bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
{formatSize(dbStats.total_size_bytes)} Used {formatSize(dbStats.total_size_bytes)} Used
</div> </div>
</div> </div>
<div className="space-y-1 pr-2 overflow-y-auto max-h-[300px] scrollbar-thin scrollbar-thumb-surface-container scrollbar-track-transparent"> <div className="space-y-1 pr-2 overflow-y-auto max-h-[300px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{backups.map((bak: any) => ( {backups.map((bak: any) => (
<div key={bak.filename} className="flex items-center justify-between p-3 bg-background/40 border border-outline/30/40 group/item hover:border-primary/30 transition-all"> <div key={bak.filename} className="flex items-center justify-between p-3 bg-background/40 border border-slate-800/40 rounded-2xl group/item hover:border-primary/30 transition-all">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-surface-container flex items-center justify-center text-secondary"> <div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center text-muted">
<Database size={14} /> <Database size={14} />
</div> </div>
<div> <div>
@@ -144,7 +144,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 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} />
@@ -157,7 +157,7 @@ export default function DatabaseManager({
<div className="grid grid-cols-2 gap-3 pt-1"> <div className="grid grid-cols-2 gap-3 pt-1">
<button <button
onClick={onExport} onClick={onExport}
className="flex items-center justify-center gap-2 py-2.5 bg-background border border-outline/30 hover:border-primary/50 text-primary text-sm font-normal transition-all active:scale-95 tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="flex items-center justify-center gap-2 py-2.5 bg-background border border-slate-800 hover:border-primary/50 text-primary rounded-2xl text-sm font-normal transition-all active:scale-95 tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Export database" aria-label="Export database"
> >
<Download size={14} /> Export Database <Download size={14} /> Export Database
@@ -170,7 +170,7 @@ export default function DatabaseManager({
className="absolute inset-0 opacity-0 cursor-pointer z-10" className="absolute inset-0 opacity-0 cursor-pointer z-10"
/> />
<button <button
className="w-full flex items-center justify-center gap-2 py-2.5 bg-background border border-outline/30 hover:border-error/50 text-error text-sm font-normal transition-all tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-error focus-visible:outline-none" className="w-full flex items-center justify-center gap-2 py-2.5 bg-background border border-slate-800 hover:border-rose-500/50 text-rose-500 rounded-2xl text-sm font-normal transition-all tracking-tight shadow-xl shadow-black/20 focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
aria-label="Import database" aria-label="Import database"
> >
<Upload size={14} /> Import Database <Upload size={14} /> Import Database

View File

@@ -29,26 +29,26 @@ export function ExportPanel() {
}; };
return ( return (
<div data-testid="admin-tab-exports" className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 flex flex-col shadow-2xl transition-all h-full"> <div data-testid="admin-tab-exports" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 flex flex-col shadow-2xl transition-all h-full">
<div className="flex items-center gap-3 mb-4 md:mb-6"> <div className="flex items-center gap-3 mb-4 md:mb-6">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
<FileDown size={20} /> <FileDown size={20} />
</div> </div>
<div> <div>
<h2 className="text-xl font-normal text-foreground tracking-tight">Export & Reports</h2> <h2 className="text-xl font-normal text-white tracking-tight">Export & Reports</h2>
<p className="text-[10px] md:text-xs text-secondary font-normal tracking-tight">Download system snapshots and action logs</p> <p className="text-[10px] md:text-xs text-muted font-normal tracking-tight">Download system snapshots and action logs</p>
</div> </div>
</div> </div>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
{/* Inventory Snapshot Section */} {/* Inventory Snapshot Section */}
<div className="p-4 bg-background/40 border border-outline/30/40 hover:border-primary/20 transition-all group flex flex-col justify-between"> <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"> <div className="mb-4">
<h3 className="text-sm font-normal text-secondary flex items-center gap-2"> <h3 className="text-sm font-normal text-secondary flex items-center gap-2">
<FileSpreadsheet size={14} className="text-primary" /> <FileSpreadsheet size={14} className="text-primary" />
Inventory Snapshot Inventory Snapshot
</h3> </h3>
<p className="text-xs text-secondary mt-1 leading-relaxed"> <p className="text-xs text-muted mt-1 leading-relaxed">
Export current inventory state with all item details and locations. Export current inventory state with all item details and locations.
</p> </p>
</div> </div>
@@ -57,7 +57,7 @@ export function ExportPanel() {
<button <button
onClick={() => handleExportSnapshot("csv")} onClick={() => handleExportSnapshot("csv")}
disabled={isLoading} disabled={isLoading}
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-foreground 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" 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 ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />} {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
@@ -66,7 +66,7 @@ export function ExportPanel() {
<button <button
onClick={() => handleExportSnapshot("xlsx")} onClick={() => handleExportSnapshot("xlsx")}
disabled={isLoading} disabled={isLoading}
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-foreground 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" 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 ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />} {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
@@ -76,13 +76,13 @@ export function ExportPanel() {
</div> </div>
{/* Audit Trail Section */} {/* Audit Trail Section */}
<div className="p-4 bg-background/40 border border-outline/30/40 hover:border-primary/20 transition-all group flex flex-col justify-between"> <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"> <div className="mb-4">
<h3 className="text-sm font-normal text-secondary flex items-center gap-2"> <h3 className="text-sm font-normal text-secondary flex items-center gap-2">
<FileText size={14} className="text-primary" /> <FileText size={14} className="text-indigo-400" />
Audit Trail Audit Trail
</h3> </h3>
<p className="text-xs text-secondary mt-1 leading-relaxed"> <p className="text-xs text-muted mt-1 leading-relaxed">
Complete history of system actions, changes, and user operations. Complete history of system actions, changes, and user operations.
</p> </p>
</div> </div>
@@ -90,7 +90,7 @@ export function ExportPanel() {
<button <button
onClick={() => handleExportAuditTrail("csv")} onClick={() => handleExportAuditTrail("csv")}
disabled={isLoading} disabled={isLoading}
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-foreground 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" 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 ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />} {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileText size={14} />}
@@ -99,7 +99,7 @@ export function ExportPanel() {
<button <button
onClick={() => handleExportAuditTrail("xlsx")} onClick={() => handleExportAuditTrail("xlsx")}
disabled={isLoading} disabled={isLoading}
className="px-5 py-1.5 bg-primary hover:bg-primary/90 text-foreground 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" 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 ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />} {isLoading ? <Loader2 size={14} className="animate-spin" /> : <FileSpreadsheet size={14} />}
@@ -110,8 +110,8 @@ export function ExportPanel() {
</div> </div>
{error && ( {error && (
<div className="mt-4 p-3 bg-error/10 border border-error/20 text-error text-[10px] flex items-center gap-2 animate-in slide-in-from-top-2 duration-300"> <div className="mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-[10px] flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<div className="w-1.5 h-1.5 bg-error animate-pulse" /> <div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
<span className="font-normal truncate">Error: {error}</span> <span className="font-normal truncate">Error: {error}</span>
</div> </div>
)} )}

View File

@@ -24,31 +24,31 @@ export default function IdentityManager({
onUpdateUserSubmit onUpdateUserSubmit
}: IdentityManagerProps) { }: IdentityManagerProps) {
return ( return (
<div className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 flex flex-col shadow-2xl transition-all group/identity h-full"> <div className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 flex flex-col shadow-2xl transition-all group/identity h-full">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<User size={20} /> <User size={20} />
</div> </div>
<h2 className="text-xl font-normal text-foreground tracking-tight">Identity Management</h2> <h2 className="text-xl font-normal text-white tracking-tight">Identity Management</h2>
</div> </div>
<button <button
onClick={onAddUser} onClick={onAddUser}
data-testid="add-user-button" data-testid="add-user-button"
className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-primary text-foreground text-sm font-normal transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-normal transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Add new user" aria-label="Add new user"
> >
<UserPlus size={16} /> Add User <UserPlus size={16} /> Add User
</button> </button>
</div> </div>
<div className="flex-1 space-y-1 pr-2 -mr-2 overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-surface-container scrollbar-track-transparent"> <div className="flex-1 space-y-1 pr-2 -mr-2 overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{users.map((user) => ( {users.map((user) => (
<div key={user.id} className="flex items-center justify-between p-3 bg-background/40 border border-outline/30/40 hover:border-primary/30 transition-all group"> <div key={user.id} className="flex items-center justify-between p-3 bg-background/40 border border-slate-800/40 rounded-2xl hover:border-primary/30 transition-all group">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<div className={cn( <div className={cn(
"w-9 h-9 flex items-center justify-center transition-colors shadow-inner shrink-0", "w-9 h-9 rounded-xl flex items-center justify-center transition-colors shadow-inner shrink-0",
user.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-surface-container text-secondary border border-outline-variant" user.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-muted border border-slate-700"
)}> )}>
{user.role === 'admin' ? <Shield size={16} /> : <User size={16} />} {user.role === 'admin' ? <Shield size={16} /> : <User size={16} />}
</div> </div>
@@ -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 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-error hover:bg-error/5 transition-all focus-visible:ring-2 focus-visible:ring-error 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} />
@@ -85,13 +85,13 @@ export default function IdentityManager({
{/* Edit User Modal */} {/* Edit User Modal */}
{editingUser && ( {editingUser && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200"> <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200">
<div className="bg-surface border border-outline/30 ] p-6 w-full max-w-md shadow-2xl"> <div className="bg-surface border border-slate-800 rounded-[2.5rem] p-6 w-full max-w-md shadow-2xl">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h3 className="text-xl font-normal text-foreground 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-secondary hover:text-foreground transition-colors 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-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} />
@@ -105,7 +105,7 @@ export default function IdentityManager({
type="text" type="text"
value={editUserForm.username} value={editUserForm.username}
onChange={(e) => setEditUserForm({ ...editUserForm, username: e.target.value })} onChange={(e) => setEditUserForm({ ...editUserForm, username: e.target.value })}
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-sm font-normal text-foreground outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -115,7 +115,7 @@ export default function IdentityManager({
value={editUserForm.password} value={editUserForm.password}
onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })} onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })}
placeholder="********" placeholder="********"
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-sm font-normal text-foreground outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 transition-all font-mono"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -123,7 +123,7 @@ export default function IdentityManager({
<select <select
value={editUserForm.role} value={editUserForm.role}
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })} onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
className="w-full bg-background/80 border border-outline/30 py-1.5 px-4 text-sm font-normal text-foreground outline-none focus:border-primary/50 transition-all appearance-none" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 px-4 text-sm font-normal text-white outline-none focus:border-primary/50 transition-all appearance-none"
> >
<option value="user">Standard User</option> <option value="user">Standard User</option>
<option value="admin">Administrator</option> <option value="admin">Administrator</option>
@@ -132,7 +132,7 @@ export default function IdentityManager({
<button <button
onClick={onUpdateUserSubmit} onClick={onUpdateUserSubmit}
className="w-full bg-primary hover:bg-primary/80 text-foreground py-2.5 text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/20 tracking-tight mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-container focus-visible:ring-primary focus-visible:outline-none" className="w-full bg-primary hover:bg-primary/80 text-white rounded-2xl py-2.5 text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/20 tracking-tight mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 focus-visible:ring-primary focus-visible:outline-none"
> >
Apply Changes Apply Changes
</button> </button>

View File

@@ -18,27 +18,27 @@ export default function LdapManager({
onUpdateLdap onUpdateLdap
}: LdapManagerProps) { }: LdapManagerProps) {
return ( return (
<div className="bg-surface/50 border border-outline/30/50 ] p-4 md:p-6 shadow-2xl space-y-3 transition-all group/ldap"> <div className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 shadow-2xl space-y-3 transition-all group/ldap">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center text-primary border border-primary/20"> <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Globe size={20} /> <Globe size={20} />
</div> </div>
<h2 className="text-xl font-normal text-foreground tracking-tight">Enterprise LDAP</h2> <h2 className="text-xl font-normal text-white tracking-tight">Enterprise LDAP</h2>
</div> </div>
</div> </div>
<div className="flex items-center justify-between p-4 bg-background/40 border border-outline/30/60 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 flex items-center justify-center transition-all shadow-inner", "w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
ldapConfig.ldap_enabled ? "bg-success/10 text-success" : "bg-surface-container text-secondary" ldapConfig.ldap_enabled ? "bg-green-500/10 text-green-500" : "bg-slate-800 text-muted"
)}> )}>
<Power size={14} /> <Power size={14} />
</div> </div>
<div> <div>
<p className="card-title leading-none">LDAP Authorization</p> <p className="card-title leading-none">LDAP Authorization</p>
<p className={cn("card-subtitle mt-1", ldapConfig.ldap_enabled ? "text-success/80" : "text-secondary")}> <p className={cn("card-subtitle mt-1", ldapConfig.ldap_enabled ? "text-green-500/80" : "text-muted")}>
{ldapConfig.ldap_enabled ? "External Directory Active" : "Internal Authentication Only"} {ldapConfig.ldap_enabled ? "External Directory Active" : "Internal Authentication Only"}
</p> </p>
</div> </div>
@@ -46,12 +46,12 @@ export default function LdapManager({
<button <button
onClick={() => setLdapConfig({...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled})} onClick={() => setLdapConfig({...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled})}
className={cn( className={cn(
"w-10 h-5 relative transition-all duration-300 shadow-inner border", "w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
ldapConfig.ldap_enabled ? "bg-primary border-primary" : "bg-surface-container border-outline-variant" ldapConfig.ldap_enabled ? "bg-primary border-primary" : "bg-slate-800 border-slate-700"
)} )}
> >
<div className={cn( <div className={cn(
"absolute top-0.5 w-3.5 h-3.5 bg-white transition-all shadow-sm", "absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
ldapConfig.ldap_enabled ? "right-0.5" : "left-0.5" ldapConfig.ldap_enabled ? "right-0.5" : "left-0.5"
)} /> )} />
</button> </button>
@@ -61,26 +61,26 @@ export default function LdapManager({
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1">LDAP URI</label> <label className="text-sm font-normal text-secondary tracking-tight ml-1">LDAP URI</label>
<div className="relative"> <div className="relative">
<Server className="absolute left-3 top-1/2 -translate-y-1/2 text-secondary" size={14} /> <Server className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={14} />
<input <input
type="text" type="text"
placeholder="ldap://host:389" placeholder="ldap://host:389"
value={ldapConfig.server_uri} value={ldapConfig.server_uri}
onChange={(e) => setLdapConfig({...ldapConfig, server_uri: e.target.value})} onChange={(e) => setLdapConfig({...ldapConfig, server_uri: e.target.value})}
className="w-full bg-background/80 border border-outline/30 py-1.5 pl-9 pr-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 pl-9 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Context DN</label> <label className="text-sm font-normal text-secondary tracking-tight ml-1">Context DN</label>
<div className="relative"> <div className="relative">
<Shield className="absolute left-3 top-1/2 -translate-y-1/2 text-secondary" size={14} /> <Shield className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={14} />
<input <input
type="text" type="text"
placeholder="dc=example,dc=com" placeholder="dc=example,dc=com"
value={ldapConfig.base_dn} value={ldapConfig.base_dn}
onChange={(e) => setLdapConfig({...ldapConfig, base_dn: e.target.value})} onChange={(e) => setLdapConfig({...ldapConfig, base_dn: e.target.value})}
className="w-full bg-background/80 border border-outline/30 py-1.5 pl-9 pr-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 pl-9 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
@@ -90,42 +90,42 @@ export default function LdapManager({
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1">User DN Template</label> <label className="text-sm font-normal text-secondary tracking-tight ml-1">User DN Template</label>
<div className="relative"> <div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 text-secondary" size={14} /> <User className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={14} />
<input <input
type="text" type="text"
placeholder="uid={username},ou=people..." placeholder="uid={username},ou=people..."
value={ldapConfig.user_template} value={ldapConfig.user_template}
onChange={(e) => setLdapConfig({...ldapConfig, user_template: e.target.value})} onChange={(e) => setLdapConfig({...ldapConfig, user_template: e.target.value})}
className="w-full bg-background/80 border border-outline/30 py-1.5 pl-9 pr-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 pl-9 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Groups Base DN</label> <label className="text-sm font-normal text-secondary tracking-tight ml-1">Groups Base DN</label>
<div className="relative"> <div className="relative">
<Layers className="absolute left-3 top-1/2 -translate-y-1/2 text-secondary" size={14} /> <Layers className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" size={14} />
<input <input
type="text" type="text"
placeholder="ou=groups" placeholder="ou=groups"
value={ldapConfig.groups_dn} value={ldapConfig.groups_dn}
onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})} onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})}
className="w-full bg-background/80 border border-outline/30 py-1.5 pl-9 pr-4 text-xs font-normal text-foreground outline-none focus:border-primary/50 transition-all font-mono" className="w-full bg-background/80 border border-slate-800 rounded-2xl py-1.5 pl-9 pr-4 text-xs font-normal text-white outline-none focus:border-primary/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center justify-between p-4 bg-background/40 border border-outline/30/60 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 flex items-center justify-center transition-all shadow-inner", "w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
ldapConfig.use_tls ? "bg-primary/10 text-primary" : "bg-surface-container text-secondary" ldapConfig.use_tls ? "bg-primary/10 text-primary" : "bg-slate-800 text-muted"
)}> )}>
<Lock size={14} /> <Lock size={14} />
</div> </div>
<div> <div>
<p className="card-title leading-none">Use TLS</p> <p className="card-title leading-none">Use TLS</p>
<p className={cn("card-subtitle mt-1", ldapConfig.use_tls ? "text-primary/80" : "text-secondary")}> <p className={cn("card-subtitle mt-1", ldapConfig.use_tls ? "text-primary/80" : "text-muted")}>
{ldapConfig.use_tls ? "Encrypted Channel" : "Standard Link"} {ldapConfig.use_tls ? "Encrypted Channel" : "Standard Link"}
</p> </p>
</div> </div>
@@ -133,12 +133,12 @@ export default function LdapManager({
<button <button
onClick={() => setLdapConfig({...ldapConfig, use_tls: !ldapConfig.use_tls})} onClick={() => setLdapConfig({...ldapConfig, use_tls: !ldapConfig.use_tls})}
className={cn( className={cn(
"w-10 h-5 relative transition-all duration-300 shadow-inner border", "w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
ldapConfig.use_tls ? "bg-primary border-primary" : "bg-surface-container border-outline-variant" ldapConfig.use_tls ? "bg-primary border-primary" : "bg-slate-800 border-slate-700"
)} )}
> >
<div className={cn( <div className={cn(
"absolute top-0.5 w-3.5 h-3.5 bg-white transition-all shadow-sm", "absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
ldapConfig.use_tls ? "right-0.5" : "left-0.5" ldapConfig.use_tls ? "right-0.5" : "left-0.5"
)} /> )} />
</button> </button>
@@ -148,14 +148,14 @@ export default function LdapManager({
<button <button
onClick={onTestLdap} onClick={onTestLdap}
disabled={testingLdap} disabled={testingLdap}
className="flex-1 bg-primary hover:bg-primary text-foreground py-1.5 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-primary/30" className="flex-1 bg-primary hover:bg-primary text-white rounded-xl py-1.5 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-primary/30"
> >
{testingLdap ? <RotateCcw size={14} className="animate-spin" /> : <Wifi size={14} />} {testingLdap ? <RotateCcw size={14} className="animate-spin" /> : <Wifi size={14} />}
Test Connection Test Connection
</button> </button>
<button <button
onClick={onUpdateLdap} onClick={onUpdateLdap}
className="flex-[2] bg-primary hover:bg-primary text-foreground py-2 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-primary/30 tracking-tight" className="flex-[2] bg-primary hover:bg-primary text-white rounded-xl py-2 text-sm font-normal shadow-xl shadow-primary/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-primary/30 tracking-tight"
> >
<Shield size={14} /> Save LDAP Policy <Shield size={14} /> Save LDAP Policy
</button> </button>

View File

@@ -56,16 +56,16 @@ export default function QuantityAdjustmentModal({
return ( return (
<div className={`fixed inset-0 z-50 bg-black/50 flex items-center justify-center transition-opacity ${isClosing ? 'opacity-0' : 'opacity-100'}`}> <div className={`fixed inset-0 z-50 bg-black/50 flex items-center justify-center transition-opacity ${isClosing ? 'opacity-0' : 'opacity-100'}`}>
<div className={`bg-background shadow-lg w-full max-w-md mx-4 transition-transform ${isClosing ? 'scale-95' : 'scale-100'}`}> <div className={`bg-slate-950 rounded-lg shadow-lg w-full max-w-md mx-4 transition-transform ${isClosing ? 'scale-95' : 'scale-100'}`}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-outline/30"> <div className="flex items-center justify-between p-4 border-b border-slate-800">
<h2 className="text-lg font-normal">Adjust quantity</h2> <h2 className="text-lg font-normal">Adjust quantity</h2>
<button <button
onClick={handleClose} onClick={handleClose}
aria-label="Close quantity adjustment modal" aria-label="Close quantity adjustment modal"
className="p-2 hover:bg-surface-container transition-colors" className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
> >
<X size={20} className="text-secondary" /> <X size={20} className="text-slate-400" />
</button> </button>
</div> </div>
@@ -73,10 +73,10 @@ export default function QuantityAdjustmentModal({
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Item Details */} {/* Item Details */}
<div> <div>
<h3 className="text-2xl font-normal text-foreground mb-2"> <h3 className="text-2xl font-normal text-slate-100 mb-2">
{item.name} {item.name}
</h3> </h3>
<div className="text-sm text-secondary space-y-1"> <div className="text-sm text-slate-500 space-y-1">
{item.part_number && ( {item.part_number && (
<div>Part Number: {item.part_number}</div> <div>Part Number: {item.part_number}</div>
)} )}
@@ -91,14 +91,14 @@ export default function QuantityAdjustmentModal({
{/* Success Message */} {/* Success Message */}
{successMessage && ( {successMessage && (
<div className="p-3 bg-success/10 border border-success/20 text-success text-sm"> <div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg text-green-500 text-sm">
{successMessage} {successMessage}
</div> </div>
)} )}
{/* Quantity Adjustment */} {/* Quantity Adjustment */}
<div> <div>
<label className="text-sm font-normal text-secondary mb-3 block"> <label className="text-sm font-normal text-slate-400 mb-3 block">
Current quantity Current quantity
</label> </label>
<QuantityDisplay <QuantityDisplay
@@ -112,7 +112,7 @@ export default function QuantityAdjustmentModal({
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
onClick={handleClose} onClick={handleClose}
className="flex-1 px-4 py-2 bg-surface-container hover:bg-surface-bright text-foreground font-normal transition-colors" className="flex-1 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-100 rounded-lg font-normal transition-colors"
> >
Keep Current Quantity Keep Current Quantity
</button> </button>

View File

@@ -83,12 +83,12 @@ export default function QuantityDisplay({
if (isEditing) { if (isEditing) {
return ( return (
<div className="flex items-center gap-2 bg-background/50 px-3 py-2"> <div className="flex items-center gap-2 bg-slate-900/50 rounded-lg px-3 py-2">
<button <button
onClick={handleDecrement} onClick={handleDecrement}
disabled={isLoading} disabled={isLoading}
aria-label="Decrease quantity" aria-label="Decrease quantity"
className="p-1 hover:bg-surface-container text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 hover:bg-slate-800 rounded text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Minus size={16} /> <Minus size={16} />
</button> </button>
@@ -110,7 +110,7 @@ export default function QuantityDisplay({
onClick={handleIncrement} onClick={handleIncrement}
disabled={isLoading} disabled={isLoading}
aria-label="Increase quantity" aria-label="Increase quantity"
className="p-1 hover:bg-surface-container text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 hover:bg-slate-800 rounded text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Plus size={16} /> <Plus size={16} />
</button> </button>
@@ -123,7 +123,7 @@ export default function QuantityDisplay({
onClick={handleTapToEdit} onClick={handleTapToEdit}
disabled={isLoading} disabled={isLoading}
aria-label={`Quantity: ${currentQuantity}. Tap to edit`} aria-label={`Quantity: ${currentQuantity}. Tap to edit`}
className="text-lg font-normal text-primary hover:bg-background/30 px-3 py-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="text-lg font-normal text-primary hover:bg-slate-900/30 px-3 py-2 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
{currentQuantity} {currentQuantity}
</button> </button>

View File

@@ -109,23 +109,23 @@ export default function SearchModal({
return ( return (
<div className="fixed inset-0 z-50 bg-black/50 flex items-start justify-center pt-20"> <div className="fixed inset-0 z-50 bg-black/50 flex items-start justify-center pt-20">
<div className="bg-background shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col border border-outline/30"> <div className="bg-slate-950 rounded-lg shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-outline/30"> <div className="flex items-center justify-between p-4 border-b border-slate-800">
<h2 className="text-xl font-normal">Search inventory</h2> <h2 className="text-xl font-normal">Search inventory</h2>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close search modal" aria-label="Close search modal"
className="p-2 hover:bg-surface-container transition-colors" className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
> >
<X size={20} className="text-secondary" /> <X size={20} className="text-slate-400" />
</button> </button>
</div> </div>
{/* Search Input */} {/* Search Input */}
<div className="p-4 border-b border-outline/30"> <div className="p-4 border-b border-slate-800">
<div className="relative"> <div className="relative">
<Search size={18} className="absolute left-3 top-3 text-secondary pointer-events-none" /> <Search size={18} className="absolute left-3 top-3 text-slate-500 pointer-events-none" />
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@@ -133,7 +133,7 @@ export default function SearchModal({
value={query} value={query}
onChange={handleInputChange} onChange={handleInputChange}
aria-label="Search items" aria-label="Search items"
className="w-full bg-surface-container border border-outline/30 pl-10 pr-4 py-2 text-foreground placeholder-secondary/50 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/50" className="w-full bg-slate-900 border border-slate-800 rounded-lg pl-10 pr-4 py-2 text-slate-100 placeholder-slate-600 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/50"
/> />
</div> </div>
</div> </div>
@@ -141,48 +141,48 @@ export default function SearchModal({
{/* Results */} {/* Results */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{error && ( {error && (
<div className="p-4 text-error bg-error/10 border border-error/20 m-4"> <div className="p-4 text-rose-500 bg-rose-500/10 border border-rose-500/20 m-4 rounded-lg">
{error} {error}
</div> </div>
)} )}
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<div className="text-secondary"> <div className="text-slate-400">
<div className="inline-block animate-spin h-6 w-6 border border-outline/30 border-t-primary"></div> <div className="inline-block animate-spin rounded-full h-6 w-6 border border-slate-600 border-t-primary"></div>
</div> </div>
</div> </div>
)} )}
{!isLoading && !error && results.length === 0 && query.length > 0 && ( {!isLoading && !error && results.length === 0 && query.length > 0 && (
<div className="p-4 text-secondary text-center"> <div className="p-4 text-slate-500 text-center">
No items found matching "{query}" No items found matching "{query}"
</div> </div>
)} )}
{!isLoading && !error && results.length === 0 && query.length === 0 && ( {!isLoading && !error && results.length === 0 && query.length === 0 && (
<div className="p-4 text-secondary text-center"> <div className="p-4 text-slate-500 text-center">
Start typing to search Start typing to search
</div> </div>
)} )}
{!isLoading && results.length > 0 && ( {!isLoading && results.length > 0 && (
<div className="divide-y divide-outline/30"> <div className="divide-y divide-slate-800">
{results.map((item) => ( {results.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => handleSelectItem(item)} onClick={() => handleSelectItem(item)}
className="w-full p-4 text-left hover:bg-surface-container transition-colors focus:outline-none focus:bg-surface-container focus:ring-2 focus:ring-primary/50" className="w-full p-4 text-left hover:bg-slate-900 transition-colors focus:outline-none focus:bg-slate-900 focus:ring-2 focus:ring-primary/50"
> >
<div className="flex justify-between items-start gap-2 mb-1"> <div className="flex justify-between items-start gap-2 mb-1">
<h3 className="font-normal text-foreground flex-1"> <h3 className="font-normal text-slate-100 flex-1">
{item.name} {item.name}
</h3> </h3>
<span className="text-primary font-normal text-sm whitespace-nowrap"> <span className="text-primary font-normal text-sm whitespace-nowrap">
Qty: {item.quantity} Qty: {item.quantity}
</span> </span>
</div> </div>
<div className="text-xs text-secondary space-y-1"> <div className="text-xs text-slate-500 space-y-1">
{item.part_number && ( {item.part_number && (
<div>PN: {item.part_number}</div> <div>PN: {item.part_number}</div>
)} )}

View File

@@ -4,4 +4,4 @@
"BACKEND_SSL_PORT": 8918, "BACKEND_SSL_PORT": 8918,
"FRONTEND_PORT": 8917, "FRONTEND_PORT": 8917,
"FRONTEND_SSL_PORT": 8919 "FRONTEND_SSL_PORT": 8919
} }

View File

@@ -4,52 +4,47 @@ const config: Config = {
content: [ content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
"./lib/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
fontSize: { fontSize: {
'xs': '13px', 'xs': '12px',
'sm': '15px', 'sm': '14px', // Design: label-md, mono-data
'base': '18px', 'base': '16px', // Design: body-md
'lg': '20px', 'lg': '18px', // Design: body-lg
'xl': '22px', 'xl': '20px',
'2xl': '26px', '2xl': '24px', // Design: headline-sm
'3xl': '32px', '3xl': '30px',
'4xl': '36px', '4xl': '32px', // Design: headline-md
'5xl': '48px', '5xl': '48px', // Design: headline-lg
'6xl': '64px',
}, },
extend: { extend: {
fontFamily: { fontFamily: {
sans: ["var(--font-body)", "Inter", "sans-serif"], sans: ["'Space Grotesk'", "sans-serif"],
data: ["var(--font-data)", "Space Grotesk", "sans-serif"],
}, },
colors: { colors: {
background: "#0c1322", background: "#131313",
foreground: "#dce2f7", foreground: "#FFFFFF",
surface: { surface: "#0A0A0A",
DEFAULT: "#0c1322", "surface-container": "#121212",
container: "#191f2f", "surface-bright": "#1A1A1A",
bright: "#323949",
},
primary: { primary: {
DEFAULT: "#ffc174", DEFAULT: "#F58618",
foreground: "#472a00", foreground: "#000000",
}, },
secondary: { secondary: {
DEFAULT: "#ffb599", DEFAULT: "#888888",
foreground: "#5a1c00", foreground: "#FFFFFF",
}, },
muted: { muted: {
DEFAULT: "#8B95AD", DEFAULT: "#444444",
foreground: "#dce2f7", foreground: "#888888",
}, },
outline: { border: "#222222",
DEFAULT: "#a08e7a", success: "#00FF41",
variant: "#534434", error: "#FF3131",
}, warning: "#F58618",
border: "#a08e7a",
success: "#10B981",
error: "#ffb4ab",
warning: "#f59e0b",
}, },
keyframes: { keyframes: {
scan: { scan: {

View File

@@ -110,7 +110,7 @@ describe('InventoryTable - Photo Display', () => {
const thumbnail = screen.getByAltText('Test Item'); const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail.parentElement).toHaveClass( expect(thumbnail.parentElement).toHaveClass(
'border-2', 'border-2',
'border-foreground' 'border-slate-300'
); );
}); });

View File

@@ -230,7 +230,7 @@ describe('ItemDetailModal', () => {
/> />
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const hasDeleteButton = buttons.some(b => b.className?.includes('error')); const hasDeleteButton = buttons.some(b => b.className?.includes('rose-500'));
expect(hasDeleteButton).toBe(true); expect(hasDeleteButton).toBe(true);
}); });
@@ -246,7 +246,7 @@ describe('ItemDetailModal', () => {
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const deleteButton = buttons.find(b => b.className?.includes('error')); const deleteButton = buttons.find(b => b.className?.includes('rose-500'));
if (deleteButton) { if (deleteButton) {
fireEvent.click(deleteButton); fireEvent.click(deleteButton);
@@ -269,7 +269,7 @@ describe('ItemDetailModal', () => {
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const deleteButton = buttons.find(b => b.className?.includes('error')); const deleteButton = buttons.find(b => b.className?.includes('rose-500'));
if (deleteButton) { if (deleteButton) {
fireEvent.click(deleteButton); fireEvent.click(deleteButton);
@@ -291,7 +291,7 @@ describe('ItemDetailModal', () => {
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const deleteButton = buttons.find(b => b.className?.includes('error')); const deleteButton = buttons.find(b => b.className?.includes('rose-500'));
if (deleteButton) { if (deleteButton) {
fireEvent.click(deleteButton); fireEvent.click(deleteButton);
@@ -313,7 +313,7 @@ describe('ItemDetailModal', () => {
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const deleteButton = buttons.find(b => b.className?.includes('error')); const deleteButton = buttons.find(b => b.className?.includes('rose-500'));
if (deleteButton) { if (deleteButton) {
fireEvent.click(deleteButton); fireEvent.click(deleteButton);
@@ -338,7 +338,7 @@ describe('ItemDetailModal', () => {
); );
const buttons = screen.getAllByRole('button'); const buttons = screen.getAllByRole('button');
const deleteButton = buttons.find(b => b.className?.includes('error')); const deleteButton = buttons.find(b => b.className?.includes('rose-500'));
if (deleteButton) { if (deleteButton) {
fireEvent.click(deleteButton); fireEvent.click(deleteButton);

View File

@@ -116,8 +116,8 @@ describe('ItemPhotoUpload Component', () => {
const cameraButton = screen.getByRole('button', { name: /camera/i }) const cameraButton = screen.getByRole('button', { name: /camera/i })
// Check for Tailwind classes (basic check) // Check for Tailwind classes (basic check)
expect(uploadButton.className).toContain('') expect(uploadButton.className).toContain('rounded')
expect(cameraButton.className).toContain('') expect(cameraButton.className).toContain('rounded')
}) })
it('should render responsive layout with gap', () => { it('should render responsive layout with gap', () => {

View File

@@ -63,7 +63,7 @@ describe('PhotoModal', () => {
// Check for rose-colored X icon // Check for rose-colored X icon
const xIcon = closeButton.querySelector('svg'); const xIcon = closeButton.querySelector('svg');
expect(xIcon).toHaveClass('text-error'); expect(xIcon).toHaveClass('text-rose-500');
}); });
}); });
@@ -229,7 +229,7 @@ describe('PhotoModal', () => {
const backdrop = screen.getByRole('dialog'); const backdrop = screen.getByRole('dialog');
// The modal content div is the second child of backdrop // The modal content div is the second child of backdrop
const modalContent = backdrop.querySelector('div[class*=""]'); const modalContent = backdrop.querySelector('div[class*="rounded-3xl"]');
expect(modalContent).toHaveClass('max-w-2xl', 'w-full', 'max-h-[90vh]'); expect(modalContent).toHaveClass('max-w-2xl', 'w-full', 'max-h-[90vh]');
}); });