Admin Portal Portlets
Overview
This skill enables AI agents to generate complete, production-grade HTML portlet pages for the Eclipse Admin Portal. A portlet is a single self-contained HTML file that is registered with the portal and appears as a native section in the navigation. Eclipse handles authentication automatically — when a portal user navigates to a portlet, Eclipse injects a signed JWT via a ?token= URL parameter.
It supports:
- List portlets — searchable, paginated tables with stat cards
- Detail portlets — profile header, tab nav, property rows
- Form portlets — split layout with live preview and confirmation modal
- Dashboard portlets — stat cards, activity tables, quick actions
Every portlet is built as a single file with zero backend and zero build step — just open in a browser or register with the portal.
Design Reference: Portlets follow a fixed design system. Do not substitute colours, fonts, or component patterns. The output should match the quality of tools like Lovable.dev.
Trigger
Trigger phrases: "build a portlet", "create a portlet", "write a portlet", "generate an Eclipse portlet", "make a portlet for", "new portlet", or any description of a feature that needs an Eclipse Admin Portal page.
Required inputs: the portlet name, the primary data domain (wallets, payments, cards, customers, etc.), and the key feature (list, detail view, form, dashboard). If tenantId is provided, embed it; otherwise use a TENANT_ID constant.
1. Design System
CDN Imports (always exactly these three)
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>Colour Palette
| Token | Value | Usage |
|---|---|---|
--navy | #050C43 | Header, icon fills, navy buttons |
--blue | #007AFF | Primary buttons, links, active tab, focus rings |
--bg | #F0F3FB | Page background, alternating table rows |
--surface | #FFFFFF | Cards, modals, inputs |
--border | #deebfd | Card borders |
--text | #0f172a | Body text |
--muted | #64748b | Labels, secondary text |
--success | #00984E | Active badges, success alerts |
--error | #F15A22 | Error badges, destructive actions |
--warn | #FF9600 | Warning states |
Primary accent is always --blue (#007AFF). Never substitute purple, teal, or other accent colours.
Tailwind Config (include verbatim)
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Inter','system-ui','-apple-system','sans-serif'] },
colors: {
navy: { DEFAULT:'#050C43', hover:'#070f52' },
accent: { DEFAULT:'#007AFF', 300:'#4DA2FF', 600:'#0072FF', 900:'#004AFF' },
'bg-page': '#F0F3FB',
'card-border':'#deebfd',
},
boxShadow: {
card: '-8px 12px 18px 0 #dadee8',
nav: '5px 7px 26px -5px #cdd4e7',
},
borderRadius: { card:'12px', btn:'6px', pill:'9999px' }
}
}
}2. HTML Structure
Every portlet follows this top-level layout:
<!-- PAGE HEADER -->
<div class="page-header" id="page-header">
<div class="ph-icon"><i data-lucide="ICON_NAME"></i></div>
<span class="ph-title">Portlet Title</span>
<div class="breadcrumb" style="margin-left:4px">
<span class="bc-sep">/</span><span>Section</span>
<span class="bc-sep">/</span><span class="bc-current">Portlet Title</span>
</div>
<div class="ph-spacer"></div>
<div class="ph-actions">
<button class="btn btn-ghost btn-sm" onclick="openJwtModal()" title="Auth">
<i data-lucide="key-round"></i>
</button>
<!-- portlet-specific action buttons -->
</div>
</div>
<!-- APP BODY -->
<div class="app-body" id="app-body">
<div id="app-banner" class="alert alert-info" style="display:none">
<i data-lucide="info" id="app-banner-icon-el"></i>
<span id="app-banner-text"></span>
<button class="btn btn-ghost btn-sm" onclick="hideBanner()" style="margin-left:auto;min-width:unset;padding:0 4px">
<i data-lucide="x"></i>
</button>
</div>
<!-- portlet content -->
</div>
<!-- JWT MODAL — copy verbatim, do not modify -->The page header always includes the key-round auth button. The #app-banner status banner is always present. The JWT modal is always included in full.
3. Authentication
Eclipse injects authentication via a ?token= URL parameter when a portal user opens the portlet. The auth infrastructure is fixed — copy it verbatim into every portlet.
How it works
- On load, the init IIFE reads
?token=from the URL - If present,
_exchangeToken()calls the Eclipse login endpoint to exchange it for a full JWT and stores it inlocalStorage - If no
?token=param, the stored JWT is used; if none exists, the JWT modal opens for manual entry - All API calls go through
apiFetch(), which waits for the JWT to be ready before sending
Key auth functions
// All API calls — use this exclusively, never raw fetch
async function apiFetch(path, options = {}) { /* injects Authorization header */ }
// JWT decode utility
function decodeJWT(token) { /* returns payload object */ }
// Token exchange — called automatically from init() when ?token= is present
async function _exchangeToken(urlToken) { /* exchanges portal token for JWT */ }Startup pattern
async function onReady() {
const tenantId = resolveTenantId();
try {
showSkeleton();
const data = await apiFetch(`/tenants/${tenantId}/YOUR_RESOURCE`);
renderData(data);
} catch(err) {
showBanner('error', err.message);
showEmpty();
}
}
(function init() {
const params = new URLSearchParams(window.location.search);
const urlToken = params.get('token');
if (urlToken) {
_exchangeToken(urlToken);
} else {
const token = loadToken();
if (token) { const p = decodeJWT(token); if (p) _scheduleRenewal(token, p); _signalJwtReady(); }
else { openJwtModal(); }
}
onReady();
})();
lucide.createIcons(); // always last line4. tenantId Resolution
Eclipse API paths are always tenant-scoped. Resolve tenantId in this priority order:
function resolveTenantId() {
// 1. Path segment: /portlets/{tenantId}/...
const seg = window.location.pathname.split('/').filter(Boolean);
const idx = seg.indexOf('portlets');
if (idx >= 0 && seg[idx+1] && /^\d+$/.test(seg[idx+1])) return seg[idx+1];
// 2. Query param
const qp = new URLSearchParams(window.location.search).get('tenantId');
if (qp) return qp;
// 3. JWT claim
const p = decodeJWT(loadToken());
if (p?.tenant) return String(p.tenant);
// 4. Fallback constant
return TENANT_ID;
}Common tenant-scoped API paths:
/tenants/{tenantId}/customers
/tenants/{tenantId}/wallets
/tenants/{tenantId}/payments
/tenants/{tenantId}/cards
/tenants/{tenantId}/wallet-types
/tenants/{tenantId}/config-items
All paths are relative — apiFetch() prepends the base URL automatically.
5. Component Patterns
List Portlet
- 4 stat cards across the top (
currentBalance, counts, etc.) - Card with search input (pill shape) and filter chips in the header
- Sortable table with status badges, mono-font IDs, and row action buttons
- Skeleton loading rows while fetching, empty state when result set is empty
- Pagination controls for lists exceeding 25 rows
Detail Portlet
- Profile header card with navy gradient, avatar icon, name, and status badge
- Tab nav (
Overview,Transactions,History, etc.) - Property rows for key fields (
prop-key/prop-valpattern) - Action buttons (Edit, Suspend, Back to list)
Form Portlet
- Split layout: form fields left, live summary panel right
- Progressive disclosure — show/hide sections based on selections
- Inline validation feedback below each field
- Confirmation modal before any destructive or financial operation
Dashboard Portlet
- 4-column stat cards with trend indicators (up/down arrows, colour-coded)
- Recent activity table (last 10–20 rows, no pagination needed)
- Quick-action buttons for the most common operations
6. UX Requirements
Every portlet must include all of the following — no exceptions:
Loading states: show .skeleton-row elements with .skeleton divs while fetching. Hide and replace with real content on arrival.
Error handling: wrap every apiFetch() call in try/catch and call showBanner('error', err.message) on failure. Surface the Eclipse traceId from the error body when present.
Empty state: show an .empty-state block (icon, title, subtitle) when the API returns an empty list.
Button loading: disable the button and show a spinner while an async action is in progress. Re-enable and restore the label when it completes.
Status badges: use the fixed badge classes — badge-active (green), badge-pending (amber), badge-error (red), badge-inactive (grey), badge-info (blue). Never invent new badge colours.
Common Patterns for AI Agents
Build a portlet from a description
Build an Eclipse portlet for [domain]. It should [key feature — list / detail / form / dashboard].
Primary entity: [wallet / customer / payment / card / etc.].
tenantId: [value or leave blank for TENANT_ID constant].
The AI generates a single complete .html file ready to register with the portal.
Add a feature to an existing portlet
Share the current .html file and describe the addition:
Add [feature] to this portlet. Keep the existing design system and auth block unchanged.
The auth block and design tokens must never be modified during incremental updates.
Generate a portlet for a specific API endpoint
Build an Eclipse portlet that calls [endpoint] and displays [fields].
Use the portlet skill design system and authentication pattern.
Use the OpenAPI Spec skill alongside this skill to get exact field names from the spec before generating.
Notes
- The auth JavaScript block and CSS design tokens are fixed infrastructure — never condense, alter, or remove any part of them
lucide.createIcons()must always be the last line in<script>- All portlet output must be a single
.htmlfile with no external dependencies beyond the three CDNs - The JWT modal (
#jwt-modal) must always be present in full — it is the fallback auth path for development and direct access - Token renewal is handled automatically by
_scheduleRenewal()— do not add manual renewal logic - Never store credentials in the portlet HTML — authentication flows entirely through the JWT
Best Practices
- Pick the most semantically appropriate Lucide icon for the page header — browse at lucide.dev
- Use skeleton rows that match the shape of the real content (same column count and approximate widths)
- Keep action buttons in the page header for global actions; put row-level actions in the table as ghost buttons
- For financial amounts, always display with the currency code and correct decimal places — never format as bare integers
- Confirm before any destructive or financial operation with a modal, not a browser
confirm()dialog - When calling list endpoints, always paginate with
limit=25and an offset-based page control
Updated 1 day ago
