Skip to content

Dashboard Example

A multi-pane dashboard demonstrating responsive layouts with useContentRect().

What It Demonstrates

  • Multi-pane layouts using flexbox with flexGrow
  • Responsive breakpoints that adapt to terminal width
  • useContentRect() usage for proportional sizing and text truncation
  • Nested layout with borders and padding

Screenshot

+------------------+---------------------+
| System Stats     | Activity Feed       |
|                  |                     |
| CPU:    [====  ] | 12:01 User login    |
| Memory: [=====  ]| 12:00 Build passed  |
| Disk:   [==     ]| 11:58 PR #42 merged |
|                  | 11:55 Deploy done   |
+------------------+---------------------+
| Recent Items                           |
|                                        |
| > project-alpha     2 hours ago        |
|   report-q4.pdf     Yesterday          |
|   config.json       3 days ago         |
+----------------------------------------+

Running the Example

bash
cd inkx
bun run examples/dashboard/app.tsx

Full Source Code

tsx
import { Box, Text, render, useContentRect, useInput, useApp, createTerm } from "inkx"
import { useState } from "react"

// Sample data
const stats = [
  { label: "CPU", value: 45 },
  { label: "Memory", value: 62 },
  { label: "Disk", value: 28 },
]

const activities = [
  { time: "12:01", message: "User logged in" },
  { time: "12:00", message: "Build passed" },
  { time: "11:58", message: "PR #42 merged" },
  { time: "11:55", message: "Deploy completed" },
  { time: "11:50", message: "Tests started" },
]

const recentItems = [
  { name: "project-alpha", date: "2 hours ago" },
  { name: "report-q4.pdf", date: "Yesterday" },
  { name: "config.json", date: "3 days ago" },
  { name: "notes.md", date: "Last week" },
]

function App() {
  const { exit } = useApp()

  useInput((input, key) => {
    if (input === "q" || key.escape) {
      exit()
    }
  })

  return (
    <Box flexDirection="column" width="100%" height="100%">
      <TopSection />
      <BottomSection />
      <StatusBar />
    </Box>
  )
}

function TopSection() {
  const { width } = useContentRect()

  // Responsive: stack vertically on narrow terminals
  const isNarrow = width < 60

  return (
    <Box flexDirection={isNarrow ? "column" : "row"} flexGrow={1}>
      <StatsPane />
      <ActivityPane />
    </Box>
  )
}

function StatsPane() {
  return (
    <Box flexDirection="column" flexGrow={1} borderStyle="single" paddingX={1}>
      <Text bold>System Stats</Text>
      <Text> </Text>
      {stats.map((stat) => (
        <StatRow key={stat.label} label={stat.label} value={stat.value} />
      ))}
    </Box>
  )
}

function StatRow({ label, value }: { label: string; value: number }) {
  const { width } = useContentRect()

  // Calculate bar width based on available space
  // Account for label (8 chars) + spacing
  const barWidth = Math.max(0, width - 12)
  const filledWidth = Math.floor((barWidth * value) / 100)
  const emptyWidth = barWidth - filledWidth

  const bar = "=".repeat(filledWidth) + " ".repeat(emptyWidth)

  return (
    <Text>
      {label.padEnd(8)} [{bar}]
    </Text>
  )
}

function ActivityPane() {
  return (
    <Box flexDirection="column" flexGrow={2} borderStyle="single" paddingX={1}>
      <Text bold>Activity Feed</Text>
      <Text> </Text>
      {activities.map((activity, i) => (
        <ActivityRow key={i} time={activity.time} message={activity.message} />
      ))}
    </Box>
  )
}

function ActivityRow({ time, message }: { time: string; message: string }) {
  const { width } = useContentRect()

  // Truncate message to fit available width
  const timeWidth = 6 // "12:01 "
  const maxMessageWidth = Math.max(0, width - timeWidth)
  const truncatedMessage = message.length > maxMessageWidth ? message.slice(0, maxMessageWidth - 1) + "..." : message

  return (
    <Text>
      <Text dimColor>{time}</Text> {truncatedMessage}
    </Text>
  )
}

function BottomSection() {
  const [selected, setSelected] = useState(0)

  useInput((input, key) => {
    if (key.downArrow) {
      setSelected((s) => Math.min(s + 1, recentItems.length - 1))
    }
    if (key.upArrow) {
      setSelected((s) => Math.max(s - 1, 0))
    }
  })

  return (
    <Box flexDirection="column" height={8} borderStyle="single" paddingX={1}>
      <Text bold>Recent Items</Text>
      <Text> </Text>
      <Box flexDirection="column" overflow="scroll" scrollTo={selected}>
        {recentItems.map((item, i) => (
          <RecentItemRow key={item.name} name={item.name} date={item.date} isSelected={i === selected} />
        ))}
      </Box>
    </Box>
  )
}

function RecentItemRow({ name, date, isSelected }: { name: string; date: string; isSelected: boolean }) {
  const { width } = useContentRect()

  // Calculate space for name, leaving room for date
  const dateWidth = date.length + 2
  const nameWidth = Math.max(0, width - dateWidth - 2)

  const truncatedName = name.length > nameWidth ? name.slice(0, nameWidth - 1) + "..." : name

  const padding = " ".repeat(Math.max(0, nameWidth - truncatedName.length))

  const prefix = isSelected ? "> " : "  "

  return (
    <Text inverse={isSelected}>
      {prefix}
      {truncatedName}
      {padding}
      <Text dimColor>{date}</Text>
    </Text>
  )
}

function StatusBar() {
  return (
    <Box paddingX={1}>
      <Text dimColor>Press q to quit | Arrow keys to navigate</Text>
    </Box>
  )
}

using term = createTerm()
await render(<App />, term)

Code Walkthrough

Responsive Layout

The TopSection component uses useContentRect() to detect narrow terminals:

tsx
function TopSection() {
  const { width } = useContentRect()
  const isNarrow = width < 60

  return (
    <Box flexDirection={isNarrow ? "column" : "row"}>
      <StatsPane />
      <ActivityPane />
    </Box>
  )
}

On narrow terminals (< 60 chars), the stats and activity panes stack vertically instead of side-by-side.

Proportional Sizing

The two top panes use flexGrow for proportional sizing:

tsx
<StatsPane />      // flexGrow={1} - takes 1/3 of space
<ActivityPane />   // flexGrow={2} - takes 2/3 of space

No width calculations needed. Yoga handles the math.

Dynamic Progress Bars

The StatRow component builds progress bars that fill available space:

tsx
function StatRow({ label, value }: { label: string; value: number }) {
  const { width } = useContentRect()

  const barWidth = Math.max(0, width - 12) // Account for label
  const filledWidth = Math.floor((barWidth * value) / 100)
  const emptyWidth = barWidth - filledWidth

  const bar = "=".repeat(filledWidth) + " ".repeat(emptyWidth)

  return (
    <Text>
      {label.padEnd(8)} [{bar}]
    </Text>
  )
}

The bar automatically resizes when the terminal is resized.

Text Truncation

The ActivityRow component truncates long messages:

tsx
function ActivityRow({ time, message }: { time: string; message: string }) {
  const { width } = useContentRect()

  const maxMessageWidth = Math.max(0, width - 6)
  const truncatedMessage = message.length > maxMessageWidth ? message.slice(0, maxMessageWidth - 1) + "..." : message

  return (
    <Text>
      <Text dimColor>{time}</Text> {truncatedMessage}
    </Text>
  )
}

No overflow, no layout bugs.

Scrollable List

The "Recent Items" section uses overflow="scroll":

tsx
<Box flexDirection="column" overflow="scroll" scrollTo={selected}>
  {recentItems.map((item, i) => (
    <RecentItemRow key={item.name} isSelected={i === selected} /* ... */ />
  ))}
</Box>

Add more items to recentItems and they'll scroll automatically.

Key inkx Features Used

FeatureUsage
useContentRect()Get dimensions for responsive layout, progress bars, text truncation
overflow="scroll"Scrollable recent items list
scrollTo={index}Keep selected item visible
flexGrowProportional pane sizing
useInput()Keyboard navigation

Exercises

  1. Add a third pane - Add a "Notifications" pane to the top section
  2. Make stats scrollable - Add more stats and make the stats pane scroll
  3. Add timestamps - Show relative timestamps that update every second
  4. Add color coding - Color progress bars red/yellow/green based on value

Released under the MIT License.