ui(step-5): update roles/runtime-roles pages to reference layout

- roles/create: navy submit button, proper form inputs, data-table
- roles/templates: table-card, navy Create button
- runtime-roles/index: fix oversized heading, data-table, navy colors
- runtime-roles/new: white header shell, proper form styling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-24 05:26:39 +01:00
parent 3ffed6c813
commit 3b98609cb5
4 changed files with 249 additions and 234 deletions

View file

@ -93,14 +93,13 @@ export default function CreateInternalRolePage() {
return ( return (
<AdminShell> <AdminShell>
<div class="mb-6 flex items-start justify-between gap-4"> <div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div> <div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-2xl font-bold text-gray-900">Create Internal Role</h1> <h1 class="text-xl font-semibold text-gray-900">Create Internal Role</h1>
<p class="mt-1 text-sm text-gray-500">Create a new internal role and choose what it can access.</p> <p class="text-sm text-gray-500 mt-0.5">Create a new internal role and choose what it can access.</p>
</div>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles">Back to Roles</A>
</div> </div>
<div class="flex-1 p-6">
<nav class="hidden" aria-label="Role Management Navigation"> <nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A> <A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A> <A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
@ -117,16 +116,18 @@ export default function CreateInternalRolePage() {
<p>Start by giving this role a clear name.</p> <p>Start by giving this role a clear name.</p>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field"> <div class="field">
<label>Role Name <span style="color:#ef4444">*</span></label> <label class="mb-1.5 block text-sm font-medium text-gray-700">Role Name <span class="text-red-500">*</span></label>
<input <input
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]"
value={roleName()} value={roleName()}
onInput={(e) => setRoleName(e.currentTarget.value)} onInput={(e) => setRoleName(e.currentTarget.value)}
placeholder="e.g. Customer Support Rep" placeholder="e.g. Customer Support Rep"
/> />
</div> </div>
<div class="field"> <div class="field">
<label>Description</label> <label class="mb-1.5 block text-sm font-medium text-gray-700">Description</label>
<input <input
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]"
value={description()} value={description()}
onInput={(e) => setDescription(e.currentTarget.value)} onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Short description of this role" placeholder="Short description of this role"
@ -150,7 +151,7 @@ export default function CreateInternalRolePage() {
class={`flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300 ${assignedModules().includes(mod) ? 'selected' : ''}`} class={`flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300 ${assignedModules().includes(mod) ? 'selected' : ''}`}
onClick={() => toggleModule(mod)} onClick={() => toggleModule(mod)}
> >
<span style={`width:14px;height:14px;border-radius:3px;border:2px solid ${assignedModules().includes(mod) ? '#c2410c' : '#cbd5e1'};background:${assignedModules().includes(mod) ? '#c2410c' : '#fff'};flex-shrink:0;display:inline-block`} /> <span class={`inline-block w-3.5 h-3.5 rounded-[3px] border-2 flex-shrink-0 ${assignedModules().includes(mod) ? 'border-[#0a1d37] bg-[#0a1d37]' : 'border-slate-300 bg-white'}`} />
{mod} {mod}
</button> </button>
))} ))}
@ -166,8 +167,8 @@ export default function CreateInternalRolePage() {
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> <div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Permissions</h3> <h3>Permissions</h3>
<p>Choose what this role can do in each selected area.</p> <p>Choose what this role can do in each selected area.</p>
<div class="overflow-x-auto"> <div class="table-card overflow-x-auto">
<table class="w-full text-sm"> <table data-table class="w-full text-sm">
<thead> <thead>
<tr> <tr>
<th style="width:45%">Area</th> <th style="width:45%">Area</th>
@ -189,13 +190,13 @@ export default function CreateInternalRolePage() {
const hasDelete = !!actionMap['Delete'] && permissionIds().includes(actionMap['Delete']); const hasDelete = !!actionMap['Delete'] && permissionIds().includes(actionMap['Delete']);
const noAccess = !hasRead && !hasCreate && !hasUpdate && !hasDelete; const noAccess = !hasRead && !hasCreate && !hasUpdate && !hasDelete;
return ( return (
<tr> <tr class="hover:bg-slate-50">
<td style="font-weight:500">{mod}</td> <td class="font-semibold text-slate-900">{mod}</td>
<td><input type="checkbox" checked={noAccess} disabled aria-label={`${mod} no access`} /></td> <td><input type="checkbox" checked={noAccess} disabled aria-label={`${mod} no access`} /></td>
<td>{actionMap['Read'] ? <input type="checkbox" checked={hasRead} onChange={() => togglePermission(actionMap['Read'])} aria-label={`${mod} read`} /> : <span style="color:#cbd5e1"></span>}</td> <td>{actionMap['Read'] ? <input type="checkbox" checked={hasRead} onChange={() => togglePermission(actionMap['Read'])} aria-label={`${mod} read`} /> : <span class="text-slate-300"></span>}</td>
<td>{actionMap['Create'] ? <input type="checkbox" checked={hasCreate} onChange={() => togglePermission(actionMap['Create'])} aria-label={`${mod} create`} /> : <span style="color:#cbd5e1"></span>}</td> <td>{actionMap['Create'] ? <input type="checkbox" checked={hasCreate} onChange={() => togglePermission(actionMap['Create'])} aria-label={`${mod} create`} /> : <span class="text-slate-300"></span>}</td>
<td>{actionMap['Update'] ? <input type="checkbox" checked={hasUpdate} onChange={() => togglePermission(actionMap['Update'])} aria-label={`${mod} update`} /> : <span style="color:#cbd5e1"></span>}</td> <td>{actionMap['Update'] ? <input type="checkbox" checked={hasUpdate} onChange={() => togglePermission(actionMap['Update'])} aria-label={`${mod} update`} /> : <span class="text-slate-300"></span>}</td>
<td>{actionMap['Delete'] ? <input type="checkbox" checked={hasDelete} onChange={() => togglePermission(actionMap['Delete'])} aria-label={`${mod} delete`} /> : <span style="color:#cbd5e1"></span>}</td> <td>{actionMap['Delete'] ? <input type="checkbox" checked={hasDelete} onChange={() => togglePermission(actionMap['Delete'])} aria-label={`${mod} delete`} /> : <span class="text-slate-300"></span>}</td>
</tr> </tr>
); );
})} })}
@ -206,15 +207,18 @@ export default function CreateInternalRolePage() {
</Show> </Show>
{/* Save */} {/* Save */}
<div style="display:flex;justify-content:flex-end;margin-top:8px"> <div class="flex justify-end gap-3 mt-2">
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles">Cancel</A>
<button <button
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" class="rounded-lg bg-[#0a1d37] px-6 py-2 text-sm font-medium text-white hover:bg-[#0f2a4e] transition-colors disabled:opacity-60"
onClick={handleSave} onClick={handleSave}
disabled={saving() || !roleName().trim()} disabled={saving() || !roleName().trim()}
> >
{saving() ? 'Creating...' : 'Create Role'} {saving() ? 'Creating...' : 'Create Role'}
</button> </button>
</div> </div>
</div>
</div>
</AdminShell> </AdminShell>
); );
} }

View file

@ -36,21 +36,25 @@ export default function RoleTemplatesPage() {
return ( return (
<AdminShell> <AdminShell>
<div class="mb-6 flex items-start justify-between gap-4"> <div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4">
<div class="flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold text-gray-900">Role Templates</h1> <h1 class="text-xl font-semibold text-gray-900">Role Templates</h1>
<p class="mt-1 text-sm text-gray-500">Starter role presets for faster internal role creation and cloning.</p> <p class="text-sm text-gray-500 mt-0.5">Starter role presets for faster internal role creation and cloning.</p>
</div>
<A class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" href="/admin/roles/create">Create Internal Role</A>
</div> </div>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/roles/create">Create Internal Role</A>
</div> </div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden"> <div class="flex-1 p-6">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid #e2e8f0"> <div class="table-card">
<h2 style="margin:0;font-size:16px">Available Templates</h2> <div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
<span style="font-size:12px;color:#64748b">{count()} template{count() !== 1 ? 's' : ''}</span> <h2 class="text-sm font-semibold text-gray-900">Available Templates</h2>
<span class="text-xs text-slate-500">{count()} template{count() !== 1 ? 's' : ''}</span>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-sm"> <table data-table class="w-full text-sm">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -61,18 +65,18 @@ export default function RoleTemplatesPage() {
</thead> </thead>
<tbody> <tbody>
<Show when={templates.loading}> <Show when={templates.loading}>
<tr><td colspan="4" style="text-align:center;padding:32px;color:#64748b">Loading templates...</td></tr> <tr><td colspan="4" class="text-center px-8 py-8 text-slate-500">Loading templates...</td></tr>
</Show> </Show>
<Show when={!templates.loading && count() === 0}> <Show when={!templates.loading && count() === 0}>
<tr><td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">No templates available yet.</td></tr> <tr><td colspan="4" class="text-center px-8 py-8 text-slate-400">No templates available yet.</td></tr>
</Show> </Show>
<Show when={!templates.loading && count() > 0}> <Show when={!templates.loading && count() > 0}>
<For each={templates()}> <For each={templates()}>
{(item) => ( {(item) => (
<tr> <tr class="hover:bg-slate-50">
<td style="font-weight:600;color:#0f172a">{item.name}</td> <td class="font-semibold text-slate-900">{item.name}</td>
<td style="color:#475569">{item.description || '—'}</td> <td class="text-slate-500">{item.description || '—'}</td>
<td style="color:#64748b;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">{item.code || '—'}</td> <td class="text-slate-500 font-mono">{item.code || '—'}</td>
<td> <td>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/roles/${item.id}`}>View</A> <A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/roles/${item.id}`}>View</A>
@ -86,7 +90,9 @@ export default function RoleTemplatesPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </div>
</div>
</div>
</AdminShell> </AdminShell>
); );
} }

View file

@ -76,49 +76,51 @@ export default function RuntimeRolesPage() {
return ( return (
<AdminShell> <AdminShell>
<div class="mb-8"> <div class="flex flex-col -mx-6 -mt-6 min-h-full">
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1> <div class="bg-white border-b border-gray-200 px-6 py-4">
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain external system roles and access privileges.</p> <h1 class="text-xl font-semibold text-gray-900">External Role Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Configure and maintain external system roles and access privileges.</p>
</div> </div>
<section class="overflow-hidden rounded-[22px] border border-[#d8dbe5] bg-white shadow-[0_14px_28px_-20px_rgba(15,23,42,0.35)]"> <div class="flex-1 p-6">
<div class="table-card">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-[860px] text-sm"> <table data-table class="w-full min-w-[860px] text-sm">
<thead> <thead>
<tr class="bg-[#071b3d] text-white"> <tr>
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th> <th>ID</th>
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</th> <th>Name</th>
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Issue Type</th> <th>Issue Type</th>
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Edit</th> <th class="text-center">Edit</th>
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Delete</th> <th class="text-center">Delete</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Show when={roles.loading}> <Show when={roles.loading}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading external roles...</td></tr> <tr><td colspan="5" class="text-center px-8 py-8 text-slate-500">Loading external roles...</td></tr>
</Show> </Show>
<Show when={!roles.loading && roles.error}> <Show when={!roles.loading && roles.error}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load external roles. Is the backend running?</td></tr> <tr><td colspan="5" class="text-center px-8 py-8 text-red-600">Failed to load external roles. Is the backend running?</td></tr>
</Show> </Show>
<Show when={!roles.loading && !roles.error && roles()?.length === 0}> <Show when={!roles.loading && !roles.error && roles()?.length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No external roles configured yet.</td></tr> <tr><td colspan="5" class="text-center px-8 py-8 text-slate-400">No external roles configured yet.</td></tr>
</Show> </Show>
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}> <Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
{roles()!.map((role) => ( {roles()!.map((role) => (
<tr class={`border-b border-[#e4e7ef] text-[17px] ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-[#fff7f2]' : ''}`}> <tr class={`hover:bg-slate-50 ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-slate-50' : ''}`}>
<td class="px-8 py-7 font-medium text-[#364152]">{role.roleKey || role.id?.slice(0, 6).toUpperCase()}</td> <td class="text-slate-500">{role.roleKey || role.id?.slice(0, 6).toUpperCase()}</td>
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.displayName}</td> <td class="font-semibold text-slate-900">{role.displayName}</td>
<td class="px-8 py-7"> <td class="text-slate-500">
<A class="inline-flex items-center gap-2 font-semibold text-[#fd6216] hover:text-orange-700" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer"> <A class="inline-flex items-center gap-1 font-medium text-[#0a1d37] hover:text-[#0f2a4e]" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer">
<span>View</span> <span>View</span>
<span class="text-[18px]"></span> <span></span>
</A> </A>
</td> </td>
<td class="px-8 py-7 text-center"> <td class="text-center">
<A class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#071b3d] hover:bg-slate-100" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role"></A> <A class="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-600 hover:bg-slate-100" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role"></A>
</td> </td>
<td class="px-8 py-7 text-center"> <td class="text-center">
<button class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#c81e1e] hover:bg-red-50" title="Delete External Role" aria-label={`Delete ${role.displayName}`}>🗑</button> <button class="inline-flex h-8 w-8 items-center justify-center rounded-md text-red-600 hover:bg-red-50" title="Delete External Role" aria-label={`Delete ${role.displayName}`}>🗑</button>
</td> </td>
</tr> </tr>
))} ))}
@ -126,17 +128,19 @@ export default function RuntimeRolesPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="flex items-center justify-between border-t border-[#e4e7ef] px-8 py-5"> <div class="flex items-center justify-between border-t border-gray-200 px-6 py-4">
<p class="text-[14px] font-semibold uppercase tracking-[0.1em] text-[#485163]">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p> <p class="text-sm text-slate-500">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'<'}</button> <button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-700 hover:bg-gray-50">{'<'}</button>
<button class="h-11 min-w-11 rounded-xl bg-[#fd6216] px-3 font-bold text-white">1</button> <button class="h-9 min-w-9 rounded-lg bg-[#0a1d37] px-3 text-sm font-medium text-white">1</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">2</button> <button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50">2</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">3</button> <button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50">3</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'>'}</button> <button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-700 hover:bg-gray-50">{'>'}</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</section>
</AdminShell> </AdminShell>
); );
} }

View file

@ -84,16 +84,15 @@ export default function CreateExternalRolePage() {
return ( return (
<AdminShell> <AdminShell>
<div class="mb-6 flex items-start justify-between gap-4"> <div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div> <div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-2xl font-bold text-gray-900">Create External Role</h1> <h1 class="text-xl font-semibold text-gray-900">Create External Role</h1>
<p class="mt-1 text-sm text-gray-500"> <p class="text-sm text-gray-500 mt-0.5">
Create a new external role and choose what it can access in the app. Create a new external role and choose what it can access in the app.
</p> </p>
</div> </div>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/runtime-roles">Back to External Roles</A>
</div>
<div class="flex-1 p-6">
<ExternalRoleTabs /> <ExternalRoleTabs />
<Show when={error()}> <Show when={error()}>
@ -107,6 +106,8 @@ export default function CreateExternalRolePage() {
submitLabel="Create External Role" submitLabel="Create External Role"
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />
</div>
</div>
</AdminShell> </AdminShell>
); );
} }