# Screens

Screens display UI in MAJK. Use React for rich interfaces with auto-generated hooks.

## React Screen

```typescript
// src/index.ts
import { definePlugin } from '@majkapp/plugin-kit';

const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
  .pluginRoot(__dirname)

  // Define functions first - they generate hooks
  .function('health', { /* ... */ })
  .function('getAnalytics', { /* ... */ })

  // React screen - loads your built React app
  .screenReact({
    id: 'my-plugin-main',           // Unique ID
    name: 'My Plugin',               // Display name in UI
    description: 'Main plugin dashboard showing health and analytics',
    route: '/plugin-screens/my-plugin/main',  // MUST start with /plugin-screens/{plugin-id}/
    pluginPath: '/index.html'        // Path to built React app (relative to dist/)
  })

  .build();
```

## React App Setup

```typescript
// ui/src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { DashboardPage } from './components/DashboardPage';
import { SettingsPage } from './components/SettingsPage';

export function App() {
  return (
    <BrowserRouter basename={window.__MAJK_IFRAME_BASE__}>
      <Routes>
        <Route path="/" element={<DashboardPage />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </BrowserRouter>
  );
}
```

**Critical:** Use `window.__MAJK_IFRAME_BASE__` as basename for routing.

Available globals:
- `window.__MAJK_BASE_URL__` - Host base URL
- `window.__MAJK_IFRAME_BASE__` - Plugin iframe base path
- `window.__MAJK_PLUGIN_ID__` - Your plugin ID

## Multiple Screens with Hash Routing

```typescript
// src/index.ts
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
  .pluginRoot(__dirname)

  // Dashboard screen - default route
  .screenReact({
    id: 'my-plugin-dashboard',
    name: 'Dashboard',
    description: 'Main dashboard with metrics and controls',
    route: '/plugin-screens/my-plugin/dashboard',
    pluginPath: '/index.html',
    pluginPathHash: '#/'           // Routes to #/ in your React app
  })

  // Settings screen - separate route
  .screenReact({
    id: 'my-plugin-settings',
    name: 'Settings',
    description: 'Plugin configuration and preferences',
    route: '/plugin-screens/my-plugin/settings',
    pluginPath: '/index.html',
    pluginPathHash: '#/settings'   // Routes to #/settings in your React app
  })

  .build();
```

MAJK will load your React app at `/index.html` and navigate to the specified hash.

## Best Practice: React Component with data-majk-* Attributes

**Always use `data-majk-*` attributes** for testability and debugging.

```typescript
// ui/src/components/DashboardPage.tsx
import { useHealth, useGetAnalytics, useClearEvents, useUiLog } from '../generated/hooks';
import { PluginStatGrid, PluginActionPanel, PluginList } from '@majkapp/plugin-ui';
import { useState, useEffect } from 'react';

export function DashboardPage() {
  // Query hooks - auto-fetch on mount
  const { data: health, loading: healthLoading, error: healthError, refetch: refetchHealth } = useHealth();
  const { data: analytics, loading: analyticsLoading } = useGetAnalytics(
    { period: '7d' },
    { refetchInterval: 30000 }  // Auto-refresh every 30s
  );

  // Mutation hooks - call .mutate() manually
  const { mutate: clearEvents, loading: clearingEvents } = useClearEvents();
  const { mutate: uiLog } = useUiLog();

  const [autoRefresh, setAutoRefresh] = useState(true);

  // Log component lifecycle for debugging
  useEffect(() => {
    uiLog({
      level: 'info',
      component: 'DashboardPage',
      message: 'Dashboard mounted',
      data: { timestamp: new Date().toISOString() }
    });

    return () => {
      uiLog({
        level: 'info',
        component: 'DashboardPage',
        message: 'Dashboard unmounting'
      });
    };
  }, [uiLog]);

  // Log errors
  useEffect(() => {
    if (healthError) {
      uiLog({
        level: 'error',
        component: 'DashboardPage',
        message: 'Health check failed',
        data: { error: healthError.message }
      });
    }
  }, [healthError, uiLog]);

  const handleRefresh = () => {
    uiLog({
      level: 'info',
      component: 'DashboardPage',
      message: 'Manual refresh triggered by user'
    });
    refetchHealth();
  };

  const handleClearEvents = async () => {
    try {
      uiLog({
        level: 'info',
        component: 'DashboardPage',
        message: 'Clear events triggered by user'
      });

      await clearEvents({});

      uiLog({
        level: 'info',
        component: 'DashboardPage',
        message: 'Events cleared successfully'
      });
    } catch (error) {
      uiLog({
        level: 'error',
        component: 'DashboardPage',
        message: 'Failed to clear events',
        data: { error: error.message }
      });
    }
  };

  return (
    <div
      data-majk-id="dashboardContainer"
      data-majk-state={healthLoading || analyticsLoading ? 'loading' : 'populated'}
    >
      {/* Action buttons - data-majk-id for bot clicking, data-majk-state for verification */}
      <PluginActionPanel
        actions={[
          {
            label: autoRefresh ? '⏸ Pause Refresh' : '▶️ Resume Refresh',
            onClick: () => {
              setAutoRefresh(!autoRefresh);
              uiLog({
                level: 'info',
                component: 'DashboardPage',
                message: `Auto-refresh ${autoRefresh ? 'paused' : 'resumed'}`
              });
            },
            'data-majk-id': 'toggleRefreshButton',
            'data-majk-state': autoRefresh ? 'refreshing' : 'paused'
          },
          {
            label: '🔄 Refresh Now',
            onClick: handleRefresh,
            disabled: healthLoading,
            'data-majk-id': 'refreshHealthButton',
            'data-majk-state': healthLoading ? 'loading' : 'idle'
          },
          {
            label: '🗑 Clear Events',
            onClick: handleClearEvents,
            disabled: clearingEvents,
            color: 'danger',
            'data-majk-id': 'clearEventsButton',
            'data-majk-state': clearingEvents ? 'clearing' : 'idle'
          }
        ]}
      />

      {/* Stats grid - data-majk-id for each stat, data-majk-state for loading/empty/populated */}
      <PluginStatGrid
        stats={[
          {
            label: 'Health Status',
            value: healthError ? 'Error' : health?.status || 'Unknown',
            trend: health?.status === 'ok' ? 'up' : 'down',
            'data-majk-id': 'healthStatus',
            'data-majk-state': healthLoading ? 'loading' : healthError ? 'error' : health ? 'populated' : 'empty'
          },
          {
            label: 'Last Check',
            value: health?.timestamp ? new Date(health.timestamp).toLocaleTimeString() : '--',
            'data-majk-id': 'lastCheckTime',
            'data-majk-state': health?.timestamp ? 'populated' : 'empty'
          },
          {
            label: 'Active Users (7d)',
            value: analytics?.metrics.activeUsers || 0,
            'data-majk-id': 'activeUsersCount',
            'data-majk-state': analyticsLoading ? 'loading' : analytics ? 'populated' : 'empty'
          },
          {
            label: 'Total Sessions',
            value: analytics?.metrics.totalSessions || 0,
            'data-majk-id': 'totalSessionsCount'
          },
          {
            label: 'Avg Duration',
            value: analytics?.metrics.avgDuration ? `${analytics.metrics.avgDuration}s` : '--',
            'data-majk-id': 'avgDurationValue'
          }
        ]}
      />

      {/* Error display - data-majk-state for testing error states */}
      {healthError && (
        <div
          data-majk-id="errorMessage"
          data-majk-state="error"
          style={{ padding: '12px', background: '#fee', color: '#c00', borderRadius: '4px' }}
        >
          Error: {healthError.message}
        </div>
      )}
    </div>
  );
}
```

## data-majk-* Attributes Reference

### data-majk-id

Identifies elements for bot testing and debugging.

**Use on:**
- All clickable elements (buttons, links)
- Key statistics and values
- Input fields
- List items
- Containers with important content

```typescript
<button data-majk-id="saveButton">Save</button>
<div data-majk-id="totalCount">{count}</div>
<input data-majk-id="nameInput" />
```

**Naming convention:** Use camelCase, be descriptive but concise.

### data-majk-state

Tracks UI state for testing assertions.

**Common states:**
- `loading` - Data is being fetched
- `empty` - No data available
- `populated` - Data is displayed
- `error` - Error occurred
- `idle` - Button/action ready
- `processing` - Action in progress
- Custom states: `paused`, `active`, `disabled`, etc.

```typescript
<div
  data-majk-id="userList"
  data-majk-state={loading ? 'loading' : users.length > 0 ? 'populated' : 'empty'}
>
  {/* content */}
</div>

<button
  data-majk-id="submitButton"
  data-majk-state={submitting ? 'processing' : 'idle'}
  disabled={submitting}
>
  {submitting ? 'Submitting...' : 'Submit'}
</button>
```

## List Components with data-majk-*

```typescript
// ui/src/components/EventsList.tsx
import { useGetEvents, useUiLog } from '../generated/hooks';
import { PluginList } from '@majkapp/plugin-ui';
import { useEffect } from 'react';

export function EventsList() {
  const { data: events, loading, error } = useGetEvents();
  const { mutate: uiLog } = useUiLog();

  useEffect(() => {
    if (error) {
      uiLog({
        level: 'error',
        component: 'EventsList',
        message: 'Failed to load events',
        data: { error: error.message }
      });
    }
  }, [error, uiLog]);

  const items = events?.events || [];

  return (
    <div
      data-majk-id="eventsContainer"
      data-majk-state={loading ? 'loading' : error ? 'error' : items.length > 0 ? 'populated' : 'empty'}
    >
      {loading && <div data-majk-id="loadingSpinner">Loading...</div>}

      {error && (
        <div data-majk-id="errorMessage" data-majk-state="error">
          Error loading events: {error.message}
        </div>
      )}

      {!loading && !error && items.length === 0 && (
        <div data-majk-id="emptyMessage" data-majk-state="empty">
          No events to display
        </div>
      )}

      {!loading && !error && items.length > 0 && (
        <PluginList
          items={items.map(event => ({
            id: event.id,
            title: `${event.entityType}.${event.eventType}`,
            description: event.entitySummary?.name || event.entityId,
            metadata: {
              timestamp: new Date(event.timestamp).toLocaleString()
            }
          }))}
          // CRITICAL: Pass data-majk-id via itemProps so each list item is testable
          itemProps={{ 'data-majk-id': 'eventItem' }}
          onItemClick={(item) => {
            uiLog({
              level: 'info',
              component: 'EventsList',
              message: 'Event clicked',
              data: { eventId: item.id }
            });
          }}
        />
      )}

      {/* Display counts for testing assertions */}
      <div style={{ marginTop: '16px', fontSize: '12px', color: '#666' }}>
        <span data-majk-id="displayedEventsCount">{items.length} events</span>
      </div>
    </div>
  );
}
```

## Testing Your Screen

```typescript
// tests/plugin/ui/unit/dashboard.test.js
import { test, bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('dashboard displays health status', async () => {
  // Mock function responses
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .withMockHandler('getAnalytics', async () => ({
      period: '7d',
      metrics: {
        activeUsers: 42,
        totalSessions: 150,
        avgDuration: 320
      }
    }))
    .build();

  const b = await bot(context);

  // Navigate to screen
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for data to load - verify data-majk-state changed to 'populated'
  await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');

  // Verify displayed values using data-majk-id
  const healthText = await b.getText('[data-majk-id="healthStatus"]');
  assert.ok(healthText.includes('ok'), `Expected 'ok' in "${healthText}"`);

  const usersText = await b.getText('[data-majk-id="activeUsersCount"]');
  assert.ok(usersText.includes('42'), `Expected '42' in "${usersText}"`);

  // Test button interaction
  await b.click('[data-majk-id="refreshHealthButton"]');

  // Verify loading state
  await b.waitForSelector('[data-majk-id="refreshHealthButton"][data-majk-state="loading"]');

  // Take screenshot for visual verification
  await b.screenshot('dashboard-after-refresh.png');

  await b.close();
});

test('dashboard handles empty state', async () => {
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .withMockHandler('getAnalytics', async () => ({
      period: '7d',
      metrics: {
        activeUsers: 0,
        totalSessions: 0,
        avgDuration: 0
      }
    }))
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for render
  await b.waitForSelector('[data-majk-id="dashboardContainer"]');

  // Verify empty state
  const usersCount = await b.getText('[data-majk-id="activeUsersCount"]');
  assert.strictEqual(usersCount, '0');

  await b.close();
});

test('dashboard toggle refresh button changes state', async () => {
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for page load
  await b.waitForSelector('[data-majk-id="toggleRefreshButton"]');

  // Initial state should be 'refreshing'
  let state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
  assert.strictEqual(state, 'refreshing');

  // Click to pause
  await b.click('[data-majk-id="toggleRefreshButton"]');

  // Verify state changed to 'paused'
  state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
  assert.strictEqual(state, 'paused');

  await b.close();
});
```

## Best Practice: Component Organization

```
ui/src/
├── App.tsx                   # Router setup
├── components/
│   ├── DashboardPage.tsx     # Page component
│   ├── SettingsPage.tsx
│   ├── EventsList.tsx        # Feature component
│   └── shared/
│       ├── ErrorBoundary.tsx
│       └── LoadingSpinner.tsx
├── hooks/
│   ├── useAutoRefresh.ts     # Custom hook
│   └── useLocalStorage.ts
├── generated/                # Auto-generated (DO NOT EDIT)
│   ├── hooks.ts              # useHealth(), useGetAnalytics(), etc.
│   ├── client.ts
│   ├── types.ts
│   └── index.ts
└── utils/
    ├── formatters.ts
    └── validators.ts
```

## Using @majkapp/plugin-ui Components

```bash
# View available components
npx @majkapp/plugin-ui
```

Commonly used components:

- `PluginActionPanel` - Action buttons with consistent styling
- `PluginStatGrid` - Display metrics/statistics
- `PluginList` - List items with metadata
- `PluginCard` - Styled containers
- `PluginTable` - Data tables
- All accept `data-majk-*` attributes via props

## Next Steps

Run `npx @majkapp/plugin-kit --hooks` - Generated React hooks reference
Run `npx @majkapp/plugin-kit --testing` - Complete UI testing guide
Run `npx @majkapp/plugin-kit --config` - vite.config.js setup
