chore: update dashboard-ui submodule
NexusGuard CI / server-core-test (push) Failing after 5s
NexusGuard CI / server-core-build (push) Has been skipped
NexusGuard CI / device-agent-test (push) Failing after 4s
NexusGuard CI / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI / dashboard-test (push) Failing after 4s
NexusGuard CI / dashboard-dist (push) Has been skipped
NexusGuard CI / server-core-test (push) Failing after 5s
NexusGuard CI / server-core-build (push) Has been skipped
NexusGuard CI / device-agent-test (push) Failing after 4s
NexusGuard CI / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI / dashboard-test (push) Failing after 4s
NexusGuard CI / dashboard-dist (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
# Button Consistency Normalization
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: Replace all raw `<button>` elements across the dashboard with `NgButton` component, normalize sizing/spacing, remove `flex-1` stretching, and ensure consistent button patterns everywhere.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - All raw `<button>` replaced with `NgButton`
|
||||
> - DeviceDetail.vue buttons de-stretched (no more `flex-1`)
|
||||
> - Servers.vue modal buttons use NgButton
|
||||
> - TrafficHistory.vue buttons + hardcoded colors fixed
|
||||
> - Consistent button hierarchy: primary/secondary/ghost/danger
|
||||
>
|
||||
> **Estimated Effort**: Quick
|
||||
> **Parallel Execution**: YES - 1 wave
|
||||
> **Critical Path**: All tasks independent, can run in parallel
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User says: "jadikan semua tombol ini sama. seperti di traffic menu. contoh di view device. tombol besar jelek. tombol diskonek jelen buat semua tombol setara"
|
||||
|
||||
Translation: Make all buttons the same. Like in the traffic menu. Example in device view — big ugly buttons, disconnect button looks bad. Make all buttons equal.
|
||||
|
||||
### Interview Summary
|
||||
**Key Discussions**:
|
||||
- DeviceDetail.vue has `flex-1` buttons that stretch to fill container — visually heavy
|
||||
- Multiple views still use raw `<button>` with hardcoded Tailwind classes instead of NgButton
|
||||
- TrafficHistory.vue itself still has hardcoded colors (`text-cyan-400`, `bg-cyan-600`, `bg-black/30`)
|
||||
- Inconsistent button patterns: some NgButton, some raw, some with `flex-1`, some without
|
||||
|
||||
### Button Hierarchy Standard
|
||||
| Context | NgButton Config | Rationale |
|
||||
|---------|----------------|-----------|
|
||||
| Form submit (Save, Create, Register) | `variant="primary" size="md"` | Primary action |
|
||||
| Cancel / Close / secondary | `variant="secondary" size="md"` | Destructive-neutral |
|
||||
| Delete (dangerous) | `variant="danger" size="md"` | Destructive |
|
||||
| Table row actions (Edit, Config, Firewall) | `variant="ghost" size="sm"` | Inline, low visual weight |
|
||||
| Small utility (Copy, Refresh) | `size="sm"` | Compact |
|
||||
| Action button row | `gap-2` NOT `space-x-3` OR `flex-1` | Consistent spacing |
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Make every button in the dashboard use `NgButton` with consistent sizing, spacing, and variant hierarchy.
|
||||
|
||||
### Concrete Deliverables
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — buttons de-stretched
|
||||
- `apps/dashboard-ui/src/views/Servers.vue` — modal buttons use NgButton
|
||||
- `apps/dashboard-ui/src/views/Users.vue` — delete button uses NgButton
|
||||
- `apps/dashboard-ui/src/views/Devices.vue` — action buttons use NgButton
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — buttons use NgButton + design tokens
|
||||
- `apps/dashboard-ui/src/App.vue` — logout button uses NgButton
|
||||
|
||||
### Must Have
|
||||
- Zero raw `<button>` with hardcoded Tailwind classes (except toggle switches and accordion chevrons)
|
||||
- All action buttons use `NgButton` with appropriate variant/size
|
||||
- No `flex-1` on button rows (causes ugly stretching)
|
||||
- Consistent `gap-2` spacing between button groups
|
||||
|
||||
### Must NOT Have (Guardrails)
|
||||
- Do NOT change NgButton component itself
|
||||
- Do NOT change any API calls or data flow
|
||||
- Do NOT change toggle switches (they're custom CSS, not buttons)
|
||||
- Do NOT change accordion chevron toggles (functional, not action buttons)
|
||||
- Do NOT touch Login.vue (already redesigned)
|
||||
- Do NOT touch FirewallEditor.vue (already redesigned)
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed.
|
||||
|
||||
### QA Policy
|
||||
Every task includes agent-executed QA scenarios.
|
||||
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||
|
||||
- **Frontend/UI**: Build check via `npm run build`
|
||||
- **Grep checks**: Verify zero raw `<button class=` patterns (excluding known exceptions)
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
```
|
||||
Wave 1 (Start Immediately — all independent):
|
||||
├── Task 1: Normalize DeviceDetail.vue buttons [quick]
|
||||
├── Task 2: Normalize Servers.vue modal buttons [quick]
|
||||
├── Task 3: Normalize Users.vue delete button [quick]
|
||||
├── Task 4: Normalize Devices.vue action buttons [quick]
|
||||
├── Task 5: Normalize TrafficHistory.vue buttons + tokens [quick]
|
||||
├── Task 6: Normalize App.vue logout button [quick]
|
||||
|
||||
Wave FINAL (After ALL tasks):
|
||||
├── Build verify: npm run build
|
||||
├── Grep verify: zero raw button patterns
|
||||
└── Present results to user
|
||||
```
|
||||
|
||||
### Dependency Matrix
|
||||
- All tasks (1-6): No dependencies — can all run in parallel
|
||||
- Final verification: After all tasks complete
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Normalize DeviceDetail.vue buttons
|
||||
|
||||
**What to do**:
|
||||
- Line 93: Keep `w-full` on save settings button (it's inside a form, full-width is correct)
|
||||
- Lines 114-124: Remove `flex-1` from all 3 action buttons (Regenerate Token, Regenerate Keys, Delete Device). Change `flex space-x-3` to `flex items-center gap-2`
|
||||
- Lines 141-145: Remove `flex-1` from Config & QR button. Remove the wrapping `<div class="flex space-x-3">` since it's a single button — just use `<NgButton>` directly
|
||||
- Line 153: Copy token button already `size="sm"` — OK
|
||||
- Line 187: Refresh button already `size="sm"` — OK
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the save settings `w-full` (form submit, full-width is correct)
|
||||
- Do NOT change toggle switches or accordion chevrons
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 2-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — Lines 114-124 (action buttons), 141-145 (config button)
|
||||
- `apps/dashboard-ui/src/components/ui/NgButton.vue` — API: variant, size, loading, disabled
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No flex-1 on NgButton in DeviceDetail
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep "flex-1" apps/dashboard-ui/src/views/DeviceDetail.vue
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-1-no-flex1.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-1-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 2-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 2. Normalize Servers.vue modal buttons
|
||||
|
||||
**What to do**:
|
||||
- Line 127: `<button type="submit" ... class="flex-1 bg-accent ...">Register Node</button>` → `<NgButton type="submit" :loading="loading">Register Node</NgButton>`
|
||||
- Line 128: `<button type="button" @click="showAddModal = false" ... class="flex-1 bg-bg-elevated ...">Cancel</button>` → `<NgButton variant="secondary" @click="showAddModal = false">Cancel</NgButton>`
|
||||
- Line 281: `<button type="submit" ... class="flex-1 bg-accent ...">Save</button>` → `<NgButton type="submit" :loading="loading">Save</NgButton>`
|
||||
- Line 282: `<button type="button" @click="closeEdit" ... class="flex-1 bg-bg-elevated ...">Cancel</button>` → `<NgButton variant="secondary" @click="closeEdit">Cancel</NgButton>`
|
||||
- Button rows in modals: wrap in `<div class="flex items-center gap-2 pt-4">`
|
||||
- Lines 156-157: Table action buttons (Edit, Delete) → `NgButton variant="ghost" size="sm"`
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change form inputs or validation logic
|
||||
- Do NOT change toggle switches
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1, 3-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Servers.vue` — Lines 127-128 (add modal buttons), 156-157 (table actions), 281-282 (edit modal buttons)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Servers modal
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Servers.vue | grep -v 'NgButton' | grep -v 'toggle' | grep -v 'chevron' | grep -v 'accordion'
|
||||
Expected Result: 0 matches (excluding toggle/accordion)
|
||||
Evidence: .sisyphus/evidence/task-2-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-2-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1, 3-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 3. Normalize Users.vue delete button
|
||||
|
||||
**What to do**:
|
||||
- Line 51: `<button v-if="user.Username !== 'admin'" @click="handleDelete(user.ID)" class="text-danger hover:text-danger text-sm font-semibold">Delete</button>` → `<NgButton v-if="user.Username !== 'admin'" variant="ghost" size="sm" @click="handleDelete(user.ID)">Delete</NgButton>`
|
||||
- Import NgButton if not already imported (it is — line 67)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the delete confirmation logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-2, 4-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Users.vue` — Line 51
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Users
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Users.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-3-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-3-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-2, 4-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 4. Normalize Devices.vue action buttons
|
||||
|
||||
**What to do**:
|
||||
- Lines 45-47: Replace raw `<button>` with `NgButton variant="ghost" size="sm"`:
|
||||
```vue
|
||||
<NgButton v-if="device.InternalIP && authStore.isAdmin" variant="ghost" size="sm" @click="openConfigModal(device.ID)">Config</NgButton>
|
||||
<NgButton variant="ghost" size="sm" @click="openFirewall(device.ID)">Firewall</NgButton>
|
||||
<NgButton variant="ghost" size="sm" @click="handleDelete(device.ID, device.Name)">Delete</NgButton>
|
||||
```
|
||||
- Import NgButton if not already imported (it is — line 78)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the delete confirmation logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-3, 5-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Devices.vue` — Lines 45-47
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Devices
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Devices.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-4-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-4-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-3, 5-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 5. Normalize TrafficHistory.vue buttons + hardcoded colors
|
||||
|
||||
**What to do**:
|
||||
- Import NgButton and useToast/useConfirm if needed
|
||||
- Line 5-11: Export CSV button → `<NgButton @click="exportToCSV" :disabled="trafficData.length === 0">Export CSV</NgButton>`
|
||||
- Lines 53-59: Apply Filters button → `<NgButton type="submit" :loading="loading">{{ loading ? 'Loading...' : 'Apply Filters' }}</NgButton>`
|
||||
- Lines 110-116: Previous button → `<NgButton size="sm" @click="currentPage--" :disabled="currentPage === 1">Previous</NgButton>`
|
||||
- Lines 118-124: Next button → `<NgButton size="sm" @click="currentPage++" :disabled="currentPage >= totalPages">Next</NgButton>`
|
||||
- Replace ALL hardcoded colors with design tokens:
|
||||
- `text-cyan-400` → `text-accent`
|
||||
- `bg-cyan-600` → `bg-accent`
|
||||
- `bg-black/30` → `bg-bg-base/30`
|
||||
- `border-white/5` → `border-border-subtle/50`
|
||||
- `text-gray-400` → `text-text-muted`
|
||||
- `text-gray-500` → `text-text-muted`
|
||||
- `bg-gradient-to-br from-gray-900/90 to-gray-800/90` → `bg-gradient-to-br from-bg-surface/90 to-bg-elevated/90`
|
||||
- `border-white/10` → `border-border-subtle`
|
||||
- `text-white` → `text-text-primary`
|
||||
- `bg-black/50` → `bg-bg-base/50`
|
||||
- `hover:bg-white/5` → `hover:bg-bg-elevated/50`
|
||||
- `hover:bg-black/50` → `hover:bg-bg-base/50`
|
||||
- `hover:text-white` → `hover:text-text-primary`
|
||||
- `text-cyan-400` → `text-accent`
|
||||
- `text-blue-400` → `text-info`
|
||||
- `hover:shadow-cyan-500/50` → `hover:shadow-accent/50`
|
||||
- Update `<NgButton>` import
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the CSV export logic
|
||||
- Do NOT change the pagination logic
|
||||
- Do NOT change the date filtering logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-4, 6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Full file (326 lines). Raw buttons + extensive hardcoded colors
|
||||
- `apps/dashboard-ui/src/components/ui/NgButton.vue` — API reference
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in TrafficHistory
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/TrafficHistory.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-5-no-raw-button.txt
|
||||
|
||||
Scenario: No hardcoded colors
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -cE "text-cyan-|bg-cyan-|bg-black|border-white/|text-gray-|text-blue-|hover:bg-white" apps/dashboard-ui/src/views/TrafficHistory.vue
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-5-no-hardcoded.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-5-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-4, 6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 6. Normalize App.vue logout button
|
||||
|
||||
**What to do**:
|
||||
- Line 16: `<button @click="authStore.logout" class="w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition">Logout</button>` → `<NgButton variant="ghost" @click="authStore.logout" class="w-full justify-start">Logout</NgButton>`
|
||||
- Line 24: `<button @click="authStore.logout" class="text-red-400 text-sm">Logout</button>` → `<NgButton variant="ghost" size="sm" @click="authStore.logout">Logout</NgButton>`
|
||||
- Import NgButton
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the logout logic
|
||||
- Do NOT change sidebar behavior
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-5)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/App.vue` — Lines 16, 24
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in App.vue
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/App.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-6-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-6-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-5)
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave
|
||||
|
||||
After ALL tasks complete:
|
||||
|
||||
```
|
||||
Wave FINAL:
|
||||
├── F1: Build verify — npm run build passes
|
||||
├── F2: Grep verify — zero raw <button class= across all .vue files
|
||||
├── F3: Grep verify — zero hardcoded colors in TrafficHistory.vue
|
||||
└── F4: Present results to user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
- **Commit D**: All button normalization changes
|
||||
- Files: `DeviceDetail.vue`, `Servers.vue`, `Users.vue`, `Devices.vue`, `TrafficHistory.vue`, `App.vue`
|
||||
- Pre-commit: `cd apps/dashboard-ui && npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Verification Commands
|
||||
```bash
|
||||
cd apps/dashboard-ui && npm run build # Expected: ✓ built in Xs
|
||||
grep -rn '<button' src/ --include="*.vue" | grep -v 'NgButton' | grep -v 'toggle' | grep -v 'chevron' | grep -v 'sr-only' # Expected: 0 matches (excluding known exceptions)
|
||||
grep -cE "text-cyan-|bg-cyan-|bg-black|border-white/" src/views/TrafficHistory.vue # Expected: 0
|
||||
```
|
||||
|
||||
### Final Checklist
|
||||
- [x] All raw `<button>` replaced with `NgButton`
|
||||
- [x] No `flex-1` on button rows
|
||||
- [x] Consistent `gap-2` spacing
|
||||
- [x] TrafficHistory.vue hardcoded colors replaced with tokens
|
||||
- [x] Frontend builds without errors
|
||||
Reference in New Issue
Block a user