Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,67 @@ cargo test

## 📜 Code of Conduct

📜 Code of Conduct
By participating in this project, you agree to maintain a respectful and inclusive environment for everyone.

---

## 🎨 Component Library & Design System

SoroScan uses a **retro-futuristic terminal aesthetic** — phosphor green, deep black backgrounds, monospace fonts, and glowing effects. All frontend UI should be built with the terminal component library.

### Running Storybook

```bash
cd soroscan-frontend
pnpm storybook # starts on http://localhost:6006
pnpm build-storybook # static build → storybook-static/
```

### Design Tokens

Defined in `app/globals.css` via Tailwind v4 `@theme`:

| Token | Value | Usage |
|-------|-------|-------|
| `terminal-black` | `#0a0e27` | Default background |
| `terminal-green` | `#00ff41` | Primary text/borders/accents |
| `terminal-cyan` | `#00d4ff` | Secondary accents, labels |
| `terminal-danger` | `#ff3366` | Errors, destructive actions |
| `terminal-warning` | `#ffaa00` | Warnings, processing states |
| `glow-green` | `0 0 20px rgba(0,255,65,0.5)` | Green glow shadow |
| `glow-cyan` | `0 0 20px rgba(0,212,255,0.5)` | Cyan glow shadow |
| `font-terminal-mono` | JetBrains Mono, IBM Plex Mono | All code and UI text |

### Available Components

All components live in `components/terminal/`:

| Component | Description |
|-----------|-------------|
| `Button` | Terminal-style button with `>` prompt prefix on hover |
| `Card` | Box-drawing bordered card with scanline effect |
| `Table` / `TableRow` etc. | Terminal grid data table |
| `Input` | Input field with `>` prompt prefix |
| `Modal` | Modal with `[TITLE]` header bar |
| `Badge` | Glowing status badge with pulse dot |
| `Alert` | Left-bordered alert with `[INFO]` / `[ERR]` prefix |
| `CodeBlock` | Scrollable code block with copy button |
| `TerminalWindow` | Window frame with title bar and status dot |
| `TerminalTabs` (Tabs) | Tab navigation with active indicator |

### Adding a New Component

1. Create `components/terminal/MyComponent.tsx` following existing patterns.
2. Use only design tokens (`terminal-green`, `terminal-black`, etc.) — no hardcoded colors.
3. Create `components/terminal/MyComponent.stories.tsx` with at least a `Default` story.
4. Export from the component file; add `tags: ["autodocs"]` to the meta for auto-documentation.
5. Ensure keyboard focus styles are applied via `:focus-visible` (globally handled in `globals.css`).

### Accessibility Guidelines (WCAG 2.1 AA)

- Minimum 4.5:1 contrast ratio for normal text. Terminal green `#00ff41` on black `#0a0e27` meets this.
- All interactive elements must be keyboard accessible.
- Use semantic HTML (`button`, `input`, `table`, `[role="alert"]`).
- The `@storybook/addon-a11y` addon runs automated checks in Storybook — review the Accessibility panel.
By participating in this project, you agree to abide by our Code of Conduct to ensure SoroScan remains a respectful, safe, and supportive space for everyone. See the [Community Standards Guide](docs/contributing/community-standards.md) for details.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,32 @@ kubectl scale deployment/soroscan-worker --replicas=3 -n soroscan

---

## 🎨 Design System

The frontend uses a **retro-futuristic terminal aesthetic** — a distinctive visual identity for a Web3 developer tool.

### Design Philosophy

- **Phosphor green on deep black**: Evokes 1980s terminal monitors. All primary UI uses `#00ff41` on `#0a0e27`.
- **Monospace everywhere**: JetBrains Mono for code and UI text reinforces the developer-first feel.
- **Box-drawing borders & glow effects**: Components use CSS box-drawing characters and `box-shadow` glow to simulate CRT screens.
- **Dark mode first**: Light mode is supported but terminal dark is the canonical experience.

### Component Library (Storybook)

Interactive component showcase with all terminal UI primitives:

```bash
cd soroscan-frontend
pnpm storybook # http://localhost:6006
```

Components: `Button`, `Card`, `Table`, `Input`, `Modal`, `Badge`, `Alert`, `CodeBlock`, `TerminalWindow`, `Tabs`

See [CONTRIBUTING.md](CONTRIBUTING.md#-component-library--design-system) for usage guidelines and how to add new components.

---

## 🤝 Contributing

1. Fork the repository and create your feature branch.
Expand Down
18 changes: 18 additions & 0 deletions soroscan-frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { StorybookConfig } from "@storybook/nextjs-vite";

const config: StorybookConfig = {
stories: [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
"../components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-docs",
"@storybook/addon-a11y",
"@chromatic-com/storybook",
],
framework: "@storybook/nextjs-vite",
staticDirs: ["../public"],
};

export default config;
31 changes: 31 additions & 0 deletions soroscan-frontend/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Preview } from "@storybook/nextjs-vite";
import React from "react";
import "../app/globals.css";

const preview: Preview = {
parameters: {
backgrounds: {
default: "terminal",
values: [
{ name: "terminal", value: "#0a0e27" },
{ name: "light", value: "#ffffff" },
],
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(Story) =>
React.createElement(
"div",
{ className: "dark min-h-screen bg-terminal-black p-8 font-mono" },
React.createElement(Story)
),
],
};

export default preview;
50 changes: 50 additions & 0 deletions soroscan-frontend/components/terminal/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
import { Alert } from "./Alert"

const meta: Meta<typeof Alert> = {
title: "Terminal/Alert",
component: Alert,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["info", "success", "warning", "error"],
},
title: { control: "text" },
},
}
export default meta

type Story = StoryObj<typeof Alert>

export const Info: Story = {
args: {
variant: "info",
title: "New events available",
children: "1,024 events indexed in the last ledger range.",
},
}

export const Success: Story = {
args: {
variant: "success",
title: "Contract deployed",
children: "Contract CXXXXXXXX successfully deployed to TESTNET.",
},
}

export const Warning: Story = {
args: {
variant: "warning",
title: "Rate limit approaching",
children: "80% of API quota consumed. Upgrade plan to avoid throttling.",
},
}

export const Error: Story = {
args: {
variant: "error",
title: "Ingestion failed",
children: "Failed to connect to Soroban RPC. Check SOROBAN_RPC_URL.",
},
}
57 changes: 57 additions & 0 deletions soroscan-frontend/components/terminal/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const alertVariants = cva(
"relative border-l-4 bg-terminal-black p-4 font-terminal-mono text-sm",
{
variants: {
variant: {
info: "border-terminal-cyan text-terminal-cyan",
success: "border-terminal-green text-terminal-green",
warning: "border-terminal-warning text-terminal-warning",
error: "border-terminal-danger text-terminal-danger",
},
},
defaultVariants: {
variant: "info",
},
}
)

const prefixMap = {
info: "[INFO]",
success: "[OK]",
warning: "[WARN]",
error: "[ERR]",
} as const

export interface AlertProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertVariants> {
title?: string
}

const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
({ className, variant = "info", title, children, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant, className }))}
{...props}
>
<div className="flex items-start gap-2">
<span className="font-bold shrink-0">
{prefixMap[variant ?? "info"]}
</span>
<div>
{title && <div className="font-bold mb-1">{title}</div>}
<div className="opacity-90">{children}</div>
</div>
</div>
</div>
)
)
Alert.displayName = "Alert"

export { Alert, alertVariants }
47 changes: 47 additions & 0 deletions soroscan-frontend/components/terminal/Badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
import React from "react"
import { Badge } from "./Badge"

const meta: Meta<typeof Badge> = {
title: "Terminal/Badge",
component: Badge,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "cyan", "danger", "warning", "muted"],
},
pulse: { control: "boolean" },
},
}
export default meta

type Story = StoryObj<typeof Badge>

export const Online: Story = {
args: { variant: "default", children: "ONLINE", pulse: true },
}

export const Offline: Story = {
args: { variant: "danger", children: "OFFLINE" },
}

export const Processing: Story = {
args: { variant: "warning", children: "PROCESSING", pulse: true },
}

export const Info: Story = {
args: { variant: "cyan", children: "TESTNET" },
}

export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-3">
<Badge variant="default" pulse>ONLINE</Badge>
<Badge variant="cyan">TESTNET</Badge>
<Badge variant="warning" pulse>SYNCING</Badge>
<Badge variant="danger">FAILED</Badge>
<Badge variant="muted">ARCHIVED</Badge>
</div>
),
}
51 changes: 51 additions & 0 deletions soroscan-frontend/components/terminal/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const badgeVariants = cva(
"inline-flex items-center gap-1 px-2 py-0.5 text-xs font-terminal-mono font-bold uppercase tracking-wider border",
{
variants: {
variant: {
default: "border-terminal-green text-terminal-green shadow-glow-green/30",
cyan: "border-terminal-cyan text-terminal-cyan shadow-glow-cyan/30",
danger: "border-terminal-danger text-terminal-danger",
warning: "border-terminal-warning text-terminal-warning",
muted: "border-terminal-gray text-terminal-gray",
},
pulse: {
true: "",
false: "",
},
},
defaultVariants: {
variant: "default",
pulse: false,
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}

const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant, pulse, children, ...props }, ref) => (
<span
ref={ref}
className={cn(badgeVariants({ variant, pulse, className }))}
{...props}
>
<span
className={cn(
"w-1.5 h-1.5 rounded-full bg-current",
pulse && "animate-pulse"
)}
/>
{children}
</span>
)
)
Badge.displayName = "Badge"

export { Badge, badgeVariants }
Loading
Loading