Welcome to the third chapter of our Passes project tutorial!
In the previous chapter, Design System (Pane), we created a consistent container (the "frame") for our application. However, a frame is boring if it's empty.
Now, we need to fill that frame with content. We need to arrange text in columns, display colorful headers, render ASCII art tickets, and add clickable links.
Historically, writing to the terminal meant printing lines of text one after another.
\x1b[31m.This is hard to maintain and breaks easily if the screen size changes.
We use a set of CLI UI Components powered by a library called Ink. This allows us to build terminal apps exactly like we build websites using HTML and CSS.
We will focus on three core components:
Box: The layout engine (like an HTML <div>).Text: The typography engine (like <span> or <p>).Link: Interactivity (like <a>).Box (Layout)
The Box is an invisible container used to group items. It uses Flexbox logic.
flexDirection="column": Items stack vertically (top to bottom).flexDirection="row": Items sit side-by-side (left to right).gap={1}: Adds a distinct space between items automatically.Text (Style)
The Text component handles how words look.
color="red": Changes font color.dimColor: Makes text grey/faint (good for secondary info).italic, bold: Standard text styling.Link (Interaction)
Modern terminals allow you to click text! The Link component wraps text and opens a URL in your default browser when clicked (or creates a copyable link).
Let's see how we use these components in Passes.tsx to build our Guest Passes screen.
We want our content to flow from top to bottom inside our Pane.
// Inside Passes.tsx
return (
<Pane>
{/* The Main Container */}
<Box flexDirection="column" gap={1}>
{/* All our content goes here */}
</Box>
</Pane>
);
Explanation: We create a vertical column. The gap={1} ensures that our header, our tickets, and our footer don't squish together; they will have one line of space between them.
First, we add the header showing how many passes are left.
<Text color="permission">
Guest passes ยท {availableCount} left
</Text>
Explanation: We use a custom color named "permission" (defined in our theme) to highlight this important text. availableCount is a variable we calculated in Chapter 1.
This is the trickiest part. We want to display 3 ASCII art tickets side-by-side.
To do this, we nest Boxes:
<Box flexDirection="row" marginLeft={2}>
{/* We map through our data to render 3 tickets */}
{sortedPasses.slice(0, 3).map(pass => renderTicket(pass))}
</Box>
Explanation: flexDirection="row" places the tickets next to each other. marginLeft={2} indents them slightly so they look centered.
Let's look at the renderTicket function. A ticket is just 3 lines of text stacked vertically.
const renderTicket = (pass: PassStatus) => {
return (
<Box key={pass.passNumber} flexDirection="column" marginRight={1}>
<Text>{'โโโโโโโโโโโโ'}</Text>
<Text>{' ) CC โฑ โ ( '}</Text>
<Text>{'โโโโโโโโโโโโ'}</Text>
</Box>
);
};
Explanation:
Box (Column) to stack the three strings.marginRight={1} ensures there is a space between this ticket and the next one in the row.Finally, we add the disclaimer text at the bottom.
<Text dimColor>
Share a free week of Claude Code.
<Link url="https://support.claude.com/...">
Terms apply.
</Link>
</Text>
Explanation: We nest the <Link> inside the <Text>. This makes "Terms apply" clickable (or visually distinct) within the sentence.
How does <Box> turn into something the terminal understands? React usually renders to the DOM (HTML), but here we are rendering to a String.
Here is the flow of data:
The magic happens in the Layout Engine (called Yoga).
While Ink handles the heavy lifting, our project wraps these components to ensure they match our brand.
For example, when we used <Text color="permission">, Ink doesn't know what "permission" means. Our internal setup maps that name to a specific hex code or ANSI color.
// Conceptual example of how Text might handle colors
if (props.color === 'permission') {
// Apply a specific green/purple hue
applyAnsiColor('\x1b[38;5;123m');
}
This abstraction allows us to change the "permission" color in one place, and every <Text> component in the app updates automatically.
By using CLI UI Components like Box, Text, and Link, we have turned a complex formatting problem into a simple stacking game.
We have:
The UI looks great, but it's currently static. We mentioned pressing "Esc" or "Enter" in the text, but we haven't actually told the code how to listen for those keys.
Next Chapter: Keybinding Management
Generated by Code IQ