Initial commit: plan, spec, tickets and dashboard skill templates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
49f8f49b65
39 files changed
+10736
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import { ThemeSwitcher } from '../theme'
|
||||
import { ROLE_LABEL } from '../auth'
|
||||
|
||||
/**
|
||||
* The app shell's top bar: brand mark, title, and the signed-in user's
|
||||
* controls. The board tab strip that used to live below it is now the left
|
||||
* sidebar (`Sidebar.tsx`) — this component no longer owns navigation.
|
||||
*
|
||||
* The gradient is the one saturated surface in the product. Everything on it
|
||||
* is white or a white alpha rather than a palette token, because a
|
||||
* token-coloured control on the band reads as a hole punched through it —
|
||||
* see `.sb-appbar` and `.sb-ghost-btn` in index.css.
|
||||
*/
|
||||
/** 'admin' is never a single global role any more — computed here from the
|
||||
* two per-project flags so the header names exactly what this account can
|
||||
* reach, rather than falling back to "Operator" for someone who is in fact
|
||||
* an admin of one or both projects. */
|
||||
function roleLabel(user: {
|
||||
role: string
|
||||
ecw_role?: string | null
|
||||
tebra_role?: string | null
|
||||
}): string {
|
||||
if (user.role === 'super_admin') return ROLE_LABEL.super_admin
|
||||
const ecw = user.ecw_role === 'admin'
|
||||
const tebra = user.tebra_role === 'admin'
|
||||
if (ecw && tebra) return 'Admin · Both'
|
||||
if (ecw) return 'Admin · ECW'
|
||||
if (tebra) return 'Admin · Tebra'
|
||||
return ROLE_LABEL.operator
|
||||
}
|
||||
|
||||
export function BrandHeader({
|
||||
user,
|
||||
onLogout,
|
||||
sidebarOpen,
|
||||
onToggleSidebar,
|
||||
}: {
|
||||
user: {
|
||||
username: string
|
||||
real_name: string | null
|
||||
role: string
|
||||
ecw_role?: string | null
|
||||
tebra_role?: string | null
|
||||
}
|
||||
onLogout: () => void
|
||||
/** Present only on screens that have the left sidebar. */
|
||||
sidebarOpen?: boolean
|
||||
onToggleSidebar?: () => void
|
||||
}) {
|
||||
return (
|
||||
<header className="sb-appbar shrink-0">
|
||||
<ShieldMark />
|
||||
{onToggleSidebar && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSidebar}
|
||||
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
||||
aria-label={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
||||
aria-expanded={sidebarOpen}
|
||||
className="sb-ghost-btn"
|
||||
style={{ padding: '3px', lineHeight: 0 }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.6" aria-hidden="true">
|
||||
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
||||
<line x1="9" y1="4.5" x2="9" y2="19.5" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{/* What the tool is, matching the browser tab and the sign-in screen.
|
||||
The brand and EMR edition — "Superbill Automation for ECW" — live in
|
||||
the sidebar's own group headers instead (`Sidebar.tsx`), which is
|
||||
where the rail has room to state them without competing here. */}
|
||||
<div className="min-w-0">
|
||||
<div className="sb-appbar-title truncate">Extraction and Automation Tool</div>
|
||||
<div className="sb-appbar-sub truncate">Extract · correct · post to eClinicalWorks</div>
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-3">
|
||||
<div className="sb-who hidden sm:block">
|
||||
<b>{user.real_name ?? user.username}</b>
|
||||
<span>{roleLabel(user)}</span>
|
||||
</div>
|
||||
<button type="button" onClick={onLogout} title="Sign out" className="sb-ghost-btn">
|
||||
Sign out
|
||||
</button>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The product mark: shield plus three ascending bars, drawn as vector.
|
||||
*
|
||||
* The same artwork the sign-in screen carries, so the two screens read as one
|
||||
* product — but flat here, with no bob and no glow: on the sign-in screen it
|
||||
* is the subject, and in a header it is a label for the page.
|
||||
*
|
||||
* Vector rather than the `logo-mark.png` raster it replaced. The raster could
|
||||
* not take the header's drop-shadow without fringing along its antialiased
|
||||
* edge, and it could not follow a theme.
|
||||
*
|
||||
* Colours are literal on purpose. The shield and its bars are artwork — the
|
||||
* mark is the same mark in light and dark, the way a printed logo is — so
|
||||
* these four values are deliberately NOT theme tokens. `--shield`, `--clean`,
|
||||
* `--verify` and `--review` hold the same hexes for anything that needs to
|
||||
* reference them.
|
||||
*
|
||||
* `aria-hidden` because the product is named in text immediately beside it.
|
||||
*/
|
||||
export function ShieldMark({ size = 34 }: { size?: number }) {
|
||||
return (
|
||||
<span className="sb-appbar-mark" style={{ width: size, height: size }}>
|
||||
<svg viewBox="0 0 64 64" fill="none" aria-hidden="true" style={{ width: size, height: size }}>
|
||||
<path
|
||||
d="M32 5 L53 13 V34 C53 45.5 43.5 53.5 32 58.5 C20.5 53.5 11 45.5 11 34 V13 Z"
|
||||
fill="#21395a"
|
||||
/>
|
||||
<rect x="19.5" y="32" width="7" height="10" rx="1.6" fill="#51a68f" />
|
||||
<rect x="30" y="24" width="7" height="18" rx="1.6" fill="#e39b34" />
|
||||
<rect x="40.5" y="18" width="7" height="24" rx="1.6" fill="#de5243" />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user