Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d5d9337a | ||
|
|
cd00a2e0fa | ||
|
|
b443fc787e | ||
|
|
4a6e6dea96 | ||
|
|
638b868649 | ||
|
|
433d0926e6 | ||
|
|
7e545140a2 | ||
|
|
e572df8558 | ||
|
|
a3afe3cb1b | ||
|
|
4841a7ad7c |
1
.gitignore
vendored
@@ -5,3 +5,4 @@ dist/
|
|||||||
test-results/
|
test-results/
|
||||||
screenshots/
|
screenshots/
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
.gitnexus
|
||||||
|
|||||||
119
CLAUDE.md
@@ -119,13 +119,114 @@ import type { ChartSeries, DataPoint } from '@cameleer/design-system'
|
|||||||
// Styles (once, at app root)
|
// Styles (once, at app root)
|
||||||
import '@cameleer/design-system/style.css'
|
import '@cameleer/design-system/style.css'
|
||||||
|
|
||||||
// Brand assets (static files — logo variants and favicon sizes)
|
// Brand assets (static files via ./assets/* export)
|
||||||
import logo from '@cameleer/design-system/logo' // full resolution PNG
|
import logo from '@cameleer/design-system/assets/cameleer3-logo.png' // full resolution
|
||||||
import logo16 from '@cameleer/design-system/logo-16' // 16×16 favicon
|
import logo32 from '@cameleer/design-system/assets/cameleer3-32.png' // 32×32 favicon
|
||||||
import logo32 from '@cameleer/design-system/logo-32' // 32×32 favicon
|
import logo180 from '@cameleer/design-system/assets/cameleer3-180.png' // Apple touch icon
|
||||||
import logo48 from '@cameleer/design-system/logo-48' // 48×48 taskbar
|
import logo192 from '@cameleer/design-system/assets/cameleer3-192.png' // Android/PWA icon
|
||||||
import logo180 from '@cameleer/design-system/logo-180' // Apple touch icon
|
import logo512 from '@cameleer/design-system/assets/cameleer3-512.png' // PWA splash, og:image
|
||||||
import logo192 from '@cameleer/design-system/logo-192' // Android/PWA icon
|
import logoSvg from '@cameleer/design-system/assets/cameleer3-logo.svg' // high-detail SVG logo
|
||||||
import logo512 from '@cameleer/design-system/logo-512' // PWA splash, og:image
|
import camelSvg from '@cameleer/design-system/assets/camel-logo.svg' // simplified camel SVG
|
||||||
import logoSvg from '@cameleer/design-system/logo-svg' // SVG vector logo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- gitnexus:start -->
|
||||||
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
|
This project is indexed by GitNexus as **design-system** (1461 symbols, 2336 relationships, 23 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||||
|
|
||||||
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
## Always Do
|
||||||
|
|
||||||
|
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||||
|
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||||
|
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||||
|
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||||
|
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||||
|
|
||||||
|
## When Debugging
|
||||||
|
|
||||||
|
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||||
|
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||||
|
3. `READ gitnexus://repo/design-system/process/{processName}` — trace the full execution flow step by step
|
||||||
|
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||||
|
|
||||||
|
## When Refactoring
|
||||||
|
|
||||||
|
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||||
|
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||||
|
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||||
|
|
||||||
|
## Never Do
|
||||||
|
|
||||||
|
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||||
|
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||||
|
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||||
|
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||||
|
|
||||||
|
## Tools Quick Reference
|
||||||
|
|
||||||
|
| Tool | When to use | Command |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||||
|
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||||
|
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||||
|
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||||
|
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||||
|
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||||
|
|
||||||
|
## Impact Risk Levels
|
||||||
|
|
||||||
|
| Depth | Meaning | Action |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||||
|
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||||
|
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
| Resource | Use for |
|
||||||
|
|----------|---------|
|
||||||
|
| `gitnexus://repo/design-system/context` | Codebase overview, check index freshness |
|
||||||
|
| `gitnexus://repo/design-system/clusters` | All functional areas |
|
||||||
|
| `gitnexus://repo/design-system/processes` | All execution flows |
|
||||||
|
| `gitnexus://repo/design-system/process/{name}` | Step-by-step execution trace |
|
||||||
|
|
||||||
|
## Self-Check Before Finishing
|
||||||
|
|
||||||
|
Before completing any code modification task, verify:
|
||||||
|
1. `gitnexus_impact` was run for all modified symbols
|
||||||
|
2. No HIGH/CRITICAL risk warnings were ignored
|
||||||
|
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||||
|
4. All d=1 (WILL BREAK) dependents were updated
|
||||||
|
|
||||||
|
## Keeping the Index Fresh
|
||||||
|
|
||||||
|
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx gitnexus analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx gitnexus analyze --embeddings
|
||||||
|
```
|
||||||
|
|
||||||
|
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||||
|
|
||||||
|
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
| Task | Read this skill file |
|
||||||
|
|------|---------------------|
|
||||||
|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||||
|
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||||
|
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||||
|
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||||
|
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||||
|
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||||
|
|
||||||
|
<!-- gitnexus:end -->
|
||||||
|
|||||||
@@ -336,23 +336,24 @@ See `CLAUDE.md` "Using This Design System in Other Apps" for full setup instruct
|
|||||||
|
|
||||||
## Brand Assets
|
## Brand Assets
|
||||||
|
|
||||||
The design system ships logo assets as static files via package exports. These are not React components — they resolve to file URLs when imported via a bundler.
|
The design system ships logo assets as static files via the `./assets/*` package export. These are not React components — they resolve to file URLs when imported via a bundler. All PNGs have transparent backgrounds.
|
||||||
|
|
||||||
| Export | Size | Use case |
|
| File | Size | Use case |
|
||||||
|--------|------|----------|
|
|------|------|----------|
|
||||||
| `@cameleer/design-system/logo` | Original | Full resolution for print/marketing |
|
| `cameleer3-logo.png` | Original | Full resolution for print/marketing |
|
||||||
| `@cameleer/design-system/logo-16` | 16×16 | Browser tab favicon |
|
| `cameleer3-16.png` | 16×16 | Browser tab favicon |
|
||||||
| `@cameleer/design-system/logo-32` | 32×32 | Standard favicon, bookmarks |
|
| `cameleer3-32.png` | 32×32 | Standard favicon, bookmarks |
|
||||||
| `@cameleer/design-system/logo-48` | 48×48 | Windows taskbar |
|
| `cameleer3-48.png` | 48×48 | Windows taskbar |
|
||||||
| `@cameleer/design-system/logo-180` | 180×180 | Apple touch icon |
|
| `cameleer3-180.png` | 180×180 | Apple touch icon |
|
||||||
| `@cameleer/design-system/logo-192` | 192×192 | Android/PWA icon |
|
| `cameleer3-192.png` | 192×192 | Android/PWA icon |
|
||||||
| `@cameleer/design-system/logo-512` | 512×512 | PWA splash, og:image |
|
| `cameleer3-512.png` | 512×512 | PWA splash, og:image |
|
||||||
| `@cameleer/design-system/logo-svg` | Vector | SVG logo for scalable usage |
|
| `cameleer3-logo.svg` | Vector | High-detail SVG logo (traced from PNG, transparent) |
|
||||||
|
| `camel-logo.svg` | Vector | Simplified camel SVG logo |
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import logo from '@cameleer/design-system/logo-512'
|
import logo from '@cameleer/design-system/assets/cameleer3-512.png'
|
||||||
<img src={logo} alt="Cameleer3" />
|
<img src={logo} alt="Cameleer3" />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 983 B After Width: | Height: | Size: 941 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 342 KiB |
112
assets/cameleer3-logo.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.6",
|
"version": "0.1.41",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.es.js",
|
"main": "./dist/index.es.js",
|
||||||
"module": "./dist/index.es.js",
|
"module": "./dist/index.es.js",
|
||||||
@@ -11,14 +11,7 @@
|
|||||||
"import": "./dist/index.es.js"
|
"import": "./dist/index.es.js"
|
||||||
},
|
},
|
||||||
"./style.css": "./dist/style.css",
|
"./style.css": "./dist/style.css",
|
||||||
"./logo": "./assets/cameleer3-logo.png",
|
"./assets/*": "./assets/*"
|
||||||
"./logo-16": "./assets/cameleer3-16.png",
|
|
||||||
"./logo-32": "./assets/cameleer3-32.png",
|
|
||||||
"./logo-48": "./assets/cameleer3-48.png",
|
|
||||||
"./logo-180": "./assets/cameleer3-180.png",
|
|
||||||
"./logo-192": "./assets/cameleer3-192.png",
|
|
||||||
"./logo-512": "./assets/cameleer3-512.png",
|
|
||||||
"./logo-svg": "./assets/camel-logo.svg"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
112
src/assets/cameleer3-logo.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@@ -72,7 +72,7 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +130,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legendLabel {
|
.legendLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Axis labels */
|
/* Axis labels */
|
||||||
.yLabel {
|
.yLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
|
|
||||||
.xLabel {
|
.xLabel {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding-left: 48px;
|
padding-left: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
background-color: var(--bg-inset);
|
background-color: var(--bg-inset);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipRow {
|
.tooltipRow {
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,11 +117,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legendLabel {
|
.legendLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yLabel {
|
.yLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
.xLabel {
|
.xLabel {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding-left: 48px;
|
padding-left: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
.sep {
|
.sep {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
border: 1px solid var(--amber-light);
|
border: 1px solid var(--amber-light);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 1px 7px;
|
padding: 1px 7px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
@@ -239,13 +239,13 @@
|
|||||||
|
|
||||||
.itemTime {
|
.itemTime {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemMeta {
|
.itemMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
|
|
||||||
/* Match context snippet */
|
/* Match context snippet */
|
||||||
.matchContext {
|
.matchContext {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
@@ -316,6 +316,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
.th {
|
.th {
|
||||||
padding: 9px 14px;
|
padding: 9px 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
.rangeInfo {
|
.rangeInfo {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationRight {
|
.paginationRight {
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pageSizeLabel {
|
.pageSizeLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
|
|
||||||
.pageNum {
|
.pageNum {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
border: 1px solid var(--amber-light);
|
border: 1px solid var(--amber-light);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
color: var(--amber-deep);
|
color: var(--amber-deep);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clearAll {
|
.clearAll {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
.trend {
|
.trend {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
.trendMuted { color: var(--text-muted); }
|
.trendMuted { color: var(--text-muted); }
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +120,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legendLabel {
|
.legendLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yLabel {
|
.yLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
|
|
||||||
.xLabel {
|
.xLabel {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding-left: 48px;
|
padding-left: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
.dividerText {
|
.dividerText {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.forgotLink {
|
.forgotLink {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--amber);
|
color: var(--amber);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row.clickable {
|
.row.clickable {
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
.dur {
|
.dur {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
|
|||||||
import { Play, Cog, Square, Diamond, AlertTriangle, EllipsisVertical } from 'lucide-react'
|
import { Play, Cog, Square, Diamond, AlertTriangle, EllipsisVertical } from 'lucide-react'
|
||||||
import styles from './RouteFlow.module.css'
|
import styles from './RouteFlow.module.css'
|
||||||
import { Dropdown } from '../Dropdown/Dropdown'
|
import { Dropdown } from '../Dropdown/Dropdown'
|
||||||
|
import { formatDuration } from '../../../utils/format-utils'
|
||||||
|
|
||||||
export interface NodeBadge {
|
export interface NodeBadge {
|
||||||
label: string
|
label: string
|
||||||
@@ -42,12 +43,6 @@ interface RouteFlowProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function durationClass(ms: number, status: string): string {
|
function durationClass(ms: number, status: string): string {
|
||||||
if (status === 'fail') return styles.durBreach
|
if (status === 'fail') return styles.durBreach
|
||||||
if (ms < 50) return styles.durFast
|
if (ms < 50) return styles.durFast
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type { Column, DataTableProps } from './DataTable/types'
|
|||||||
export { DetailPanel } from './DetailPanel/DetailPanel'
|
export { DetailPanel } from './DetailPanel/DetailPanel'
|
||||||
export { EntityList } from './EntityList/EntityList'
|
export { EntityList } from './EntityList/EntityList'
|
||||||
export { Dropdown } from './Dropdown/Dropdown'
|
export { Dropdown } from './Dropdown/Dropdown'
|
||||||
|
export type { DropdownItem } from './Dropdown/Dropdown'
|
||||||
export { EventFeed } from './EventFeed/EventFeed'
|
export { EventFeed } from './EventFeed/EventFeed'
|
||||||
export { GroupCard } from './GroupCard/GroupCard'
|
export { GroupCard } from './GroupCard/GroupCard'
|
||||||
export { KpiStrip } from './KpiStrip/KpiStrip'
|
export { KpiStrip } from './KpiStrip/KpiStrip'
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
.version {
|
.version {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.treeSectionLabel {
|
.treeSectionLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -345,7 +345,7 @@
|
|||||||
/* Badge */
|
/* Badge */
|
||||||
.treeBadge {
|
.treeBadge {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
|
|||||||
22
src/design-system/layout/TopBar/AutoRefreshToggle.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import styles from './TopBar.module.css'
|
||||||
|
|
||||||
|
interface AutoRefreshToggleProps {
|
||||||
|
active: boolean
|
||||||
|
onChange: (active: boolean) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AutoRefreshToggle({ active, onChange, className }: AutoRefreshToggleProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`${styles.liveToggle} ${active ? styles.liveToggleActive : ''} ${className ?? ''}`}
|
||||||
|
onClick={() => onChange(!active)}
|
||||||
|
type="button"
|
||||||
|
aria-label={active ? 'Disable auto-refresh' : 'Enable auto-refresh'}
|
||||||
|
title={active ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'}
|
||||||
|
>
|
||||||
|
<span className={styles.liveDot} />
|
||||||
|
{active ? 'AUTO' : 'MANUAL'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
src/design-system/layout/TopBar/SearchTrigger.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import styles from './TopBar.module.css'
|
||||||
|
|
||||||
|
interface SearchTriggerProps {
|
||||||
|
onClick: () => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchTrigger({ onClick, className }: SearchTriggerProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`${styles.search} ${className ?? ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
aria-label="Open search"
|
||||||
|
>
|
||||||
|
<span className={styles.searchIcon} aria-hidden="true">
|
||||||
|
<Search size={13} />
|
||||||
|
</span>
|
||||||
|
<span className={styles.searchPlaceholder}>Search... ⌘K</span>
|
||||||
|
<span className={styles.kbd}>Ctrl+K</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
.kbd {
|
.kbd {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
import { Search, Moon, Sun, Power } from 'lucide-react'
|
import { Moon, Sun, Power } from 'lucide-react'
|
||||||
import styles from './TopBar.module.css'
|
import styles from './TopBar.module.css'
|
||||||
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
|
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
|
||||||
import { Dropdown } from '../../composites/Dropdown/Dropdown'
|
import { Dropdown } from '../../composites/Dropdown/Dropdown'
|
||||||
import { Avatar } from '../../primitives/Avatar/Avatar'
|
import { Avatar } from '../../primitives/Avatar/Avatar'
|
||||||
import { ButtonGroup } from '../../primitives/ButtonGroup/ButtonGroup'
|
|
||||||
import type { ButtonGroupItem } from '../../primitives/ButtonGroup/ButtonGroup'
|
|
||||||
import { TimeRangeDropdown } from '../../primitives/TimeRangeDropdown/TimeRangeDropdown'
|
|
||||||
import { useGlobalFilters } from '../../providers/GlobalFilterProvider'
|
|
||||||
import { useCommandPalette } from '../../providers/CommandPaletteProvider'
|
|
||||||
import { useTheme } from '../../providers/ThemeProvider'
|
import { useTheme } from '../../providers/ThemeProvider'
|
||||||
import { useBreadcrumbOverride } from '../../providers/BreadcrumbProvider'
|
import { useBreadcrumbOverride } from '../../providers/BreadcrumbProvider'
|
||||||
import type { BreadcrumbItem } from '../../providers/BreadcrumbProvider'
|
import type { BreadcrumbItem } from '../../providers/BreadcrumbProvider'
|
||||||
@@ -17,26 +12,21 @@ interface TopBarProps {
|
|||||||
breadcrumb: BreadcrumbItem[]
|
breadcrumb: BreadcrumbItem[]
|
||||||
environment?: ReactNode
|
environment?: ReactNode
|
||||||
user?: { name: string }
|
user?: { name: string }
|
||||||
|
userMenuItems?: import('../../composites/Dropdown/Dropdown').DropdownItem[]
|
||||||
onLogout?: () => void
|
onLogout?: () => void
|
||||||
className?: string
|
className?: string
|
||||||
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_ITEMS: ButtonGroupItem[] = [
|
|
||||||
{ value: 'completed', label: 'OK', color: 'var(--success)' },
|
|
||||||
{ value: 'warning', label: 'Warn', color: 'var(--warning)' },
|
|
||||||
{ value: 'failed', label: 'Error', color: 'var(--error)' },
|
|
||||||
{ value: 'running', label: 'Running', color: 'var(--running)' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
environment,
|
environment,
|
||||||
user,
|
user,
|
||||||
|
userMenuItems,
|
||||||
onLogout,
|
onLogout,
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
}: TopBarProps) {
|
}: TopBarProps) {
|
||||||
const globalFilters = useGlobalFilters()
|
|
||||||
const commandPalette = useCommandPalette()
|
|
||||||
const { theme, toggleTheme } = useTheme()
|
const { theme, toggleTheme } = useTheme()
|
||||||
const breadcrumbOverride = useBreadcrumbOverride()
|
const breadcrumbOverride = useBreadcrumbOverride()
|
||||||
|
|
||||||
@@ -45,54 +35,11 @@ export function TopBar({
|
|||||||
{/* Left: Breadcrumb */}
|
{/* Left: Breadcrumb */}
|
||||||
<Breadcrumb items={breadcrumbOverride ?? breadcrumb} className={styles.breadcrumb} />
|
<Breadcrumb items={breadcrumbOverride ?? breadcrumb} className={styles.breadcrumb} />
|
||||||
|
|
||||||
{/* Search trigger */}
|
{/* Center: consumer-provided controls */}
|
||||||
<button
|
{children}
|
||||||
className={styles.search}
|
|
||||||
onClick={() => commandPalette.setOpen(true)}
|
|
||||||
type="button"
|
|
||||||
aria-label="Open search"
|
|
||||||
>
|
|
||||||
<span className={styles.searchIcon} aria-hidden="true">
|
|
||||||
<Search size={13} />
|
|
||||||
</span>
|
|
||||||
<span className={styles.searchPlaceholder}>Search... ⌘K</span>
|
|
||||||
<span className={styles.kbd}>Ctrl+K</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Status filter group */}
|
{/* Right: theme toggle, env badge, user */}
|
||||||
<ButtonGroup
|
|
||||||
items={STATUS_ITEMS}
|
|
||||||
value={globalFilters.statusFilters}
|
|
||||||
onChange={(selected) => {
|
|
||||||
// Sync with global filter by toggling the diff
|
|
||||||
const current = globalFilters.statusFilters
|
|
||||||
for (const v of selected) {
|
|
||||||
if (!current.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
|
|
||||||
}
|
|
||||||
for (const v of current) {
|
|
||||||
if (!selected.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Time range pills */}
|
|
||||||
<TimeRangeDropdown
|
|
||||||
value={globalFilters.timeRange}
|
|
||||||
onChange={globalFilters.setTimeRange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Right: auto-refresh toggle, theme toggle, env badge, user */}
|
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
<button
|
|
||||||
className={`${styles.liveToggle} ${globalFilters.autoRefresh ? styles.liveToggleActive : ''}`}
|
|
||||||
onClick={() => globalFilters.setAutoRefresh(!globalFilters.autoRefresh)}
|
|
||||||
type="button"
|
|
||||||
aria-label={globalFilters.autoRefresh ? 'Disable auto-refresh' : 'Enable auto-refresh'}
|
|
||||||
title={globalFilters.autoRefresh ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'}
|
|
||||||
>
|
|
||||||
<span className={styles.liveDot} />
|
|
||||||
{globalFilters.autoRefresh ? 'AUTO' : 'MANUAL'}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className={styles.themeToggle}
|
className={styles.themeToggle}
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
@@ -114,6 +61,8 @@ export function TopBar({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
items={[
|
items={[
|
||||||
|
...(userMenuItems ?? []),
|
||||||
|
...(userMenuItems?.length ? [{ label: '', divider: true }] : []),
|
||||||
{ label: 'Logout', icon: <Power size={14} />, onClick: onLogout },
|
{ label: 'Logout', icon: <Power size={14} />, onClick: onLogout },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ export { SidebarTree } from './Sidebar/SidebarTree'
|
|||||||
export type { SidebarTreeNode } from './Sidebar/SidebarTree'
|
export type { SidebarTreeNode } from './Sidebar/SidebarTree'
|
||||||
export { useStarred } from './Sidebar/useStarred'
|
export { useStarred } from './Sidebar/useStarred'
|
||||||
export { TopBar } from './TopBar/TopBar'
|
export { TopBar } from './TopBar/TopBar'
|
||||||
|
export { SearchTrigger } from './TopBar/SearchTrigger'
|
||||||
|
export { AutoRefreshToggle } from './TopBar/AutoRefreshToggle'
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.sm { width: 24px; height: 24px; font-size: 9px; }
|
.sm { width: 24px; height: 24px; font-size: 9px; }
|
||||||
.md { width: 28px; height: 28px; font-size: 11px; }
|
.md { width: 28px; height: 28px; font-size: 12px; }
|
||||||
.lg { width: 40px; height: 40px; font-size: 14px; }
|
.lg { width: 40px; height: 40px; font-size: 14px; }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
.sm { padding: 4px 10px; font-size: 11px; }
|
.sm { padding: 4px 10px; font-size: 12px; }
|
||||||
.md { padding: 6px 14px; font-size: 12px; }
|
.md { padding: 6px 14px; font-size: 12px; }
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.titleText {
|
.titleText {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
@@ -49,5 +49,5 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dayHeader {
|
.dayHeader {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeLabel {
|
.timeLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.kbd {
|
.kbd {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.mono { font-family: var(--font-mono); }
|
.mono { font-family: var(--font-mono); }
|
||||||
.xs { font-size: 10px; }
|
.xs { font-size: 12px; }
|
||||||
.sm { font-size: 11px; }
|
.sm { font-size: 12px; }
|
||||||
.md { font-size: 13px; }
|
.md { font-size: 13px; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1.2px;
|
letter-spacing: 1.2px;
|
||||||
|
|||||||
@@ -35,6 +35,6 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,8 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.6s linear infinite;
|
animation: spin 0.6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1.2px;
|
letter-spacing: 1.2px;
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trend {
|
.trend {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
.trend.neutral { color: var(--text-muted); }
|
.trend.neutral { color: var(--text-muted); }
|
||||||
|
|
||||||
.detail {
|
.detail {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
padding: 5px 9px;
|
padding: 5px 9px;
|
||||||
background: var(--text-primary);
|
background: var(--text-primary);
|
||||||
color: var(--bg-surface);
|
color: var(--bg-surface);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
/* Typography */
|
/* Typography */
|
||||||
--font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
|
--font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
--font-size-min: 12px;
|
||||||
|
|
||||||
/* Spacing & Radii */
|
/* Spacing & Radii */
|
||||||
--radius-sm: 5px;
|
--radius-sm: 5px;
|
||||||
|
|||||||
@@ -3,34 +3,7 @@ import { exchanges, type Exchange } from './exchanges'
|
|||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
import { agents } from './agents'
|
import { agents } from './agents'
|
||||||
import { SIDEBAR_APPS, buildRouteToAppMap, type SidebarApp } from './sidebar'
|
import { SIDEBAR_APPS, buildRouteToAppMap, type SidebarApp } from './sidebar'
|
||||||
|
import { formatDuration, statusLabel, statusToVariant, formatTimestamp } from '../utils/format-utils'
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusLabel(status: Exchange['status']): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'OK'
|
|
||||||
case 'failed': return 'ERR'
|
|
||||||
case 'running': return 'RUN'
|
|
||||||
case 'warning': return 'WARN'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusToVariant(status: Exchange['status']): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'success'
|
|
||||||
case 'failed': return 'error'
|
|
||||||
case 'running': return 'running'
|
|
||||||
case 'warning': return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(date: Date): string {
|
|
||||||
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function healthToColor(health: SidebarApp['health']): string {
|
function healthToColor(health: SidebarApp['health']): string {
|
||||||
switch (health) {
|
switch (health) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableMeta {
|
.tableMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detailLabel {
|
.detailLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.entityMeta {
|
.entityMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inheritedNote {
|
.inheritedNote {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
.breakdown {
|
.breakdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
.scopeSep {
|
.scopeSep {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopeCurrent {
|
.scopeCurrent {
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sectionMeta {
|
.sectionMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: var(--error-bg);
|
background: var(--error-bg);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chartTitle {
|
.chartTitle {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
.scopeSep {
|
.scopeSep {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopeCurrent {
|
.scopeCurrent {
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chartMeta {
|
.chartMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableMeta {
|
.tableMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,14 +128,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inlineErrorText {
|
.inlineErrorText {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inlineErrorHint {
|
.inlineErrorHint {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panelSectionTitle {
|
.panelSectionTitle {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
.panelSectionMeta {
|
.panelSectionMeta {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overviewLabel {
|
.overviewLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
@@ -209,14 +209,14 @@
|
|||||||
|
|
||||||
.errorClass {
|
.errorClass {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import { Badge } from '../../design-system/primitives/Badge/Badge'
|
|||||||
// Global filters
|
// Global filters
|
||||||
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { formatDuration, statusToVariant, statusLabel, toRouteNodeType, durationClass } from '../../utils/format-utils'
|
||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
||||||
import { kpiMetrics, type KpiMetric } from '../../mocks/metrics'
|
import { kpiMetrics, type KpiMetric } from '../../mocks/metrics'
|
||||||
@@ -66,12 +69,6 @@ const kpiItems: KpiItem[] = kpiMetrics.map((m) => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(date: Date): string {
|
function formatTimestamp(date: Date): string {
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
const mo = String(date.getMonth() + 1).padStart(2, '0')
|
const mo = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
@@ -82,24 +79,6 @@ function formatTimestamp(date: Date): string {
|
|||||||
return `${y}-${mo}-${d} ${h}:${mi}:${s}`
|
return `${y}-${mo}-${d} ${h}:${mi}:${s}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'success'
|
|
||||||
case 'failed': return 'error'
|
|
||||||
case 'running': return 'running'
|
|
||||||
case 'warning': return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusLabel(status: Exchange['status']): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'OK'
|
|
||||||
case 'failed': return 'ERR'
|
|
||||||
case 'running': return 'RUN'
|
|
||||||
case 'warning': return 'WARN'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Table columns (base, without navigate action) ──────────────────────────
|
// ─── Table columns (base, without navigate action) ──────────────────────────
|
||||||
const BASE_COLUMNS: Column<Exchange>[] = [
|
const BASE_COLUMNS: Column<Exchange>[] = [
|
||||||
{
|
{
|
||||||
@@ -150,7 +129,7 @@ const BASE_COLUMNS: Column<Exchange>[] = [
|
|||||||
header: 'Duration',
|
header: 'Duration',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<MonoText size="sm" className={durationClass(row.durationMs, row.status)}>
|
<MonoText size="sm" className={durationClass(row.durationMs, row.status, styles)}>
|
||||||
{formatDuration(row.durationMs)}
|
{formatDuration(row.durationMs)}
|
||||||
</MonoText>
|
</MonoText>
|
||||||
),
|
),
|
||||||
@@ -167,14 +146,6 @@ const BASE_COLUMNS: Column<Exchange>[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function durationClass(ms: number, status: Exchange['status']): string {
|
|
||||||
if (status === 'failed') return styles.durBreach
|
|
||||||
if (ms < 100) return styles.durFast
|
|
||||||
if (ms < 200) return styles.durNormal
|
|
||||||
if (ms < 300) return styles.durSlow
|
|
||||||
return styles.durBreach
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHORTCUTS = [
|
const SHORTCUTS = [
|
||||||
{ keys: 'Ctrl+K', label: 'Search' },
|
{ keys: 'Ctrl+K', label: 'Search' },
|
||||||
{ keys: '↑↓', label: 'Navigate rows' },
|
{ keys: '↑↓', label: 'Navigate rows' },
|
||||||
@@ -257,16 +228,6 @@ export function Dashboard() {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map processor types to RouteNode types
|
|
||||||
function toRouteNodeType(procType: string): RouteNode['type'] {
|
|
||||||
switch (procType) {
|
|
||||||
case 'consumer': return 'from'
|
|
||||||
case 'transform': return 'process'
|
|
||||||
case 'enrich': return 'process'
|
|
||||||
default: return procType as RouteNode['type']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build RouteFlow nodes from exchange processors
|
// Build RouteFlow nodes from exchange processors
|
||||||
const routeNodes: RouteNode[] = selectedExchange
|
const routeNodes: RouteNode[] = selectedExchange
|
||||||
? selectedExchange.processors.map((p) => ({
|
? selectedExchange.processors.map((p) => ({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.headerStatLabel {
|
.headerStatLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chainLabel {
|
.chainLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
|
|
||||||
.procCount {
|
.procCount {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
|
|
||||||
.toggleBtn {
|
.toggleBtn {
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
|
|
||||||
.panelTag {
|
.panelTag {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
grid-template-columns: 140px 1fr;
|
grid-template-columns: 140px 1fr;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerKvRow:last-child {
|
.headerKvRow:last-child {
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sectionLabel {
|
.sectionLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
@@ -369,7 +369,7 @@
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--error-bg);
|
background: var(--error-bg);
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
|
|
||||||
.errorMessageBox {
|
.errorMessageBox {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: var(--error-bg);
|
background: var(--error-bg);
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -414,7 +414,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 120px 1fr;
|
grid-template-columns: 120px 1fr;
|
||||||
gap: 4px 12px;
|
gap: 4px 12px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorDetailLabel {
|
.errorDetailLabel {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
|||||||
import { CodeBlock } from '../../design-system/primitives/CodeBlock/CodeBlock'
|
import { CodeBlock } from '../../design-system/primitives/CodeBlock/CodeBlock'
|
||||||
import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout'
|
import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { formatDuration, statusToVariant, toRouteNodeType } from '../../utils/format-utils'
|
||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
import { exchanges } from '../../mocks/exchanges'
|
import { exchanges } from '../../mocks/exchanges'
|
||||||
import { buildRouteToAppMap } from '../../mocks/sidebar'
|
import { buildRouteToAppMap } from '../../mocks/sidebar'
|
||||||
@@ -25,21 +28,6 @@ import { buildRouteToAppMap } from '../../mocks/sidebar'
|
|||||||
const ROUTE_TO_APP = buildRouteToAppMap()
|
const ROUTE_TO_APP = buildRouteToAppMap()
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusToVariant(status: 'completed' | 'failed' | 'running' | 'warning'): 'success' | 'error' | 'running' | 'warning' {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'success'
|
|
||||||
case 'failed': return 'error'
|
|
||||||
case 'running': return 'running'
|
|
||||||
case 'warning': return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusToLabel(status: 'completed' | 'failed' | 'running' | 'warning'): string {
|
function statusToLabel(status: 'completed' | 'failed' | 'running' | 'warning'): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed': return 'COMPLETED'
|
case 'completed': return 'COMPLETED'
|
||||||
@@ -145,16 +133,6 @@ function generateExchangeSnapshotOut(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map processor types to RouteNode types
|
|
||||||
function toRouteNodeType(procType: string): RouteNode['type'] {
|
|
||||||
switch (procType) {
|
|
||||||
case 'consumer': return 'from'
|
|
||||||
case 'transform': return 'process'
|
|
||||||
case 'enrich': return 'process'
|
|
||||||
default: return procType as RouteNode['type']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ExchangeDetail component ─────────────────────────────────────────────────
|
// ─── ExchangeDetail component ─────────────────────────────────────────────────
|
||||||
export function ExchangeDetail() {
|
export function ExchangeDetail() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navLabel {
|
.navLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logoExport {
|
.logoExport {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: var(--bg-body);
|
background: var(--bg-body);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import styles from './BrandAssetsSection.module.css'
|
import styles from './BrandAssetsSection.module.css'
|
||||||
import camelLogoSvg from '../../../assets/camel-logo.svg'
|
import camelLogoSvg from '../../../assets/camel-logo.svg'
|
||||||
import cameleer3Logo from '../../../assets/cameleer3-logo.png'
|
import cameleer3Logo from '../../../assets/cameleer3-logo.png'
|
||||||
|
import cameleer3LogoSvg from '../../../assets/cameleer3-logo.svg'
|
||||||
|
|
||||||
const LOGO_SIZES = [16, 32, 48, 180, 192, 512] as const
|
const LOGO_SIZES = [16, 32, 48, 180, 192, 512] as const
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export function BrandAssetsSection() {
|
|||||||
<h3 className={styles.componentTitle}>Cameleer3 Logo (PNG)</h3>
|
<h3 className={styles.componentTitle}>Cameleer3 Logo (PNG)</h3>
|
||||||
<p className={styles.componentDesc}>
|
<p className={styles.componentDesc}>
|
||||||
Full-resolution logo and pre-generated size variants for favicons, PWA icons, and social images.
|
Full-resolution logo and pre-generated size variants for favicons, PWA icons, and social images.
|
||||||
Shipped as static assets via package exports.
|
Shipped as static assets via <code>@cameleer/design-system/assets/*</code> export.
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.demoArea}>
|
<div className={styles.demoArea}>
|
||||||
<div className={styles.logoGrid}>
|
<div className={styles.logoGrid}>
|
||||||
@@ -28,7 +29,7 @@ export function BrandAssetsSection() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.logoLabel}>{size}×{size}</span>
|
<span className={styles.logoLabel}>{size}×{size}</span>
|
||||||
<code className={styles.logoExport}>@cameleer/design-system/logo-{size}</code>
|
<code className={styles.logoExport}>assets/cameleer3-{size}.png</code>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className={styles.logoItem}>
|
<div className={styles.logoItem}>
|
||||||
@@ -41,25 +42,32 @@ export function BrandAssetsSection() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.logoLabel}>Original</span>
|
<span className={styles.logoLabel}>Original</span>
|
||||||
<code className={styles.logoExport}>@cameleer/design-system/logo</code>
|
<code className={styles.logoExport}>assets/cameleer3-logo.png</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.componentCard}>
|
<div className={styles.componentCard}>
|
||||||
<h3 className={styles.componentTitle}>Camel Logo (SVG)</h3>
|
<h3 className={styles.componentTitle}>SVG Logos</h3>
|
||||||
<p className={styles.componentDesc}>
|
<p className={styles.componentDesc}>
|
||||||
Vector logo for scalable usage. Shipped as a static asset via package export.
|
Vector logos for scalable usage. Transparent backgrounds, infinitely scalable.
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.demoArea}>
|
<div className={styles.demoArea}>
|
||||||
<div className={styles.logoGrid}>
|
<div className={styles.logoGrid}>
|
||||||
|
<div className={styles.logoItem}>
|
||||||
|
<div className={styles.logoPreview}>
|
||||||
|
<img src={cameleer3LogoSvg} alt="Cameleer3 SVG logo" width={96} height={96} />
|
||||||
|
</div>
|
||||||
|
<span className={styles.logoLabel}>Cameleer3 SVG</span>
|
||||||
|
<code className={styles.logoExport}>assets/cameleer3-logo.svg</code>
|
||||||
|
</div>
|
||||||
<div className={styles.logoItem}>
|
<div className={styles.logoItem}>
|
||||||
<div className={styles.logoPreview}>
|
<div className={styles.logoPreview}>
|
||||||
<img src={camelLogoSvg} alt="Camel SVG logo" width={96} height={96} />
|
<img src={camelLogoSvg} alt="Camel SVG logo" width={96} height={96} />
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.logoLabel}>SVG</span>
|
<span className={styles.logoLabel}>Camel SVG</span>
|
||||||
<code className={styles.logoExport}>@cameleer/design-system/logo-svg</code>
|
<code className={styles.logoExport}>assets/camel-logo.svg</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoLabel {
|
.demoLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoLabel {
|
.demoLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { SidebarTreeNode } from '../../../design-system/layout/Sidebar/Side
|
|||||||
import { StatusDot } from '../../../design-system/primitives/StatusDot/StatusDot'
|
import { StatusDot } from '../../../design-system/primitives/StatusDot/StatusDot'
|
||||||
import { Box, Settings, FileText, ChevronRight } from 'lucide-react'
|
import { Box, Settings, FileText, ChevronRight } from 'lucide-react'
|
||||||
import { TopBar } from '../../../design-system/layout/TopBar/TopBar'
|
import { TopBar } from '../../../design-system/layout/TopBar/TopBar'
|
||||||
|
import { SearchTrigger } from '../../../design-system/layout/TopBar/SearchTrigger'
|
||||||
|
import { AutoRefreshToggle } from '../../../design-system/layout/TopBar/AutoRefreshToggle'
|
||||||
|
|
||||||
// ── DemoCard helper ──────────────────────────────────────────────────────────
|
// ── DemoCard helper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ export function LayoutSection() {
|
|||||||
<DemoCard
|
<DemoCard
|
||||||
id="topbar"
|
id="topbar"
|
||||||
title="TopBar"
|
title="TopBar"
|
||||||
description="Top navigation bar with breadcrumb, search trigger, status filters, time range, environment badge, and user avatar."
|
description="Composable top navigation bar. Consumers pass children for the center slot (search, filters, etc.). Shell provides breadcrumb, theme toggle, env badge, and user menu."
|
||||||
>
|
>
|
||||||
<div className={styles.topbarPreview}>
|
<div className={styles.topbarPreview}>
|
||||||
<TopBar
|
<TopBar
|
||||||
@@ -132,9 +134,11 @@ export function LayoutSection() {
|
|||||||
{ label: 'order-ingest' },
|
{ label: 'order-ingest' },
|
||||||
]}
|
]}
|
||||||
environment="production"
|
environment="production"
|
||||||
|
|
||||||
user={{ name: 'Hendrik' }}
|
user={{ name: 'Hendrik' }}
|
||||||
/>
|
>
|
||||||
|
<SearchTrigger onClick={() => {}} />
|
||||||
|
<AutoRefreshToggle active={true} onChange={() => {}} />
|
||||||
|
</TopBar>
|
||||||
</div>
|
</div>
|
||||||
</DemoCard>
|
</DemoCard>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoLabel {
|
.demoLabel {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.routeGroup {
|
.routeGroup {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kpiLabel {
|
.kpiLabel {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sectionMeta {
|
.sectionMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableMeta {
|
.tableMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,14 +228,14 @@
|
|||||||
|
|
||||||
.errorClass {
|
.errorClass {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorText {
|
.errorText {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.errorPatternMessage {
|
.errorPatternMessage {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.errorPatternTime {
|
.errorPatternTime {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,39 +17,14 @@ import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
|||||||
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||||
import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout'
|
import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCallout'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { formatDuration, statusToVariant, statusLabel, formatTimestamp, durationClass } from '../../utils/format-utils'
|
||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
import { routes } from '../../mocks/routes'
|
import { routes } from '../../mocks/routes'
|
||||||
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(date: Date): string {
|
|
||||||
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'success'
|
|
||||||
case 'failed': return 'error'
|
|
||||||
case 'running': return 'running'
|
|
||||||
case 'warning': return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusLabel(status: Exchange['status']): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'OK'
|
|
||||||
case 'failed': return 'ERR'
|
|
||||||
case 'running': return 'RUN'
|
|
||||||
case 'warning': return 'WARN'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function routeStatusVariant(status: 'healthy' | 'degraded' | 'down'): 'success' | 'warning' | 'error' {
|
function routeStatusVariant(status: 'healthy' | 'degraded' | 'down'): 'success' | 'warning' | 'error' {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'healthy': return 'success'
|
case 'healthy': return 'success'
|
||||||
@@ -58,14 +33,6 @@ function routeStatusVariant(status: 'healthy' | 'degraded' | 'down'): 'success'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function durationClass(ms: number, status: Exchange['status']): string {
|
|
||||||
if (status === 'failed') return styles.durBreach
|
|
||||||
if (ms < 100) return styles.durFast
|
|
||||||
if (ms < 200) return styles.durNormal
|
|
||||||
if (ms < 300) return styles.durSlow
|
|
||||||
return styles.durBreach
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Columns for exchanges table ────────────────────────────────────────────
|
// ─── Columns for exchanges table ────────────────────────────────────────────
|
||||||
const EXCHANGE_COLUMNS: Column<Exchange>[] = [
|
const EXCHANGE_COLUMNS: Column<Exchange>[] = [
|
||||||
{
|
{
|
||||||
@@ -106,7 +73,7 @@ const EXCHANGE_COLUMNS: Column<Exchange>[] = [
|
|||||||
header: 'Duration',
|
header: 'Duration',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<MonoText size="sm" className={durationClass(row.durationMs, row.status)}>
|
<MonoText size="sm" className={durationClass(row.durationMs, row.status, styles)}>
|
||||||
{formatDuration(row.durationMs)}
|
{formatDuration(row.durationMs)}
|
||||||
</MonoText>
|
</MonoText>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refreshText {
|
.refreshText {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableMeta {
|
.tableMeta {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/utils/format-utils.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { RouteNode } from '../design-system/composites/RouteFlow/RouteFlow'
|
||||||
|
|
||||||
|
export function formatDuration(ms: number): string {
|
||||||
|
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
||||||
|
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
||||||
|
return `${ms}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusToVariant(status: string): 'success' | 'error' | 'running' | 'warning' {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'failed': return 'error'
|
||||||
|
case 'running': return 'running'
|
||||||
|
case 'warning': return 'warning'
|
||||||
|
default: return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusLabel(s: string): string {
|
||||||
|
switch (s) {
|
||||||
|
case 'completed': return 'OK'
|
||||||
|
case 'failed': return 'ERR'
|
||||||
|
case 'running': return 'RUN'
|
||||||
|
case 'warning': return 'WARN'
|
||||||
|
default: return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimestamp(date: Date): string {
|
||||||
|
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRouteNodeType(procType: string): RouteNode['type'] {
|
||||||
|
switch (procType) {
|
||||||
|
case 'consumer': return 'from'
|
||||||
|
case 'transform': return 'process'
|
||||||
|
case 'enrich': return 'process'
|
||||||
|
default: return procType as RouteNode['type']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DurationStyles {
|
||||||
|
durBreach: string
|
||||||
|
durFast: string
|
||||||
|
durNormal: string
|
||||||
|
durSlow: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function durationClass(ms: number, status: string, styles: DurationStyles): string {
|
||||||
|
if (status === 'failed') return styles.durBreach
|
||||||
|
if (ms < 100) return styles.durFast
|
||||||
|
if (ms < 200) return styles.durNormal
|
||||||
|
if (ms < 300) return styles.durSlow
|
||||||
|
return styles.durBreach
|
||||||
|
}
|
||||||