From 5bce61b969af5eeada20838140e0d0e4ecc7a549 Mon Sep 17 00:00:00 2001
From: Shavin Chandrawansha
Date: Wed, 19 Aug 2026 13:57:31 +0530
Subject: [PATCH 01/12] feat(apicp): enhance theme management and add claude
skills
---
.../.claude/oxygen-ui/components.md | 1431 +++++++++++++++++
.../.claude/oxygen-ui/migration.md | 281 ++++
.../.claude/oxygen-ui/patterns.md | 1197 ++++++++++++++
.../.claude/oxygen-ui/theming.md | 334 ++++
.../.claude/skills/oxygen-component/SKILL.md | 147 ++
.../.claude/skills/oxygen-form/SKILL.md | 410 +++++
.../.claude/skills/oxygen-layout/SKILL.md | 233 +++
.../.claude/skills/oxygen-migrate/SKILL.md | 280 ++++
portals/api-control-plane/CLAUDE.md | 5 +
portals/api-control-plane/package-lock.json | 1188 +++++++++++---
portals/api-control-plane/package.json | 9 +-
.../src/theme/AppThemeProvider.tsx | 37 +
.../src/theme/emotionCache.ts | 33 +
portals/api-control-plane/src/theme/index.ts | 31 +
.../api-control-plane/src/theme/receipes.ts | 147 ++
portals/api-control-plane/src/theme/themes.ts | 28 +
16 files changed, 5526 insertions(+), 265 deletions(-)
create mode 100644 portals/api-control-plane/.claude/oxygen-ui/components.md
create mode 100644 portals/api-control-plane/.claude/oxygen-ui/migration.md
create mode 100644 portals/api-control-plane/.claude/oxygen-ui/patterns.md
create mode 100644 portals/api-control-plane/.claude/oxygen-ui/theming.md
create mode 100644 portals/api-control-plane/.claude/skills/oxygen-component/SKILL.md
create mode 100644 portals/api-control-plane/.claude/skills/oxygen-form/SKILL.md
create mode 100644 portals/api-control-plane/.claude/skills/oxygen-layout/SKILL.md
create mode 100644 portals/api-control-plane/.claude/skills/oxygen-migrate/SKILL.md
create mode 100644 portals/api-control-plane/CLAUDE.md
create mode 100644 portals/api-control-plane/src/theme/AppThemeProvider.tsx
create mode 100644 portals/api-control-plane/src/theme/emotionCache.ts
create mode 100644 portals/api-control-plane/src/theme/index.ts
create mode 100644 portals/api-control-plane/src/theme/receipes.ts
create mode 100644 portals/api-control-plane/src/theme/themes.ts
diff --git a/portals/api-control-plane/.claude/oxygen-ui/components.md b/portals/api-control-plane/.claude/oxygen-ui/components.md
new file mode 100644
index 0000000000..fe912c6f81
--- /dev/null
+++ b/portals/api-control-plane/.claude/oxygen-ui/components.md
@@ -0,0 +1,1431 @@
+# Oxygen UI Component Reference
+
+Complete API reference for custom Oxygen UI components. For MUI components, refer to [Material-UI documentation](https://mui.com/material-ui/).
+
+## Table of Contents
+
+- [OxygenUIThemeProvider](#oxygenuitthemeprovider)
+- [ListingTable](#listingtable)
+- [AppShell](#appshell)
+- [Layout](#layout)
+- [Header](#header)
+- [Sidebar](#sidebar)
+- [Footer](#footer)
+- [UserMenu](#usermenu)
+- [Form](#form)
+- [NotificationPanel](#notificationpanel)
+- [NotificationBanner](#notificationbanner)
+- [ParticleBackground](#particlebackground)
+- [Other Components](#other-components)
+- [MUI X Namespaces](#mui-x-namespaces)
+- [Hooks](#hooks)
+- [Utilities](#utilities)
+
+---
+
+## OxygenUIThemeProvider
+
+Theme provider that wraps your application. Required at the root level.
+
+```tsx
+import { OxygenUIThemeProvider, OxygenTheme } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `children` | `ReactNode` | required | App content |
+| `theme` | `Theme` | `OxygenTheme` | Single theme (disables switching) |
+| `themes` | `ThemeOption[]` | - | Array of themes (enables switching) |
+| `initialTheme` | `string` | First theme key | Initial theme key |
+
+### ThemeOption Type
+
+```tsx
+interface ThemeOption {
+ key: string; // Unique identifier
+ label: string; // Display name
+ theme: Theme; // MUI Theme object
+}
+```
+
+### Basic Usage
+
+```tsx
+// Single theme (no switching)
+
+
+
+```
+
+### Theme Switching
+
+```tsx
+import {
+ OxygenUIThemeProvider,
+ OxygenTheme,
+ AcrylicOrangeTheme,
+ AcrylicPurpleTheme,
+} from '@wso2/oxygen-ui';
+
+const themes = [
+ { key: 'default', label: 'Default', theme: OxygenTheme },
+ { key: 'orange', label: 'Orange', theme: AcrylicOrangeTheme },
+ { key: 'purple', label: 'Purple', theme: AcrylicPurpleTheme },
+];
+
+
+
+
+```
+
+---
+
+## ListingTable
+
+Advanced data table with compound component pattern. Supports search, sort, pagination, selection, and bulk actions.
+
+```tsx
+import { ListingTable } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `ListingTable.Provider` | Context provider for state management |
+| `ListingTable.Container` | Paper wrapper with styling |
+| `ListingTable.Toolbar` | Search, filters, and actions bar |
+| `ListingTable.Head` | Table header wrapper |
+| `ListingTable.Body` | Table body wrapper |
+| `ListingTable.Footer` | Table footer (pagination) |
+| `ListingTable.Row` | Table row |
+| `ListingTable.Cell` | Table cell |
+| `ListingTable.SortLabel` | Sortable column header |
+| `ListingTable.DensityControl` | Density toggle button |
+| `ListingTable.EmptyState` | Empty state display |
+| `ListingTable.RowActions` | Row action buttons |
+| `ListingTable.CellIcon` | Icon in cell |
+
+### ListingTable Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `variant` | `'table' \| 'card'` | `'table'` | Display style |
+| `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Row padding |
+| `striped` | `boolean` | `false` | Alternating row colors |
+| `bordered` | `boolean` | `false` | Cell borders |
+
+### Provider Props (Context State)
+
+```tsx
+interface ListingTableProviderProps {
+ // Search
+ searchValue?: string;
+ onSearchChange?: (value: string) => void;
+
+ // Sort
+ sortField?: string;
+ sortDirection?: 'asc' | 'desc';
+ onSortChange?: (field: string, direction: 'asc' | 'desc') => void;
+
+ // Pagination
+ page?: number; // 0-indexed
+ rowsPerPage?: number;
+ totalCount?: number;
+ onPageChange?: (page: number) => void;
+ onRowsPerPageChange?: (rowsPerPage: number) => void;
+
+ // Selection
+ selected?: readonly string[];
+ onSelectionChange?: (selected: readonly string[]) => void;
+ onSelectAll?: () => void;
+ onClearSelection?: () => void;
+ isSelected?: (id: string) => boolean;
+
+ // Filters
+ filters?: Record;
+ onFilterChange?: (key: string, value: unknown) => void;
+ onClearFilters?: () => void;
+
+ // Bulk Actions
+ onBulkDelete?: (ids: readonly string[]) => void;
+ onBulkAction?: (actionId: string, ids: readonly string[]) => void;
+
+ // UI State
+ density?: 'compact' | 'standard' | 'comfortable';
+ onDensityChange?: (density: ListingTableDensity) => void;
+ loading?: boolean;
+}
+```
+
+### Toolbar Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `showSearch` | `boolean` | `false` | Show built-in search |
+| `searchSlot` | `ReactNode` | - | Custom search component |
+| `searchPlaceholder` | `string` | `'Search...'` | Search placeholder |
+| `actions` | `ReactNode` | - | Right-side actions |
+| `children` | `ReactNode` | - | Center content |
+
+### Basic Table
+
+```tsx
+
+
+
+
+ Name
+ Status
+ Actions
+
+
+
+ {data.map((row) => (
+
+ {row.name}
+ {row.status}
+
+ edit(row.id) },
+ { id: 'delete', label: 'Delete', onClick: () => delete(row.id) },
+ ]}
+ />
+
+
+ ))}
+
+
+
+```
+
+### Full-Featured Table with Context
+
+```tsx
+const [search, setSearch] = useState('');
+const [sortField, setSortField] = useState('name');
+const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
+const [page, setPage] = useState(0);
+const [rowsPerPage, setRowsPerPage] = useState(10);
+
+ {
+ setSortField(field);
+ setSortDirection(dir);
+ }}
+ page={page}
+ rowsPerPage={rowsPerPage}
+ totalCount={data.length}
+ onPageChange={setPage}
+ onRowsPerPageChange={setRowsPerPage}
+>
+
+ }
+ />
+
+
+
+
+ Name
+
+
+ Status
+
+
+
+
+ {paginatedData.map((row) => (
+
+ {row.name}
+ {row.status}
+
+ ))}
+
+
+
+
+
+```
+
+---
+
+## AppShell
+
+Application layout wrapper using compound component pattern.
+
+```tsx
+import { AppShell } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `AppShell.Navbar` | Top navigation area (header) |
+| `AppShell.Sidebar` | Left sidebar area |
+| `AppShell.Main` | Main content area |
+| `AppShell.Footer` | Bottom footer area |
+| `AppShell.NotificationPanel` | Overlay notification panel |
+
+### Usage
+
+```tsx
+
+
+
+
+
+
+ ...
+
+
+
+ {/* React Router outlet */}
+
+
+
+
+
+
+
+
+ ...
+
+
+
+```
+
+---
+
+## Layout
+
+Lightweight layout compound component for building flex-based page structures.
+
+```tsx
+import { Layout } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `Layout.Sidebar` | Sidebar area |
+| `Layout.Navbar` | Top navigation area |
+| `Layout.Content` | Main content area |
+| `Layout.Header` | Header area |
+
+### Props
+
+Extends MUI `BoxProps`. The root component renders a flex row container.
+
+### Usage
+
+```tsx
+
+
+
+
+
+ ...
+
+
+ {/* Page content */}
+
+
+```
+
+---
+
+## Header
+
+Top navigation bar compound component.
+
+```tsx
+import { Header } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `Header.Toggle` | Sidebar collapse toggle button |
+| `Header.Brand` | Logo and title container |
+| `Header.BrandLogo` | Logo image/icon |
+| `Header.BrandTitle` | Application title |
+| `Header.Switchers` | Context switcher container |
+| `Header.Actions` | Right-side action buttons |
+| `Header.Spacer` | Flexible spacer |
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `minimal` | `boolean` | `false` | Hide switchers section |
+| `sx` | `SxProps` | - | Custom styles |
+
+### Usage
+
+```tsx
+
+ setCollapsed(!collapsed)} />
+
+
+
+
+
+ My Application
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Sidebar
+
+Collapsible navigation sidebar compound component.
+
+```tsx
+import { Sidebar, SIDEBAR_WIDTH, COLLAPSED_SIDEBAR_WIDTH } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `Sidebar.Nav` | Scrollable navigation container |
+| `Sidebar.Category` | Menu category group |
+| `Sidebar.CategoryLabel` | Category heading text |
+| `Sidebar.Item` | Navigation menu item |
+| `Sidebar.ItemIcon` | Item icon |
+| `Sidebar.ItemLabel` | Item text label |
+| `Sidebar.ItemBadge` | Badge indicator |
+| `Sidebar.Footer` | Fixed bottom section |
+| `Sidebar.User` | User profile section |
+| `Sidebar.UserAvatar` | User avatar |
+| `Sidebar.UserName` | User name text |
+| `Sidebar.UserEmail` | User email text |
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `collapsed` | `boolean` | `false` | Collapsed state |
+| `activeItem` | `string` | - | Active menu item ID |
+| `expandedMenus` | `Record` | `{}` | Expanded submenu map |
+| `onSelect` | `(id: string) => void` | - | Item selection handler |
+| `onToggleExpand` | `(id: string) => void` | - | Submenu toggle handler |
+| `width` | `number` | `250` | Expanded width (px) |
+| `collapsedWidth` | `number` | `64` | Collapsed width (px) |
+
+### Usage
+
+```tsx
+import { HomeIcon, SettingsIcon } from '@wso2/oxygen-ui-icons-react';
+
+ toggleExpand(id)}
+>
+
+
+ Main
+
+
+
+ Home
+
+
+
+
+ Dashboard
+ 5
+
+
+
+
+
+
+
+ Settings
+
+
+
+```
+
+---
+
+## Form
+
+Form components namespace for building forms with card-based layouts and wizards.
+
+```tsx
+import { Form } from '@wso2/oxygen-ui';
+```
+
+### Components
+
+| Component | Description |
+|-----------|-------------|
+| `Form.CardButton` | Clickable card button for selection |
+| `Form.DisappearingCardButtonContent` | Content that fades when card is selected |
+| `Form.CardHeader` | Card header section |
+| `Form.CardContent` | Card content section |
+| `Form.CardActions` | Card action buttons section |
+| `Form.CardMedia` | Card media/image section |
+| `Form.Stack` | Form field stack layout |
+| `Form.Header` | Section header text |
+| `Form.Subheader` | Section subheader text |
+| `Form.Body` | Section body text |
+| `Form.Section` | Form section with heading |
+| `Form.Wizard` | Multi-step form wizard |
+| `Form.ElementWrapper` | Wrapper for form elements |
+
+### Wizard Props
+
+```tsx
+interface WizardStep {
+ id: string;
+ label: string;
+ content: ReactNode;
+ optional?: boolean;
+}
+
+interface WizardProps {
+ steps: WizardStep[];
+ activeStep: number;
+ onStepChange: (step: number) => void;
+ onComplete?: () => void;
+}
+```
+
+### Usage
+
+```tsx
+// Card-based selection form
+
+ setSelected('option1')}>
+
+
+ Description of option 1
+
+
+
+ setSelected('option2')}>
+
+
+ Description of option 2
+
+
+
+
+// Section with typography
+
+ User Details
+ Enter your information
+ Additional context about the form section.
+
+```
+
+---
+
+## NotificationPanel
+
+Slide-out notification panel with tabs and compound component pattern.
+
+```tsx
+import { NotificationPanel } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `NotificationPanel.Header` | Panel header container |
+| `NotificationPanel.HeaderIcon` | Header icon |
+| `NotificationPanel.HeaderTitle` | Header title text |
+| `NotificationPanel.HeaderBadge` | Unread count badge |
+| `NotificationPanel.HeaderClose` | Close button |
+| `NotificationPanel.Tabs` | Tab navigation |
+| `NotificationPanel.Actions` | Action buttons container |
+| `NotificationPanel.List` | Notification list container |
+| `NotificationPanel.Item` | Individual notification item |
+| `NotificationPanel.ItemAvatar` | Item avatar |
+| `NotificationPanel.ItemTitle` | Item title |
+| `NotificationPanel.ItemMessage` | Item message text |
+| `NotificationPanel.ItemTimestamp` | Item timestamp |
+| `NotificationPanel.ItemAction` | Item action button |
+| `NotificationPanel.EmptyState` | Empty state display |
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `open` | `boolean` | required | Panel visibility |
+| `onClose` | `() => void` | required | Close handler |
+
+### Tabs Props
+
+```tsx
+interface NotificationTabConfig {
+ id: string;
+ label: string;
+ badge?: number;
+}
+
+interface NotificationTabsProps {
+ tabs: NotificationTabConfig[];
+ activeTab: string;
+ onTabChange: (tabId: string) => void;
+}
+```
+
+### Usage
+
+```tsx
+ setOpen(false)}>
+
+
+ Notifications
+
+
+
+
+
+
+
+
+
+
+
+ {notifications.map((n) => (
+
+
+ {n.title}
+ {n.message}
+ {n.timestamp}
+ handleAction(n.id)}>
+ View
+
+
+ ))}
+
+
+ {notifications.length === 0 && (
+
+ )}
+
+```
+
+### Hooks
+
+```tsx
+import { useNotificationPanel } from '@wso2/oxygen-ui';
+
+// Access panel context inside NotificationPanel
+const { open, onClose } = useNotificationPanel();
+```
+
+---
+
+## SearchBar
+
+Search input components with optional advanced filtering.
+
+```tsx
+import { SearchBar, SearchBarWithAdvancedFilter } from '@wso2/oxygen-ui';
+```
+
+### SearchBar Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `value` | `string` | - | Search input value |
+| `onChange` | `(value: string) => void` | - | Change handler |
+| `placeholder` | `string` | `'Search...'` | Placeholder text |
+
+### SearchBarWithAdvancedFilter Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `value` | `string` | - | Search input value |
+| `onChange` | `(value: string) => void` | - | Change handler |
+| `advancedFilters` | `AdvancedFilterState` | - | Filter state |
+| `onAdvancedFilterChange` | `(filters: AdvancedFilterState) => void` | - | Filter change handler |
+
+### Usage
+
+```tsx
+// Basic search
+
+
+// With advanced filters
+
+```
+
+---
+
+## StatCard
+
+Statistics display card for dashboards.
+
+```tsx
+import { StatCard } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `title` | `string` | required | Stat label |
+| `value` | `string \| number` | required | Stat value |
+| `icon` | `ReactNode` | - | Optional icon |
+| `change` | `number` | - | Percentage change |
+| `trend` | `'up' \| 'down'` | - | Trend direction |
+
+### Usage
+
+```tsx
+
+```
+
+---
+
+## PageContent
+
+Page content wrapper with consistent padding and max-width.
+
+```tsx
+import { PageContent } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `children` | `ReactNode` | required | Page content |
+| `maxWidth` | `string \| number` | - | Maximum width |
+
+### Usage
+
+```tsx
+
+ Dashboard
+ {/* Page content */}
+
+```
+
+---
+
+## ComplexSelect
+
+Enhanced select component with search and multi-select capabilities.
+
+```tsx
+import { ComplexSelect } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `options` | `Option[]` | required | Select options |
+| `value` | `string \| string[]` | - | Selected value(s) |
+| `onChange` | `(value) => void` | - | Change handler |
+| `multiple` | `boolean` | `false` | Enable multi-select |
+| `searchable` | `boolean` | `false` | Enable search |
+
+### Usage
+
+```tsx
+
+```
+
+---
+
+## NotificationBanner
+
+Dismissible system alert banner for top-level announcements.
+
+```tsx
+import { NotificationBanner } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `visible` | `boolean` | `true` | Whether the banner is visible |
+| `severity` | `'info' \| 'warning' \| 'error' \| 'success'` | `'info'` | Banner severity/type |
+| `title` | `string` | - | Optional banner title |
+| `message` | `string` | required | Banner message text |
+| `actionLabel` | `string` | - | Optional action button label |
+| `onAction` | `() => void` | - | Action button callback |
+| `onDismiss` | `() => void` | - | Dismiss callback |
+| `sx` | `SxProps` | - | Custom styles |
+
+### Usage
+
+```tsx
+ openMaintenanceInfo()}
+ onDismiss={() => setShowBanner(false)}
+/>
+```
+
+---
+
+## ParticleBackground
+
+Animated particle canvas background with mouse interaction. Adapts to light/dark color scheme.
+
+```tsx
+import { ParticleBackground } from '@wso2/oxygen-ui';
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `opacity` | `number` | `0.5` | Canvas opacity |
+| `baseDensity` | `number` | `0.12` | Particles per 10,000 px² |
+| `maxSpeed` | `number` | `0.3` | Maximum speed in px per frame |
+| `radius` | `[number, number]` | `[2.0, 3.2]` | Min and max particle radius |
+| `linkDist` | `number` | `210` | Maximum distance to draw lines between particles |
+| `linkAlpha` | `number` | `0.2` | Base opacity of the lines |
+| `mouseInfluence` | `number` | `110` | Radius of mouse influence |
+| `repelStrength` | `number` | `0.35` | Strength of the repel effect |
+| `clickBurst` | `number` | `120` | Impulse on click |
+
+### Usage
+
+```tsx
+// Fixed background behind content
+
+
+// Higher density with stronger interaction
+
+```
+
+---
+
+## Other Components
+
+### ColorSchemeToggle
+
+Dark/light mode toggle button.
+
+```tsx
+import { ColorSchemeToggle } from '@wso2/oxygen-ui';
+
+
+```
+
+### ThemeSwitcher
+
+Theme selection UI component.
+
+```tsx
+import { ThemeSwitcher } from '@wso2/oxygen-ui';
+
+ // Requires OxygenUIThemeProvider with themes
+```
+
+---
+
+## Footer
+
+Application footer compound component with composition pattern.
+
+```tsx
+import { Footer } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `Footer.Copyright` | Copyright text display |
+| `Footer.Version` | Version number display (monospace, muted) |
+| `Footer.Link` | Footer link (auto-placed in right section) |
+| `Footer.Divider` | Visual divider between footer items |
+
+### Footer Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `children` | `ReactNode` | required | Footer sub-components |
+| `sx` | `SxProps` | - | Custom styles |
+
+### Usage
+
+```tsx
+
+```
+
+> **Note:** `Footer.Copyright`, `Footer.Version`, and `Footer.Divider` are placed in the left section. `Footer.Link` elements are placed in the right section. Layout is responsive (stacks on mobile).
+
+---
+
+## UserMenu
+
+User profile dropdown menu compound component.
+
+```tsx
+import { UserMenu } from '@wso2/oxygen-ui';
+```
+
+### Sub-components
+
+| Component | Description |
+|-----------|-------------|
+| `UserMenu.Trigger` | Avatar button that opens the menu |
+| `UserMenu.Header` | User info section (name, email, avatar, role badge) |
+| `UserMenu.Item` | Menu item with icon, label, and optional badge |
+| `UserMenu.Logout` | Destructive logout menu item |
+| `UserMenu.Divider` | Menu divider |
+
+### UserMenu Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `children` | `ReactNode` | required | Menu sub-components |
+
+### UserMenuTrigger Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `name` | `string` | required | User display name |
+| `avatar` | `string \| null` | - | Avatar image URL |
+| `showName` | `boolean` | `false` | Show name text after avatar |
+
+### UserMenuHeader Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `name` | `string` | required | User display name |
+| `email` | `string` | required | User email address |
+| `avatar` | `string \| null` | - | Avatar image URL |
+| `role` | `string` | - | Role badge text (e.g., "Pro", "Admin") |
+
+### UserMenuItem Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `icon` | `ReactNode` | - | Left-side icon |
+| `label` | `string` | required | Item label text |
+| `badge` | `string` | - | Right-side badge/chip |
+| `onClick` | `() => void` | - | Click handler |
+
+### UserMenuLogout Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `icon` | `ReactNode` | - | Left-side icon |
+| `label` | `string` | `'Log out'` | Item label text |
+| `onClick` | `() => void` | - | Click handler |
+
+### UserMenuUser Type
+
+```tsx
+interface UserMenuUser {
+ name: string;
+ email: string;
+ avatar?: string;
+ role?: string;
+}
+```
+
+### Usage
+
+```tsx
+import { UserMenu } from '@wso2/oxygen-ui';
+import { UserIcon, SettingsIcon, LogOutIcon } from '@wso2/oxygen-ui-icons-react';
+
+
+
+
+ } label="Profile" onClick={() => {}} />
+ } label="Settings" onClick={() => {}} />
+
+ } onClick={() => signOut()} />
+
+```
+
+### PageTitle
+
+Page header with avatar and breadcrumb support.
+
+```tsx
+import { PageTitle } from '@wso2/oxygen-ui';
+
+
+
+ Dashboard
+ Overview of your projects
+
+```
+
+### CodeBlock
+
+Syntax-highlighted code display.
+
+```tsx
+import { CodeBlock } from '@wso2/oxygen-ui';
+
+
+ {`const greeting = "Hello, World!";`}
+
+```
+
+---
+
+## MUI X Namespaces
+
+MUI X components are exported as namespaces to avoid naming conflicts.
+
+```tsx
+import { DataGrid, DatePickers, TreeView, AdapterDateFns } from '@wso2/oxygen-ui';
+```
+
+> **Note:** Charts are available in a separate package `@wso2/oxygen-ui-charts-react` built on Recharts.
+
+### DataGrid
+
+```tsx
+
+
+
+```
+
+### DatePickers
+
+```tsx
+
+
+
+
+
+
+
+```
+
+### TreeView
+
+```tsx
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Hooks
+
+### useThemeSwitcher
+
+Access theme switching context.
+
+```tsx
+import { useThemeSwitcher } from '@wso2/oxygen-ui';
+
+function ThemeSelector() {
+ const { currentTheme, themes, setTheme, isActive } = useThemeSwitcher();
+
+ return (
+
+ );
+}
+```
+
+### useThemeContent
+
+Get content based on current color scheme (light/dark).
+
+```tsx
+import { useThemeContent } from '@wso2/oxygen-ui';
+
+function AdaptiveContent() {
+ const content = useThemeContent({
+ light: 'Light mode content',
+ dark: 'Dark mode content',
+ });
+
+ return {content};
+}
+```
+
+### useAppShell
+
+Manage AppShell state (sidebar collapsed, notifications open, etc.).
+
+```tsx
+import { useAppShell } from '@wso2/oxygen-ui';
+
+function MyComponent() {
+ const {
+ sidebarCollapsed,
+ setSidebarCollapsed,
+ notificationPanelOpen,
+ setNotificationPanelOpen,
+ } = useAppShell();
+
+ return (
+
+ );
+}
+```
+
+### useNotifications
+
+Manage notification state and actions.
+
+```tsx
+import { useNotifications } from '@wso2/oxygen-ui';
+
+function NotificationManager() {
+ const {
+ notifications,
+ unreadCount,
+ addNotification,
+ markAsRead,
+ markAllAsRead,
+ removeNotification,
+ } = useNotifications();
+
+ return (
+
+
+
+ );
+}
+```
+
+### useListingTable
+
+Access ListingTable context (returns null outside Provider).
+
+```tsx
+import { useListingTable } from '@wso2/oxygen-ui';
+
+function CustomFilter() {
+ const context = useListingTable();
+ if (!context) return null;
+
+ return (
+ context.onSearchChange?.(e.target.value)}
+ />
+ );
+}
+```
+
+### useListingTableRequired
+
+Access ListingTable context (throws if outside Provider).
+
+```tsx
+import { useListingTableRequired } from '@wso2/oxygen-ui';
+
+function TableControls() {
+ const { density, onDensityChange } = useListingTableRequired();
+ // ...
+}
+```
+
+### Component Context Hooks
+
+```tsx
+// Header context
+import { useHeader } from '@wso2/oxygen-ui';
+const headerContext = useHeader();
+
+// Sidebar context
+import { useSidebar } from '@wso2/oxygen-ui';
+const sidebarContext = useSidebar();
+
+// NotificationPanel context
+import { useNotificationPanel } from '@wso2/oxygen-ui';
+const panelContext = useNotificationPanel();
+```
+
+---
+
+## Utilities
+
+### formatRelativeTime
+
+Format a date as a relative time string.
+
+```tsx
+import { formatRelativeTime } from '@wso2/oxygen-ui';
+
+formatRelativeTime(new Date(Date.now() - 1000 * 60 * 5));
+// Returns: "5 minutes ago"
+
+formatRelativeTime(new Date(Date.now() - 1000 * 60 * 60 * 2));
+// Returns: "2 hours ago"
+```
+
+Returns: `"Just now"` | `"X minute(s) ago"` | `"X hour(s) ago"` | `"X day(s) ago"` | locale date string (after 7 days).
+
+### pxToRem
+
+Convert pixel values to rem based on 14px base font size.
+
+```tsx
+import { pxToRem } from '@wso2/oxygen-ui';
+
+pxToRem(14); // "1rem"
+pxToRem(28); // "2rem"
+pxToRem(7); // "0.5rem"
+```
+
+---
+
+## Type Exports
+
+Import types for TypeScript usage:
+
+```tsx
+import type {
+ // Theme
+ ThemeOption,
+ ThemeSwitcherContextValue,
+
+ // ListingTable
+ ListingTableProps,
+ ListingTableContainerProps,
+ ListingTableHeadProps,
+ ListingTableBodyProps,
+ ListingTableFooterProps,
+ ListingTableRowProps,
+ ListingTableCellProps,
+ ListingTableProviderProps,
+ ListingTableContextValue,
+ ListingTableToolbarProps,
+ ListingTableEmptyStateProps,
+ ListingTableDensityControlProps,
+ ListingTableSortLabelProps,
+ ListingTableRowActionsProps,
+ ListingTableCellIconProps,
+ ListingTableDensity,
+ ListingTableVariant,
+ ListingTableSortDirection,
+ ListingSortDirection,
+
+ // AppShell
+ AppShellProps,
+ AppShellNavbarProps,
+ AppShellSidebarProps,
+ AppShellMainProps,
+ AppShellFooterProps,
+ AppShellNotificationPanelProps,
+
+ // Header
+ HeaderProps,
+ HeaderSpacerProps,
+ HeaderToggleProps,
+ HeaderBrandProps,
+ HeaderBrandLogoProps,
+ HeaderBrandTitleProps,
+ HeaderSwitchersProps,
+ HeaderActionsProps,
+ HeaderContextValue,
+
+ // Sidebar
+ SidebarProps,
+ SidebarNavProps,
+ SidebarCategoryProps,
+ SidebarCategoryLabelProps,
+ SidebarItemProps,
+ SidebarItemIconProps,
+ SidebarItemLabelProps,
+ SidebarItemBadgeProps,
+ SidebarFooterProps,
+ SidebarUserProps,
+ SidebarUserAvatarProps,
+ SidebarUserNameProps,
+ SidebarUserEmailProps,
+ SidebarContextValue,
+ SidebarItemContextValue,
+
+ // NotificationPanel
+ NotificationPanelProps,
+ NotificationHeaderProps,
+ NotificationHeaderIconProps,
+ NotificationHeaderTitleProps,
+ NotificationHeaderBadgeProps,
+ NotificationHeaderCloseProps,
+ NotificationTabsProps,
+ NotificationTabConfig,
+ NotificationActionsProps,
+ NotificationListProps,
+ NotificationItemProps,
+ NotificationItemAvatarProps,
+ NotificationItemTitleProps,
+ NotificationItemMessageProps,
+ NotificationItemTimestampProps,
+ NotificationItemActionProps,
+ NotificationItemContextValue,
+ NotificationEmptyStateProps,
+ NotificationPanelContextValue,
+ NotificationType,
+ NotificationTypeProps,
+
+ // Footer
+ FooterProps,
+ FooterCopyrightProps,
+ FooterVersionProps,
+ FooterLinkProps,
+
+ // UserMenu
+ UserMenuProps,
+ UserMenuUser,
+ UserMenuTriggerProps,
+ UserMenuHeaderProps,
+ UserMenuItemProps,
+ UserMenuLogoutProps,
+
+ // NotificationBanner
+ NotificationBannerProps,
+
+ // ParticleBackground
+ ParticleBackgroundProps,
+
+ // Other components
+ CodeBlockProps,
+ ColorSchemeImageProps,
+ ColorSchemeImageAttribute,
+ ComplexSelectProps,
+ PageContentProps,
+ PageTitleProps,
+ PageTitleAvatarProps,
+ PageTitleHeaderProps,
+ PageTitleSubHeaderProps,
+ PageTitleLinkProps,
+ StatCardProps,
+ ThemeSwitcherProps,
+ ThemeSelectProps,
+ SearchBarProps,
+ SearchBarWithAdvancedFilterProps,
+ AdvancedFilterState,
+
+ // Form
+ WizardProps,
+ WizardStep,
+
+ // Hooks
+ AppShellState,
+ AppShellActions,
+ UseAppShellOptions,
+ UseAppShellReturn,
+ NotificationItem,
+ NotificationActions,
+ UseNotificationsOptions,
+ UseNotificationsReturn,
+} from '@wso2/oxygen-ui';
+```
diff --git a/portals/api-control-plane/.claude/oxygen-ui/migration.md b/portals/api-control-plane/.claude/oxygen-ui/migration.md
new file mode 100644
index 0000000000..fc10ed5e88
--- /dev/null
+++ b/portals/api-control-plane/.claude/oxygen-ui/migration.md
@@ -0,0 +1,281 @@
+# Migration Guide to Oxygen UI
+
+## Table of Contents
+- [Overview](#overview)
+- [Install Packages](#install-packages)
+- [Update Imports](#update-imports)
+- [ESLint Plugin Setup](#eslint-plugin-setup)
+- [Common Patterns](#common-patterns)
+
+---
+
+## Overview
+
+Migrating to Oxygen UI involves:
+1. Installing Oxygen UI packages
+2. Replacing direct `@mui/*` imports with `@wso2/oxygen-ui`
+3. Replacing `lucide-react` imports with `@wso2/oxygen-ui-icons-react`
+4. Wrapping app with `OxygenUIThemeProvider`
+5. Setting up ESLint plugin to prevent regressions
+
+---
+
+## Install Packages
+
+```bash
+# Add Oxygen UI packages
+pnpm add @wso2/oxygen-ui @wso2/oxygen-ui-icons-react
+
+# Add ESLint plugin (dev dependency)
+pnpm add -D @wso2/eslint-plugin-oxygen-ui
+
+# Ensure peer dependencies
+pnpm add @emotion/react @emotion/styled date-fns
+```
+
+---
+
+## Update Imports
+
+### MUI Components
+
+**Before:**
+```tsx
+import { Box, Stack, Button } from '@mui/material';
+import Typography from '@mui/material/Typography';
+```
+
+**After:**
+```tsx
+import { Box, Stack, Button, Typography } from '@wso2/oxygen-ui';
+```
+
+### MUI X Components
+
+**Before:**
+```tsx
+import { DataGrid } from '@mui/x-data-grid';
+import { DatePicker } from '@mui/x-date-pickers';
+import { BarChart } from '@mui/x-charts';
+```
+
+**After:**
+```tsx
+import { DataGrid, DatePickers } from '@wso2/oxygen-ui';
+
+const { DataGrid: DataGridComponent } = DataGrid;
+const { DatePicker } = DatePickers;
+
+// For Charts, use the separate package
+import { BarChart } from '@wso2/oxygen-ui-charts-react';
+```
+
+### Icons
+
+**Before:**
+```tsx
+import { Settings, Bell, User } from 'lucide-react';
+```
+
+**After:**
+```tsx
+import { Settings, Bell, User } from '@wso2/oxygen-ui-icons-react';
+```
+
+### Theme Provider
+
+**Before:**
+```tsx
+import { ThemeProvider, createTheme } from '@mui/material';
+
+const theme = createTheme({ ... });
+
+
+
+
+```
+
+**After:**
+```tsx
+import { OxygenUIThemeProvider } from '@wso2/oxygen-ui';
+
+
+
+
+```
+
+---
+
+## ESLint Plugin Setup
+
+### ESLint 9+ (Flat Config)
+
+```js
+// eslint.config.js
+import oxygenUIPlugin from '@wso2/eslint-plugin-oxygen-ui';
+
+export default [
+ oxygenUIPlugin.configs.recommended,
+ // ... other configs
+];
+```
+
+### Custom Configuration
+
+```js
+// eslint.config.js
+import oxygenUIPlugin from '@wso2/eslint-plugin-oxygen-ui';
+
+export default [
+ {
+ plugins: {
+ '@wso2/oxygen-ui': oxygenUIPlugin,
+ },
+ rules: {
+ '@wso2/oxygen-ui/no-direct-mui-imports': ['error', {
+ suggestedPackage: '@wso2/oxygen-ui',
+ allowedPackages: [], // Temporarily allow specific MUI packages during migration
+ }],
+ '@wso2/oxygen-ui/no-direct-lucide-imports': ['error', {
+ suggestedPackage: '@wso2/oxygen-ui-icons-react',
+ }],
+ },
+ },
+];
+```
+
+### Auto-fix Imports
+
+```bash
+# Find violations
+pnpm eslint src/
+
+# Auto-fix imports
+pnpm eslint src/ --fix
+```
+
+### Gradual Migration
+
+During migration, temporarily allow specific MUI packages:
+
+```js
+'@wso2/oxygen-ui/no-direct-mui-imports': ['warn', {
+ allowedPackages: ['@mui/x-data-grid', '@mui/x-date-pickers'],
+}],
+```
+
+---
+
+## Common Patterns
+
+### Layout Migration
+
+**Before (custom layout):**
+```tsx
+
+ ...
+ ...
+ ...
+
+```
+
+**After (Oxygen AppShell):**
+```tsx
+import { AppShell, Header, Sidebar } from '@wso2/oxygen-ui';
+
+
+
+
+
+
+ ...
+
+ ...
+
+```
+
+### Theme Customization Migration
+
+**Before:**
+```tsx
+const theme = createTheme({
+ palette: {
+ primary: { main: '#1976d2' },
+ },
+});
+```
+
+**After:**
+```tsx
+import { extendTheme } from '@mui/material/styles';
+import { OxygenThemeBase, OxygenUIThemeProvider } from '@wso2/oxygen-ui';
+
+const customTheme = extendTheme({
+ ...OxygenThemeBase,
+ colorSchemes: {
+ light: {
+ palette: { primary: { main: '#1976d2' } },
+ },
+ },
+});
+
+
+
+
+```
+
+### Icon Button Migration
+
+**Before:**
+```tsx
+import IconButton from '@mui/material/IconButton';
+import SettingsIcon from '@mui/icons-material/Settings';
+
+
+```
+
+**After:**
+```tsx
+import { IconButton } from '@wso2/oxygen-ui';
+import { Settings } from '@wso2/oxygen-ui-icons-react';
+
+
+```
+
+### Color Mode Migration
+
+**Before:**
+```tsx
+import { useColorScheme } from '@mui/material';
+
+const { mode, setMode } = useColorScheme();
+```
+
+**After:**
+```tsx
+import { ColorSchemeToggle, useThemeContent } from '@wso2/oxygen-ui';
+
+// Simple toggle
+
+
+// Conditional content
+const content = useThemeContent({
+ light: 'Light content',
+ dark: 'Dark content',
+});
+```
+
+---
+
+## Migration Checklist
+
+- [ ] Install `@wso2/oxygen-ui` and `@wso2/oxygen-ui-icons-react`
+- [ ] Install peer dependencies (`@emotion/react`, `@emotion/styled`, `date-fns`)
+- [ ] Wrap root component with `OxygenUIThemeProvider`
+- [ ] Replace `@mui/material` imports with `@wso2/oxygen-ui`
+- [ ] Replace `@mui/icons-material` or `lucide-react` with `@wso2/oxygen-ui-icons-react`
+- [ ] Update MUI X imports to use namespaced exports
+- [ ] Install and configure `@wso2/eslint-plugin-oxygen-ui`
+- [ ] Run ESLint with `--fix` to auto-correct remaining imports
+- [ ] Test light/dark mode functionality
+- [ ] Verify theme styling matches expectations
diff --git a/portals/api-control-plane/.claude/oxygen-ui/patterns.md b/portals/api-control-plane/.claude/oxygen-ui/patterns.md
new file mode 100644
index 0000000000..a2f48cc0ce
--- /dev/null
+++ b/portals/api-control-plane/.claude/oxygen-ui/patterns.md
@@ -0,0 +1,1197 @@
+# Oxygen UI Common Patterns
+
+Ready-to-use UI patterns for common application scenarios.
+
+## Table of Contents
+
+- [App Shell Layout](#app-shell-layout)
+- [Authentication Forms](#authentication-forms)
+- [Dashboard Layout](#dashboard-layout)
+- [Data Tables](#data-tables)
+- [Modal Dialogs](#modal-dialogs)
+- [Error States](#error-states)
+- [Form Patterns](#form-patterns)
+- [Navigation Patterns](#navigation-patterns)
+- [Theme Switching](#theme-switching)
+
+---
+
+## App Shell Layout
+
+Complete application shell with header, sidebar, and content area.
+
+```tsx
+import { useState } from 'react';
+import {
+ OxygenUIThemeProvider,
+ OxygenTheme,
+ AppShell,
+ Header,
+ Sidebar,
+ Footer,
+ ColorSchemeToggle,
+ UserMenu,
+} from '@wso2/oxygen-ui';
+import { HomeIcon, DashboardIcon, SettingsIcon } from '@wso2/oxygen-ui-icons-react';
+
+function App() {
+ const [collapsed, setCollapsed] = useState(false);
+ const [activeItem, setActiveItem] = useState('home');
+
+ return (
+
+
+
+
+ setCollapsed(!collapsed)}
+ />
+
+
+
+
+ My App
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Main
+
+
+ Home
+
+
+
+ Dashboard
+
+
+
+
+
+
+ Settings
+
+
+
+
+
+
+
+ {/* Page content goes here */}
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+---
+
+## Authentication Forms
+
+### Login Form
+
+```tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Typography,
+ TextField,
+ Button,
+ Link,
+ Divider,
+ Alert,
+} from '@wso2/oxygen-ui';
+
+function LoginForm() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+ try {
+ // Login logic here
+ } catch (err) {
+ setError('Invalid credentials');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Sign In
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ setEmail(e.target.value)}
+ margin="normal"
+ required
+ />
+
+ setPassword(e.target.value)}
+ margin="normal"
+ required
+ />
+
+
+
+
+
+ Forgot password?
+
+
+ Create account
+
+
+
+ or
+
+ }>
+ Continue with Google
+
+
+
+
+ );
+}
+```
+
+### Password Reset Form
+
+```tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Typography,
+ TextField,
+ Button,
+ Alert,
+} from '@wso2/oxygen-ui';
+
+function PasswordResetForm() {
+ const [email, setEmail] = useState('');
+ const [submitted, setSubmitted] = useState(false);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ // Send reset email
+ setSubmitted(true);
+ };
+
+ if (submitted) {
+ return (
+
+
+ If an account exists for {email}, you will receive a password reset link.
+
+
+
+ );
+ }
+
+ return (
+
+
+ Reset Password
+
+
+ Enter your email address and we'll send you a reset link.
+
+
+
+ setEmail(e.target.value)}
+ required
+ />
+
+
+
+ );
+}
+```
+
+---
+
+## Dashboard Layout
+
+### Stats Cards
+
+```tsx
+import { Grid, Paper, Typography, Box } from '@wso2/oxygen-ui';
+import { TrendingUpIcon, TrendingDownIcon } from '@wso2/oxygen-ui-icons-react';
+
+interface StatCardProps {
+ title: string;
+ value: string | number;
+ change?: number;
+ icon?: React.ReactNode;
+}
+
+function StatCard({ title, value, change, icon }: StatCardProps) {
+ const isPositive = change && change > 0;
+
+ return (
+
+
+
+ {title}
+
+ {icon}
+
+
+
+ {value}
+
+
+ {change !== undefined && (
+
+ {isPositive ? : }
+
+ {Math.abs(change)}% from last month
+
+
+ )}
+
+ );
+}
+
+function Dashboard() {
+ return (
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+### Dashboard with Charts
+
+> **Note:** Charts are available in a separate package `@wso2/oxygen-ui-charts-react` built on Recharts.
+
+```tsx
+import { Grid, Paper, Typography, Box } from '@wso2/oxygen-ui';
+import { LineChart, PieChart } from '@wso2/oxygen-ui-charts-react';
+
+function DashboardCharts() {
+ const salesData = [
+ { month: 'Jan', sales: 4000, orders: 240 },
+ { month: 'Feb', sales: 3000, orders: 139 },
+ { month: 'Mar', sales: 2000, orders: 980 },
+ { month: 'Apr', sales: 2780, orders: 390 },
+ { month: 'May', sales: 1890, orders: 480 },
+ { month: 'Jun', sales: 2390, orders: 380 },
+ ];
+
+ return (
+
+
+
+
+ Sales Overview
+
+ d.sales), label: 'Sales' },
+ { data: salesData.map((d) => d.orders), label: 'Orders' },
+ ]}
+ xAxis={[{ data: salesData.map((d) => d.month), scaleType: 'band' }]}
+ />
+
+
+
+
+
+
+ Traffic Sources
+
+
+
+
+
+ );
+}
+```
+
+---
+
+## Data Tables
+
+### Basic Data Table
+
+```tsx
+import { useState, useMemo } from 'react';
+import { ListingTable, Button, IconButton, Chip } from '@wso2/oxygen-ui';
+import { EditIcon, TrashIcon, MoreVerticalIcon } from '@wso2/oxygen-ui-icons-react';
+
+interface User {
+ id: string;
+ name: string;
+ email: string;
+ role: string;
+ status: 'active' | 'inactive';
+}
+
+function UsersTable({ users }: { users: User[] }) {
+ const [search, setSearch] = useState('');
+ const [sortField, setSortField] = useState('name');
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+
+ const filteredData = useMemo(() => {
+ let result = [...users];
+
+ // Filter by search
+ if (search) {
+ result = result.filter(
+ (u) =>
+ u.name.toLowerCase().includes(search.toLowerCase()) ||
+ u.email.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ // Sort
+ result.sort((a, b) => {
+ const aVal = a[sortField as keyof User];
+ const bVal = b[sortField as keyof User];
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
+ return sortDirection === 'asc' ? cmp : -cmp;
+ });
+
+ return result;
+ }, [users, search, sortField, sortDirection]);
+
+ const paginatedData = filteredData.slice(
+ page * rowsPerPage,
+ page * rowsPerPage + rowsPerPage
+ );
+
+ return (
+ {
+ setSortField(field);
+ setSortDirection(dir);
+ }}
+ page={page}
+ rowsPerPage={rowsPerPage}
+ totalCount={filteredData.length}
+ onPageChange={setPage}
+ onRowsPerPageChange={setRowsPerPage}
+ >
+
+
+ Add User
+
+ }
+ />
+
+
+
+
+
+ Name
+
+
+ Email
+
+ Role
+ Status
+ Actions
+
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+
+
+
+ ) : (
+ paginatedData.map((user) => (
+
+ {user.name}
+ {user.email}
+ {user.role}
+
+
+
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+ );
+}
+```
+
+### Card Variant Table
+
+```tsx
+
+
+
+ Project
+ Status
+ Last Updated
+ Actions
+
+
+
+ {projects.map((project) => (
+
+
+
+
+
+
+
+ {project.name}
+
+ {project.description}
+
+
+
+
+
+
+
+ {project.updatedAt}
+
+ {} },
+ { id: 'edit', label: 'Edit', onClick: () => {} },
+ { id: 'delete', label: 'Delete', onClick: () => {}, color: 'error' },
+ ]}
+ />
+
+
+ ))}
+
+
+```
+
+---
+
+## Modal Dialogs
+
+### Confirmation Dialog
+
+```tsx
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button,
+} from '@wso2/oxygen-ui';
+
+interface ConfirmDialogProps {
+ open: boolean;
+ title: string;
+ message: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ loading?: boolean;
+ destructive?: boolean;
+}
+
+function ConfirmDialog({
+ open,
+ title,
+ message,
+ confirmLabel = 'Confirm',
+ cancelLabel = 'Cancel',
+ onConfirm,
+ onCancel,
+ loading = false,
+ destructive = false,
+}: ConfirmDialogProps) {
+ return (
+
+ );
+}
+
+// Usage
+ setDeleteDialogOpen(false)}
+ destructive
+/>
+```
+
+### Form Dialog
+
+```tsx
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ TextField,
+ Box,
+} from '@wso2/oxygen-ui';
+
+function CreateUserDialog({ open, onClose, onSubmit }) {
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+
+ const handleSubmit = () => {
+ onSubmit({ name, email });
+ onClose();
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Error States
+
+### 404 Page
+
+```tsx
+import { Box, Typography, Button } from '@wso2/oxygen-ui';
+
+function NotFoundPage() {
+ return (
+
+
+ 404
+
+
+ Page not found
+
+
+ The page you're looking for doesn't exist or has been moved.
+
+
+
+ );
+}
+```
+
+### Error Boundary Fallback
+
+```tsx
+import { Box, Typography, Button, Paper } from '@wso2/oxygen-ui';
+import { AlertTriangleIcon } from '@wso2/oxygen-ui-icons-react';
+
+function ErrorFallback({ error, resetError }) {
+ return (
+
+
+
+
+ Something went wrong
+
+
+ {error?.message || 'An unexpected error occurred'}
+
+
+
+
+ );
+}
+```
+
+### Empty State
+
+```tsx
+import { Box, Typography, Button } from '@wso2/oxygen-ui';
+import { InboxIcon } from '@wso2/oxygen-ui-icons-react';
+
+function EmptyState({
+ icon = ,
+ title,
+ description,
+ action,
+ actionLabel,
+}) {
+ return (
+
+ {icon}
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+ {action && actionLabel && (
+
+ )}
+
+ );
+}
+
+// Usage
+ setCreateDialogOpen(true)}
+/>
+```
+
+---
+
+## Form Patterns
+
+### Multi-Step Wizard
+
+```tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Stepper,
+ Step,
+ StepLabel,
+ Button,
+ Typography,
+ TextField,
+} from '@wso2/oxygen-ui';
+
+const steps = ['Account', 'Profile', 'Preferences'];
+
+function RegistrationWizard() {
+ const [activeStep, setActiveStep] = useState(0);
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ name: '',
+ company: '',
+ notifications: true,
+ });
+
+ const handleNext = () => {
+ setActiveStep((prev) => prev + 1);
+ };
+
+ const handleBack = () => {
+ setActiveStep((prev) => prev - 1);
+ };
+
+ const handleSubmit = () => {
+ console.log('Form submitted:', formData);
+ };
+
+ const renderStepContent = (step: number) => {
+ switch (step) {
+ case 0:
+ return (
+ <>
+ setFormData({ ...formData, email: e.target.value })}
+ margin="normal"
+ />
+ setFormData({ ...formData, password: e.target.value })}
+ margin="normal"
+ />
+ >
+ );
+ case 1:
+ return (
+ <>
+ setFormData({ ...formData, name: e.target.value })}
+ margin="normal"
+ />
+ setFormData({ ...formData, company: e.target.value })}
+ margin="normal"
+ />
+ >
+ );
+ case 2:
+ return (
+
+ Review your information and click Submit to complete registration.
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {steps.map((label) => (
+
+ {label}
+
+ ))}
+
+
+ {renderStepContent(activeStep)}
+
+
+
+ {activeStep === steps.length - 1 ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+```
+
+### Form with Validation
+
+```tsx
+import { useState } from 'react';
+import { Box, TextField, Button, Alert } from '@wso2/oxygen-ui';
+
+interface FormErrors {
+ [key: string]: string;
+}
+
+function ValidatedForm() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [errors, setErrors] = useState({});
+
+ const validate = (): boolean => {
+ const newErrors: FormErrors = {};
+
+ if (!email) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(email)) {
+ newErrors.email = 'Invalid email format';
+ }
+
+ if (!password) {
+ newErrors.password = 'Password is required';
+ } else if (password.length < 8) {
+ newErrors.password = 'Password must be at least 8 characters';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (validate()) {
+ // Submit form
+ }
+ };
+
+ return (
+
+ setEmail(e.target.value)}
+ error={!!errors.email}
+ helperText={errors.email}
+ margin="normal"
+ />
+
+ setPassword(e.target.value)}
+ error={!!errors.password}
+ helperText={errors.password}
+ margin="normal"
+ />
+
+
+
+ );
+}
+```
+
+---
+
+## Navigation Patterns
+
+### Breadcrumbs
+
+```tsx
+import { Breadcrumbs, Link, Typography } from '@wso2/oxygen-ui';
+import { ChevronRightIcon } from '@wso2/oxygen-ui-icons-react';
+
+function PageBreadcrumbs({ items }: { items: { label: string; href?: string }[] }) {
+ return (
+ }
+ sx={{ mb: 2 }}
+ >
+ {items.map((item, index) => {
+ const isLast = index === items.length - 1;
+ return isLast ? (
+
+ {item.label}
+
+ ) : (
+
+ {item.label}
+
+ );
+ })}
+
+ );
+}
+
+// Usage
+
+```
+
+### Tab Navigation
+
+```tsx
+import { useState } from 'react';
+import { Tabs, Tab, Box } from '@wso2/oxygen-ui';
+
+function TabNavigation() {
+ const [value, setValue] = useState(0);
+
+ return (
+
+ setValue(newValue)}>
+
+
+
+
+
+
+ {value === 0 && Overview content
}
+ {value === 1 && Settings content
}
+ {value === 2 && Members content
}
+
+
+ );
+}
+```
+
+---
+
+## Theme Switching
+
+### Complete Theme Switcher Setup
+
+```tsx
+import {
+ OxygenUIThemeProvider,
+ OxygenTheme,
+ AcrylicOrangeTheme,
+ AcrylicPurpleTheme,
+ ChoreoTheme,
+ ClassicTheme,
+ HighContrastTheme,
+ PaleBaseTheme,
+ PaleGrayTheme,
+ PaleIndigoTheme,
+ useThemeSwitcher,
+ Select,
+ MenuItem,
+} from '@wso2/oxygen-ui';
+
+const themes = [
+ { key: 'default', label: 'Default', theme: OxygenTheme },
+ { key: 'orange', label: 'Acrylic Orange', theme: AcrylicOrangeTheme },
+ { key: 'purple', label: 'Acrylic Purple', theme: AcrylicPurpleTheme },
+ { key: 'choreo', label: 'Choreo', theme: ChoreoTheme },
+ { key: 'classic', label: 'Classic', theme: ClassicTheme },
+ { key: 'highContrast', label: 'High Contrast', theme: HighContrastTheme },
+ { key: 'paleBase', label: 'Pale Base', theme: PaleBaseTheme },
+ { key: 'paleGray', label: 'Pale Gray', theme: PaleGrayTheme },
+ { key: 'paleIndigo', label: 'Pale Indigo', theme: PaleIndigoTheme },
+];
+
+function ThemeSelector() {
+ const { currentTheme, themes, setTheme } = useThemeSwitcher();
+
+ return (
+
+ );
+}
+
+function App() {
+ return (
+
+
+ {/* Rest of app */}
+
+ );
+}
+```
diff --git a/portals/api-control-plane/.claude/oxygen-ui/theming.md b/portals/api-control-plane/.claude/oxygen-ui/theming.md
new file mode 100644
index 0000000000..6894d10dba
--- /dev/null
+++ b/portals/api-control-plane/.claude/oxygen-ui/theming.md
@@ -0,0 +1,334 @@
+# Oxygen UI Theming Guide
+
+## Table of Contents
+- [Available Themes](#available-themes)
+- [Theme Provider](#theme-provider)
+- [Theme Properties](#theme-properties)
+- [Dark Mode](#dark-mode)
+- [CSS Variables](#css-variables)
+- [Custom Themes](#custom-themes)
+
+---
+
+## Available Themes
+
+| Theme | Import | Description |
+|-------|--------|-------------|
+| OxygenTheme | `OxygenTheme` | Default theme (Acrylic Base) |
+| Acrylic Orange | `AcrylicOrangeTheme` | Glass-morphism effects, orange primary |
+| Acrylic Purple | `AcrylicPurpleTheme` | Glass-morphism with purple accents |
+| Choreo | `ChoreoTheme` | Choreo product theme (indigo-violet gradient) |
+| Classic | `ClassicTheme` | Standard flat design |
+| High Contrast | `HighContrastTheme` | Accessibility-focused with high contrast colors |
+| Pale Base | `PaleBaseTheme` | Minimal color palette for subtle interfaces |
+| Pale Gray | `PaleGrayTheme` | Soft gray tones for a muted appearance |
+| Pale Indigo | `PaleIndigoTheme` | Soft indigo tones for a calm interface |
+
+```tsx
+import {
+ OxygenTheme,
+ AcrylicOrangeTheme,
+ AcrylicPurpleTheme,
+ ChoreoTheme,
+ ClassicTheme,
+ HighContrastTheme,
+ PaleBaseTheme,
+ PaleGrayTheme,
+ PaleIndigoTheme,
+} from '@wso2/oxygen-ui';
+```
+
+---
+
+## Theme Provider
+
+### Single Theme (Default)
+```tsx
+import { OxygenUIThemeProvider } from '@wso2/oxygen-ui';
+
+
+
+
+```
+
+### Single Custom Theme
+```tsx
+import { OxygenUIThemeProvider, ClassicTheme } from '@wso2/oxygen-ui';
+
+
+
+
+```
+
+### Multiple Themes with Switching
+```tsx
+import {
+ OxygenUIThemeProvider,
+ OxygenTheme,
+ AcrylicOrangeTheme,
+ AcrylicPurpleTheme,
+ ChoreoTheme,
+ ClassicTheme,
+ HighContrastTheme,
+ PaleBaseTheme,
+ PaleGrayTheme,
+ PaleIndigoTheme,
+} from '@wso2/oxygen-ui';
+
+
+
+
+```
+
+### Provider Props
+| Prop | Type | Description |
+|------|------|-------------|
+| `theme` | `Theme` | Single theme object |
+| `themes` | `ThemeOption[]` | Array of theme options for switching |
+| `initialTheme` | `string` | Initial theme key (when using `themes`) |
+
+---
+
+## Theme Properties
+
+### Color Palette
+```tsx
+// Primary colors (orange gradient)
+theme.palette.primary.main // #fa7b3f
+theme.palette.primary.light
+theme.palette.primary.dark
+
+// Secondary, error, warning, info, success
+theme.palette.secondary.main
+theme.palette.error.main
+theme.palette.warning.main
+theme.palette.info.main
+theme.palette.success.main
+
+// Background
+theme.palette.background.default
+theme.palette.background.paper
+```
+
+### Typography
+Font: **Inter Variable** (auto-bundled, no setup required)
+
+```tsx
+theme.typography.fontFamily // 'Inter Variable', sans-serif
+theme.typography.fontSize // 14 (base)
+
+// Heading variants
+theme.typography.h1 // 36px, bold
+theme.typography.h2 // 30px
+theme.typography.h3 // 24px
+theme.typography.h4 // 20px
+theme.typography.h5 // 16px
+theme.typography.h6 // 14px
+
+// Body variants
+theme.typography.body1 // 14px
+theme.typography.body2 // 12px
+theme.typography.caption // 12px
+```
+
+### Custom Oxygen Properties
+```tsx
+// Glass-morphism blur effects
+theme.oxygen.blur.light // 4px
+theme.oxygen.blur.medium // 8px
+theme.oxygen.blur.heavy // 16px
+
+// Border settings
+theme.oxygen.border.width // '1px'
+theme.oxygen.border.style // 'solid'
+
+// Gradient
+theme.oxygen.gradient.primary // linear-gradient(135deg, #e74420 0%, #fa7b3f 100%)
+
+// Transparent paper (for glass effects)
+theme.oxygen.paperTransparent.light // rgba(255,255,255,0.7)
+theme.oxygen.paperTransparent.dark // rgba(0,0,0,0.5)
+
+// Syntax highlighting colors
+theme.oxygen.syntax.light.keyword
+theme.oxygen.syntax.dark.keyword
+```
+
+---
+
+## Dark Mode
+
+### Color Scheme Toggle
+```tsx
+import { ColorSchemeToggle } from '@wso2/oxygen-ui';
+
+// Cycles through: light → dark → system
+
+```
+
+### Adaptive Images
+```tsx
+import { ColorSchemeImage } from '@wso2/oxygen-ui';
+
+
+```
+
+### Programmatic Mode Access
+```tsx
+import { useTheme } from '@wso2/oxygen-ui';
+
+const theme = useTheme();
+const isDark = theme.palette.mode === 'dark';
+```
+
+### useThemeContent Hook
+```tsx
+import { useThemeContent } from '@wso2/oxygen-ui';
+
+const content = useThemeContent({
+ light: 'Show this in light mode',
+ dark: 'Show this in dark mode'
+});
+```
+
+---
+
+## CSS Variables
+
+Oxygen UI uses CSS variables with the `--oxygen` prefix.
+
+### Key Variables
+```css
+/* Colors */
+--oxygen-palette-primary-main
+--oxygen-palette-background-default
+--oxygen-palette-background-paper
+--oxygen-palette-text-primary
+
+/* Typography */
+--oxygen-typography-fontFamily
+--oxygen-typography-fontSize
+
+/* Shadows (disabled by default) */
+--oxygen-shadows-0
+```
+
+### Accessing in Styles
+```tsx
+
+```
+
+### Color Scheme Selector
+The theme uses `data-color-scheme` attribute for mode switching:
+
+```css
+[data-color-scheme="light"] {
+ --oxygen-palette-background-default: #ffffff;
+}
+
+[data-color-scheme="dark"] {
+ --oxygen-palette-background-default: #121212;
+}
+```
+
+---
+
+## Custom Themes
+
+### Extending Base Theme
+```tsx
+import { extendTheme } from '@mui/material/styles';
+import { OxygenThemeBase } from '@wso2/oxygen-ui';
+
+const customTheme = extendTheme({
+ ...OxygenThemeBase,
+ colorSchemes: {
+ light: {
+ palette: {
+ primary: {
+ main: '#1976d2', // Custom blue
+ },
+ },
+ },
+ dark: {
+ palette: {
+ primary: {
+ main: '#90caf9',
+ },
+ },
+ },
+ },
+});
+
+
+
+
+```
+
+### Component Overrides
+```tsx
+const customTheme = extendTheme({
+ ...OxygenThemeBase,
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ borderRadius: 8,
+ textTransform: 'none',
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ backdropFilter: 'blur(8px)',
+ },
+ },
+ },
+ },
+});
+```
+
+### Theme Switching in Components
+```tsx
+import { useThemeSwitcher } from '@wso2/oxygen-ui';
+
+function ThemeSelector() {
+ const { currentTheme, setTheme, themes } = useThemeSwitcher();
+
+ return (
+
+ );
+}
+```
+
+### LocalStorage Persistence
+Theme selection is automatically persisted to localStorage with the key `oxygen-theme`. The provider reads the saved theme on mount and applies it.
diff --git a/portals/api-control-plane/.claude/skills/oxygen-component/SKILL.md b/portals/api-control-plane/.claude/skills/oxygen-component/SKILL.md
new file mode 100644
index 0000000000..a88c11a321
--- /dev/null
+++ b/portals/api-control-plane/.claude/skills/oxygen-component/SKILL.md
@@ -0,0 +1,147 @@
+---
+name: oxygen-component
+description: Generate Oxygen UI React components following best practices. Use when creating new components, data tables, cards, or UI elements with the Oxygen UI library.
+---
+
+# Generate Oxygen UI Component
+
+## Instructions
+
+1. Read `.claude/oxygen-ui/CLAUDE.md` for critical rules
+2. Read `.claude/oxygen-ui/components.md` for API reference
+3. Generate component using Oxygen UI patterns
+
+## Critical Rules
+
+- Import ALL components from `@wso2/oxygen-ui` (never from `@mui/material`)
+- Import icons from `@wso2/oxygen-ui-icons-react`
+- Use theme tokens via `sx` prop (e.g., `p: 2`, `bgcolor: 'background.paper'`)
+- Never hardcode colors or spacing values
+- For data tables, prefer `ListingTable` over MUI's `Table`
+- For layouts, use `AppShell`, `Header`, `Sidebar`
+
+## Component Template
+
+```tsx
+import { Box, Typography, Button } from '@wso2/oxygen-ui';
+import { IconName } from '@wso2/oxygen-ui-icons-react';
+
+interface MyComponentProps {
+ // Define props
+}
+
+function MyComponent({ ...props }: MyComponentProps) {
+ return (
+
+ {/* Component content */}
+
+ );
+}
+
+export default MyComponent;
+```
+
+## Common Patterns
+
+### Data Table Component
+
+```tsx
+import { ListingTable, Chip, IconButton } from '@wso2/oxygen-ui';
+import { EditIcon, TrashIcon } from '@wso2/oxygen-ui-icons-react';
+
+
+ Add Item} />
+
+
+
+ Name
+ Status
+ Actions
+
+
+
+ {data.map((item) => (
+
+ {item.name}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+```
+
+### Card Component
+
+```tsx
+import { Paper, Typography, Box, Button } from '@wso2/oxygen-ui';
+
+
+ Card Title
+
+ Card description text
+
+
+
+
+
+
+```
+
+### Dialog Component
+
+```tsx
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button,
+} from '@wso2/oxygen-ui';
+
+
+```
+
+## MUI X Components (Use Namespaces)
+
+```tsx
+import { DataGrid, DatePickers, TreeView } from '@wso2/oxygen-ui';
+
+// DataGrid
+
+
+// DatePickers
+
+
+// TreeView
+
+
+
+```
+
+## Charts (Separate Package)
+
+```tsx
+// Charts are in a separate package built on Recharts
+import { LineChart, BarChart, PieChart } from '@wso2/oxygen-ui-charts-react';
+
+
+
+
+```
diff --git a/portals/api-control-plane/.claude/skills/oxygen-form/SKILL.md b/portals/api-control-plane/.claude/skills/oxygen-form/SKILL.md
new file mode 100644
index 0000000000..e9fb243eba
--- /dev/null
+++ b/portals/api-control-plane/.claude/skills/oxygen-form/SKILL.md
@@ -0,0 +1,410 @@
+---
+name: oxygen-form
+description: Generate forms with Form.* components and validation. Use when creating input forms, multi-step wizards, or form validation.
+---
+
+# Generate Oxygen UI Form
+
+## Instructions
+
+1. Read `.claude/oxygen-ui/components.md` for Form.* API
+2. Use `Form.Section` for grouping fields
+3. Use `Form.Stack` for field layout
+4. Add validation with `error` and `helperText` props
+
+## Critical Rules
+
+- Import form components from `@wso2/oxygen-ui`
+- Use controlled components with state
+- Show validation errors with `error` and `helperText` props
+- Use theme tokens for spacing
+
+## Basic Form
+
+```tsx
+import { useState } from 'react';
+import { Box, TextField, Button, Alert } from '@wso2/oxygen-ui';
+
+function BasicForm() {
+ const [formData, setFormData] = useState({ name: '', email: '' });
+ const [errors, setErrors] = useState>({});
+
+ const validate = () => {
+ const newErrors: Record = {};
+
+ if (!formData.name) {
+ newErrors.name = 'Name is required';
+ }
+
+ if (!formData.email) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Invalid email format';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (validate()) {
+ console.log('Form submitted:', formData);
+ }
+ };
+
+ return (
+
+ setFormData({ ...formData, name: e.target.value })}
+ error={!!errors.name}
+ helperText={errors.name}
+ margin="normal"
+ required
+ />
+
+ setFormData({ ...formData, email: e.target.value })}
+ error={!!errors.email}
+ helperText={errors.email}
+ margin="normal"
+ required
+ />
+
+
+
+ );
+}
+```
+
+## Form with Sections
+
+```tsx
+import { Form, TextField, Button, Box } from '@wso2/oxygen-ui';
+
+function SectionedForm() {
+ return (
+
+
+ Personal Information
+ Enter your basic details
+
+
+
+
+
+
+
+
+
+ Company Details
+ Optional information about your organization
+
+
+
+
+ Developer
+ Designer
+ Manager
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Login Form
+
+```tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Typography,
+ TextField,
+ Button,
+ Link,
+ Divider,
+ Alert,
+} from '@wso2/oxygen-ui';
+
+function LoginForm() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ try {
+ // Login logic here
+ await login(email, password);
+ } catch (err) {
+ setError('Invalid credentials');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Sign In
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ setEmail(e.target.value)}
+ margin="normal"
+ required
+ />
+
+ setPassword(e.target.value)}
+ margin="normal"
+ required
+ />
+
+
+
+
+
+ Forgot password?
+
+
+ Create account
+
+
+
+
+
+ );
+}
+```
+
+## Multi-Step Wizard
+
+```tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Stepper,
+ Step,
+ StepLabel,
+ Button,
+ Typography,
+ TextField,
+} from '@wso2/oxygen-ui';
+
+const steps = ['Account', 'Profile', 'Review'];
+
+function RegistrationWizard() {
+ const [activeStep, setActiveStep] = useState(0);
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ name: '',
+ company: '',
+ });
+
+ const handleNext = () => setActiveStep((prev) => prev + 1);
+ const handleBack = () => setActiveStep((prev) => prev - 1);
+
+ const handleSubmit = () => {
+ console.log('Form submitted:', formData);
+ };
+
+ const renderStepContent = (step: number) => {
+ switch (step) {
+ case 0:
+ return (
+ <>
+ setFormData({ ...formData, email: e.target.value })}
+ margin="normal"
+ />
+ setFormData({ ...formData, password: e.target.value })}
+ margin="normal"
+ />
+ >
+ );
+ case 1:
+ return (
+ <>
+ setFormData({ ...formData, name: e.target.value })}
+ margin="normal"
+ />
+ setFormData({ ...formData, company: e.target.value })}
+ margin="normal"
+ />
+ >
+ );
+ case 2:
+ return (
+
+
+ Email: {formData.email}
+
+
+ Name: {formData.name}
+
+
+ Company: {formData.company}
+
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {steps.map((label) => (
+
+ {label}
+
+ ))}
+
+
+ {renderStepContent(activeStep)}
+
+
+
+ {activeStep === steps.length - 1 ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+```
+
+## Form with Select and Autocomplete
+
+```tsx
+import { useState } from 'react';
+import { Box, TextField, Button, MenuItem, Autocomplete } from '@wso2/oxygen-ui';
+
+const countries = [
+ { label: 'United States', code: 'US' },
+ { label: 'United Kingdom', code: 'UK' },
+ { label: 'Canada', code: 'CA' },
+ // ...more
+];
+
+function AddressForm() {
+ const [country, setCountry] = useState(null);
+ const [state, setState] = useState('');
+
+ return (
+
+ setCountry(newValue)}
+ renderInput={(params) => (
+
+ )}
+ />
+
+ setState(e.target.value)}
+ margin="normal"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
diff --git a/portals/api-control-plane/.claude/skills/oxygen-layout/SKILL.md b/portals/api-control-plane/.claude/skills/oxygen-layout/SKILL.md
new file mode 100644
index 0000000000..bdf8fce9f1
--- /dev/null
+++ b/portals/api-control-plane/.claude/skills/oxygen-layout/SKILL.md
@@ -0,0 +1,233 @@
+---
+name: oxygen-layout
+description: Generate application layouts with AppShell, Header, and Sidebar. Use when creating app shells, navigation structures, or dashboard layouts.
+---
+
+# Generate Oxygen UI Layout
+
+## Instructions
+
+1. Read `.claude/oxygen-ui/patterns.md` for layout patterns
+2. Use `AppShell` as the main wrapper
+3. Configure `Header` with navigation
+4. Set up `Sidebar` with menu items
+
+## Critical Rules
+
+- Always use `OxygenUIThemeProvider` at the root
+- Use compound component pattern for `AppShell`, `Header`, `Sidebar`
+- Import icons from `@wso2/oxygen-ui-icons-react`
+- Use theme tokens for all styling
+
+## Complete App Shell Layout
+
+```tsx
+import { useState } from 'react';
+import {
+ OxygenUIThemeProvider,
+ OxygenTheme,
+ AppShell,
+ Header,
+ Sidebar,
+ Footer,
+ Box,
+ ColorSchemeToggle,
+ UserMenu,
+} from '@wso2/oxygen-ui';
+import {
+ HomeIcon,
+ DashboardIcon,
+ SettingsIcon,
+ UsersIcon,
+ FileIcon,
+} from '@wso2/oxygen-ui-icons-react';
+
+function App() {
+ const [collapsed, setCollapsed] = useState(false);
+ const [activeItem, setActiveItem] = useState('home');
+
+ return (
+
+
+
+
+ setCollapsed(!collapsed)}
+ />
+
+
+
+
+ My Application
+
+
+
+
+ console.log('Sign out')}
+ />
+
+
+
+
+
+
+
+
+ Main
+
+
+ Home
+
+
+
+ Dashboard
+
+
+
+
+ Management
+
+
+ Users
+ 12
+
+
+
+ Files
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
+
+
+ {/* Page content goes here */}
+ Welcome
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+```
+
+## Header with Context Switchers
+
+```tsx
+
+
+
+
+ App Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Minimal Header (No Switchers)
+
+```tsx
+
+
+ Simple App
+
+
+
+
+
+
+```
+
+## Sidebar with Nested Items
+
+```tsx
+
+
+
+
+ Parent Item
+
+
+ {expandedMenus['parent'] && (
+ <>
+
+ Child 1
+
+
+ Child 2
+
+ >
+ )}
+
+
+```
+
+## Dashboard Layout with Stats
+
+```tsx
+import { Grid, Paper, Typography, Box } from '@wso2/oxygen-ui';
+
+function DashboardContent() {
+ return (
+
+ Dashboard
+
+
+
+
+ Total Users
+ 12,345
+
+
+
+
+ Revenue
+ $45,678
+
+
+ {/* More stats... */}
+
+
+ );
+}
+```
diff --git a/portals/api-control-plane/.claude/skills/oxygen-migrate/SKILL.md b/portals/api-control-plane/.claude/skills/oxygen-migrate/SKILL.md
new file mode 100644
index 0000000000..ae093fb3f2
--- /dev/null
+++ b/portals/api-control-plane/.claude/skills/oxygen-migrate/SKILL.md
@@ -0,0 +1,280 @@
+---
+name: oxygen-migrate
+description: Migrate existing MUI code to Oxygen UI patterns. Use when converting @mui/material imports, replacing MUI components, or updating to Oxygen UI conventions.
+---
+
+# Migrate to Oxygen UI
+
+## Instructions
+
+1. Read `.claude/oxygen-ui/migration.md` for full guide
+2. Update imports from `@mui/*` to `@wso2/oxygen-ui`
+3. Replace `lucide-react` icons with `@wso2/oxygen-ui-icons-react`
+4. Add `OxygenUIThemeProvider` wrapper
+
+## Migration Checklist
+
+- [ ] Replace `@mui/material` imports with `@wso2/oxygen-ui`
+- [ ] Replace `@mui/icons-material` or `lucide-react` with `@wso2/oxygen-ui-icons-react`
+- [ ] Update MUI X imports to use namespaced exports
+- [ ] Wrap root with `OxygenUIThemeProvider`
+- [ ] Replace `ThemeProvider` with `OxygenUIThemeProvider`
+- [ ] Update custom theme to use `OxygenThemeBase`
+
+## Import Migration
+
+### MUI Components
+
+**Before:**
+```tsx
+import { Box, Stack, Button } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import { styled } from '@mui/material/styles';
+```
+
+**After:**
+```tsx
+import { Box, Stack, Button, Typography, styled } from '@wso2/oxygen-ui';
+```
+
+### Icons
+
+**Before (MUI Icons):**
+```tsx
+import SettingsIcon from '@mui/icons-material/Settings';
+import HomeIcon from '@mui/icons-material/Home';
+import DeleteIcon from '@mui/icons-material/Delete';
+```
+
+**Before (Lucide React):**
+```tsx
+import { Settings, Home, Trash2 } from 'lucide-react';
+```
+
+**After:**
+```tsx
+import { SettingsIcon, HomeIcon, TrashIcon } from '@wso2/oxygen-ui-icons-react';
+```
+
+### MUI X Components
+
+**Before:**
+```tsx
+import { DataGrid } from '@mui/x-data-grid';
+import { DatePicker } from '@mui/x-date-pickers';
+import { BarChart } from '@mui/x-charts';
+import { SimpleTreeView } from '@mui/x-tree-view';
+```
+
+**After:**
+```tsx
+import { DataGrid, DatePickers, TreeView } from '@wso2/oxygen-ui';
+
+// Use as namespaces
+
+
+
+
+// For Charts, use the separate package
+import { BarChart } from '@wso2/oxygen-ui-charts-react';
+
+```
+
+## Theme Provider Migration
+
+**Before:**
+```tsx
+import { ThemeProvider, createTheme } from '@mui/material';
+
+const theme = createTheme({
+ palette: {
+ primary: { main: '#1976d2' },
+ },
+});
+
+
+
+
+```
+
+**After:**
+```tsx
+import { OxygenUIThemeProvider, OxygenTheme } from '@wso2/oxygen-ui';
+
+// Use default theme
+
+
+
+
+// Or with custom theme
+import { extendTheme } from '@mui/material/styles';
+import { OxygenThemeBase } from '@wso2/oxygen-ui';
+
+const customTheme = extendTheme({
+ ...OxygenThemeBase,
+ colorSchemes: {
+ light: {
+ palette: { primary: { main: '#1976d2' } },
+ },
+ },
+});
+
+
+
+
+```
+
+## Layout Migration
+
+**Before (Custom Layout):**
+```tsx
+import { AppBar, Toolbar, Drawer, Box } from '@mui/material';
+
+
+
+
+ My App
+
+
+
+ {/* Menu items */}
+
+
+ {/* Content */}
+
+
+```
+
+**After (Oxygen AppShell):**
+```tsx
+import { AppShell, Header, Sidebar, Footer } from '@wso2/oxygen-ui';
+
+
+
+
+
+
+
+
+
+ {/* Menu items */}
+
+
+
+
+
+ {/* Content */}
+
+
+
+
+
+
+```
+
+## Table Migration
+
+**Before (MUI Table):**
+```tsx
+import {
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableCell,
+ Paper,
+} from '@mui/material';
+
+
+
+
+
+ Name
+ Status
+
+
+
+ {data.map((row) => (
+
+ {row.name}
+ {row.status}
+
+ ))}
+
+
+
+```
+
+**After (Oxygen ListingTable):**
+```tsx
+import { ListingTable } from '@wso2/oxygen-ui';
+
+
+
+
+
+
+ Name
+ Status
+
+
+
+ {data.map((row) => (
+
+ {row.name}
+ {row.status}
+
+ ))}
+
+
+
+
+```
+
+## Color Mode Migration
+
+**Before:**
+```tsx
+import { useColorScheme } from '@mui/material';
+
+const { mode, setMode } = useColorScheme();
+```
+
+**After:**
+```tsx
+import { ColorSchemeToggle, useTheme } from '@wso2/oxygen-ui';
+
+// Simple toggle component
+
+
+// Check current mode
+const theme = useTheme();
+const isDark = theme.palette.mode === 'dark';
+```
+
+## ESLint Plugin Setup
+
+After migration, add the ESLint plugin to prevent regressions:
+
+```bash
+pnpm add -D @wso2/eslint-plugin-oxygen-ui
+```
+
+```js
+// eslint.config.js
+import oxygenUIPlugin from '@wso2/eslint-plugin-oxygen-ui';
+
+export default [
+ oxygenUIPlugin.configs.recommended,
+ // ... other configs
+];
+```
+
+This will:
+- Error on direct `@mui/material` imports
+- Error on direct `lucide-react` imports
+- Auto-fix imports with `eslint --fix`
diff --git a/portals/api-control-plane/CLAUDE.md b/portals/api-control-plane/CLAUDE.md
new file mode 100644
index 0000000000..23c155d894
--- /dev/null
+++ b/portals/api-control-plane/CLAUDE.md
@@ -0,0 +1,5 @@
+# Project Guidelines
+
+## Oxygen UI
+
+For Oxygen UI component guidelines and patterns, see [.claude/oxygen-ui/CLAUDE.md](.claude/oxygen-ui/CLAUDE.md).
diff --git a/portals/api-control-plane/package-lock.json b/portals/api-control-plane/package-lock.json
index b8e5b3c40f..3da7a22de3 100644
--- a/portals/api-control-plane/package-lock.json
+++ b/portals/api-control-plane/package-lock.json
@@ -9,9 +9,14 @@
"version": "0.1.0",
"dependencies": {
"@babel/runtime-corejs3": "7.29.7",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mui/material": "^9.3.1",
+ "@mui/x-data-grid": "^9.11.0",
+ "@mui/x-date-pickers": "^9.11.0",
"@tanstack/react-query": "5.101.0",
- "@wso2/oxygen-ui": "0.11.0",
- "@wso2/oxygen-ui-icons-react": "0.11.0",
+ "@wso2/oxygen-ui": "^0.13.1",
+ "@wso2/oxygen-ui-icons-react": "^0.13.1",
"axios": "1.19.0",
"js-yaml": "4.1.0",
"react": "19.2.3",
@@ -354,6 +359,9 @@
},
"node_modules/@base-ui-components/utils": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@base-ui-components/utils/-/utils-0.1.2.tgz",
+ "integrity": "sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==",
+ "deprecated": "Package was renamed to @base-ui/utils",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
@@ -372,6 +380,28 @@
}
}
},
+ "node_modules/@base-ui/utils": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz",
+ "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "@floating-ui/utils": "^0.2.12",
+ "reselect": "^5.2.0",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^17 || ^18 || ^19",
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@bcoe/v8-coverage": {
"version": "0.2.3",
"dev": true,
@@ -531,6 +561,8 @@
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
@@ -548,10 +580,14 @@
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
@@ -563,10 +599,14 @@
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0"
@@ -574,10 +614,14 @@
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
@@ -600,6 +644,8 @@
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
@@ -611,10 +657,14 @@
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.14.1",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
@@ -636,10 +686,14 @@
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
@@ -647,10 +701,14 @@
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1194,10 +1252,14 @@
},
"node_modules/@floating-ui/utils": {
"version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@fontsource-variable/inter": {
"version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
+ "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -1811,7 +1873,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "7.3.11",
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz",
+ "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -1819,20 +1883,22 @@
}
},
"node_modules/@mui/material": {
- "version": "7.3.4",
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz",
+ "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.4",
- "@mui/core-downloads-tracker": "^7.3.4",
- "@mui/system": "^7.3.3",
- "@mui/types": "^7.4.7",
- "@mui/utils": "^7.3.3",
+ "@babel/runtime": "^7.29.7",
+ "@mui/core-downloads-tracker": "^9.3.1",
+ "@mui/system": "^9.3.0",
+ "@mui/types": "^9.3.0",
+ "@mui/utils": "^9.3.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
- "csstype": "^3.1.3",
+ "csstype": "^3.2.3",
"prop-types": "^15.8.1",
- "react-is": "^19.1.1",
+ "react-is": "^19.2.8",
"react-transition-group": "^4.4.5"
},
"engines": {
@@ -1845,7 +1911,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^7.3.3",
+ "@mui/material-pigment-css": "^9.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1865,40 +1931,14 @@
}
}
},
- "node_modules/@mui/material/node_modules/@mui/utils": {
- "version": "7.3.11",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/private-theming": {
- "version": "7.3.11",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz",
+ "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/utils": "^7.3.11",
+ "@babel/runtime": "^7.29.7",
+ "@mui/utils": "^9.3.0",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1918,39 +1958,13 @@
}
}
},
- "node_modules/@mui/private-theming/node_modules/@mui/utils": {
- "version": "7.3.11",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/styled-engine": {
- "version": "7.3.10",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz",
+ "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6",
+ "@babel/runtime": "^7.29.7",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
@@ -1979,14 +1993,16 @@
}
},
"node_modules/@mui/system": {
- "version": "7.3.11",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz",
+ "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/private-theming": "^7.3.11",
- "@mui/styled-engine": "^7.3.10",
- "@mui/types": "^7.4.12",
- "@mui/utils": "^7.3.11",
+ "@babel/runtime": "^7.29.7",
+ "@mui/private-theming": "^9.3.0",
+ "@mui/styled-engine": "^9.3.0",
+ "@mui/types": "^9.3.0",
+ "@mui/utils": "^9.3.0",
"clsx": "^2.1.1",
"csstype": "^3.2.3",
"prop-types": "^15.8.1"
@@ -2016,39 +2032,13 @@
}
}
},
- "node_modules/@mui/system/node_modules/@mui/utils": {
- "version": "7.3.11",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/types": {
- "version": "7.4.12",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz",
+ "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6"
+ "@babel/runtime": "^7.29.7"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -2060,15 +2050,17 @@
}
},
"node_modules/@mui/utils": {
- "version": "9.0.0",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz",
+ "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.29.2",
- "@mui/types": "^9.0.0",
+ "@babel/runtime": "^7.29.7",
+ "@mui/types": "^9.3.0",
"@types/prop-types": "^15.7.15",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
- "react-is": "^19.2.4"
+ "react-is": "^19.2.8"
},
"engines": {
"node": ">=14.0.0"
@@ -2087,29 +2079,17 @@
}
}
},
- "node_modules/@mui/utils/node_modules/@mui/types": {
- "version": "9.1.1",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.29.2"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/x-data-grid": {
- "version": "8.16.0",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-9.11.0.tgz",
+ "integrity": "sha512-p/KHQSMOl+gV2lfkUXUSeNLnfXiQ3pUqJQ9oHLvNPdvauw87apx0o9iPiohCyw7WOeaqtosNLWCsbBkCy1YQpQ==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.4",
- "@mui/utils": "^7.3.3",
- "@mui/x-internals": "8.16.0",
- "@mui/x-virtualizer": "0.2.6",
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "^0.3.1",
+ "@mui/utils": "^9.3.0",
+ "@mui/x-internals": "^9.11.0",
+ "@mui/x-virtualizer": "0.6.3",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"use-sync-external-store": "^1.6.0"
@@ -2124,8 +2104,8 @@
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
- "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
- "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/material": "^7.3.0 || ^9.0.0",
+ "@mui/system": "^7.3.0 || ^9.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -2138,16 +2118,17 @@
}
}
},
- "node_modules/@mui/x-data-grid/node_modules/@mui/utils": {
- "version": "7.3.11",
+ "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": {
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz",
+ "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "^0.3.1",
+ "@mui/utils": "^9.3.0",
+ "reselect": "^5.2.0",
+ "use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
@@ -2157,22 +2138,41 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@mui/x-data-grid/node_modules/@mui/x-virtualizer": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.6.3.tgz",
+ "integrity": "sha512-12EEt7/qG7/6YGmzjcqDRzDkYnTSELoQxOrTf30XLWxhIdkYn1uygZzSPG+fg811ZzbF301HodEVY05GMLCQmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "^0.3.1",
+ "@mui/utils": "^9.3.0",
+ "@mui/x-internals": "^9.11.0"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/x-date-pickers": {
- "version": "8.16.0",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.11.0.tgz",
+ "integrity": "sha512-3vLmkn1wG+hNBaGobnk/9R8APCRhOiAXBTC78m7wZ2OWZ2NoMqi+O09HwTGlIgK6mScZZCeCQTSj5phNLrK1hw==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.4",
- "@mui/utils": "^7.3.3",
- "@mui/x-internals": "8.16.0",
+ "@babel/runtime": "^7.29.7",
+ "@mui/utils": "^9.3.0",
+ "@mui/x-internals": "^9.11.0",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -2188,8 +2188,8 @@
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
- "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
- "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/material": "^7.3.0 || ^9.0.0",
+ "@mui/system": "^7.3.0 || ^9.0.0",
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
"dayjs": "^1.10.7",
@@ -2230,16 +2230,17 @@
}
}
},
- "node_modules/@mui/x-date-pickers/node_modules/@mui/utils": {
- "version": "7.3.11",
+ "node_modules/@mui/x-date-pickers/node_modules/@mui/x-internals": {
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz",
+ "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "^0.3.1",
+ "@mui/utils": "^9.3.0",
+ "reselect": "^5.2.0",
+ "use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
@@ -2249,17 +2250,13 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
}
},
"node_modules/@mui/x-internals": {
"version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.16.0.tgz",
+ "integrity": "sha512-JR53WOFqmQYQzurOpB0H91K7/9uMcte1ooxHxTLGB+97PgB+rKY6siRWvUALGS56XyPV+1a2ALI33hd2E7+Rgg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
@@ -2278,8 +2275,27 @@
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@mui/x-internals/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mui/x-internals/node_modules/@mui/utils": {
"version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
@@ -2306,18 +2322,15 @@
}
}
},
- "node_modules/@mui/x-tree-view": {
- "version": "8.14.0",
+ "node_modules/@mui/x-virtualizer": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.6.tgz",
+ "integrity": "sha512-t45EHhD9kStSwIYMkqYYQIFbZNVQws9LRANktf0e/+j+MxsRTFk41r0rgiazMSOSugJlCuSh/H8xUUuMCZdtow==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
- "@base-ui-components/utils": "0.1.2",
"@mui/utils": "^7.3.3",
- "@mui/x-internals": "8.14.0",
- "@types/react-transition-group": "^4.4.12",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-transition-group": "^4.4.5"
+ "@mui/x-internals": "8.16.0"
},
"engines": {
"node": ">=14.0.0"
@@ -2327,24 +2340,31 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@emotion/react": "^11.9.0",
- "@emotion/styled": "^11.8.1",
- "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
- "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@mui/x-virtualizer/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
+ "@types/react": {
"optional": true
}
}
},
- "node_modules/@mui/x-tree-view/node_modules/@mui/utils": {
+ "node_modules/@mui/x-virtualizer/node_modules/@mui/utils": {
"version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
@@ -2371,87 +2391,19 @@
}
}
},
- "node_modules/@mui/x-tree-view/node_modules/@mui/x-internals": {
- "version": "8.14.0",
+ "node_modules/@nestjs/common": {
+ "version": "8.4.4",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.28.4",
- "@mui/utils": "^7.3.3",
- "reselect": "^5.1.1",
- "use-sync-external-store": "^1.6.0"
- },
- "engines": {
- "node": ">=14.0.0"
+ "axios": "0.26.1",
+ "iterare": "1.2.1",
+ "tslib": "2.3.1",
+ "uuid": "8.3.2"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@mui/x-virtualizer": {
- "version": "0.2.6",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.4",
- "@mui/utils": "^7.3.3",
- "@mui/x-internals": "8.16.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@mui/x-virtualizer/node_modules/@mui/utils": {
- "version": "7.3.11",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6",
- "@mui/types": "^7.4.12",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.3"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@nestjs/common": {
- "version": "8.4.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "axios": "0.26.1",
- "iterare": "1.2.1",
- "tslib": "2.3.1",
- "uuid": "8.3.2"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/nest"
+ "url": "https://opencollective.com/nest"
},
"peerDependencies": {
"cache-manager": "*",
@@ -2640,6 +2592,8 @@
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -3324,6 +3278,8 @@
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/picomatch": {
@@ -3335,6 +3291,8 @@
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
@@ -3354,6 +3312,8 @@
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
@@ -3763,9 +3723,12 @@
}
},
"node_modules/@wso2/oxygen-ui": {
- "version": "0.11.0",
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/@wso2/oxygen-ui/-/oxygen-ui-0.13.1.tgz",
+ "integrity": "sha512-naLAZdIBBYS76Rx9cncLquyLZHrXxMym9CXO+W+CZn5YWnYQWxsE+0IIW03Ap/eF0JNX+d/AJVlENA/+WE9WoQ==",
"license": "Apache-2.0",
"dependencies": {
+ "@emotion/cache": "11.14.0",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@fontsource-variable/inter": "5.2.8",
@@ -3787,7 +3750,9 @@
}
},
"node_modules/@wso2/oxygen-ui-icons-react": {
- "version": "0.11.0",
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/@wso2/oxygen-ui-icons-react/-/oxygen-ui-icons-react-0.13.1.tgz",
+ "integrity": "sha512-F4TbWA/u9Ov1sSUhA7Ze7TREKuqAG0vDaeQvjeLGTuU2gNYFkQgKvvLDztAtIwxsyiN39Jb1KWf6q/RCncsq1Q==",
"license": "Apache-2.0",
"dependencies": {
"lucide-react": "1.16.0"
@@ -3796,6 +3761,643 @@
"react": "19.2.3"
}
},
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/core-downloads-tracker": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz",
+ "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/material": {
+ "version": "7.3.4",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz",
+ "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/core-downloads-tracker": "^7.3.4",
+ "@mui/system": "^7.3.3",
+ "@mui/types": "^7.4.7",
+ "@mui/utils": "^7.3.3",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.1.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material-pigment-css": "^7.3.3",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/material/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/material/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/private-theming": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz",
+ "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/utils": "^7.3.11",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/private-theming/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/private-theming/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/styled-engine": {
+ "version": "7.3.10",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz",
+ "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/sheet": "^1.4.0",
+ "csstype": "^3.2.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/system": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz",
+ "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/private-theming": "^7.3.11",
+ "@mui/styled-engine": "^7.3.10",
+ "@mui/types": "^7.4.12",
+ "@mui/utils": "^7.3.11",
+ "clsx": "^2.1.1",
+ "csstype": "^3.2.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/system/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/system/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/utils": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz",
+ "integrity": "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "@mui/types": "^9.0.0",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-data-grid": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.16.0.tgz",
+ "integrity": "sha512-yJ+v+E1yI1HxrEUdOfgrUTCxobAFvotGggU6cy6MnM7c7/TPPg9d5mDzjzxb0imOCJ6WyiM/vtd5WKbY/5sUNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.3",
+ "@mui/x-internals": "8.16.0",
+ "@mui/x-virtualizer": "0.2.6",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-data-grid/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-data-grid/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-date-pickers": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.16.0.tgz",
+ "integrity": "sha512-zvUoO9ImWiKRaOWvQVbB1vCa6aUQIX5GM0tJ+nAyNNIVV0VqpXz3CvkRR6ovBBFzIcChc7FXlqrMKcJ//EhePQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.3",
+ "@mui/x-internals": "8.16.0",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-date-pickers/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-date-pickers/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-tree-view": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-8.14.0.tgz",
+ "integrity": "sha512-LyB48R6ANSY/1nP84Qw1AEdEAjsMM6iCxPtCpUqO1GVJSFNP3Jy2xZuPkTO9Ilsd2Ud1p2sjy7AtQVpMRsDTyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@base-ui-components/utils": "0.1.2",
+ "@mui/utils": "^7.3.3",
+ "@mui/x-internals": "8.14.0",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-tree-view/node_modules/@mui/types": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
+ "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-tree-view/node_modules/@mui/utils": {
+ "version": "7.3.11",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
+ "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "@mui/types": "^7.4.12",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wso2/oxygen-ui/node_modules/@mui/x-tree-view/node_modules/@mui/x-internals": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.14.0.tgz",
+ "integrity": "sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.3",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"dev": true,
@@ -4006,6 +4608,8 @@
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
@@ -4373,6 +4977,8 @@
},
"node_modules/clsx": {
"version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4577,6 +5183,8 @@
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
@@ -4642,6 +5250,8 @@
},
"node_modules/date-fns": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -4740,6 +5350,8 @@
},
"node_modules/dom-helpers": {
"version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
@@ -4807,6 +5419,8 @@
},
"node_modules/error-ex": {
"version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
@@ -5409,6 +6023,8 @@
},
"node_modules/find-root": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
@@ -5739,6 +6355,8 @@
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
@@ -5746,6 +6364,8 @@
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/hono": {
@@ -5973,10 +6593,14 @@
},
"node_modules/is-arrayish": {
"version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
@@ -6215,6 +6839,8 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -6306,6 +6932,8 @@
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/locate-path": {
@@ -6349,6 +6977,8 @@
},
"node_modules/loose-envify": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -6372,6 +7002,8 @@
},
"node_modules/lucide-react": {
"version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
+ "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -6965,6 +7597,8 @@
},
"node_modules/parse-json": {
"version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
@@ -7024,6 +7658,8 @@
},
"node_modules/path-parse": {
"version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -7053,6 +7689,8 @@
},
"node_modules/path-type": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7168,6 +7806,8 @@
},
"node_modules/prismjs": {
"version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -7175,6 +7815,8 @@
},
"node_modules/prop-types": {
"version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -7184,6 +7826,8 @@
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
@@ -7332,6 +7976,8 @@
},
"node_modules/react-is": {
"version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
"license": "MIT"
},
"node_modules/react-refresh": {
@@ -7389,6 +8035,8 @@
},
"node_modules/react-transition-group": {
"version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
@@ -7454,10 +8102,14 @@
},
"node_modules/reselect": {
"version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
+ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -7845,6 +8497,8 @@
},
"node_modules/source-map": {
"version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -7967,6 +8621,8 @@
},
"node_modules/stylis": {
"version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/supports-color": {
@@ -7982,6 +8638,8 @@
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8376,6 +9034,8 @@
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -8730,6 +9390,8 @@
},
"node_modules/yaml": {
"version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
+ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"license": "ISC",
"engines": {
"node": ">= 6"
diff --git a/portals/api-control-plane/package.json b/portals/api-control-plane/package.json
index e1f2c6cd62..3529f252b0 100644
--- a/portals/api-control-plane/package.json
+++ b/portals/api-control-plane/package.json
@@ -38,9 +38,14 @@
},
"dependencies": {
"@babel/runtime-corejs3": "7.29.7",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mui/material": "^9.3.1",
+ "@mui/x-data-grid": "^9.11.0",
+ "@mui/x-date-pickers": "^9.11.0",
"@tanstack/react-query": "5.101.0",
- "@wso2/oxygen-ui": "0.11.0",
- "@wso2/oxygen-ui-icons-react": "0.11.0",
+ "@wso2/oxygen-ui": "^0.13.1",
+ "@wso2/oxygen-ui-icons-react": "^0.13.1",
"axios": "1.19.0",
"js-yaml": "4.1.0",
"react": "19.2.3",
diff --git a/portals/api-control-plane/src/theme/AppThemeProvider.tsx b/portals/api-control-plane/src/theme/AppThemeProvider.tsx
new file mode 100644
index 0000000000..ed0223f696
--- /dev/null
+++ b/portals/api-control-plane/src/theme/AppThemeProvider.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// src/theme/AppThemeProvider.tsx
+import { PropsWithChildren } from 'react';
+import { OxygenUIThemeProvider, CssBaseline } from '@wso2/oxygen-ui';
+import { appEmotionCache } from './emotionCache';
+import { themeRegistry, INITIAL_THEME } from './themes';
+
+export function AppThemeProvider({ children }: PropsWithChildren) {
+ return (
+
+ {/* Normalizes browser styles using theme tokens */}
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/portals/api-control-plane/src/theme/emotionCache.ts b/portals/api-control-plane/src/theme/emotionCache.ts
new file mode 100644
index 0000000000..dfa827ff27
--- /dev/null
+++ b/portals/api-control-plane/src/theme/emotionCache.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// src/theme/emotionCache.ts
+import { createEmotionCache } from '@wso2/oxygen-ui';
+
+// If you enforce a CSP, read the per-request nonce from a meta tag
+// (see §10). Otherwise this can be omitted.
+const nonce =
+ document
+ .querySelector('meta[property="csp-nonce"]')
+ ?.nonce ?? undefined;
+
+export const appEmotionCache = createEmotionCache({
+ key: 'css',
+ prepend: true, // keep Oxygen styles first so app styles can override
+ nonce,
+});
\ No newline at end of file
diff --git a/portals/api-control-plane/src/theme/index.ts b/portals/api-control-plane/src/theme/index.ts
new file mode 100644
index 0000000000..eb366236a7
--- /dev/null
+++ b/portals/api-control-plane/src/theme/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { AppThemeProvider } from './AppThemeProvider';
+import {
+ glassSurfaceSx,
+ interactiveCardSx,
+ stickyBottomBarSx,
+} from './receipes';
+
+export {
+ AppThemeProvider,
+ glassSurfaceSx,
+ interactiveCardSx,
+ stickyBottomBarSx,
+};
\ No newline at end of file
diff --git a/portals/api-control-plane/src/theme/receipes.ts b/portals/api-control-plane/src/theme/receipes.ts
new file mode 100644
index 0000000000..e89564132e
--- /dev/null
+++ b/portals/api-control-plane/src/theme/receipes.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// src/theme/recipes.ts
+//
+// Shared style recipes — the middle tier between the Oxygen theme and one-off
+// layout `sx`.
+//
+// 1. Theme — global design decisions, owned by @wso2/oxygen-ui (and any
+// app-level override registered in ./themes).
+// 2. Recipes — repeated multi-property treatments that are too
+// instance-specific to be a global component override. THIS
+// FILE. One definition, imported by every call site.
+// 3. Local `sx` — layout only (flex, gap, grid columns, min/max sizing).
+//
+// Everything here resolves through theme tokens. No colour, radius, blur or
+// border literals belong in this file or in any call site.
+
+import { alpha, type Theme } from '@wso2/oxygen-ui';
+
+/**
+ * The `border` shorthand for a one-pixel rule, from `theme.border` rather than
+ * a `'1px solid'` literal. Pair it with a `borderColor` token — the colour is
+ * the part that actually varies between light, dark and high-contrast themes.
+ */
+export const hairline = (theme: Theme) =>
+ `${theme.border.width} ${theme.border.style}`;
+
+/**
+ * Square metadata chip used across the API and gateway card family. A function
+ * of the theme so the border comes from `theme.border` rather than a literal —
+ * compose it as `sx={(theme) => ({ ...chipSx(theme), ...overrides })}`.
+ */
+export const chipSx = (theme: Theme) =>
+ ({
+ alignItems: 'center',
+ bgcolor: 'action.hover',
+ border: hairline(theme),
+ borderColor: 'divider',
+ borderRadius: 1,
+ color: 'text.secondary',
+ display: 'inline-flex',
+ fontSize: 12,
+ fontWeight: 500,
+ gap: 0.75,
+ px: 1.25,
+ py: 0.5,
+ }) as const;
+
+/**
+ * The same chip tinted with the brand colour — the "kind" chip on API cards.
+ * Pass straight through as `sx={tintedChipSx}`.
+ */
+export const tintedChipSx = (theme: Theme) => ({
+ ...chipSx(theme),
+ bgcolor: alpha(theme.palette.primary.main, 0.14),
+ borderColor: alpha(theme.palette.primary.main, 0.3),
+ color: 'primary.main',
+ fontWeight: 600,
+});
+
+/** Blur radius behind a glass surface. One value, so every pane matches. */
+const GLASS_BLUR = '14px';
+
+/**
+ * Translucent "glass" surface: what sits behind the element shows through,
+ * blurred, instead of the flat `background.paper` fill.
+ *
+ * Both gradient stops derive from `background.paper`, so the sheen that makes
+ * it read as glass stays correct in light, dark and high-contrast themes
+ * without branching on the palette mode. Compose it as
+ * `sx={(theme) => ({ ...glassSurfaceSx(theme), ...layout })}`.
+ */
+export const glassSurfaceSx = (theme: Theme) =>
+ ({
+ backdropFilter: `blur(${GLASS_BLUR})`,
+ WebkitBackdropFilter: `blur(${GLASS_BLUR})`,
+ backgroundColor: 'transparent',
+ backgroundImage: `linear-gradient(135deg, ${alpha(
+ theme.palette.background.paper,
+ 0.6
+ )}, ${alpha(theme.palette.background.paper, 0.25)})`,
+ border: hairline(theme),
+ borderColor: alpha(theme.palette.divider, 0.6),
+ borderRadius: 1,
+ }) as const;
+
+/**
+ * Hover treatment for a card that behaves as a button (the whole surface
+ * navigates). Deliberately NOT a global `MuiCard` override: non-interactive
+ * cards — the tab shell, the save bar, the Explore More panel — must not lift
+ * under the cursor.
+ */
+export const interactiveCardSx = {
+ cursor: 'pointer',
+ transition:
+ 'transform .18s ease, border-color .18s ease, box-shadow .18s ease',
+ '&:hover': {
+ borderColor: 'primary.main',
+ boxShadow: 4,
+ transform: 'translateY(-3px)',
+ },
+} as const;
+
+/**
+ * Upward elevation for a bar that floats over scrolling content (the develop
+ * tabs' sticky save bar). `theme.shadows` is entirely downward-casting, and the
+ * duplicated @mui/material install blocks adding a typed custom theme token, so
+ * this lives here as the single definition rather than inline at the call site.
+ */
+export const overlayBarShadow = '0 -2px 10px rgba(0, 0, 0, 0.16)';
+
+/**
+ * A bar pinned to the bottom of a page's scroll area — the develop tabs' save
+ * bar, a list page's pagination row.
+ *
+ * Blurred, bordered and shadowed upward so the content scrolling underneath
+ * stays readable behind it instead of colliding with it. It deliberately does
+ * **not** set a `bottom` offset: the app footer shares this scroll area, so each
+ * call site pairs this with `bottom: useFooterHeight()` rather than 0, or the bar
+ * ends up behind the footer.
+ */
+export const stickyBottomBarSx = (theme: Theme) =>
+ ({
+ backdropFilter: 'blur(10px)',
+ WebkitBackdropFilter: 'blur(10px)',
+ borderColor: 'divider',
+ borderTop: hairline(theme),
+ boxShadow: overlayBarShadow,
+ position: 'sticky',
+ zIndex: theme.zIndex.appBar,
+ }) as const;
diff --git a/portals/api-control-plane/src/theme/themes.ts b/portals/api-control-plane/src/theme/themes.ts
new file mode 100644
index 0000000000..3cf7d866af
--- /dev/null
+++ b/portals/api-control-plane/src/theme/themes.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// src/theme/themes.ts
+import { AcrylicOrangeTheme } from '@wso2/oxygen-ui';
+
+// Use the library-defined AcrylicOrangeTheme
+export const themeRegistry = [
+ { key: 'acrylicOrange', label: 'Acrylic Orange', theme: AcrylicOrangeTheme },
+];
+
+export type ThemeKey = (typeof themeRegistry)[number]['key'];
+export const INITIAL_THEME: ThemeKey = 'acrylicOrange';
\ No newline at end of file
From 90dbc1c830b8bf1b874213b661cd04fd2e3dcf2f Mon Sep 17 00:00:00 2001
From: Shavin Chandrawansha
Date: Wed, 19 Aug 2026 14:42:29 +0530
Subject: [PATCH 02/12] enhance(apicp): restructure the pages, hooks, contexts
- Restructure the pages, components, hooks and contexts.
- Restructure pages, components, hooks, and contexts
- Fix issues introduced by the restructuring
- Enhance project listing with pagination, sorting, and improved UI
- Add an index to the project API resource section
- Add form controls to project-related forms
- Temporarily make the app sidebar static
---
.../api-control-plane/src/App.smoke.test.tsx | 11 +
portals/api-control-plane/src/App.tsx | 10 +-
.../src/api/core/http.test.ts | 2 +-
.../api-control-plane/src/api/core/http.ts | 2 +-
.../api/organizations/organizationClient.ts | 2 +-
.../src/api/platform/platformClient.test.ts | 2 +-
.../src/api/platform/platformClient.ts | 2 +-
.../src/api/resources/projects/index.ts | 53 ++
.../api/resources/projects/projects.hooks.ts | 14 +-
.../src/components/ConfirmDialog.tsx | 88 ++--
.../src/components/cards/ProjectCard.tsx | 251 +++++----
.../src/components/cards/ProjectsGrid.tsx | 81 ---
.../auth/AuthProvider.test.tsx | 0
.../auth/AuthProvider.tsx | 0
.../auth/AuthStateContext.ts | 0
.../auth/authConstants.ts | 0
.../{features => contexts}/auth/authTypes.ts | 0
.../src/features/apis/ApiDetailPage.tsx | 67 ---
.../features/projects/NewProjectDialog.tsx | 148 ------
.../projects/ProjectListPage.test.tsx | 121 -----
.../src/features/projects/ProjectListPage.tsx | 192 -------
.../billing => hooks}/ProductActivation.tsx | 4 +-
.../src/i18n/messages/en.json | 137 ++++-
.../src/navigation/useNavigationItems.ts | 20 +-
.../{layouts => pages/appShell}/AppHeader.tsx | 6 +-
.../{layouts => pages/appShell}/AppLayout.tsx | 8 +-
.../appShell}/AppSidebar.tsx | 2 +-
.../appShell}/appLayoutConstants.ts | 0
.../apis/ApiCreatePage.test.tsx | 2 +-
.../appShellPages}/apis/ApiCreatePage.tsx | 8 +-
.../apis/ApiDetailPage.test.tsx | 8 +-
.../appShellPages/apis/ApiDetailPage.tsx | 212 ++++++++
.../appShellPages}/apis/ApiListPage.test.tsx | 10 +-
.../appShellPages}/apis/ApiListPage.tsx | 18 +-
.../apis/apiCapabilities.test.ts | 2 +-
.../appShellPages}/apis/apiCapabilities.ts | 2 +-
.../apis/develop/AttachedPolicyList.tsx | 2 +-
.../apis/develop/AvailablePoliciesPanel.tsx | 8 +-
.../apis/develop/DocumentsTab.tsx | 2 +-
.../apis/develop/PolicyConfigDrawer.tsx | 10 +-
.../appShellPages}/apis/develop/PolicyTab.tsx | 10 +-
.../apis/develop/RoutingTab.test.tsx | 6 +-
.../apis/develop/RoutingTab.tsx | 8 +-
.../appShellPages}/apis/develop/SaveBar.tsx | 45 +-
.../apis/develop/SchemaField.tsx | 2 +-
.../apis/develop/backendDiscovery.test.ts | 2 +-
.../apis/develop/backendDiscovery.ts | 0
.../apis/develop/developEdit.test.ts | 2 +-
.../apis/develop/developEdit.ts | 2 +-
.../apis/develop/policyDnd.test.ts | 0
.../appShellPages}/apis/develop/policyDnd.ts | 2 +-
.../apis/overview/ApiKeysPanel.tsx | 6 +-
.../apis/overview/InvokeUrlPanel.tsx | 4 +-
.../apis/overview/OverviewTab.test.tsx | 8 +-
.../apis/overview/OverviewTab.tsx | 4 +-
.../apis/overview/ProgressBanner.tsx | 4 +-
.../apis/overview/ResourcesPanel.tsx | 2 +-
.../appShellPages}/deploy/DeployPage.test.tsx | 8 +-
.../appShellPages}/deploy/DeployPage.tsx | 6 +-
.../deploy/GatewayDeployCard.tsx | 6 +-
.../deploy/GatewayDeployEnvCard.tsx | 8 +-
.../deploy/GatewayDeploymentHistory.tsx | 6 +-
.../deploy/GatewayDeploymentRow.tsx | 4 +-
.../deploy/GatewayDeploymentSelector.tsx | 8 +-
.../deploy/gatewayDeployUtils.ts | 2 +-
.../gateways/CopyableCommand.tsx | 0
.../gateways/GatewayCreatePage.tsx | 8 +-
.../gateways/GatewayDetailPage.tsx | 14 +-
.../appShellPages}/gateways/GatewaysPage.tsx | 10 +-
.../gateways/gatewayEnvironments.test.ts | 2 +-
.../gateways/gatewayEnvironments.ts | 2 +-
.../appShellPages}/gateways/gatewaysUi.css | 0
.../appShellPages}/logs/RuntimeLogsPage.tsx | 0
.../appShellPages}/manage/ManagePage.tsx | 4 +-
.../organizations/ExploreMoreCard.tsx | 150 ++++++
.../organizations/OrganizationHomePage.tsx | 28 +-
.../projects/NewProjectDialog.test.tsx | 71 ++-
.../projects/NewProjectDialog.tsx | 167 ++++++
.../projects/ProjectHomePage.tsx | 16 +-
.../projects/ProjectListPage.test.tsx | 207 ++++++++
.../projects/ProjectListPage.tsx | 483 ++++++++++++++++++
.../appShellPages/projects/ProjectsGrid.tsx | 67 +++
.../appShellPages}/settings/SettingsPage.tsx | 0
.../appShellPages}/system/SystemPages.tsx | 8 +-
.../appShell/appShellPages}/test/TestPage.tsx | 4 +-
.../src/pages/appShell/useFooterHeight.ts | 53 ++
.../auth/AuthCallbackPage.tsx | 0
.../{features => pages}/auth/LoginPage.tsx | 2 +-
.../src/routes/AppRoutes.tsx | 36 +-
.../src/routes/ProtectedRoute.tsx | 2 +-
.../src/scope/ConsoleScopeContext.ts | 2 +-
.../src/scope/ConsoleScopeProvider.tsx | 4 +-
.../src/test/mockAuthState.ts | 2 +-
.../api-control-plane/src/test/mockScope.ts | 2 +-
portals/api-control-plane/src/test/utils.tsx | 4 +-
95 files changed, 2008 insertions(+), 1002 deletions(-)
create mode 100644 portals/api-control-plane/src/api/resources/projects/index.ts
delete mode 100644 portals/api-control-plane/src/components/cards/ProjectsGrid.tsx
rename portals/api-control-plane/src/{features => contexts}/auth/AuthProvider.test.tsx (100%)
rename portals/api-control-plane/src/{features => contexts}/auth/AuthProvider.tsx (100%)
rename portals/api-control-plane/src/{features => contexts}/auth/AuthStateContext.ts (100%)
rename portals/api-control-plane/src/{features => contexts}/auth/authConstants.ts (100%)
rename portals/api-control-plane/src/{features => contexts}/auth/authTypes.ts (100%)
delete mode 100644 portals/api-control-plane/src/features/apis/ApiDetailPage.tsx
delete mode 100644 portals/api-control-plane/src/features/projects/NewProjectDialog.tsx
delete mode 100644 portals/api-control-plane/src/features/projects/ProjectListPage.test.tsx
delete mode 100644 portals/api-control-plane/src/features/projects/ProjectListPage.tsx
rename portals/api-control-plane/src/{features/billing => hooks}/ProductActivation.tsx (95%)
rename portals/api-control-plane/src/{layouts => pages/appShell}/AppHeader.tsx (96%)
rename portals/api-control-plane/src/{layouts => pages/appShell}/AppLayout.tsx (94%)
rename portals/api-control-plane/src/{layouts => pages/appShell}/AppSidebar.tsx (95%)
rename portals/api-control-plane/src/{layouts => pages/appShell}/appLayoutConstants.ts (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/ApiCreatePage.test.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/ApiCreatePage.tsx (99%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/ApiDetailPage.test.tsx (91%)
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.tsx
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/ApiListPage.test.tsx (91%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/ApiListPage.tsx (92%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/apiCapabilities.test.ts (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/apiCapabilities.ts (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/AttachedPolicyList.tsx (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/AvailablePoliciesPanel.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/DocumentsTab.tsx (92%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/PolicyConfigDrawer.tsx (94%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/PolicyTab.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/RoutingTab.test.tsx (94%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/RoutingTab.tsx (99%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/SaveBar.tsx (51%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/SchemaField.tsx (99%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/backendDiscovery.test.ts (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/backendDiscovery.ts (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/developEdit.test.ts (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/developEdit.ts (99%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/policyDnd.test.ts (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/develop/policyDnd.ts (95%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/ApiKeysPanel.tsx (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/InvokeUrlPanel.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/OverviewTab.test.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/OverviewTab.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/ProgressBanner.tsx (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/apis/overview/ResourcesPanel.tsx (98%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/DeployPage.test.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/DeployPage.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/GatewayDeployCard.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/GatewayDeployEnvCard.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/GatewayDeploymentHistory.tsx (94%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/GatewayDeploymentRow.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/GatewayDeploymentSelector.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/deploy/gatewayDeployUtils.ts (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/CopyableCommand.tsx (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/GatewayCreatePage.tsx (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/GatewayDetailPage.tsx (95%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/GatewaysPage.tsx (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/gatewayEnvironments.test.ts (96%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/gatewayEnvironments.ts (97%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/gateways/gatewaysUi.css (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/logs/RuntimeLogsPage.tsx (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/manage/ManagePage.tsx (92%)
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/organizations/ExploreMoreCard.tsx
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/organizations/OrganizationHomePage.tsx (84%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/projects/NewProjectDialog.test.tsx (54%)
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/projects/NewProjectDialog.tsx
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/projects/ProjectHomePage.tsx (91%)
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.test.tsx
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.tsx
create mode 100644 portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectsGrid.tsx
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/settings/SettingsPage.tsx (100%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/system/SystemPages.tsx (92%)
rename portals/api-control-plane/src/{features => pages/appShell/appShellPages}/test/TestPage.tsx (91%)
create mode 100644 portals/api-control-plane/src/pages/appShell/useFooterHeight.ts
rename portals/api-control-plane/src/{features => pages}/auth/AuthCallbackPage.tsx (100%)
rename portals/api-control-plane/src/{features => pages}/auth/LoginPage.tsx (99%)
diff --git a/portals/api-control-plane/src/App.smoke.test.tsx b/portals/api-control-plane/src/App.smoke.test.tsx
index 8eff14c1b3..f324f29b8f 100644
--- a/portals/api-control-plane/src/App.smoke.test.tsx
+++ b/portals/api-control-plane/src/App.smoke.test.tsx
@@ -19,7 +19,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AppRoutes } from './routes/AppRoutes';
+import { aProject, collection } from './test/msw';
import { authStatePresets } from './test/mockAuthState';
+import { server } from './test/server';
import { renderWithProviders, screen } from './test/utils';
// Smoke test: render the REAL route tree (ProtectedRoute → ConsoleScopeProvider
@@ -45,6 +47,15 @@ describe('App smoke (mock mode, authenticated)', () => {
});
it('navigates to the projects list and renders project cards', async () => {
+ // The project list reads through the resource hooks, which always go to the
+ // real transport — `VITE_USE_MOCK_API` only governs the legacy client, so
+ // these endpoints are stubbed at the network layer instead.
+ server.use(
+ collection('/projects', [
+ aProject({ id: 'retail', displayName: 'Retail APIs' }),
+ ]),
+ collection('/rest-apis', [])
+ );
renderWithProviders(, {
route: '/organizations/api-platform-demo/projects',
authState: authStatePresets.authenticated(),
diff --git a/portals/api-control-plane/src/App.tsx b/portals/api-control-plane/src/App.tsx
index 03b5c0cafa..e78a2dcdb3 100644
--- a/portals/api-control-plane/src/App.tsx
+++ b/portals/api-control-plane/src/App.tsx
@@ -18,7 +18,6 @@
import { type ReactNode, useState } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
-import { OxygenUIThemeProvider, OxygenTheme } from '@wso2/oxygen-ui';
import { BrowserRouter } from 'react-router-dom';
import { ApiClientProvider } from './api/ApiClientProvider';
@@ -26,14 +25,15 @@ import { createQueryClient } from './api/core/queryClient';
import { ErrorBoundary } from './components/ErrorBoundary';
import { NotificationProvider, useNotifications } from './components/Notifications';
import { runtimeConfig } from './config/runtime';
-import { AuthProvider } from './features/auth/AuthProvider';
-import { ProductActivation } from './features/billing/ProductActivation';
+import { AuthProvider } from './contexts/auth/AuthProvider';
+import { ProductActivation } from './hooks/ProductActivation';
import { AppRoutes } from './routes/AppRoutes';
import {
ExtensionsProvider,
type ApiControlPlaneExtension,
} from './extensions';
import { I18nProvider } from './i18n';
+import { AppThemeProvider } from './theme/AppThemeProvider';
/**
* Builds the app's QueryClient with the notification handler already attached,
@@ -69,7 +69,7 @@ export default function App({ extensions = [] }: AppProps) {
return (
-
+
@@ -86,7 +86,7 @@ export default function App({ extensions = [] }: AppProps) {
-
+
);
}
diff --git a/portals/api-control-plane/src/api/core/http.test.ts b/portals/api-control-plane/src/api/core/http.test.ts
index 368e563ae6..d44a3acd53 100644
--- a/portals/api-control-plane/src/api/core/http.test.ts
+++ b/portals/api-control-plane/src/api/core/http.test.ts
@@ -24,7 +24,7 @@ import {
} from 'msw';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../features/auth/authConstants';
+import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../contexts/auth/authConstants';
import { server } from '../../test/server';
import { ApiError, ClientErrorCode, ErrorCode } from './errors';
import {
diff --git a/portals/api-control-plane/src/api/core/http.ts b/portals/api-control-plane/src/api/core/http.ts
index f9fc71327d..48749dd115 100644
--- a/portals/api-control-plane/src/api/core/http.ts
+++ b/portals/api-control-plane/src/api/core/http.ts
@@ -28,7 +28,7 @@ import { runtimeConfig } from '../../config/runtime';
import {
CSRF_HEADER,
CSRF_HEADER_VALUE,
-} from '../../features/auth/authConstants';
+} from '../../contexts/auth/authConstants';
import { ApiErrorKind, platformErrorFromBody, platformErrorFromTransport } from './errors';
import { notifySessionExpired } from './sessionEvents';
diff --git a/portals/api-control-plane/src/api/organizations/organizationClient.ts b/portals/api-control-plane/src/api/organizations/organizationClient.ts
index dcb9c0c396..fa5fbb1e26 100644
--- a/portals/api-control-plane/src/api/organizations/organizationClient.ts
+++ b/portals/api-control-plane/src/api/organizations/organizationClient.ts
@@ -17,7 +17,7 @@
*/
import { runtimeConfig } from '../../config/runtime';
-import type { AuthUser } from '../../features/auth/authTypes';
+import type { AuthUser } from '../../contexts/auth/authTypes';
import type { Organization } from '../../types/domain';
import { toOrganization } from '../adapters';
import { getJson } from '../client';
diff --git a/portals/api-control-plane/src/api/platform/platformClient.test.ts b/portals/api-control-plane/src/api/platform/platformClient.test.ts
index e91bef3768..7b71b25a7e 100644
--- a/portals/api-control-plane/src/api/platform/platformClient.test.ts
+++ b/portals/api-control-plane/src/api/platform/platformClient.test.ts
@@ -19,7 +19,7 @@
import { http, HttpResponse } from 'msw';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../features/auth/authConstants';
+import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../contexts/auth/authConstants';
import { server } from '../../test/server';
const BASE = 'http://platform.test';
diff --git a/portals/api-control-plane/src/api/platform/platformClient.ts b/portals/api-control-plane/src/api/platform/platformClient.ts
index 9bee2df51b..fe73384a36 100644
--- a/portals/api-control-plane/src/api/platform/platformClient.ts
+++ b/portals/api-control-plane/src/api/platform/platformClient.ts
@@ -17,7 +17,7 @@
*/
import { runtimeConfig } from '../../config/runtime';
-import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../features/auth/authConstants';
+import { CSRF_HEADER, CSRF_HEADER_VALUE } from '../../contexts/auth/authConstants';
import { ApiError, type ApiErrorCode } from '../types/errors';
/** True when read flows should go to platform-api REST (via BML). */
diff --git a/portals/api-control-plane/src/api/resources/projects/index.ts b/portals/api-control-plane/src/api/resources/projects/index.ts
new file mode 100644
index 0000000000..181eb03b4a
--- /dev/null
+++ b/portals/api-control-plane/src/api/resources/projects/index.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {
+ Project,
+ ProjectListResponse,
+ ListProjectsQuery,
+ CreateProjectBody,
+ UpdateProjectBody
+} from './projects.endpoints';
+import {
+ useProjects,
+ useProject,
+ useCreateProject,
+ useUpdateProject,
+ useDeleteProject,
+ useProjectOptions,
+} from './projects.hooks';
+
+import type { ProjectListFilters } from './projects.hooks';
+
+export type {
+ Project,
+ ProjectListResponse,
+ ListProjectsQuery,
+ CreateProjectBody,
+ UpdateProjectBody,
+ ProjectListFilters
+};
+
+export {
+ useProjects,
+ useProject,
+ useCreateProject,
+ useUpdateProject,
+ useDeleteProject,
+ useProjectOptions,
+};
\ No newline at end of file
diff --git a/portals/api-control-plane/src/api/resources/projects/projects.hooks.ts b/portals/api-control-plane/src/api/resources/projects/projects.hooks.ts
index 5f98b365a3..97b74e556e 100644
--- a/portals/api-control-plane/src/api/resources/projects/projects.hooks.ts
+++ b/portals/api-control-plane/src/api/resources/projects/projects.hooks.ts
@@ -16,7 +16,12 @@
* under the License.
*/
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import {
+ keepPreviousData,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query';
import type { ApiError } from '../../core/errors';
import { requireOrgScope, useApiScope } from '../../core/scope';
@@ -54,6 +59,12 @@ export type ProjectListFilters = ListProjectsQuery;
* The query does not run until the organization is known, so this never fires a
* request the server would reject — and, critically, never writes into a cache
* entry keyed by an empty scope.
+ *
+ * `keepPreviousData` is the one pagination-specific exception the query client
+ * deliberately leaves to individual list queries: paging or searching changes
+ * the key, and without it the grid would unmount into a loading state on every
+ * page change. The previous page stays on screen (flagged by
+ * `isPlaceholderData`) until the next one arrives.
*/
export const useProjects = (
filters: ProjectListFilters = {},
@@ -64,6 +75,7 @@ export const useProjects = (
return useQuery({
...projectQueries.list(org!, filters),
enabled: Boolean(org),
+ placeholderData: keepPreviousData,
});
};
diff --git a/portals/api-control-plane/src/components/ConfirmDialog.tsx b/portals/api-control-plane/src/components/ConfirmDialog.tsx
index 17f56676a4..671eb8a035 100644
--- a/portals/api-control-plane/src/components/ConfirmDialog.tsx
+++ b/portals/api-control-plane/src/components/ConfirmDialog.tsx
@@ -16,15 +16,17 @@
* under the License.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useId, useState, type FormEvent } from 'react';
import {
+ Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
- TextField,
+ Form,
+ OutlinedInput,
} from '@wso2/oxygen-ui';
export type ConfirmDialogProps = {
@@ -62,6 +64,10 @@ export function ConfirmDialog({
onCancel,
}: ConfirmDialogProps) {
const [typed, setTyped] = useState('');
+ // Generated rather than a constant: this dialog is rendered by several pages,
+ // and two mounted at once would otherwise share one id, pointing both labels
+ // at whichever input the DOM saw first.
+ const fieldId = useId();
// Reset the typed phrase whenever the dialog opens/closes.
useEffect(() => {
@@ -71,40 +77,56 @@ export function ConfirmDialog({
const matched = !confirmPhrase || typed === confirmPhrase;
const canConfirm = matched && !loading;
+ // A real `form` element, so Enter in the phrase field confirms the way a form
+ // is expected to — no key handler duplicating the browser's own behaviour.
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ if (canConfirm) onConfirm();
+ };
+
return (
);
}
diff --git a/portals/api-control-plane/src/components/cards/ProjectCard.tsx b/portals/api-control-plane/src/components/cards/ProjectCard.tsx
index 67c9bace63..dd5050533b 100644
--- a/portals/api-control-plane/src/components/cards/ProjectCard.tsx
+++ b/portals/api-control-plane/src/components/cards/ProjectCard.tsx
@@ -18,8 +18,9 @@
import { useState, type MouseEvent } from 'react';
import {
+ alpha,
Box,
- Button,
+ Card,
Chip,
IconButton,
ListItemIcon,
@@ -31,21 +32,22 @@ import {
Typography,
} from '@wso2/oxygen-ui';
import {
- ArrowRight,
Boxes,
Clock,
- GitBranch,
+ Layers,
MoreVertical,
Rocket,
Settings,
Trash2,
} from '@wso2/oxygen-ui-icons-react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
-import { useApis } from '../../api/hooks/useMvpQueries';
+import { useRestApis } from '../../api/resources/restApis/restApis.hooks';
+import type { Project } from '../../api/resources/projects';
import { routes } from '../../routes/paths';
-import type { Project } from '../../types/domain';
import { relativeTime } from '../../utils/relativeTime';
+import {interactiveCardSx } from '../../theme';
type ProjectCardProps = {
project: Project;
@@ -54,17 +56,70 @@ type ProjectCardProps = {
onDelete?: (project: Project) => void;
};
-// Decorative brand accents (read on both light and dark surfaces): WSO2 orange
-// for regular projects, a cyan tone for the default project.
-const ORANGE_ACCENT = 'linear-gradient(90deg, #F47B20, #EF4223)';
-const CYAN_ACCENT = 'linear-gradient(90deg, #3AA0D6, #5CD1FF)';
+/**
+ * Tint strength of the metadata strip, per color scheme.
+ *
+ * Two factors rather than one because the strip is tinted with opposite ink in
+ * each scheme — black over a light card, white over a dark one — and the two
+ * need different strengths to read as the same depth of recess. They are applied
+ * to `common.black`/`common.white`, which are identical in both schemes, so the
+ * value is never taken from the wrong scheme's palette.
+ */
+const METADATA_TINT = { dark: 0.08, light: 0.06 } as const;
-const repoTypeLabel = (type: Project['type']) =>
- type === 'MONO_REPO'
- ? 'Mono repo'
- : type === 'MULTI_REPO'
- ? 'Multi repo'
- : undefined;
+const messages = defineMessages({
+ actionsLabel: {
+ id: 'project.card.actionsLabel',
+ defaultMessage: 'Project actions',
+ description: 'Accessible label for the button opening the card overflow menu.',
+ },
+ apiCount: {
+ id: 'project.card.apiCount',
+ defaultMessage: '{count, plural, one {# API} other {# APIs}}',
+ },
+ apiCountLoading: {
+ id: 'project.card.apiCountLoading',
+ defaultMessage: '… APIs',
+ description: 'Placeholder shown while the API count is still loading.',
+ },
+ defaultBadge: {
+ id: 'project.card.defaultBadge',
+ defaultMessage: 'DEFAULT',
+ description: 'Badge marking the organization’s default project.',
+ },
+ delete: {
+ id: 'project.card.delete',
+ defaultMessage: 'Delete',
+ },
+ deployedCount: {
+ id: 'project.card.deployedCount',
+ defaultMessage: '{count} deployed',
+ },
+ deployedCountLoading: {
+ id: 'project.card.deployedCountLoading',
+ defaultMessage: '… deployed',
+ description: 'Placeholder shown while the deployed count is still loading.',
+ },
+ fallbackDescription: {
+ id: 'project.card.fallbackDescription',
+ defaultMessage: 'Project workspace',
+ description: 'Shown in place of a description when the project has none.',
+ },
+ neverUpdated: {
+ id: 'project.card.neverUpdated',
+ defaultMessage: 'Not updated yet',
+ },
+ settingsLabel: {
+ id: 'project.card.settingsLabel',
+ defaultMessage: 'Project settings',
+ },
+ updatedAt: {
+ id: 'project.card.updatedAt',
+ defaultMessage: 'Updated {relative}',
+ description:
+ 'Footer timestamp; {relative} is a phrase such as "3 hours ago".',
+ },
+});
export function ProjectCard({
project,
@@ -72,6 +127,7 @@ export function ProjectCard({
onOpen,
onDelete,
}: ProjectCardProps) {
+ const intl = useIntl();
const stopCardClick = (event: MouseEvent) => event.stopPropagation();
const [menuAnchor, setMenuAnchor] = useState(null);
@@ -80,71 +136,55 @@ export function ProjectCard({
setMenuAnchor(null);
};
const isDefault =
- project.handler === 'default' || project.name.toLowerCase() === 'default';
- const accent = isDefault ? CYAN_ACCENT : ORANGE_ACCENT;
- const iconTint = isDefault ? 'rgba(92,209,255,0.14)' : 'rgba(255,115,0,0.14)';
- const iconColor = isDefault ? '#3AA0D6' : '#FF7300';
- const repoLabel = repoTypeLabel(project.type);
+ project.id === 'default' || project.displayName.toLowerCase() === 'default';
- const apisQuery = useApis(orgHandle, project.handler);
- const apiCount = apisQuery.data?.length;
- const deployedCount = apisQuery.data?.filter(
- (api) => api.status === 'ACTIVE'
+ // Scoped to this card's project rather than the route's, so the counts belong
+ // to the card the user is looking at and not the project they are currently in.
+ const apisQuery = useRestApis({}, { projectId: project.id });
+ const apiCount = apisQuery.data?.pagination?.total ?? apisQuery.data?.count;
+ const deployedCount = apisQuery.data?.list?.filter(
+ (api) => api.lifeCycleStatus === 'PUBLISHED'
).length;
return (
- onOpen(project)}
- sx={{
- bgcolor: 'background.paper',
- border: '1px solid',
- borderColor: 'divider',
- borderRadius: 1,
- cursor: 'pointer',
+ sx={() => ({
+ ...interactiveCardSx,
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'hidden',
- transition:
- 'transform .18s ease, border-color .18s ease, box-shadow .18s ease',
- '&:hover': {
- borderColor: 'primary.main',
- boxShadow: 4,
- transform: 'translateY(-3px)',
- },
- }}
+ })}
>
- {/* accent strip */}
-
-
+
({
alignItems: 'center',
- bgcolor: iconTint,
- border: '1px solid',
- borderColor: 'divider',
- borderRadius: 1,
- color: iconColor,
+ bgcolor: `primary.light`,
+ color: `primary.contrastText`,
+ borderRadius: 2,
display: 'flex',
flex: 'none',
height: 46,
justifyContent: 'center',
width: 46,
- }}
+ })}
>
-
+
- {project.name}
+ {project.displayName}
{isDefault && (
- {project.description || 'Project workspace'}
+ {project.description || (
+
+ )}
@@ -166,17 +208,24 @@ export function ProjectCard({
({
+ alignItems: 'center',
+ bgcolor: alpha(theme.palette.common.black, METADATA_TINT.light),
+ borderRadius: 1,
+ color: 'text.secondary',
+ mt: 2.25,
+ px: 1.75,
+ py: 1.25,
+ }),
+ // Emitted under the dark color-scheme selector, so it follows the
+ // theme the user is actually on. Must come last in the array —
+ // `applyStyles` returns a nested selector, not a flat value.
+ (theme) =>
+ theme.applyStyles('dark', {
+ bgcolor: alpha(theme.palette.common.white, METADATA_TINT.dark),
+ }),
+ ]}
>
- {apisQuery.isLoading
- ? '… APIs'
- : `${apiCount ?? 0} ${apiCount === 1 ? 'API' : 'APIs'}`}
+ {apisQuery.isLoading ? (
+
+ ) : (
+
+ )}
- {apisQuery.isLoading
- ? '… deployed'
- : `${deployedCount ?? 0} deployed`}
+ {apisQuery.isLoading ? (
+
+ ) : (
+
+ )}
- {repoLabel && (
-
-
-
- {repoLabel}
-
-
- )}
@@ -219,9 +270,6 @@ export function ProjectCard({
- {project.updatedAt
- ? `Updated ${relativeTime(project.updatedAt)}`
- : 'Not updated yet'}
+ {project.updatedAt ? (
+
+ ) : (
+
+ )}
-
+
{onDelete && (
<>
-
+
{
event.stopPropagation();
setMenuAnchor(event.currentTarget);
@@ -276,24 +329,14 @@ export function ProjectCard({
- Delete
+
+
+
>
)}
- }
- onClick={(event) => {
- stopCardClick(event);
- onOpen(project);
- }}
- size="small"
- sx={{ borderRadius: 5 }}
- variant="outlined"
- >
- Open
-
-
+
);
}
diff --git a/portals/api-control-plane/src/components/cards/ProjectsGrid.tsx b/portals/api-control-plane/src/components/cards/ProjectsGrid.tsx
deleted file mode 100644
index 52af5baea9..0000000000
--- a/portals/api-control-plane/src/components/cards/ProjectsGrid.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { useState } from 'react';
-import { Box, TablePagination } from '@wso2/oxygen-ui';
-
-import type { Project } from '../../types/domain';
-import { ProjectCard } from './ProjectCard';
-
-type ProjectsGridProps = {
- projects: Project[];
- orgHandle: string;
- onOpen: (project: Project) => void;
- onDelete?: (project: Project) => void;
-};
-
-export function ProjectsGrid({
- projects,
- orgHandle,
- onOpen,
- onDelete,
-}: ProjectsGridProps) {
- const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(12);
- const paged = projects.slice(
- page * rowsPerPage,
- page * rowsPerPage + rowsPerPage
- );
-
- return (
- <>
-
- {paged.map((project) => (
-
- ))}
-
- {projects.length > rowsPerPage && (
- setPage(nextPage)}
- onRowsPerPageChange={(event) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0);
- }}
- page={page}
- rowsPerPage={rowsPerPage}
- rowsPerPageOptions={[12, 24, 48]}
- sx={{ mt: 2 }}
- />
- )}
- >
- );
-}
diff --git a/portals/api-control-plane/src/features/auth/AuthProvider.test.tsx b/portals/api-control-plane/src/contexts/auth/AuthProvider.test.tsx
similarity index 100%
rename from portals/api-control-plane/src/features/auth/AuthProvider.test.tsx
rename to portals/api-control-plane/src/contexts/auth/AuthProvider.test.tsx
diff --git a/portals/api-control-plane/src/features/auth/AuthProvider.tsx b/portals/api-control-plane/src/contexts/auth/AuthProvider.tsx
similarity index 100%
rename from portals/api-control-plane/src/features/auth/AuthProvider.tsx
rename to portals/api-control-plane/src/contexts/auth/AuthProvider.tsx
diff --git a/portals/api-control-plane/src/features/auth/AuthStateContext.ts b/portals/api-control-plane/src/contexts/auth/AuthStateContext.ts
similarity index 100%
rename from portals/api-control-plane/src/features/auth/AuthStateContext.ts
rename to portals/api-control-plane/src/contexts/auth/AuthStateContext.ts
diff --git a/portals/api-control-plane/src/features/auth/authConstants.ts b/portals/api-control-plane/src/contexts/auth/authConstants.ts
similarity index 100%
rename from portals/api-control-plane/src/features/auth/authConstants.ts
rename to portals/api-control-plane/src/contexts/auth/authConstants.ts
diff --git a/portals/api-control-plane/src/features/auth/authTypes.ts b/portals/api-control-plane/src/contexts/auth/authTypes.ts
similarity index 100%
rename from portals/api-control-plane/src/features/auth/authTypes.ts
rename to portals/api-control-plane/src/contexts/auth/authTypes.ts
diff --git a/portals/api-control-plane/src/features/apis/ApiDetailPage.tsx b/portals/api-control-plane/src/features/apis/ApiDetailPage.tsx
deleted file mode 100644
index ee8e571d3d..0000000000
--- a/portals/api-control-plane/src/features/apis/ApiDetailPage.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { useState } from 'react';
-import { PageContent, PageTitle, Tab, Tabs } from '@wso2/oxygen-ui';
-
-import { useApiDetail } from '../../api/hooks/useMvpQueries';
-import { ErrorState, LoadingState } from '../../components/StateViews';
-import { DocumentsTab } from './develop/DocumentsTab';
-import { PolicyTab } from './develop/PolicyTab';
-import { RoutingTab } from './develop/RoutingTab';
-import { OverviewTab } from './overview/OverviewTab';
-
-export function ApiDetailPage() {
- const detailQuery = useApiDetail();
- const [tab, setTab] = useState(0);
-
- if (detailQuery.isLoading) return ;
- if (detailQuery.error || !detailQuery.data) {
- return ;
- }
-
- const detail = detailQuery.data;
-
- // Develop tab set mirrors the product: Overview, then Policy/Routing/Documents.
- const tabs = ['Overview', 'Policy', 'Routing', 'Documents'] as const;
- const active = tabs[tab] ?? 'Overview';
-
- return (
-
-
- {detail.displayName}
- {detail.description}
-
-
- setTab(value)}
- sx={{ mb: 3 }}
- value={tab}
- >
- {tabs.map((label) => (
-
- ))}
-
-
- {active === 'Overview' && }
- {active === 'Policy' && }
- {active === 'Routing' && }
- {active === 'Documents' && }
-
- );
-}
diff --git a/portals/api-control-plane/src/features/projects/NewProjectDialog.tsx b/portals/api-control-plane/src/features/projects/NewProjectDialog.tsx
deleted file mode 100644
index e58072df01..0000000000
--- a/portals/api-control-plane/src/features/projects/NewProjectDialog.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import {
- Button,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- Stack,
- TextField,
-} from '@wso2/oxygen-ui';
-import { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { useCreateProject } from '../../api/hooks/useMvpQueries';
-import { useNotifications } from '../../components/Notifications';
-import { routes } from '../../routes/paths';
-
-const NAME_MAX = 120;
-
-export type NewProjectDialogProps = {
- open: boolean;
- orgHandle: string;
- onClose: () => void;
-};
-
-/**
- * Create a project (platform-api `POST /api/v1/projects`). The backend persists
- * only name + description — the organization comes from the bearer token — so
- * the form is intentionally minimal. On success it navigates to the new
- * project's home.
- */
-export function NewProjectDialog({
- open,
- orgHandle,
- onClose,
-}: NewProjectDialogProps) {
- const navigate = useNavigate();
- const { notify } = useNotifications();
- const mutation = useCreateProject(orgHandle);
- const [name, setName] = useState('');
- const [description, setDescription] = useState('');
-
- // Reset fields each time the dialog opens.
- useEffect(() => {
- if (open) {
- setName('');
- setDescription('');
- }
- }, [open]);
-
- const trimmedName = name.trim();
- const canSubmit = trimmedName.length > 0 && !mutation.isPending;
-
- const handleSubmit = async () => {
- if (!canSubmit) return;
- try {
- const project = await mutation.mutateAsync({
- name: trimmedName,
- description: description.trim() || undefined,
- });
- notify('Project created', 'success');
- onClose();
- navigate(routes.projectHome(orgHandle, project.handler));
- } catch (error) {
- notify(
- error instanceof Error ? error.message : 'Failed to create project',
- 'error'
- );
- }
- };
-
- return (
-
- );
-}
diff --git a/portals/api-control-plane/src/features/projects/ProjectListPage.test.tsx b/portals/api-control-plane/src/features/projects/ProjectListPage.test.tsx
deleted file mode 100644
index c1cff68293..0000000000
--- a/portals/api-control-plane/src/features/projects/ProjectListPage.test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { Route, Routes } from 'react-router-dom';
-import type { UseQueryResult } from '@tanstack/react-query';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { projects as mockProjects } from '../../api/mocks/data';
-import type { ApiClient } from '../../api/ApiClientProvider';
-import { renderWithProviders, screen, waitFor, within } from '../../test/utils';
-import { makeConsoleScope } from '../../test/mockScope';
-import type { Project } from '../../types/domain';
-
-vi.mock('../../api/hooks/useMvpQueries', async (importActual) => ({
- ...(await importActual()),
- useProjects: vi.fn(),
-}));
-
-import { useProjects } from '../../api/hooks/useMvpQueries';
-import { ProjectListPage } from './ProjectListPage';
-
-const ORG = 'api-platform-demo';
-
-// Minimal cast helper for the bits of the query result the page reads.
-const queryResult = (overrides: Partial>) =>
- overrides as UseQueryResult;
-
-function renderPage(apiClient?: Partial) {
- return renderWithProviders(
-
- }
- />
- ,
- {
- route: `/organizations/${ORG}/projects`,
- scope: makeConsoleScope(),
- apiClient,
- }
- );
-}
-
-describe('ProjectListPage', () => {
- beforeEach(() => vi.clearAllMocks());
-
- it('shows the loading state', () => {
- vi.mocked(useProjects).mockReturnValue(queryResult({ isLoading: true }));
- renderPage();
- expect(screen.getByText('Loading projects')).toBeInTheDocument();
- });
-
- it('shows an error state with the message', () => {
- vi.mocked(useProjects).mockReturnValue(
- queryResult({ isLoading: false, error: new Error('boom') })
- );
- renderPage();
- expect(screen.getByText(/Unable to load projects\. boom/)).toBeInTheDocument();
- });
-
- it('shows the empty state when there are no projects', () => {
- vi.mocked(useProjects).mockReturnValue(
- queryResult({ isLoading: false, data: [] })
- );
- renderPage();
- expect(screen.getByText('No projects found')).toBeInTheDocument();
- });
-
- it('renders the projects and filters by search', async () => {
- vi.mocked(useProjects).mockReturnValue(
- queryResult({ isLoading: false, data: mockProjects })
- );
- const { user } = renderPage();
-
- expect(screen.getByText('Retail APIs')).toBeInTheDocument();
- expect(screen.getByText('Internal Tools')).toBeInTheDocument();
-
- await user.type(screen.getByPlaceholderText('Search projects'), 'internal');
-
- expect(screen.queryByText('Retail APIs')).not.toBeInTheDocument();
- expect(screen.getByText('Internal Tools')).toBeInTheDocument();
- });
-
- it('deletes a project after type-to-confirm', async () => {
- vi.mocked(useProjects).mockReturnValue(
- queryResult({ isLoading: false, data: mockProjects })
- );
- const deleteProject = vi.fn().mockResolvedValue(undefined);
- const { user } = renderPage({ deleteProject });
-
- // Open the actions menu on the first card (Retail APIs) and choose Delete.
- await user.click(screen.getAllByLabelText('Project actions')[0]);
- await user.click(screen.getByRole('menuitem', { name: /Delete/ }));
-
- // Type-to-confirm guards the irreversible delete.
- const dialog = screen.getByRole('dialog');
- const confirmButton = within(dialog).getByRole('button', { name: 'Delete' });
- expect(confirmButton).toBeDisabled();
-
- await user.type(within(dialog).getByRole('textbox'), 'Retail APIs');
- await user.click(confirmButton);
-
- await waitFor(() => expect(deleteProject).toHaveBeenCalledTimes(1));
- expect(deleteProject.mock.calls[0][1]).toMatchObject({ name: 'Retail APIs' });
- });
-});
diff --git a/portals/api-control-plane/src/features/projects/ProjectListPage.tsx b/portals/api-control-plane/src/features/projects/ProjectListPage.tsx
deleted file mode 100644
index 68adf63aed..0000000000
--- a/portals/api-control-plane/src/features/projects/ProjectListPage.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import {
- Box,
- Button,
- InputAdornment,
- PageContent,
- PageTitle,
- Stack,
- TextField,
- Typography,
-} from '@wso2/oxygen-ui';
-import { Plus, Search } from '@wso2/oxygen-ui-icons-react';
-import { useMemo, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-
-import { useDeleteProject, useProjects } from '../../api/hooks/useMvpQueries';
-import { ProjectsGrid } from '../../components/cards/ProjectsGrid';
-import { ConfirmDialog } from '../../components/ConfirmDialog';
-import { useNotifications } from '../../components/Notifications';
-import { EmptyState, ErrorState, LoadingState } from '../../components/StateViews';
-import { routes } from '../../routes/paths';
-import { useConsoleScope } from '../../scope/ConsoleScopeProvider';
-import type { Project } from '../../types/domain';
-import { NewProjectDialog } from './NewProjectDialog';
-
-export function ProjectListPage() {
- const { orgHandle = '' } = useParams();
- const navigate = useNavigate();
- const { organization } = useConsoleScope();
- const { notify } = useNotifications();
- const [search, setSearch] = useState('');
- const [createOpen, setCreateOpen] = useState(false);
- const [toDelete, setToDelete] = useState(null);
- const projectsQuery = useProjects();
- const deleteProjectMutation = useDeleteProject();
- const projects = projectsQuery.data || [];
-
- const confirmDelete = () => {
- if (!toDelete) return;
- deleteProjectMutation.mutate(toDelete, {
- onSuccess: () => {
- notify(`Deleted "${toDelete.name}".`, 'success');
- setToDelete(null);
- },
- onError: (error) =>
- notify(
- error instanceof Error ? error.message : 'Delete failed',
- 'error'
- ),
- });
- };
-
- const filteredProjects = useMemo(() => {
- const term = search.trim().toLowerCase();
- if (!term) return projects;
- return projects.filter((project) =>
- [project.name, project.handler, project.region, project.description]
- .filter(Boolean)
- .some((field) => field!.toLowerCase().includes(term))
- );
- }, [projects, search]);
-
- const openProject = (project: Project) =>
- navigate(routes.projectHome(orgHandle, project.handler));
-
- if (projectsQuery.isLoading) return ;
- if (projectsQuery.error) {
- return (
-
- );
- }
-
- return (
-
-
- Projects
-
- {organization?.name
- ? `Project workspaces in ${organization.name}.`
- : 'Select a project to manage APIs.'}
-
-
-
-
-
-
- {projects.length === 0 ? (
- setCreateOpen(true)}
- title="No projects found"
- description="Create a project to organize and manage your APIs."
- />
- ) : (
-
-
-
- {filteredProjects.length} project
- {filteredProjects.length === 1 ? '' : 's'}
-
- setSearch(event.target.value)}
- placeholder="Search projects"
- size="small"
- slotProps={{
- input: {
- startAdornment: (
-
-
-
- ),
- },
- }}
- sx={{ minWidth: 260 }}
- value={search}
- />
-
- {filteredProjects.length === 0 ? (
-
- ) : (
-
- )}
-
- )}
-
- setCreateOpen(false)}
- open={createOpen}
- orgHandle={orgHandle}
- />
-
- setToDelete(null)}
- onConfirm={confirmDelete}
- open={toDelete !== null}
- title="Delete project"
- />
-
- );
-}
diff --git a/portals/api-control-plane/src/features/billing/ProductActivation.tsx b/portals/api-control-plane/src/hooks/ProductActivation.tsx
similarity index 95%
rename from portals/api-control-plane/src/features/billing/ProductActivation.tsx
rename to portals/api-control-plane/src/hooks/ProductActivation.tsx
index 384f065def..fa0b7e4344 100644
--- a/portals/api-control-plane/src/features/billing/ProductActivation.tsx
+++ b/portals/api-control-plane/src/hooks/ProductActivation.tsx
@@ -18,8 +18,8 @@
import { useEffect, useRef } from 'react';
-import { runtimeConfig } from '../../config/runtime';
-import { useAuth } from '../auth/AuthProvider';
+import { runtimeConfig } from '../config/runtime';
+import { useAuth } from '../contexts/auth/AuthProvider';
// Product code this console activates on first login. Must match the billing
// product whose subscription drives APIP gateway provisioning.
diff --git a/portals/api-control-plane/src/i18n/messages/en.json b/portals/api-control-plane/src/i18n/messages/en.json
index 0967ef424b..3dbbfaa067 100644
--- a/portals/api-control-plane/src/i18n/messages/en.json
+++ b/portals/api-control-plane/src/i18n/messages/en.json
@@ -1 +1,136 @@
-{}
+{
+ "aiWorkspace.pages.appShell.appShellPages.projects.ExploreMoreCard.explore.more": {
+ "defaultMessage": "Explore More"
+ },
+ "aiWorkspace.pages.appShell.appShellPages.proxies.LLMProxyOverview.context.label": {
+ "defaultMessage": "Context :"
+ },
+ "aiWorkspace.pages.appShell.appShellPages.proxies.LLMProxyOverview.last.updated": {
+ "defaultMessage": "Last updated :"
+ },
+ "organization.home.getStarted": {
+ "defaultMessage": "Get started",
+ "description": "Organization home page get started section title"
+ },
+ "project.card.actionsLabel": {
+ "defaultMessage": "Project actions",
+ "description": "Accessible label for the button opening the card overflow menu."
+ },
+ "project.card.apiCount": {
+ "defaultMessage": "{count, plural, one {# API} other {# APIs}}"
+ },
+ "project.card.apiCountLoading": {
+ "defaultMessage": "… APIs",
+ "description": "Placeholder shown while the API count is still loading."
+ },
+ "project.card.defaultBadge": {
+ "defaultMessage": "DEFAULT",
+ "description": "Badge marking the organization’s default project."
+ },
+ "project.card.delete": {
+ "defaultMessage": "Delete"
+ },
+ "project.card.deployedCount": {
+ "defaultMessage": "{count} deployed"
+ },
+ "project.card.deployedCountLoading": {
+ "defaultMessage": "… deployed",
+ "description": "Placeholder shown while the deployed count is still loading."
+ },
+ "project.card.fallbackDescription": {
+ "defaultMessage": "Project workspace",
+ "description": "Shown in place of a description when the project has none."
+ },
+ "project.card.neverUpdated": {
+ "defaultMessage": "Not updated yet"
+ },
+ "project.card.settingsLabel": {
+ "defaultMessage": "Project settings"
+ },
+ "project.card.updatedAt": {
+ "defaultMessage": "Updated {relative}",
+ "description": "Footer timestamp; {relative} is a phrase such as \"3 hours ago\"."
+ },
+ "project.list.count": {
+ "defaultMessage": "{count, plural, one {# project} other {# projects}}"
+ },
+ "project.list.createProjectButton": {
+ "defaultMessage": "Create Project"
+ },
+ "project.list.delete.confirmInputLabel": {
+ "defaultMessage": "Type \"{name}\" to confirm",
+ "description": "Label for the type-to-confirm field guarding an irreversible delete."
+ },
+ "project.list.delete.confirmLabel": {
+ "defaultMessage": "Delete"
+ },
+ "project.list.delete.failed": {
+ "defaultMessage": "Delete failed",
+ "description": "Fallback toast when the server gives no reason for a failure."
+ },
+ "project.list.delete.message": {
+ "defaultMessage": "This permanently deletes the project \"{name}\" and its configuration. A project that still has APIs cannot be deleted. This action is irreversible."
+ },
+ "project.list.delete.succeeded": {
+ "defaultMessage": "Deleted \"{name}\"."
+ },
+ "project.list.delete.title": {
+ "defaultMessage": "Delete project"
+ },
+ "project.list.empty.action": {
+ "defaultMessage": "Create project"
+ },
+ "project.list.empty.description": {
+ "defaultMessage": "Create a project to organize and manage your APIs."
+ },
+ "project.list.empty.title": {
+ "defaultMessage": "No projects found"
+ },
+ "project.list.error.message": {
+ "defaultMessage": "Unable to load projects. {reason}"
+ },
+ "project.list.loading": {
+ "defaultMessage": "Loading projects"
+ },
+ "project.list.noMatches.description": {
+ "defaultMessage": "Try a different project name or handle."
+ },
+ "project.list.noMatches.title": {
+ "defaultMessage": "No matching projects"
+ },
+ "project.list.rowsPerPage": {
+ "defaultMessage": "Projects per page"
+ },
+ "project.list.searchPlaceholder": {
+ "defaultMessage": "Search projects"
+ },
+ "project.list.sort.nameAscending": {
+ "defaultMessage": "Name (A–Z)",
+ "description": "Sort option: alphabetical by project name, ascending."
+ },
+ "project.list.sort.nameDescending": {
+ "defaultMessage": "Name (Z–A)",
+ "description": "Sort option: alphabetical by project name, descending."
+ },
+ "project.list.sort.newest": {
+ "defaultMessage": "Newest first",
+ "description": "Sort option: by creation date, most recent project first."
+ },
+ "project.list.sort.oldest": {
+ "defaultMessage": "Oldest first",
+ "description": "Sort option: by creation date, earliest project first."
+ },
+ "project.list.sortLabel": {
+ "defaultMessage": "Sort by",
+ "description": "Label for the control choosing the project list order."
+ },
+ "project.list.subHeader.default": {
+ "defaultMessage": "Select a project to manage APIs."
+ },
+ "project.list.subHeader.withOrganization": {
+ "defaultMessage": "Project workspaces in {organizationName}."
+ },
+ "project.list.title": {
+ "defaultMessage": "Projects"
+ }
+}
diff --git a/portals/api-control-plane/src/navigation/useNavigationItems.ts b/portals/api-control-plane/src/navigation/useNavigationItems.ts
index 31bf338ba0..bd7c5b2027 100644
--- a/portals/api-control-plane/src/navigation/useNavigationItems.ts
+++ b/portals/api-control-plane/src/navigation/useNavigationItems.ts
@@ -30,14 +30,14 @@ import {
type NavigationItem,
} from './navigationTypes';
-const isLevelAvailable = (
- definition: NavigationDefinition,
- scope: ReturnType
-) => {
- if (definition.level === 'organization') return scope.isOrganizationScope;
- if (definition.level === 'project') return scope.isProjectScope;
- return scope.isApiScope;
-};
+// const isLevelAvailable = (
+// definition: NavigationDefinition,
+// scope: ReturnType
+// ) => {
+// if (definition.level === 'organization') return scope.isOrganizationScope;
+// if (definition.level === 'project') return scope.isProjectScope;
+// return scope.isApiScope;
+// };
const isFeatureEnabled = (definition: NavigationDefinition) =>
!definition.featureKey ||
@@ -95,12 +95,12 @@ export const useNavigationItems = (): NavigationItem[] => {
const combinedRegistry = [...navigationRegistry, ...extensionDefinitions];
return combinedRegistry
- .filter((definition) => isLevelAvailable(definition, scope))
+ // .filter((definition) => isLevelAvailable(definition, scope))
.filter(isFeatureEnabled)
.filter((definition) => definition.isVisible?.(scope) ?? true)
.map((definition) => {
const to = definition.to(scope);
- if (!to) return undefined;
+ // if (!to) return undefined;
return {
group: definition.group ?? NAVIGATION_GROUP_BY_LEVEL[definition.level],
icon: definition.icon,
diff --git a/portals/api-control-plane/src/layouts/AppHeader.tsx b/portals/api-control-plane/src/pages/appShell/AppHeader.tsx
similarity index 96%
rename from portals/api-control-plane/src/layouts/AppHeader.tsx
rename to portals/api-control-plane/src/pages/appShell/AppHeader.tsx
index 3d6bb7ceab..76b8f13160 100644
--- a/portals/api-control-plane/src/layouts/AppHeader.tsx
+++ b/portals/api-control-plane/src/pages/appShell/AppHeader.tsx
@@ -29,9 +29,9 @@ import {
import { Bell, Boxes, Building2, LogOut, WSO2 } from '@wso2/oxygen-ui-icons-react';
import { useNavigate } from 'react-router-dom';
-import { useAuth } from '../features/auth/AuthProvider';
-import { routes } from '../routes/paths';
-import { useConsoleScope } from '../scope/ConsoleScopeProvider';
+import { useAuth } from '../../contexts/auth/AuthProvider';
+import { routes } from '../../routes/paths';
+import { useConsoleScope } from '../../scope/ConsoleScopeProvider';
export function AppHeader() {
const navigate = useNavigate();
diff --git a/portals/api-control-plane/src/layouts/AppLayout.tsx b/portals/api-control-plane/src/pages/appShell/AppLayout.tsx
similarity index 94%
rename from portals/api-control-plane/src/layouts/AppLayout.tsx
rename to portals/api-control-plane/src/pages/appShell/AppLayout.tsx
index 9ae307a555..4db03b5c6c 100644
--- a/portals/api-control-plane/src/layouts/AppLayout.tsx
+++ b/portals/api-control-plane/src/pages/appShell/AppLayout.tsx
@@ -28,10 +28,10 @@ import { Bell } from '@wso2/oxygen-ui-icons-react';
import { Suspense } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
-import { LoadingState } from '../components/StateViews';
-import { runtimeConfig } from '../config/runtime';
-import { routes } from '../routes/paths';
-import { useConsoleScope } from '../scope/ConsoleScopeProvider';
+import { LoadingState } from '../../components/StateViews';
+import { runtimeConfig } from '../../config/runtime';
+import { routes } from '../../routes/paths';
+import { useConsoleScope } from '../../scope/ConsoleScopeProvider';
import { AppHeader } from './AppHeader';
import { APP_FOOTER_ID } from './appLayoutConstants';
import { AppSidebar } from './AppSidebar';
diff --git a/portals/api-control-plane/src/layouts/AppSidebar.tsx b/portals/api-control-plane/src/pages/appShell/AppSidebar.tsx
similarity index 95%
rename from portals/api-control-plane/src/layouts/AppSidebar.tsx
rename to portals/api-control-plane/src/pages/appShell/AppSidebar.tsx
index 49837f0263..2795e1d47c 100644
--- a/portals/api-control-plane/src/layouts/AppSidebar.tsx
+++ b/portals/api-control-plane/src/pages/appShell/AppSidebar.tsx
@@ -19,7 +19,7 @@
import { Sidebar, useAppShell } from '@wso2/oxygen-ui';
import { Link } from 'react-router-dom';
-import { useNavigationGroups } from '../navigation/useNavigationItems';
+import { useNavigationGroups } from '../../navigation/useNavigationItems';
export function AppSidebar() {
const groups = useNavigationGroups();
diff --git a/portals/api-control-plane/src/layouts/appLayoutConstants.ts b/portals/api-control-plane/src/pages/appShell/appLayoutConstants.ts
similarity index 100%
rename from portals/api-control-plane/src/layouts/appLayoutConstants.ts
rename to portals/api-control-plane/src/pages/appShell/appLayoutConstants.ts
diff --git a/portals/api-control-plane/src/features/apis/ApiCreatePage.test.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.test.tsx
similarity index 97%
rename from portals/api-control-plane/src/features/apis/ApiCreatePage.test.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.test.tsx
index 0f21a35159..bc8aee7cb1 100644
--- a/portals/api-control-plane/src/features/apis/ApiCreatePage.test.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.test.tsx
@@ -19,7 +19,7 @@
import { Route, Routes } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { renderWithProviders, screen, waitFor } from '../../test/utils';
+import { renderWithProviders, screen, waitFor } from '../../../../test/utils';
// Capture the create mutation so we can assert the submitted input.
const { mutateAsync } = vi.hoisted(() => ({ mutateAsync: vi.fn() }));
diff --git a/portals/api-control-plane/src/features/apis/ApiCreatePage.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.tsx
similarity index 99%
rename from portals/api-control-plane/src/features/apis/ApiCreatePage.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.tsx
index 77c650cb0a..361f5214c1 100644
--- a/portals/api-control-plane/src/features/apis/ApiCreatePage.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiCreatePage.tsx
@@ -58,16 +58,16 @@ import {
import yaml from 'js-yaml';
import { useNavigate, useParams } from 'react-router-dom';
-import { useCreateApi } from '../../api/hooks/useMvpQueries';
-import { useNotifications } from '../../components/Notifications';
-import { routes } from '../../routes/paths';
+import { useCreateApi } from '../../../../api/hooks/useMvpQueries';
+import { useNotifications } from '../../../../components/Notifications';
+import { routes } from '../../../../routes/paths';
import type {
ApiOperation,
CreateApiInput,
CreateApiSource,
HttpMethod,
UpstreamAuth,
-} from '../../types/domain';
+} from '../../../../types/domain';
import { isValidUrl, methodColor } from './develop/developEdit';
const OAS_METHODS = [
diff --git a/portals/api-control-plane/src/features/apis/ApiDetailPage.test.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.test.tsx
similarity index 91%
rename from portals/api-control-plane/src/features/apis/ApiDetailPage.test.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.test.tsx
index 117dc9e259..db0a18d484 100644
--- a/portals/api-control-plane/src/features/apis/ApiDetailPage.test.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.test.tsx
@@ -20,13 +20,13 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { Route, Routes } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { useApiDetail } from '../../api/hooks/useMvpQueries';
-import { renderWithProviders, screen } from '../../test/utils';
-import type { ApiDetail } from '../../types/domain';
+import { useApiDetail } from '../../../../api/hooks/useMvpQueries';
+import { renderWithProviders, screen } from '../../../../test/utils';
+import type { ApiDetail } from '../../../../types/domain';
import { ApiDetailPage } from './ApiDetailPage';
vi.mock('../../api/hooks/useMvpQueries', async (importActual) => ({
- ...(await importActual()),
+ ...(await importActual()),
useApiDetail: vi.fn(),
}));
diff --git a/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.tsx
new file mode 100644
index 0000000000..aeb7f2d99f
--- /dev/null
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiDetailPage.tsx
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState } from 'react';
+import { Avatar, Box, Button, Card, Chip, PageContent, Stack, Tab, Tabs, Typography } from '@wso2/oxygen-ui';
+
+import { useApiDetail } from '../../../../api/hooks/useMvpQueries';
+import { ErrorState, LoadingState } from '../../../../components/StateViews';
+import { DocumentsTab } from './develop/DocumentsTab';
+import { PolicyTab } from './develop/PolicyTab';
+import { RoutingTab } from './develop/RoutingTab';
+import { OverviewTab } from './overview/OverviewTab';
+import { FormattedMessage } from 'react-intl';
+
+export function ApiDetailPage() {
+ const detailQuery = useApiDetail();
+ const [tab, setTab] = useState(0);
+
+ if (detailQuery.isLoading) return ;
+ if (detailQuery.error || !detailQuery.data) {
+ return ;
+ }
+
+ const detail = detailQuery.data;
+
+ // Develop tab set mirrors the product: Overview, then Policy/Routing/Documents.
+ const tabs = ['Overview', 'Policy', 'Routing', 'Documents'] as const;
+ const active = tabs[tab] ?? 'Overview';
+
+ const truncateProviderDisplayName = (
+ name?: string | null,
+ maxLength = 30
+): string => {
+ const normalizedName = name?.trim() ?? '';
+ if (normalizedName.length <= maxLength) {
+ return normalizedName;
+ }
+
+ return `${normalizedName.slice(0, maxLength).trim()}…`;
+};
+
+ return (
+
+
+
+ {/* Header card with editable fields */}
+
+
+
+
+
+ {(detail.displayName || '\u2014').trim().slice(0, 2).toUpperCase()}
+
+
+
+
+
+ {truncateProviderDisplayName(detail.displayName || '\u2014')}
+
+
+ {/* Edit page (name/version/context/description). Enabled even
+ for gateway-created proxies — the page keeps the runtime
+ fields read-only and allows only the description. */}
+ {/*
+
+
+
+
+
+ */}
+
+
+
+
+
+
+
+ {detail.context || '/'}
+
+
+
+
+
+
+
+ {detail.updatedAt}
+
+
+
+
+
+
+
+ {/* Deployments remain viewable for gateway-created proxies (deploy/
+ redeploy/restore/undeploy are disabled on the page itself), so the
+ button navigates but is relabelled "View Deployments". */}
+
+ {/*
+ setDeleteDialogOpen(true)}
+ aria-label="Delete proxy"
+ >
+
+
+ */}
+
+
+
+
+
+
+
+ setTab(value)}
+ sx={{ mb: 3 }}
+ value={tab}
+ >
+ {tabs.map((label) => (
+
+ ))}
+
+
+ {active === 'Overview' && }
+ {active === 'Policy' && }
+ {active === 'Routing' && }
+ {active === 'Documents' && }
+
+ );
+}
diff --git a/portals/api-control-plane/src/features/apis/ApiListPage.test.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.test.tsx
similarity index 91%
rename from portals/api-control-plane/src/features/apis/ApiListPage.test.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.test.tsx
index 22e5d7139b..8ad80528af 100644
--- a/portals/api-control-plane/src/features/apis/ApiListPage.test.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.test.tsx
@@ -20,17 +20,17 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { Route, Routes } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { components as mockComponents } from '../../api/mocks/data';
-import { renderWithProviders, screen } from '../../test/utils';
-import type { Api } from '../../types/domain';
+import { components as mockComponents } from '../../../../api/mocks/data';
+import { renderWithProviders, screen } from '../../../../test/utils';
+import type { Api } from '../../../../types/domain';
vi.mock('../../api/hooks/useMvpQueries', async (importActual) => ({
- ...(await importActual()),
+ ...(await importActual()),
useApis: vi.fn(),
useDeleteApi: vi.fn(),
}));
-import { useApis, useDeleteApi } from '../../api/hooks/useMvpQueries';
+import { useApis, useDeleteApi } from '../../../../api/hooks/useMvpQueries';
import { ApiListPage } from './ApiListPage';
const ROUTE = '/organizations/acme/projects/retail/apis';
diff --git a/portals/api-control-plane/src/features/apis/ApiListPage.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.tsx
similarity index 92%
rename from portals/api-control-plane/src/features/apis/ApiListPage.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.tsx
index 5c89ff4c3e..91d21a9287 100644
--- a/portals/api-control-plane/src/features/apis/ApiListPage.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListPage.tsx
@@ -39,19 +39,19 @@ import {
} from '@wso2/oxygen-ui-icons-react';
import { useNavigate, useParams } from 'react-router-dom';
-import { useApis, useDeleteApi } from '../../api/hooks/useMvpQueries';
-import { ApiCardGrid } from '../../components/cards/ApiCardGrid';
-import { filterApis, groupApisByKind } from '../../components/cards/apiDisplay';
-import { ApiListView } from '../../components/cards/ApiListView';
-import { ConfirmDialog } from '../../components/ConfirmDialog';
-import { useNotifications } from '../../components/Notifications';
+import { useApis, useDeleteApi } from '../../../../api/hooks/useMvpQueries';
+import { ApiCardGrid } from '../../../../components/cards/ApiCardGrid';
+import { filterApis, groupApisByKind } from '../../../../components/cards/apiDisplay';
+import { ApiListView } from '../../../../components/cards/ApiListView';
+import { ConfirmDialog } from '../../../../components/ConfirmDialog';
+import { useNotifications } from '../../../../components/Notifications';
import {
EmptyState,
ErrorState,
LoadingState,
-} from '../../components/StateViews';
-import { routes } from '../../routes/paths';
-import type { Api } from '../../types/domain';
+} from '../../../../components/StateViews';
+import { routes } from '../../../../routes/paths';
+import type { Api } from '../../../../types/domain';
type ViewMode = 'grid' | 'list';
diff --git a/portals/api-control-plane/src/features/apis/apiCapabilities.test.ts b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.test.ts
similarity index 96%
rename from portals/api-control-plane/src/features/apis/apiCapabilities.test.ts
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.test.ts
index cee82a6cfe..23e81354c4 100644
--- a/portals/api-control-plane/src/features/apis/apiCapabilities.test.ts
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.test.ts
@@ -18,7 +18,7 @@
import { describe, expect, it } from 'vitest';
-import type { Api } from '../../types/domain';
+import type { Api } from '../../../../types/domain';
import { getApiCapabilities } from './apiCapabilities';
const baseApi: Api = {
diff --git a/portals/api-control-plane/src/features/apis/apiCapabilities.ts b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.ts
similarity index 97%
rename from portals/api-control-plane/src/features/apis/apiCapabilities.ts
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.ts
index 79d9e08b09..e72e866293 100644
--- a/portals/api-control-plane/src/features/apis/apiCapabilities.ts
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/apiCapabilities.ts
@@ -16,7 +16,7 @@
* under the License.
*/
-import type { Api } from '../../types/domain';
+import type { Api } from '../../../../types/domain';
export type ApiTestMode = 'curl' | 'none';
diff --git a/portals/api-control-plane/src/features/apis/develop/AttachedPolicyList.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AttachedPolicyList.tsx
similarity index 98%
rename from portals/api-control-plane/src/features/apis/develop/AttachedPolicyList.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AttachedPolicyList.tsx
index 2b0b401efb..80cb37dac0 100644
--- a/portals/api-control-plane/src/features/apis/develop/AttachedPolicyList.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AttachedPolicyList.tsx
@@ -28,7 +28,7 @@ import {
import { GripVertical, Pencil, Plus, Shield, Trash2 } from '@wso2/oxygen-ui-icons-react';
import { useState } from 'react';
-import type { ApiPolicy } from '../../../types/domain';
+import type { ApiPolicy } from '../../../../../types/domain';
/**
* Renders a flat, ordered list of attached policies (the Hybrid-style policy
diff --git a/portals/api-control-plane/src/features/apis/develop/AvailablePoliciesPanel.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AvailablePoliciesPanel.tsx
similarity index 96%
rename from portals/api-control-plane/src/features/apis/develop/AvailablePoliciesPanel.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AvailablePoliciesPanel.tsx
index dff8f23e28..648c4bafc8 100644
--- a/portals/api-control-plane/src/features/apis/develop/AvailablePoliciesPanel.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/AvailablePoliciesPanel.tsx
@@ -30,13 +30,13 @@ import {
import { ExternalLink, GripVertical, Search, Shield } from '@wso2/oxygen-ui-icons-react';
import { useMemo, useState } from 'react';
-import type { PolicySummary } from '../../../api/policyHub/policyHubClient';
+import type { PolicySummary } from '../../../../../api/policyHub/policyHubClient';
import {
usePolicyHubCategories,
usePolicyHubPolicies,
-} from '../../../api/policyHub/usePolicyHub';
-import { EmptyState, ErrorState } from '../../../components/StateViews';
-import { runtimeConfig } from '../../../config/runtime';
+} from '../../../../../api/policyHub/usePolicyHub';
+import { EmptyState, ErrorState } from '../../../../../components/StateViews';
+import { runtimeConfig } from '../../../../../config/runtime';
import { POLICY_DND_MIME, setDraggedPolicy } from './policyDnd';
const PAGE_SIZE = 20;
diff --git a/portals/api-control-plane/src/features/apis/develop/DocumentsTab.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/DocumentsTab.tsx
similarity index 92%
rename from portals/api-control-plane/src/features/apis/develop/DocumentsTab.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/DocumentsTab.tsx
index d799365747..3b7650bd85 100644
--- a/portals/api-control-plane/src/features/apis/develop/DocumentsTab.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/DocumentsTab.tsx
@@ -16,7 +16,7 @@
* under the License.
*/
-import { EmptyState } from '../../../components/StateViews';
+import { EmptyState } from '../../../../../components/StateViews';
export function DocumentsTab() {
return (
diff --git a/portals/api-control-plane/src/features/apis/develop/PolicyConfigDrawer.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyConfigDrawer.tsx
similarity index 94%
rename from portals/api-control-plane/src/features/apis/develop/PolicyConfigDrawer.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyConfigDrawer.tsx
index 9a04657fb6..76453d4674 100644
--- a/portals/api-control-plane/src/features/apis/develop/PolicyConfigDrawer.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyConfigDrawer.tsx
@@ -28,7 +28,7 @@ import {
import { ChevronLeft, X } from '@wso2/oxygen-ui-icons-react';
import { useMemo, useState } from 'react';
-import type { PolicySummary } from '../../../api/policyHub/policyHubClient';
+import type { PolicySummary } from '../../../../../api/policyHub/policyHubClient';
import {
getByPath,
initValues,
@@ -36,10 +36,10 @@ import {
type ParameterValues,
setByPath,
topLevelRequiredMissing,
-} from '../../../api/policyHub/policySchema';
-import { usePolicyDefinition } from '../../../api/policyHub/usePolicyHub';
-import { ErrorState } from '../../../components/StateViews';
-import type { ApiPolicy } from '../../../types/domain';
+} from '../../../../../api/policyHub/policySchema';
+import { usePolicyDefinition } from '../../../../../api/policyHub/usePolicyHub';
+import { ErrorState } from '../../../../../components/StateViews';
+import type { ApiPolicy } from '../../../../../types/domain';
import { defaultForSchema, SchemaField } from './SchemaField';
/** Minimal reference needed to load a policy's definition. */
diff --git a/portals/api-control-plane/src/features/apis/develop/PolicyTab.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyTab.tsx
similarity index 97%
rename from portals/api-control-plane/src/features/apis/develop/PolicyTab.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyTab.tsx
index edf12bff3c..49d337b401 100644
--- a/portals/api-control-plane/src/features/apis/develop/PolicyTab.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/PolicyTab.tsx
@@ -31,15 +31,15 @@ import {
import { ChevronDown, Globe } from '@wso2/oxygen-ui-icons-react';
import { useState } from 'react';
-import { useUpdateApi } from '../../../api/hooks/useMvpQueries';
-import type { PolicySummary } from '../../../api/policyHub/policyHubClient';
-import { usePolicyHub } from '../../../api/policyHub/usePolicyHub';
-import { useNotifications } from '../../../components/Notifications';
+import { useUpdateApi } from '../../../../../api/hooks/useMvpQueries';
+import type { PolicySummary } from '../../../../../api/policyHub/policyHubClient';
+import { usePolicyHub } from '../../../../../api/policyHub/usePolicyHub';
+import { useNotifications } from '../../../../../components/Notifications';
import type {
ApiOperation,
ApiPolicy,
ApiDetail,
-} from '../../../types/domain';
+} from '../../../../../types/domain';
import { AttachedPolicyList } from './AttachedPolicyList';
import { AvailablePoliciesPanel } from './AvailablePoliciesPanel';
import { SaveBar } from './SaveBar';
diff --git a/portals/api-control-plane/src/features/apis/develop/RoutingTab.test.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.test.tsx
similarity index 94%
rename from portals/api-control-plane/src/features/apis/develop/RoutingTab.test.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.test.tsx
index 3943d1bb23..634a2a40bf 100644
--- a/portals/api-control-plane/src/features/apis/develop/RoutingTab.test.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.test.tsx
@@ -19,9 +19,9 @@
import { http, HttpResponse } from 'msw';
import { describe, expect, it } from 'vitest';
-import { server } from '../../../test/server';
-import { renderWithProviders, screen } from '../../../test/utils';
-import type { ApiDetail } from '../../../types/domain';
+import { server } from '../../../../../test/server';
+import { renderWithProviders, screen } from '../../../../../test/utils';
+import type { ApiDetail } from '../../../../../types/domain';
import { RoutingTab } from './RoutingTab';
const detail: ApiDetail = {
diff --git a/portals/api-control-plane/src/features/apis/develop/RoutingTab.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.tsx
similarity index 99%
rename from portals/api-control-plane/src/features/apis/develop/RoutingTab.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.tsx
index 3ea5771def..6664f04622 100644
--- a/portals/api-control-plane/src/features/apis/develop/RoutingTab.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/RoutingTab.tsx
@@ -46,10 +46,10 @@ import {
X,
} from '@wso2/oxygen-ui-icons-react';
-import { useUpdateApi } from '../../../api/hooks/useMvpQueries';
-import { ConfirmDialog } from '../../../components/ConfirmDialog';
-import { useNotifications } from '../../../components/Notifications';
-import type { ApiOperation, ApiDetail } from '../../../types/domain';
+import { useUpdateApi } from '../../../../../api/hooks/useMvpQueries';
+import { ConfirmDialog } from '../../../../../components/ConfirmDialog';
+import { useNotifications } from '../../../../../components/Notifications';
+import type { ApiOperation, ApiDetail } from '../../../../../types/domain';
import {
type BackendResource,
discoverBackendResources,
diff --git a/portals/api-control-plane/src/features/apis/develop/SaveBar.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/SaveBar.tsx
similarity index 51%
rename from portals/api-control-plane/src/features/apis/develop/SaveBar.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/SaveBar.tsx
index 133f735cd5..0e5233a5e7 100644
--- a/portals/api-control-plane/src/features/apis/develop/SaveBar.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/SaveBar.tsx
@@ -17,30 +17,17 @@
*/
import { Box, Button } from '@wso2/oxygen-ui';
-import { useEffect, useState } from 'react';
-import { APP_FOOTER_ID } from '../../../layouts/appLayoutConstants';
+import { useFooterHeight } from '../../../../appShell/useFooterHeight';
+import { stickyBottomBarSx } from '../../../../../theme';
-/**
- * Upward elevation shadow for the bottom action bar. Kept as a named token
- * (theme.shadows are all downward) rather than an inline magic value.
- */
-const SAVE_BAR_SHADOW = '0 -2px 10px rgba(0, 0, 0, 0.16)';
-
-/** Static styles for the sticky save bar, kept out of JSX (theme-token based). */
-const saveBarBaseSx = {
- // alignItems: 'center',
- // Solid surface (palette token) so scrolling content never shows through.
- borderColor: 'divider',
- borderTop: '1px solid',
- boxShadow: SAVE_BAR_SHADOW,
+/** Layout for the save bar's own content; the sticky treatment is shared. */
+const saveBarLayoutSx = {
display: 'flex',
gap: 1,
justifyContent: 'flex-end',
mt: 1,
- position: 'sticky',
py: 1.5,
- backdropFilter: 'blur(10px)',
} as const;
type SaveBarProps = {
@@ -53,25 +40,7 @@ type SaveBarProps = {
};
/**
- * Measures the app footer's height (it sits at the bottom of the same scroll
- * area this bar sticks to). Returns 0 when the footer is absent (e.g. in tests).
- */
-function useFooterHeight(): number {
- const [height, setHeight] = useState(0);
- useEffect(() => {
- const el = document.getElementById(APP_FOOTER_ID);
- if (!el) return;
- const update = () => setHeight(el.offsetHeight);
- update();
- const observer = new ResizeObserver(update);
- observer.observe(el);
- return () => observer.disconnect();
- }, []);
- return height;
-}
-
-/**
- * Solid save action bar pinned to the bottom of a develop tab's scroll area
+ * Save action bar pinned to the bottom of a develop tab's scroll area
* (`position: sticky`), offset above the app footer so it is never covered.
*/
export function SaveBar({ disabled, saving, onSave, label = 'Save changes' }: SaveBarProps) {
@@ -79,9 +48,9 @@ export function SaveBar({ disabled, saving, onSave, label = 'Save changes' }: Sa
return (
({ zIndex: theme.zIndex.appBar }),
]}
>
- {components.length === 0 ? (
+ {apis.length === 0 ? (
@@ -203,30 +184,23 @@ export function ApiListPage() {
- {matchCount === 0 ? (
+ {searched.length === 0 ? (
+ ) : view === 'grid' ? (
+
) : (
-
- }
- onDelete={setToDelete}
- onOpen={openApi}
- title="API Proxies"
- view={view}
- />
- }
- onDelete={setToDelete}
- onOpen={openApi}
- title="Other APIs"
- view={view}
- />
-
+
)}
)}
@@ -239,7 +213,7 @@ export function ApiListPage() {
loading={deleteApiMutation.isPending}
message={
toDelete
- ? `This permanently deletes the API proxy "${toDelete.displayName}" ` +
+ ? `This permanently deletes the API "${toDelete.displayName}" ` +
'and all related details. This action is irreversible.'
: ''
}
@@ -248,6 +222,6 @@ export function ApiListPage() {
open={toDelete !== null}
title="Delete API"
/>
-
+ >
);
}
diff --git a/portals/api-control-plane/src/components/cards/ApiListView.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListView.tsx
similarity index 71%
rename from portals/api-control-plane/src/components/cards/ApiListView.tsx
rename to portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListView.tsx
index 80fe9dca1e..f312effc10 100644
--- a/portals/api-control-plane/src/components/cards/ApiListView.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/ApiListView.tsx
@@ -26,26 +26,32 @@ import {
ListItemText,
Menu,
MenuItem,
+ Tooltip,
Typography,
} from '@wso2/oxygen-ui';
import { Clock, MoreVertical, Trash2 } from '@wso2/oxygen-ui-icons-react';
-import type { Api } from '../../types/domain';
-import { relativeTime } from '../../utils/relativeTime';
-import { COMPONENT_KIND_LABEL } from './apiDisplay';
-import { EnvStatusChips } from './EnvStatusChips';
-import { KindIconTile } from './KindIconTile';
-import { StatusPill } from './StatusPill';
+import type { RestApi } from '../../../../api/resources/restApis';
+import { relativeTime } from '../../../../utils/relativeTime';
+import { KindIconTile } from '../../../../components/cards/KindIconTile';
+import {
+ DeploymentStateLabel,
+ GatewayChips,
+ LifecycleChip,
+} from './components/RestApiChips';
+import { apiKindLabel, useApiDeploymentState } from './restApiDisplay';
type ApiRowProps = {
- component: Api;
- onOpen: (component: Api) => void;
- onDelete?: (component: Api) => void;
+ api: RestApi;
+ onOpen: (api: RestApi) => void;
+ onDelete?: (api: RestApi) => void;
};
-function ApiRow({ component, onOpen, onDelete }: ApiRowProps) {
- const updated = component.updatedAt || component.createdAt;
+function ApiRow({ api, onOpen, onDelete }: ApiRowProps) {
const [menuAnchor, setMenuAnchor] = useState(null);
+ const { gatewayIds, state } = useApiDeploymentState(api.id);
+
+ const updated = api.updatedAt || api.createdAt;
const closeMenu = (event?: React.MouseEvent) => {
event?.stopPropagation();
@@ -54,10 +60,10 @@ function ApiRow({ component, onOpen, onDelete }: ApiRowProps) {
return (
onOpen(component)}
- sx={{
+ onClick={() => onOpen(api)}
+ sx={(theme) => ({
alignItems: 'center',
- borderBottom: '1px solid',
+ borderBottom: `${theme.border.width} ${theme.border.style}`,
borderColor: 'divider',
cursor: 'pointer',
display: 'flex',
@@ -67,12 +73,16 @@ function ApiRow({ component, onOpen, onDelete }: ApiRowProps) {
transition: 'background-color 250ms',
'&:hover': { bgcolor: 'action.hover' },
'&:last-of-type': { borderBottom: 0 },
- }}
+ })}
>
-
+
+
+
+
+
- {component.displayName}
+ {api.displayName}
- {COMPONENT_KIND_LABEL[component.kind]}
- {component.version ? ` · v${component.version}` : ''}
+ {api.context}
+ {api.version ? ` · v${api.version}` : ''}
-
+
-
+
+
- {component.owner && (
+ {api.createdBy && (
- {component.owner.charAt(0).toUpperCase()}
+ {api.createdBy.charAt(0).toUpperCase()}
- {component.owner}
+ {api.createdBy}
)}
@@ -146,7 +157,7 @@ function ApiRow({ component, onOpen, onDelete }: ApiRowProps) {
diff --git a/portals/api-control-plane/src/pages/appShell/appShellPages/organizations/ExploreMoreCard.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/organizations/ExploreMoreCard.tsx
index cd5c413c21..3a5b8e55bc 100644
--- a/portals/api-control-plane/src/pages/appShell/appShellPages/organizations/ExploreMoreCard.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/organizations/ExploreMoreCard.tsx
@@ -95,8 +95,8 @@ export default function ExploreMoreCard() {
diff --git a/portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.tsx
index a265563c9d..d3ac42177d 100644
--- a/portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/projects/ProjectListPage.tsx
@@ -49,7 +49,6 @@ import { useNotifications } from '../../../../components/Notifications';
import { EmptyState, ErrorState, LoadingState } from '../../../../components/StateViews';
import { routes } from '../../../../routes/paths';
import { useConsoleScope } from '../../../../scope/ConsoleScopeProvider';
-import { useFooterHeight } from '../../useFooterHeight';
import { NewProjectDialog } from './NewProjectDialog';
const PAGE_SIZE_OPTIONS = [12, 24, 48];
@@ -220,8 +219,6 @@ export function ProjectListPage() {
const intl = useIntl();
const { organization } = useConsoleScope();
const { notify } = useNotifications();
- // The app footer shares this scroll area, so the sticky bar sits above it.
- const footerHeight = useFooterHeight();
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
@@ -420,11 +417,7 @@ export function ProjectListPage() {
/>
{total > PAGE_SIZE_OPTIONS[0] && (
-
+
{
getRouteParamsFromPathname(build(ORG, PROJECT, API))
).toMatchObject({ apiHandler: API, orgHandle: ORG, projectHandler: PROJECT });
});
+
+ /*
+ * `newApi` is the one page whose suffix sits in a handle slot (`.../apis/new`),
+ * so the alias convention above cannot protect it — the parser's reserved-segment
+ * list does. Read back as a handle, `new` would name a phantom API in the header
+ * switcher and breadcrumbs, turn `isApiScope` on before the API exists, and fire
+ * a detail request for it.
+ */
+ it('newApi: the create page is not an API called "new"', () => {
+ const params = getRouteParamsFromPathname(routes.newApi(ORG, PROJECT));
+
+ expect(params.orgHandle).toBe(ORG);
+ expect(params.projectHandler).toBe(PROJECT);
+ expect(params.apiHandler).toBeUndefined();
+ });
+
+ it('an API legitimately handled "new" is unreachable, by design', () => {
+ // Documents the trade-off rather than asserting a wish: the reserved segment
+ // wins, so the backend must never mint `new` as an API handle.
+ expect(
+ getRouteParamsFromPathname(routes.api(ORG, PROJECT, 'new')).apiHandler
+ ).toBeUndefined();
+ });
});
describe('path builders', () => {
diff --git a/portals/api-control-plane/src/scope/ScopeGate.tsx b/portals/api-control-plane/src/scope/ScopeGate.tsx
index 9252415857..2cc6d43334 100644
--- a/portals/api-control-plane/src/scope/ScopeGate.tsx
+++ b/portals/api-control-plane/src/scope/ScopeGate.tsx
@@ -230,7 +230,7 @@ function ScopeSelection({
) : projects.length === 0 ? (
@@ -273,7 +273,7 @@ function ScopeSelection({
} />
+
+ ,
+ { route: '/apis' }
+ );
+
+ expect(
+ screen.getByText('This page could not be displayed')
+ ).toBeInTheDocument();
+ // The whole point: chrome survives, so there is a way out of the failure.
+ expect(screen.getByRole('link', { name: 'Gateways' })).toBeInTheDocument();
+ expect(screen.getByText('console footer')).toBeInTheDocument();
+
+ await user.click(screen.getByRole('link', { name: 'Gateways' }));
+
+ expect(await screen.findByText('gateways page')).toBeInTheDocument();
+ expect(
+ screen.queryByText('This page could not be displayed')
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/portals/api-control-plane/src/components/errors/ErrorBoundary.tsx b/portals/api-control-plane/src/components/errors/ErrorBoundary.tsx
new file mode 100644
index 0000000000..c424d19cc9
--- /dev/null
+++ b/portals/api-control-plane/src/components/errors/ErrorBoundary.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, type ErrorInfo, type ReactNode } from 'react';
+
+import { AppErrorFallback } from './ErrorFallback';
+
+type ErrorBoundaryProps = {
+ children: ReactNode;
+ fallback?: (error: Error, reset: () => void) => ReactNode;
+ /**
+ * Values that, when any of them changes, clear a caught error and re-render
+ * `children`.
+ *
+ * The page-level boundary passes the current pathname: without it the
+ * fallback stays latched after the user navigates away, so the shell's
+ * sidebar would appear to be broken too.
+ */
+ resetKeys?: readonly unknown[];
+};
+
+type ErrorBoundaryState = {
+ error?: Error;
+ /** The `resetKeys` this state was derived against; compared, never rendered. */
+ resetKeys: readonly unknown[];
+};
+
+const EMPTY_RESET_KEYS: readonly unknown[] = [];
+
+const sameResetKeys = (a: readonly unknown[], b: readonly unknown[]) =>
+ a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
+
+/**
+ * Catches a render-time throw in its subtree and renders a fallback in its
+ * place, so the failure is contained to that subtree rather than unmounting
+ * everything above it.
+ *
+ * Mounted at two levels, and the difference matters:
+ *
+ * - Around the routed page in `AppLayout`, which is what keeps a page fault
+ * from taking the header, sidebar and footer down with it. This is the one
+ * that catches almost everything in practice, including a `lazy()` chunk that
+ * fails to load after a deploy.
+ * - At the top of `App`, above `BrowserRouter`, as the last resort for anything
+ * that throws outside the routed page (the auth provider, the scope provider,
+ * the shell itself).
+ *
+ * It does not catch errors thrown from event handlers, timers, or unawaited
+ * promises, React boundaries never do. Those still need handling at the call
+ * site.
+ */
+export class ErrorBoundary extends Component<
+ ErrorBoundaryProps,
+ ErrorBoundaryState
+> {
+ state: ErrorBoundaryState = { resetKeys: EMPTY_RESET_KEYS };
+
+ static getDerivedStateFromError(error: Error): Partial {
+ return { error };
+ }
+
+ /**
+ * Drops a caught error once `resetKeys` change.
+ *
+ * Done here rather than in an effect because a boundary showing its fallback
+ * renders no children, so a child effect can never run to clear it — the
+ * decision has to happen during the render that follows the key change.
+ */
+ static getDerivedStateFromProps(
+ props: ErrorBoundaryProps,
+ state: ErrorBoundaryState
+ ): Partial | null {
+ const keys = props.resetKeys ?? EMPTY_RESET_KEYS;
+ if (sameResetKeys(keys, state.resetKeys)) return null;
+ return { error: undefined, resetKeys: keys };
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ // Kept in every build: the fallback shows the user sterile copy, so the
+ // console is the only place the real message and component stack survive.
+ // eslint-disable-next-line no-console
+ console.error('Unhandled error in oxygen-console', error, info);
+ }
+
+ reset = () => {
+ this.setState({ error: undefined });
+ };
+
+ render() {
+ const { error } = this.state;
+ const { children, fallback } = this.props;
+
+ if (!error) return children;
+ if (fallback) return fallback(error, this.reset);
+
+ return ;
+ }
+}
diff --git a/portals/api-control-plane/src/components/errors/ErrorFallback.tsx b/portals/api-control-plane/src/components/errors/ErrorFallback.tsx
new file mode 100644
index 0000000000..4af9e88e2b
--- /dev/null
+++ b/portals/api-control-plane/src/components/errors/ErrorFallback.tsx
@@ -0,0 +1,351 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ Box,
+ Button,
+ Sidebar,
+ Stack,
+ Tooltip,
+ Typography,
+} from '@wso2/oxygen-ui';
+import { House, RefreshCw, RotateCcw, TriangleAlert } from '@wso2/oxygen-ui-icons-react';
+import type { ReactNode } from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { useNavigate } from 'react-router-dom';
+
+import { runtimeConfig } from '../../config/runtime';
+import { isChunkLoadError } from '../../utils/errors/errorClassification';
+
+const messages = defineMessages({
+ appBody: {
+ id: 'apiControlPlane.components.ErrorFallback.app.body',
+ defaultMessage:
+ 'The console ran into an unexpected problem and could not continue.',
+ },
+ appTitle: {
+ id: 'apiControlPlane.components.ErrorFallback.app.title',
+ defaultMessage: 'Something went wrong',
+ },
+ navigationUnavailable: {
+ id: 'apiControlPlane.components.ErrorFallback.chrome.navigationUnavailable',
+ defaultMessage:
+ 'Navigation is unavailable. Reload the page to restore it.',
+ description:
+ 'Tooltip on the marker left in place of the sidebar when it fails to render.',
+ },
+ goHome: {
+ id: 'apiControlPlane.components.ErrorFallback.action.goHome',
+ defaultMessage: 'Go to console home',
+ description:
+ 'Leaves the failing page for the console landing page. Escapes a page that fails every time it is opened.',
+ },
+ pageBody: {
+ id: 'apiControlPlane.components.ErrorFallback.page.body',
+ defaultMessage:
+ 'Something went wrong while displaying this page. The rest of the console is still available.',
+ },
+ pageTitle: {
+ id: 'apiControlPlane.components.ErrorFallback.page.title',
+ defaultMessage: 'This page could not be displayed',
+ },
+ reload: {
+ id: 'apiControlPlane.components.ErrorFallback.action.reload',
+ defaultMessage: 'Reload',
+ description: 'Reloads the browser tab. Verb, not a noun.',
+ },
+ staleBody: {
+ id: 'apiControlPlane.components.ErrorFallback.stale.body',
+ defaultMessage:
+ 'This page could not be loaded because the console has been updated since you opened it. Reload to get the latest version.',
+ description:
+ 'Shown when a code-split chunk 404s, which happens to an open tab after a deployment.',
+ },
+ staleTitle: {
+ id: 'apiControlPlane.components.ErrorFallback.stale.title',
+ defaultMessage: 'A newer version of the console is available',
+ },
+ switchersUnavailable: {
+ id: 'apiControlPlane.components.ErrorFallback.chrome.switchersUnavailable',
+ defaultMessage:
+ 'Organization, project and API switchers are unavailable. Reload the page to restore them.',
+ description:
+ 'Tooltip on the marker left in place of the header switchers when they fail to render.',
+ },
+ tryAgain: {
+ id: 'apiControlPlane.components.ErrorFallback.action.tryAgain',
+ defaultMessage: 'Try again',
+ description:
+ 're-renders the failed area without reloading the browser tab.',
+ },
+});
+
+/** What every fallback below renders, differing only in copy and actions. */
+function ErrorFallbackLayout({
+ actions,
+ body,
+ error,
+ title,
+}: {
+ actions: ReactNode;
+ body: ReactNode;
+ error: Error;
+ title: ReactNode;
+}) {
+ return (
+
+
+
+
+ {title}
+ {body}
+
+ {/*
+ The raw message is a developer artefact ("Cannot read properties of
+ undefined"), so it is shown only in a dev build. `componentDidCatch`
+ logs it to the console in every build, which is where support should
+ read it from.
+ */}
+ {import.meta.env.DEV && error.message && (
+
+ {error.message}
+
+ )}
+
+
+ {actions}
+
+
+ );
+}
+
+export type ErrorFallbackProps = {
+ error: Error;
+ /**
+ * Clears the boundary's error state and re-renders its children. Recovers a
+ * transient fault without discarding the router, the query cache, or any
+ * state held above the boundary — which is what a full reload costs.
+ */
+ reset: () => void;
+};
+
+/**
+ * Fallback for the boundary around the routed page, inside `AppLayout`.
+ *
+ * Deliberately renders only where the page would: the header, sidebar,
+ * breadcrumbs and footer stay mounted, so the user navigates away from a broken
+ * page instead of being left with a reload button that reproduces the same
+ * failure. That is also why "go home" is a real route navigation here rather
+ * than a document load.
+ */
+export function PageErrorFallback({ error, reset }: ErrorFallbackProps) {
+ const navigate = useNavigate();
+
+ if (isChunkLoadError(error)) {
+ return (
+ window.location.reload()}
+ startIcon={}
+ variant="contained"
+ >
+
+
+ }
+ body={}
+ error={error}
+ title={}
+ />
+ );
+ }
+
+ return (
+
+ }
+ variant="contained"
+ >
+
+
+ navigate('/')} startIcon={}>
+
+
+ >
+ }
+ body={}
+ error={error}
+ title={}
+ />
+ );
+}
+
+/**
+ * Fallback for the outermost boundary, which sits above `BrowserRouter`.
+ *
+ * Reached only when something outside the routed page throws, so there is no
+ * router to navigate with — "go home" has to be a document load. It points at
+ * the app's base path rather than reloading the current URL on purpose: when
+ * the failure is deterministic for this route, reloading it reproduces the
+ * error forever, and this is the one action that escapes that loop.
+ */
+export function AppErrorFallback({ error, reset }: ErrorFallbackProps) {
+ const stale = isChunkLoadError(error);
+
+ return (
+
+ {stale ? (
+ window.location.reload()}
+ startIcon={}
+ variant="contained"
+ >
+
+
+ ) : (
+ }
+ variant="contained"
+ >
+
+
+ )}
+
+ window.location.assign(`${runtimeConfig.appBasePath || ''}/`)
+ }
+ startIcon={}
+ >
+
+
+ >
+ }
+ body={
+
+ }
+ error={error}
+ title={
+
+ }
+ />
+ );
+}
+
+/* -------------------------------------------------------------------------- */
+/* Persistent chrome */
+/* -------------------------------------------------------------------------- */
+
+/**
+ * The marker a failed piece of persistent chrome leaves behind.
+ *
+ * A header switcher or the sidebar has no room for the full-page treatment
+ * above, and no useful action of its own — the surrounding chrome is still
+ * working, so the recovery is "carry on, or reload". What it must not do is
+ * disappear silently: a console shipped with a missing org switcher and no
+ * visible trace is a bug nobody reports. Hence a small, deliberately
+ * unobtrusive marker with the explanation in a tooltip.
+ *
+ * `role="status"` rather than `alert`: this is a degraded region the user may
+ * never need, not something demanding immediate attention.
+ */
+function ChromeErrorMarker({ description }: { description: string }) {
+ return (
+
+
+
+
+
+ );
+}
+
+/**
+ * Fallback for the boundary around the header's scope switchers.
+ *
+ * Scoped to the switchers alone so the rest of the header survives — the brand,
+ * the colour-scheme toggle, the notification bell, and above all the user menu
+ * with **logout** in it. Losing the ability to sign out because an org lookup
+ * returned an unexpected shape is a far worse outcome than losing a switcher.
+ */
+export function HeaderSwitchersErrorFallback() {
+ const intl = useIntl();
+ return (
+
+ );
+}
+
+/**
+ * Fallback for the boundary around the sidebar.
+ *
+ * Renders a real but empty `Sidebar` rather than nothing, so the rail keeps its
+ * width and the shell's grid does not reflow around a missing column. It reads
+ * `collapsed`/`width` from `AppShellContext` on its own, so the fallback rail
+ * matches whatever the user had before the failure.
+ *
+ * No synthesised nav items: building a fallback menu would re-run the same
+ * scope-dependent logic that just threw. The header's switchers and brand are
+ * the way out from here.
+ */
+export function SidebarErrorFallback() {
+ const intl = useIntl();
+ return (
+
+
+
+
+
+ );
+}
diff --git a/portals/api-control-plane/src/pages/appShell/useFooterHeight.ts b/portals/api-control-plane/src/hooks/useFooterHeight.ts
similarity index 96%
rename from portals/api-control-plane/src/pages/appShell/useFooterHeight.ts
rename to portals/api-control-plane/src/hooks/useFooterHeight.ts
index cda18af72e..e3f543dde3 100644
--- a/portals/api-control-plane/src/pages/appShell/useFooterHeight.ts
+++ b/portals/api-control-plane/src/hooks/useFooterHeight.ts
@@ -18,7 +18,7 @@
import { useEffect, useState } from 'react';
-import { APP_FOOTER_ID } from './appLayoutConstants';
+import { APP_FOOTER_ID } from '../pages/appShell/appLayoutConstants';
/**
* Measures the app footer's height.
diff --git a/portals/api-control-plane/src/pages/appShell/AppHeader.test.tsx b/portals/api-control-plane/src/pages/appShell/AppHeader.test.tsx
new file mode 100644
index 0000000000..5e5b00b209
--- /dev/null
+++ b/portals/api-control-plane/src/pages/appShell/AppHeader.test.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { AppShell } from '@wso2/oxygen-ui';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { SidebarErrorFallback } from '../../components/errors/ErrorFallback';
+import { renderWithProviders, screen } from '../../test/utils';
+
+// The switchers own every scope-dependent hook in the header, so making the
+// whole module throw is exactly the failure the boundary around it exists for.
+vi.mock('./HeaderScopeSwitchers', () => ({
+ HeaderScopeSwitchers: () => {
+ throw new Error('scope lookup returned an unexpected shape');
+ },
+}));
+
+import { AppHeader } from './AppHeader';
+
+/** `useAppShell()` throws outside a provider, so the header needs its real slot. */
+const renderHeader = () =>
+ renderWithProviders(
+
+
+
+
+
+ );
+
+beforeEach(() => {
+ vi.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe('AppHeader', () => {
+ it('keeps the rest of the header usable when the switchers throw', async () => {
+ const { user } = renderHeader();
+
+ // Brand and actions are unaffected — they read nothing from scope.
+ expect(screen.getByText('API Platform')).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Notifications' })
+ ).toBeInTheDocument();
+
+ // The one that matters: losing a switcher must never cost the user their
+ // way out of the session.
+ await user.click(screen.getByRole('button', { name: 'Account' }));
+ expect(await screen.findByText('Test User')).toBeInTheDocument();
+ expect(
+ await screen.findByText(/log ?out|sign out/i)
+ ).toBeInTheDocument();
+ });
+
+ it('leaves a visible marker rather than silently dropping the switchers', () => {
+ renderHeader();
+
+ expect(
+ screen.getByRole('status', {
+ name: /switchers are unavailable/i,
+ })
+ ).toBeInTheDocument();
+ expect(console.error).toHaveBeenCalled();
+ });
+});
+
+describe('SidebarErrorFallback', () => {
+ it('renders an empty rail carrying a marker', () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByRole('status', { name: /navigation is unavailable/i })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/portals/api-control-plane/src/pages/appShell/AppHeader.tsx b/portals/api-control-plane/src/pages/appShell/AppHeader.tsx
index 5371e3c889..e5bbec4fa9 100644
--- a/portals/api-control-plane/src/pages/appShell/AppHeader.tsx
+++ b/portals/api-control-plane/src/pages/appShell/AppHeader.tsx
@@ -18,116 +18,31 @@
import {
Badge,
- Box,
ColorSchemeToggle,
- ComplexSelect,
Header,
IconButton,
Tooltip,
UserMenu,
useAppShell,
} from '@wso2/oxygen-ui';
-import { Bell, Boxes, Building, Layers, LogOut, WSO2, X } from '@wso2/oxygen-ui-icons-react';
-import { useNavigate } from 'react-router-dom';
-
-import { useRestApis } from '../../api/resources/restApis';
-import { useAuth } from '../../contexts/auth/AuthProvider';
-import { routes } from '../../routes/paths';
-import { useConsoleScope } from '../../scope/ConsoleScopeProvider';
+import { Bell, LogOut, WSO2 } from '@wso2/oxygen-ui-icons-react';
import { FormattedMessage, useIntl } from 'react-intl';
-import ProjectQuickSelector from './ProjectQuickSelector';
-import APIQuickSelector from './APIQuickSelector';
-import SearchableComplexSelect from '../../components/common/SearchableComplexSelect';
-
-// Switcher options can carry long display names/handles; bound the trigger width
-// and let the option text ellipsize instead of overflowing the header.
-const SWITCHER_SELECT_SX = { minWidth: 220, maxWidth: 260 };
+import { useLocation } from 'react-router-dom';
-const TRUNCATED_OPTION_TEXT_SLOT_PROPS = {
- primary: { noWrap: true },
- secondary: { variant: 'caption' as const, noWrap: true },
-};
+import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
+import { HeaderSwitchersErrorFallback } from '../../components/errors/ErrorFallback';
+import { useAuth } from '../../contexts/auth/AuthProvider';
+import { HeaderScopeSwitchers } from './HeaderScopeSwitchers';
export function AppHeader() {
- const navigate = useNavigate();
const intl = useIntl();
+ const location = useLocation();
const { actions } = useAppShell();
- const { component, organization, organizations, params, project, projects, isLoading, projectsError } =
- useConsoleScope();
const auth = useAuth();
const userName = auth.user?.name || 'User';
const userEmail = auth.user?.email || '';
- const changeOrganization = (orgHandle: string) => {
- if (!orgHandle || orgHandle === params.orgHandle) return;
- navigate(routes.organizationHome(orgHandle));
- };
-
- const changeProject = (projectHandler: string) => {
- if (!params.orgHandle || !projectHandler) return;
- navigate(routes.projectHome(params.orgHandle, projectHandler));
- };
-
- const changeApi = (apiHandler: string) => {
- if (!params.orgHandle || !params.projectHandler || !apiHandler) return;
- navigate(
- routes.api(params.orgHandle, params.projectHandler, apiHandler)
- );
- }
-
- const clearProjectSelection = () => {
- if (!params.orgHandle) return;
- navigate(routes.organizationHome(params.orgHandle));
- };
-
- const clearApiSelection = () => {
- if (!params.orgHandle || !params.projectHandler) return;
- navigate(routes.projectHome(params.orgHandle, params.projectHandler));
- };
-
- // organizations may not be loaded yet on first paint; keep the current org
- // selectable so the switcher never renders an out-of-range value.
- const orgOptions: { handle: string; name: string }[] =
- organizations.length > 0
- ? organizations.map((org) => ({ handle: org.id, name: org.displayName || org.id }))
- : params.orgHandle
- ? [{ handle: params.orgHandle, name: organization?.displayName || params.orgHandle }]
- : [];
-
- const projectOptions: { handler: string; name: string }[] =
- projects.length > 0
- ? projects.map((project) => ({ handler: project.id, name: project.displayName || project.id }))
- : params.projectHandler && project ? [{ handler: params.projectHandler, name: project.displayName || params.projectHandler }]
- : [];
-
- const apisQuery = useRestApis(
- {},
- { projectId: project?.id, orgId: organization?.id }
- );
- const apis = apisQuery.data?.list ?? [];
- const loadedApiOptions: { handler: string; name: string }[] =
- (projects.length > 0 && project) ?
- apis
- .filter(
- (
- api
- ): api is NonNullable & { id: string; displayName?: string } =>
- typeof api?.id === 'string' && api.id.length > 0
- )
- .map((api) => ({ handler: api.id, name: api.displayName ?? api.id }))
- : [];
-
- // apis may not be loaded yet on first paint; keep the current API selectable
- // so the switcher never renders an out-of-range value.
- const apiOptions: { handler: string; name: string }[] =
- loadedApiOptions.length > 0
- ? loadedApiOptions
- : params.apiHandler
- ? [{ handler: params.apiHandler, name: component?.displayName || params.apiHandler }]
- : [];
-
-
return (
@@ -143,202 +58,16 @@ export function AppHeader() {
- {params.orgHandle && (
-
- item.handle === params.orgHandle).map(item => ({
- id: item.handle,
- handler: item.handle,
- name: item.name,
- }))[0] || undefined}
- onChange={(id) => {
- changeOrganization(id);
- }}
- options={orgOptions.map((item) => ({
- id: item.handle,
- handler: item.handle,
- name: item.name,
- }))}
- renderOptionContent={(option) => (
- <>
-
-
-
-
- >
- )}
- searchPlaceholder={intl.formatMessage({ id: 'appShell.header.org.placeholder', defaultMessage: 'Search organizations...' })}
- emptyMessage={intl.formatMessage({ id: 'appShell.header.org.empty', defaultMessage: 'No organizations found' })}
- noResultsMessage={intl.formatMessage({ id: 'appShell.header.org.noResults', defaultMessage: 'No matching organizations' })}
- sx={SWITCHER_SELECT_SX}
- />
-
- {params.projectHandler && (
-
- item.handler === params.projectHandler).map(item => ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))[0] || undefined}
- onChange={(id) => {
- changeProject(id);
- }}
- options={projectOptions.map((item) => ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))}
- renderOptionContent={(option) => (
- <>
-
-
-
-
- >
- )}
- searchPlaceholder={intl.formatMessage({ id: 'appShell.header.project.placeholder', defaultMessage: 'Search projects...' })}
- emptyMessage={intl.formatMessage({ id: 'appShell.header.project.empty', defaultMessage: 'No projects found' })}
- noResultsMessage={intl.formatMessage({ id: 'appShell.header.project.noResults', defaultMessage: 'No matching projects' })}
- sx={SWITCHER_SELECT_SX}
- />
-
- {
- event.preventDefault();
- event.stopPropagation();
- }}
- onClick={(event) => {
- event.preventDefault();
- event.stopPropagation();
- clearProjectSelection();
- }}
- sx={{
- position: "absolute",
- top: 6,
- right: 2,
- zIndex: 1,
- width: 20,
- height: 10,
- }}
- >
-
-
-
- )}
-
- {!params.projectHandler && (
- ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))}
- onSelectProject={(projectHandler) => {
- changeProject(projectHandler);
- }}
- />
- )}
-
- {params.apiHandler && (
-
- item.handler === params.apiHandler).map(item => ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))[0] || undefined}
- onChange={(id) => {
- changeApi(id);
- }}
- options={apiOptions.map((item) => ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))}
- renderOptionContent={(option) => (
- <>
-
-
-
-
- >
- )}
- searchPlaceholder={intl.formatMessage({ id: 'appShell.header.api.placeholder', defaultMessage: 'Search APIs...' })}
- emptyMessage={intl.formatMessage({ id: 'appShell.header.api.empty', defaultMessage: 'No APIs found' })}
- noResultsMessage={intl.formatMessage({ id: 'appShell.header.api.noResults', defaultMessage: 'No matching APIs' })}
- sx={SWITCHER_SELECT_SX}
- />
-
- {
- event.preventDefault();
- event.stopPropagation();
- }}
- onClick={(event) => {
- event.preventDefault();
- event.stopPropagation();
- clearApiSelection();
- }}
- sx={{
- position: "absolute",
- top: 6,
- right: 2,
- zIndex: 1,
- width: 20,
- height: 10,
- }}
- >
-
-
-
- )}
-
- {!params.apiHandler && params.projectHandler && (
- ({
- id: item.handler,
- handler: item.handler,
- name: item.name,
- }))}
- onSelectApi={(apiHandler) => {
- changeApi(apiHandler);
- }}
- />
- )}
-
- )}
+ {/* Guard only the switchers: their data (orgs, projects, APIs, etc.) may be
+ missing or malformed. Keep the brand/actions outside so logout stays
+ available. Use `resetKeys` with pathname so a broken switcher recovers
+ after navigation. */}
+ }
+ resetKeys={[location.pathname]}
+ >
+
+
diff --git a/portals/api-control-plane/src/pages/appShell/AppLayout.tsx b/portals/api-control-plane/src/pages/appShell/AppLayout.tsx
index 3633ebb487..8d710e89b4 100644
--- a/portals/api-control-plane/src/pages/appShell/AppLayout.tsx
+++ b/portals/api-control-plane/src/pages/appShell/AppLayout.tsx
@@ -30,6 +30,11 @@ import { Bell } from '@wso2/oxygen-ui-icons-react';
import { Suspense } from 'react';
import { matchPath, Outlet, useLocation, useNavigate } from 'react-router-dom';
+import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
+import {
+ PageErrorFallback,
+ SidebarErrorFallback,
+} from '../../components/errors/ErrorFallback';
import { LoadingState } from '../../components/StateViews';
import { runtimeConfig } from '../../config/runtime';
import { routes } from '../../routes/paths';
@@ -115,22 +120,35 @@ export default function AppLayout() {
-
+ {/* Keep this outside so Sidebar.Category can inspect children */}
+ }
+ resetKeys={[location.pathname]}
+ >
+
+
-
- }>
-
-
- {!hidesBreadcrumbs && breadcrumbItems.length > 1 && (
-
- )}
+
+
+ {!hidesBreadcrumbs && breadcrumbItems.length > 1 && (
+
+ )}
+ {/* Error boundary scoped to routed page only; resets on pathname change */}
+ (
+
+ )}
+ resetKeys={[location.pathname]}
+ >
+ }>
-
-
-
+
+
+
+
diff --git a/portals/api-control-plane/src/pages/appShell/HeaderScopeSwitchers.tsx b/portals/api-control-plane/src/pages/appShell/HeaderScopeSwitchers.tsx
new file mode 100644
index 0000000000..a8a3327aa9
--- /dev/null
+++ b/portals/api-control-plane/src/pages/appShell/HeaderScopeSwitchers.tsx
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Box, ComplexSelect, Header, IconButton } from '@wso2/oxygen-ui';
+import { Boxes, Building, Layers, X } from '@wso2/oxygen-ui-icons-react';
+import { useIntl } from 'react-intl';
+import { useNavigate } from 'react-router-dom';
+
+import { useRestApis } from '../../api/resources/restApis';
+import SearchableComplexSelect from '../../components/common/SearchableComplexSelect';
+import { routes } from '../../routes/paths';
+import { useConsoleScope } from '../../scope/ConsoleScopeProvider';
+import APIQuickSelector from './APIQuickSelector';
+import ProjectQuickSelector from './ProjectQuickSelector';
+
+// Switcher options can carry long display names/handles; bound the trigger width
+// and let the option text ellipsize instead of overflowing the header.
+const SWITCHER_SELECT_SX = { minWidth: 220, maxWidth: 260 };
+
+const TRUNCATED_OPTION_TEXT_SLOT_PROPS = {
+ primary: { noWrap: true },
+ secondary: { variant: 'caption' as const, noWrap: true },
+};
+
+/**
+ * The organization / project / API switchers in the header.
+ *
+ * Split out of `AppHeader` so it can sit behind its own error boundary. Every
+ * scope-dependent hook and every derivation over that data lives here, which is
+ * the point: those run before any JSX is returned, so a boundary placed inside
+ * the header's markup could never catch them. Keeping them in a child component
+ * is what makes the failure containable — the brand, the notification bell and
+ * the user menu's logout stay reachable when this throws.
+ *
+ * It owns the `params.orgHandle` guard too. Lifting that back into `AppHeader`
+ * would drag `useConsoleScope()` up with it and put the throw back outside the
+ * boundary.
+ */
+export function HeaderScopeSwitchers() {
+ const navigate = useNavigate();
+ const intl = useIntl();
+ const {
+ component,
+ organization,
+ organizations,
+ params,
+ project,
+ projects,
+ isLoading,
+ projectsError,
+ } = useConsoleScope();
+
+ const changeOrganization = (orgHandle: string) => {
+ if (!orgHandle || orgHandle === params.orgHandle) return;
+ navigate(routes.organizationHome(orgHandle));
+ };
+
+ const changeProject = (projectHandler: string) => {
+ if (!params.orgHandle || !projectHandler) return;
+ navigate(routes.projectHome(params.orgHandle, projectHandler));
+ };
+
+ const changeApi = (apiHandler: string) => {
+ if (!params.orgHandle || !params.projectHandler || !apiHandler) return;
+ navigate(routes.api(params.orgHandle, params.projectHandler, apiHandler));
+ };
+
+ const clearProjectSelection = () => {
+ if (!params.orgHandle) return;
+ navigate(routes.organizationHome(params.orgHandle));
+ };
+
+ const clearApiSelection = () => {
+ if (!params.orgHandle || !params.projectHandler) return;
+ navigate(routes.projectHome(params.orgHandle, params.projectHandler));
+ };
+
+ // organizations may not be loaded yet on first paint; keep the current org
+ // selectable so the switcher never renders an out-of-range value.
+ const orgOptions: { handle: string; name: string }[] =
+ organizations.length > 0
+ ? organizations.map((org) => ({ handle: org.id, name: org.displayName || org.id }))
+ : params.orgHandle
+ ? [{ handle: params.orgHandle, name: organization?.displayName || params.orgHandle }]
+ : [];
+
+ const projectOptions: { handler: string; name: string }[] =
+ projects.length > 0
+ ? projects.map((project) => ({ handler: project.id, name: project.displayName || project.id }))
+ : params.projectHandler && project ? [{ handler: params.projectHandler, name: project.displayName || params.projectHandler }]
+ : [];
+
+ const apisQuery = useRestApis(
+ {},
+ { projectId: project?.id, orgId: organization?.id }
+ );
+ const apis = apisQuery.data?.list ?? [];
+ const loadedApiOptions: { handler: string; name: string }[] =
+ (projects.length > 0 && project) ?
+ apis
+ .filter(
+ (
+ api
+ ): api is NonNullable & { id: string; displayName?: string } =>
+ typeof api?.id === 'string' && api.id.length > 0
+ )
+ .map((api) => ({ handler: api.id, name: api.displayName ?? api.id }))
+ : [];
+
+ // apis may not be loaded yet on first paint; keep the current API selectable
+ // so the switcher never renders an out-of-range value.
+ const apiOptions: { handler: string; name: string }[] =
+ loadedApiOptions.length > 0
+ ? loadedApiOptions
+ : params.apiHandler
+ ? [{ handler: params.apiHandler, name: component?.displayName || params.apiHandler }]
+ : [];
+
+ if (!params.orgHandle) return null;
+
+ return (
+
+ item.handle === params.orgHandle).map(item => ({
+ id: item.handle,
+ handler: item.handle,
+ name: item.name,
+ }))[0] || undefined}
+ onChange={(id) => {
+ changeOrganization(id);
+ }}
+ options={orgOptions.map((item) => ({
+ id: item.handle,
+ handler: item.handle,
+ name: item.name,
+ }))}
+ renderOptionContent={(option) => (
+ <>
+
+
+
+
+ >
+ )}
+ searchPlaceholder={intl.formatMessage({ id: 'appShell.header.org.placeholder', defaultMessage: 'Search organizations...' })}
+ emptyMessage={intl.formatMessage({ id: 'appShell.header.org.empty', defaultMessage: 'No organizations found' })}
+ noResultsMessage={intl.formatMessage({ id: 'appShell.header.org.noResults', defaultMessage: 'No matching organizations' })}
+ sx={SWITCHER_SELECT_SX}
+ />
+
+ {params.projectHandler && (
+
+ item.handler === params.projectHandler).map(item => ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))[0] || undefined}
+ onChange={(id) => {
+ changeProject(id);
+ }}
+ options={projectOptions.map((item) => ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))}
+ renderOptionContent={(option) => (
+ <>
+
+
+
+
+ >
+ )}
+ searchPlaceholder={intl.formatMessage({ id: 'appShell.header.project.placeholder', defaultMessage: 'Search projects...' })}
+ emptyMessage={intl.formatMessage({ id: 'appShell.header.project.empty', defaultMessage: 'No projects found' })}
+ noResultsMessage={intl.formatMessage({ id: 'appShell.header.project.noResults', defaultMessage: 'No matching projects' })}
+ sx={SWITCHER_SELECT_SX}
+ />
+
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ onClick={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ clearProjectSelection();
+ }}
+ sx={{
+ position: "absolute",
+ top: 6,
+ right: 2,
+ zIndex: 1,
+ width: 20,
+ height: 10,
+ }}
+ >
+
+
+
+ )}
+
+ {!params.projectHandler && (
+ ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))}
+ onSelectProject={(projectHandler) => {
+ changeProject(projectHandler);
+ }}
+ />
+ )}
+
+ {params.apiHandler && (
+
+ item.handler === params.apiHandler).map(item => ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))[0] || undefined}
+ onChange={(id) => {
+ changeApi(id);
+ }}
+ options={apiOptions.map((item) => ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))}
+ renderOptionContent={(option) => (
+ <>
+
+
+
+
+ >
+ )}
+ searchPlaceholder={intl.formatMessage({ id: 'appShell.header.api.placeholder', defaultMessage: 'Search APIs...' })}
+ emptyMessage={intl.formatMessage({ id: 'appShell.header.api.empty', defaultMessage: 'No APIs found' })}
+ noResultsMessage={intl.formatMessage({ id: 'appShell.header.api.noResults', defaultMessage: 'No matching APIs' })}
+ sx={SWITCHER_SELECT_SX}
+ />
+
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ onClick={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ clearApiSelection();
+ }}
+ sx={{
+ position: "absolute",
+ top: 6,
+ right: 2,
+ zIndex: 1,
+ width: 20,
+ height: 10,
+ }}
+ >
+
+
+
+ )}
+
+ {!params.apiHandler && params.projectHandler && (
+ ({
+ id: item.handler,
+ handler: item.handler,
+ name: item.name,
+ }))}
+ onSelectApi={(apiHandler) => {
+ changeApi(apiHandler);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/components/SaveBar.tsx b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/components/SaveBar.tsx
index c64f058adf..fe689b9915 100644
--- a/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/components/SaveBar.tsx
+++ b/portals/api-control-plane/src/pages/appShell/appShellPages/apis/develop/components/SaveBar.tsx
@@ -18,7 +18,7 @@
import { Box, Button } from '@wso2/oxygen-ui';
-import { useFooterHeight } from '../../../../useFooterHeight';
+import { useFooterHeight } from '../../../../../../hooks/useFooterHeight';
import { stickyBottomBarSx } from '../../../../../../theme';
/** Layout for the save bar's own content; the sticky treatment is shared. */
diff --git a/portals/api-control-plane/src/utils/errors/errorClassification.ts b/portals/api-control-plane/src/utils/errors/errorClassification.ts
new file mode 100644
index 0000000000..b88d7c6dd4
--- /dev/null
+++ b/portals/api-control-plane/src/utils/errors/errorClassification.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Substrings browsers use when a dynamic `import()` cannot be fetched. Matched
+ * case-insensitively because the wording differs per engine: Chrome says
+ * "Failed to fetch dynamically imported module", Firefox "error loading
+ * dynamically imported module", Safari "Importing a module script failed".
+ */
+const CHUNK_LOAD_MESSAGES = [
+ 'failed to fetch dynamically imported module',
+ 'error loading dynamically imported module',
+ 'importing a module script failed',
+ 'unable to preload css',
+ 'loading chunk',
+ 'loading css chunk',
+];
+
+/**
+ * Whether a caught error is a failed code-split chunk fetch rather than a fault
+ * in the page's own logic.
+ *
+ * Worth separating because the two need opposite recovery actions. Every page
+ * in `AppRoutes` is `lazy()`, so after a deploy an already-open tab asks for a
+ * chunk hash the server no longer has: the code is fine, the *bundle* the tab
+ * is running is stale. Reloading fetches the new index and fixes it, whereas
+ * for a genuine render fault reloading the same URL just reproduces it — which
+ * is why the generic fallback offers "try again"/"go home" instead.
+ */
+export const isChunkLoadError = (error: unknown): boolean => {
+ if (!error) return false;
+
+ const named = (error as { name?: unknown }).name;
+ if (named === 'ChunkLoadError') return true;
+
+ const message = (error as { message?: unknown }).message;
+ if (typeof message !== 'string') return false;
+
+ const normalized = message.toLowerCase();
+ return CHUNK_LOAD_MESSAGES.some((needle) => normalized.includes(needle));
+};