Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dba3aa5a85 | ||
|
|
d775df61e4 | ||
|
|
7c6d383ac9 | ||
|
|
70a5106cca | ||
|
|
b0bd9a4ce2 |
@@ -1,7 +1,7 @@
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **design-system** (1479 symbols, 2371 relationships, 24 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
This project is indexed by GitNexus as **design-system** (1536 symbols, 2408 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.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ Always read `COMPONENT_GUIDE.md` before building any UI feature. It contains dec
|
|||||||
### Import Paths
|
### Import Paths
|
||||||
```tsx
|
```tsx
|
||||||
import { Button, Input } from '../design-system/primitives'
|
import { Button, Input } from '../design-system/primitives'
|
||||||
import { Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer } from '../design-system/composites'
|
import { Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer, LineChart, AreaChart, BarChart } from '../design-system/composites'
|
||||||
import type { Column, KpiItem, LogEntry } from '../design-system/composites'
|
import type { Column, KpiItem, LogEntry, ChartSeries } from '../design-system/composites'
|
||||||
import { AppShell } from '../design-system/layout/AppShell'
|
import { AppShell } from '../design-system/layout/AppShell'
|
||||||
import { Sidebar } from '../design-system/layout/Sidebar/Sidebar'
|
import { Sidebar } from '../design-system/layout/Sidebar/Sidebar'
|
||||||
import { SidebarTree } from '../design-system/layout/Sidebar/SidebarTree'
|
import { SidebarTree } from '../design-system/layout/Sidebar/SidebarTree'
|
||||||
@@ -95,7 +95,7 @@ import { Button, AppShell, ThemeProvider } from '@cameleer/design-system'
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// All components from single entry
|
// All components from single entry
|
||||||
import { Button, Input, Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer, StatusText, AppShell } from '@cameleer/design-system'
|
import { Button, Input, Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer, StatusText, AppShell, LineChart, AreaChart, BarChart } from '@cameleer/design-system'
|
||||||
|
|
||||||
// Sidebar (compound component — compose your own navigation)
|
// Sidebar (compound component — compose your own navigation)
|
||||||
import { Sidebar, SidebarTree, useStarred } from '@cameleer/design-system'
|
import { Sidebar, SidebarTree, useStarred } from '@cameleer/design-system'
|
||||||
@@ -132,7 +132,7 @@ import camelSvg from '@cameleer/design-system/assets/camel-logo.svg' // simp
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **design-system** (1479 symbols, 2371 relationships, 24 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
This project is indexed by GitNexus as **design-system** (1536 symbols, 2408 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.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,10 @@
|
|||||||
### "I need to display data"
|
### "I need to display data"
|
||||||
- Key metrics → **StatCard** (with optional sparkline/trend)
|
- Key metrics → **StatCard** (with optional sparkline/trend)
|
||||||
- Tabular data → **DataTable** (sortable, paginated)
|
- Tabular data → **DataTable** (sortable, paginated)
|
||||||
- Time series → **ThemedChart** with `<Line>` or `<Area>`
|
- Time series (quick) → **LineChart** or **AreaChart** (convenience wrappers with series data)
|
||||||
- Categorical comparison → **ThemedChart** with `<Bar>`
|
- Categorical comparison (quick) → **BarChart** (convenience wrapper with series data)
|
||||||
|
- Time series (custom) → **ThemedChart** with `<Line>` or `<Area>`
|
||||||
|
- Categorical comparison (custom) → **ThemedChart** with `<Bar>`
|
||||||
- Inline trend → **Sparkline**
|
- Inline trend → **Sparkline**
|
||||||
- Advanced charts (treemap, radar, heatmap, pie, etc.) → **Recharts** with `rechartsTheme` (see [Charting Strategy](#charting-strategy))
|
- Advanced charts (treemap, radar, heatmap, pie, etc.) → **Recharts** with `rechartsTheme` (see [Charting Strategy](#charting-strategy))
|
||||||
- Event log → **EventFeed**
|
- Event log → **EventFeed**
|
||||||
@@ -109,6 +111,9 @@ Sidebar compound API:
|
|||||||
<Sidebar.Section label="str" icon={node} open={bool} onToggle={fn} active={bool}>
|
<Sidebar.Section label="str" icon={node} open={bool} onToggle={fn} active={bool}>
|
||||||
<SidebarTree nodes={[...]} selectedPath="..." filterQuery="..." ... />
|
<SidebarTree nodes={[...]} selectedPath="..." filterQuery="..." ... />
|
||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
|
<Sidebar.Section label="str" icon={node} open={bool} onToggle={fn} position="bottom" maxHeight="200px">
|
||||||
|
<SidebarTree nodes={[...]} ... />
|
||||||
|
</Sidebar.Section>
|
||||||
<Sidebar.Footer>
|
<Sidebar.Footer>
|
||||||
<Sidebar.FooterLink icon={node} label="str" onClick={fn} active={bool} />
|
<Sidebar.FooterLink icon={node} label="str" onClick={fn} active={bool} />
|
||||||
</Sidebar.Footer>
|
</Sidebar.Footer>
|
||||||
@@ -119,6 +124,11 @@ Notes:
|
|||||||
- Section headers have no chevron — the entire row is clickable to toggle
|
- Section headers have no chevron — the entire row is clickable to toggle
|
||||||
- The app controls all content — sections, order, tree data, collapse state
|
- The app controls all content — sections, order, tree data, collapse state
|
||||||
- Sidebar provides the frame, search input, and icon-rail collapse mode
|
- Sidebar provides the frame, search input, and icon-rail collapse mode
|
||||||
|
- `position="bottom"` stacks sections above the footer; a spacer separates top/bottom groups
|
||||||
|
- `maxHeight` (CSS string) constrains the content area — section header stays visible, children scroll
|
||||||
|
- Both groups scroll independently when the viewport is short
|
||||||
|
- Custom thin scrollbars match the dark sidebar aesthetic
|
||||||
|
- No expand button when collapsed — clicking any section icon expands the sidebar and opens that section
|
||||||
```
|
```
|
||||||
|
|
||||||
### Data page pattern
|
### Data page pattern
|
||||||
@@ -182,9 +192,38 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
|
|||||||
|
|
||||||
## Charting Strategy
|
## Charting Strategy
|
||||||
|
|
||||||
The design system provides a **ThemedChart** wrapper component that applies consistent styling to Recharts charts. Recharts is bundled as a dependency — consumers do not need to install it separately.
|
The design system provides convenience chart wrappers (**LineChart**, **AreaChart**, **BarChart**) for common use cases, plus a lower-level **ThemedChart** wrapper for full Recharts control. Recharts is bundled as a dependency — consumers do not need to install it separately.
|
||||||
|
|
||||||
### Usage
|
### Quick Charts (convenience wrappers)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { LineChart, AreaChart, BarChart } from '@cameleer/design-system'
|
||||||
|
import type { ChartSeries } from '@cameleer/design-system'
|
||||||
|
|
||||||
|
const series: ChartSeries[] = [
|
||||||
|
{ label: 'CPU', data: [{ x: '10:00', y: 45 }, { x: '10:05', y: 62 }] },
|
||||||
|
{ label: 'Memory', data: [{ x: '10:00', y: 70 }, { x: '10:05', y: 72 }] },
|
||||||
|
]
|
||||||
|
|
||||||
|
<LineChart series={series} height={200} yLabel="%" />
|
||||||
|
<AreaChart series={series} height={200} yLabel="%" thresholdValue={85} thresholdLabel="Alert" />
|
||||||
|
<BarChart series={series} height={200} stacked />
|
||||||
|
```
|
||||||
|
|
||||||
|
| Prop | LineChart | AreaChart | BarChart | Description |
|
||||||
|
|------|:---------:|:---------:|:--------:|-------------|
|
||||||
|
| `series` | required | required | required | `ChartSeries[]` — `{ label, data: { x, y }[], color? }` |
|
||||||
|
| `height` | optional | optional | optional | Chart height in pixels |
|
||||||
|
| `width` | optional | optional | optional | Container width in pixels |
|
||||||
|
| `yLabel` | optional | optional | optional | Y-axis label |
|
||||||
|
| `xLabel` | optional | optional | optional | X-axis label |
|
||||||
|
| `className` | optional | optional | optional | Container CSS class |
|
||||||
|
| `threshold` | `{ value, label }` | — | — | Horizontal reference line |
|
||||||
|
| `thresholdValue` | — | optional | — | Threshold y-value |
|
||||||
|
| `thresholdLabel` | — | optional | — | Threshold label |
|
||||||
|
| `stacked` | — | — | optional | Stack bars instead of grouping |
|
||||||
|
|
||||||
|
### Custom Charts (ThemedChart)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { ThemedChart, Line, Area, ReferenceLine, CHART_COLORS } from '@cameleer/design-system'
|
import { ThemedChart, Line, Area, ReferenceLine, CHART_COLORS } from '@cameleer/design-system'
|
||||||
@@ -231,6 +270,8 @@ For chart types not covered (treemap, radar, pie, sankey), import from `recharts
|
|||||||
| Accordion | composite | Multiple collapsible sections, single or multi-open mode |
|
| Accordion | composite | Multiple collapsible sections, single or multi-open mode |
|
||||||
| Alert | primitive | Page-level attention banner with variant colors |
|
| Alert | primitive | Page-level attention banner with variant colors |
|
||||||
| AlertDialog | composite | Confirmation dialog for destructive/important actions |
|
| AlertDialog | composite | Confirmation dialog for destructive/important actions |
|
||||||
|
| AreaChart | composite | Convenience area chart wrapper — pass `series` data, get themed chart with fills |
|
||||||
|
| BarChart | composite | Convenience bar chart wrapper — grouped or `stacked` mode |
|
||||||
| Avatar | primitive | User representation with initials and color |
|
| Avatar | primitive | User representation with initials and color |
|
||||||
| AvatarGroup | composite | Stacked overlapping avatars with overflow count |
|
| AvatarGroup | composite | Stacked overlapping avatars with overflow count |
|
||||||
| Badge | primitive | Labeled status indicator with semantic colors |
|
| Badge | primitive | Labeled status indicator with semantic colors |
|
||||||
@@ -261,7 +302,7 @@ For chart types not covered (treemap, radar, pie, sankey), import from `recharts
|
|||||||
| KeyboardHint | primitive | Keyboard shortcut display |
|
| KeyboardHint | primitive | Keyboard shortcut display |
|
||||||
| KpiStrip | composite | Horizontal row of KPI cards with colored left border, trend, subtitle, optional sparkline |
|
| KpiStrip | composite | Horizontal row of KPI cards with colored left border, trend, subtitle, optional sparkline |
|
||||||
| Label | primitive | Form label with optional required asterisk |
|
| Label | primitive | Form label with optional required asterisk |
|
||||||
| ThemedChart | composite | Recharts wrapper with themed axes, grid, and tooltip |
|
| LineChart | composite | Convenience line chart wrapper — pass `series` data, get themed chart with lines |
|
||||||
| LogViewer | composite | Scrollable log output with timestamped, severity-colored monospace entries |
|
| LogViewer | composite | Scrollable log output with timestamped, severity-colored monospace entries |
|
||||||
| MenuItem | composite | Sidebar navigation item with health/count |
|
| MenuItem | composite | Sidebar navigation item with health/count |
|
||||||
| Modal | composite | Generic dialog overlay with backdrop |
|
| Modal | composite | Generic dialog overlay with backdrop |
|
||||||
@@ -298,7 +339,7 @@ For chart types not covered (treemap, radar, pie, sankey), import from `recharts
|
|||||||
| Component | Purpose |
|
| Component | Purpose |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| AppShell | Page shell: sidebar + topbar + main + optional detail panel |
|
| AppShell | Page shell: sidebar + topbar + main + optional detail panel |
|
||||||
| Sidebar | Composable compound sidebar shell with icon-rail collapse mode. Sub-components: `Sidebar.Header`, `Sidebar.Section`, `Sidebar.Footer`, `Sidebar.FooterLink`. The app controls all content via children — the DS provides the frame. |
|
| Sidebar | Composable compound sidebar shell with icon-rail collapse mode. Sub-components: `Sidebar.Header`, `Sidebar.Section` (supports `position="bottom"` and `maxHeight`), `Sidebar.Footer`, `Sidebar.FooterLink`. The app controls all content via children — the DS provides the frame. |
|
||||||
| SidebarTree | Data-driven tree for sidebar sections. Accepts `nodes: SidebarTreeNode[]` with expand/collapse, starring, keyboard nav, search filter, and path-based selection highlighting. |
|
| SidebarTree | Data-driven tree for sidebar sections. Accepts `nodes: SidebarTreeNode[]` with expand/collapse, starring, keyboard nav, search filter, and path-based selection highlighting. |
|
||||||
| useStarred | Hook for localStorage-backed starred item IDs. Returns `{ starredIds, isStarred, toggleStar }`. |
|
| useStarred | Hook for localStorage-backed starred item IDs. Returns `{ starredIds, isStarred, toggleStar }`. |
|
||||||
| TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment slot (`ReactNode` — pass a string for a static label or a custom dropdown for interactive selection), user avatar |
|
| TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment slot (`ReactNode` — pass a string for a static label or a custom dropdown for interactive selection), user avatar |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.53",
|
"version": "0.1.55",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.es.js",
|
"main": "./dist/index.es.js",
|
||||||
"module": "./dist/index.es.js",
|
"module": "./dist/index.es.js",
|
||||||
|
|||||||
107
src/design-system/composites/AreaChart/AreaChart.tsx
Normal file
107
src/design-system/composites/AreaChart/AreaChart.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Area, ReferenceLine } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AreaChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
thresholdValue?: number
|
||||||
|
thresholdLabel?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AreaChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
thresholdValue,
|
||||||
|
thresholdLabel,
|
||||||
|
className,
|
||||||
|
}: AreaChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType={hasDateX ? 'category' : 'category'}
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => {
|
||||||
|
const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length]
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={s.label}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={s.label}
|
||||||
|
stroke={color}
|
||||||
|
fill={color}
|
||||||
|
fillOpacity={0.15}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{thresholdValue != null && (
|
||||||
|
<ReferenceLine
|
||||||
|
y={thresholdValue}
|
||||||
|
stroke="var(--text-muted)"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
label={thresholdLabel ? {
|
||||||
|
value: thresholdLabel,
|
||||||
|
position: 'insideTopRight',
|
||||||
|
style: { fontSize: 10, fill: 'var(--text-muted)' },
|
||||||
|
} : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
88
src/design-system/composites/BarChart/BarChart.tsx
Normal file
88
src/design-system/composites/BarChart/BarChart.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Bar } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
stacked?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
stacked,
|
||||||
|
className,
|
||||||
|
}: BarChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType="category"
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => (
|
||||||
|
<Bar
|
||||||
|
key={s.label}
|
||||||
|
dataKey={s.label}
|
||||||
|
fill={s.color ?? CHART_COLORS[i % CHART_COLORS.length]}
|
||||||
|
radius={[2, 2, 0, 0]}
|
||||||
|
{...(stacked ? { stackId: 'stack' } : {})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
101
src/design-system/composites/LineChart/LineChart.tsx
Normal file
101
src/design-system/composites/LineChart/LineChart.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Line, ReferenceLine } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LineChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
threshold?: { value: number; label: string }
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
threshold,
|
||||||
|
className,
|
||||||
|
}: LineChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType={hasDateX ? 'category' : 'category'}
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => (
|
||||||
|
<Line
|
||||||
|
key={s.label}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={s.label}
|
||||||
|
stroke={s.color ?? CHART_COLORS[i % CHART_COLORS.length]}
|
||||||
|
dot={false}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{threshold && (
|
||||||
|
<ReferenceLine
|
||||||
|
y={threshold.value}
|
||||||
|
stroke="var(--text-muted)"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
label={{
|
||||||
|
value: threshold.label,
|
||||||
|
position: 'insideTopRight',
|
||||||
|
style: { fontSize: 10, fill: 'var(--text-muted)' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@ export { TreeView } from './TreeView/TreeView'
|
|||||||
|
|
||||||
// Charts — ThemedChart wrapper + Recharts re-exports
|
// Charts — ThemedChart wrapper + Recharts re-exports
|
||||||
export { ThemedChart } from './ThemedChart/ThemedChart'
|
export { ThemedChart } from './ThemedChart/ThemedChart'
|
||||||
|
export { LineChart } from './LineChart/LineChart'
|
||||||
|
export { AreaChart } from './AreaChart/AreaChart'
|
||||||
|
export { BarChart } from './BarChart/BarChart'
|
||||||
|
export type { ChartSeries, DataPoint } from './LineChart/LineChart'
|
||||||
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
|
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
|
||||||
export {
|
export {
|
||||||
Line, Area, Bar,
|
Line, Area, Bar,
|
||||||
|
|||||||
@@ -53,11 +53,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarCollapsed .collapseToggle {
|
|
||||||
top: 52px;
|
|
||||||
right: 50%;
|
|
||||||
transform: translateX(50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoImg {
|
.logoImg {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@@ -458,7 +453,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 7px 12px;
|
padding: 7px 6px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ describe('Sidebar compound component', () => {
|
|||||||
expect(onCollapseToggle).toHaveBeenCalledTimes(1)
|
expect(onCollapseToggle).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 7. renders expand toggle label when collapsed
|
// 7. hides collapse toggle when sidebar is collapsed
|
||||||
it('renders expand toggle when sidebar is collapsed', () => {
|
it('hides collapse toggle when sidebar is collapsed', () => {
|
||||||
render(
|
render(
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Sidebar collapsed onCollapseToggle={vi.fn()}>
|
<Sidebar collapsed onCollapseToggle={vi.fn()}>
|
||||||
@@ -141,7 +141,8 @@ describe('Sidebar compound component', () => {
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
</Wrapper>,
|
</Wrapper>,
|
||||||
)
|
)
|
||||||
expect(screen.getByRole('button', { name: /expand sidebar/i })).toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /collapse sidebar/i })).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('button', { name: /expand sidebar/i })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 8. renders search input and calls onSearchChange
|
// 8. renders search input and calls onSearchChange
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
ChevronsLeft,
|
ChevronsLeft,
|
||||||
ChevronsRight,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import styles from './Sidebar.module.css'
|
import styles from './Sidebar.module.css'
|
||||||
import { SidebarContext, useSidebarContext } from './SidebarContext'
|
import { SidebarContext, useSidebarContext } from './SidebarContext'
|
||||||
@@ -194,14 +193,14 @@ function SidebarRoot({
|
|||||||
className ?? '',
|
className ?? '',
|
||||||
].filter(Boolean).join(' ')}
|
].filter(Boolean).join(' ')}
|
||||||
>
|
>
|
||||||
{/* Collapse toggle */}
|
{/* Collapse toggle (hidden when collapsed — sections expand on click) */}
|
||||||
{onCollapseToggle && (
|
{onCollapseToggle && !collapsed && (
|
||||||
<button
|
<button
|
||||||
className={styles.collapseToggle}
|
className={styles.collapseToggle}
|
||||||
onClick={onCollapseToggle}
|
onClick={onCollapseToggle}
|
||||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
aria-label="Collapse sidebar"
|
||||||
>
|
>
|
||||||
{collapsed ? <ChevronsRight size={14} /> : <ChevronsLeft size={14} />}
|
<ChevronsLeft size={14} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +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 cameleerLogo from '../../../assets/cameleer-logo.png'
|
import cameleerLogo from '../../../assets/cameleer3-logo.png'
|
||||||
import cameleerLogoSvg from '../../../assets/cameleer-logo.svg'
|
import cameleerLogoSvg 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user