Skip to content
Draft
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
1,540 changes: 1,540 additions & 0 deletions ui/docs/EXTENSION_DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions ui/examples/demo-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Demo Extension for zrok UI

This is an example extension demonstrating the capabilities of the zrok UI extension system.

## Features Demonstrated

- **Custom Routes**: Adds `/demo` and `/demo/settings` pages
- **Navigation Items**: Adds a "Demo" button to the navbar
- **Panel Extensions**: Adds a "Billing" tab to the Account panel
- **Slot Injections**: Adds a notification badge to the navbar
- **State Management**: Demonstrates persistent state with a counter
- **Lifecycle Hooks**: Shows `onInit`, `onUserLogin`, and `onUserLogout`

## Development

### Prerequisites

- Node.js 18+
- npm or yarn
- zrok repository cloned

### Important Note

This demo extension is designed to be used via **path imports** from the zrok UI project.
Do **not** run `npm install` directly in this directory. All dependencies come from the
parent `zrok/ui` project.

### Setup for Local Development

1. First, install dependencies in the main zrok UI:

```bash
cd ui
npm install
```

2. Enable the extension in the zrok UI:

Edit `ui/src/extensions.config.ts`:

```typescript
import { extensionRegistry } from './extensions/registry';
import demoExtension from '../examples/demo-extension/src';

export function loadExtensions(): void {
extensionRegistry.register(demoExtension);
}
```

3. Start the zrok UI development server:

```bash
cd ui
npm run dev
```

4. The demo extension features should now be visible in the UI.

## File Structure

```
demo-extension/
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── README.md # This file
└── src/
├── index.ts # Extension manifest (main entry)
├── DemoIcon.tsx # Icon component for nav item
├── DemoPage.tsx # Main demo page (/demo)
├── DemoSettingsPage.tsx # Settings page (/demo/settings)
├── AccountBillingTab.tsx # Tab added to Account panel
└── DemoNavbarSlot.tsx # Component for navbar slot
```

## Extension Manifest

The main entry point (`src/index.ts`) exports an `ExtensionManifest` object:

```typescript
const manifest: ExtensionManifest = {
id: 'demo-extension',
name: 'Demo Extension',
version: '1.0.0',

routes: [...],
navItems: [...],
panelExtensions: [...],
slots: {...},
initialState: {...},

onInit: async (context) => {...},
onUserLogin: (user, context) => {...},
onUserLogout: (context) => {...},
};
```

## State Management

The extension uses a typed state interface:

```typescript
interface DemoExtensionState {
counter: number;
lastVisited: string | null;
settings: {
enableFeatureX: boolean;
theme: 'light' | 'dark';
};
}
```

Access state in components via the context:

```typescript
const state = context.getState<DemoExtensionState>();
context.setState<DemoExtensionState>({ counter: state.counter + 1 });
```

## Building for Production

This extension is designed to be used as a TypeScript source during development.
For production distribution as an npm package, you would:

1. Add a build step to compile TypeScript
2. Configure `package.json` to point to compiled output
3. Publish to npm or a private registry

See the Extension Developer Guide for complete packaging instructions.
30 changes: 30 additions & 0 deletions ui/examples/demo-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@example/zrok-demo-extension",
"version": "1.0.0",
"description": "Example extension for the zrok web UI demonstrating extension capabilities",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"@mui/material": "^6.0.0",
"@mui/icons-material": "^6.0.0",
"@xyflow/react": "^12.0.0",
"react-router": "^7.0.0"
},
"devDependencies": {
"@types/react": "^18.3.12 || ^19.0.0",
"typescript": "~5.6.2"
},
"keywords": [
"zrok",
"extension",
"demo"
],
"license": "Apache-2.0",
"private": true,
"readme": "This extension is designed to be used via path imports from the zrok UI. Do not run npm install here directly. Instead, import from the parent zrok/ui project."
}
133 changes: 133 additions & 0 deletions ui/examples/demo-extension/src/AccountBillingTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Account Billing Tab
*
* A tab added to the Account panel demonstrating panel extensions.
* In a real billing extension, this would show subscription info, invoices, etc.
*/

import React, { useState } from 'react';
import {
Box,
Button,
Card,
CardContent,
Chip,
Divider,
List,
ListItem,
ListItemText,
Typography,
} from '@mui/material';
import CreditCardIcon from '@mui/icons-material/CreditCard';
import ReceiptIcon from '@mui/icons-material/Receipt';
import { PanelExtensionProps } from '../../../src/extensions';

// Mock data for demonstration
const mockSubscription = {
plan: 'Professional',
status: 'active',
nextBillingDate: '2024-02-15',
amount: '$49.00/month',
};

const mockInvoices = [
{ id: 'INV-001', date: '2024-01-15', amount: '$49.00', status: 'paid' },
{ id: 'INV-002', date: '2023-12-15', amount: '$49.00', status: 'paid' },
{ id: 'INV-003', date: '2023-11-15', amount: '$49.00', status: 'paid' },
];

const AccountBillingTab: React.FC<PanelExtensionProps> = ({ node, user, context }) => {
const [loading, setLoading] = useState(false);

const handleManageSubscription = () => {
context.notify('Opening subscription management...', 'info');
// In a real extension, this would open a modal or navigate to a billing page
context.navigate('/demo');
};

const handleViewInvoice = (invoiceId: string) => {
context.notify(`Viewing invoice ${invoiceId}`, 'info');
};

return (
<Box sx={{ p: 1 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CreditCardIcon />
Billing & Subscription
</Typography>

<Card variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" fontWeight="bold">
Current Plan
</Typography>
<Chip
label={mockSubscription.status}
color={mockSubscription.status === 'active' ? 'success' : 'default'}
size="small"
/>
</Box>

<Typography variant="h5" color="primary" gutterBottom>
{mockSubscription.plan}
</Typography>

<Typography variant="body2" color="text.secondary">
{mockSubscription.amount}
</Typography>

<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Next billing: {mockSubscription.nextBillingDate}
</Typography>

<Button
variant="outlined"
size="small"
sx={{ mt: 2 }}
onClick={handleManageSubscription}
>
Manage Subscription
</Button>
</CardContent>
</Card>

<Typography variant="subtitle1" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ReceiptIcon />
Recent Invoices
</Typography>

<List dense>
{mockInvoices.map((invoice, index) => (
<React.Fragment key={invoice.id}>
<ListItem
secondaryAction={
<Button size="small" onClick={() => handleViewInvoice(invoice.id)}>
View
</Button>
}
>
<ListItemText
primary={invoice.id}
secondary={`${invoice.date} - ${invoice.amount}`}
/>
<Chip
label={invoice.status}
color={invoice.status === 'paid' ? 'success' : 'warning'}
size="small"
sx={{ mr: 1 }}
/>
</ListItem>
{index < mockInvoices.length - 1 && <Divider />}
</React.Fragment>
))}
</List>

<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 2 }}>
This is a demo billing tab. In a real extension, this would connect to your billing system.
</Typography>
</Box>
);
};

export default AccountBillingTab;
18 changes: 18 additions & 0 deletions ui/examples/demo-extension/src/DemoIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Demo Icon Component
*
* A simple icon component for the demo extension.
*/

import React from 'react';
import ScienceIcon from '@mui/icons-material/Science';

interface DemoIconProps {
fontSize?: 'small' | 'medium' | 'large';
}

const DemoIcon: React.FC<DemoIconProps> = ({ fontSize = 'medium' }) => {
return <ScienceIcon fontSize={fontSize} />;
};

export default DemoIcon;
37 changes: 37 additions & 0 deletions ui/examples/demo-extension/src/DemoNavbarSlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Demo Navbar Slot Component
*
* A component injected into the NAVBAR_RIGHT slot.
* Demonstrates how extensions can inject UI into predefined slots.
*/

import React from 'react';
import { Badge, Button, Tooltip } from '@mui/material';
import NotificationsIcon from '@mui/icons-material/Notifications';
import { SlotProps } from '../../../src/extensions';
import { DemoExtensionState } from './index';

const DemoNavbarSlot: React.FC<SlotProps> = ({ user, context }) => {
// Get the counter from extension state to show as a badge
const state = context.getState<DemoExtensionState>();
const counter = state?.counter ?? 0;

const handleClick = () => {
context.notify(`You have ${counter} notifications (demo)`, 'info');
};

// Only show if user is logged in
if (!user) return null;

return (
<Tooltip title="Demo Notifications">
<Button color="inherit" onClick={handleClick}>
<Badge badgeContent={counter} color="error" max={99}>
<NotificationsIcon />
</Badge>
</Button>
</Tooltip>
);
};

export default DemoNavbarSlot;
Loading
Loading