- rpa-dashboard-ai/SKILL.md: trigger description and the shared question flow; points at the no-AI frontend.md, themes and palette instead of copying them. - templates/contract.md: runs, review rows, endpoints and the server approve gate. - Superbill extracts: patient names and the client name removed, every file carries an EXAMPLE ONLY banner. tests/test_reference_scan.py checks both skills for patient data, internal IPs, credentials and keys. - tests/fixtures/d-ai-extraction and its eval entry, which runs page_check_ai on the built folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
|
|
/**
|
|
* A proportional ring.
|
|
*
|
|
* Earns its place only because the quantity it draws is genuinely a share of a
|
|
* whole — the split of every graded field across the confidence bands. A ring
|
|
* standing in for a single number would be decoration; this one is the only
|
|
* shape on the board that shows three parts against their total at once.
|
|
*
|
|
* It is `aria-hidden`, and that is deliberate rather than lazy: the legend
|
|
* beside it states every segment's name and count as text, so a screen reader
|
|
* gets the data in full and is not read a list of arc lengths. The chart is the
|
|
* fast path for someone looking; the legend is the actual record.
|
|
*/
|
|
|
|
export interface Slice {
|
|
key: string
|
|
label: string
|
|
value: number
|
|
color: string
|
|
}
|
|
|
|
export function Donut({
|
|
slices,
|
|
centerValue,
|
|
centerLabel,
|
|
size = 150,
|
|
thickness = 20,
|
|
/** Arc-length notch between segments, so two adjacent colours stay countable. */
|
|
gap = 3,
|
|
}: {
|
|
slices: Slice[]
|
|
centerValue: string
|
|
centerLabel: string
|
|
size?: number
|
|
thickness?: number
|
|
gap?: number
|
|
}) {
|
|
const r = (size - thickness) / 2
|
|
const c = 2 * Math.PI * r
|
|
const total = slices.reduce((n, s) => n + s.value, 0)
|
|
|
|
// Running start angle, in arc length. Accumulated rather than derived per
|
|
// index so a zero-value slice consumes nothing at all instead of leaving a
|
|
// gap where no data is.
|
|
let at = 0
|
|
const drawn = slices
|
|
.filter((s) => s.value > 0)
|
|
.map((s) => {
|
|
const len = (s.value / total) * c
|
|
const seg = { ...s, len: Math.max(len - gap, 0.5), offset: at }
|
|
at += len
|
|
return seg
|
|
})
|
|
|
|
return (
|
|
<div className="relative shrink-0" style={{ width: size, height: size }}>
|
|
<svg width={size} height={size} aria-hidden="true" className="-rotate-90">
|
|
{/* The track carries the empty case on its own, so a board with nothing
|
|
graded yet still draws a ring rather than blank space. */}
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={r}
|
|
fill="none"
|
|
strokeWidth={thickness}
|
|
stroke="var(--surface-2)"
|
|
/>
|
|
{drawn.map((s) => (
|
|
<circle
|
|
key={s.key}
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={r}
|
|
fill="none"
|
|
stroke={s.color}
|
|
strokeWidth={thickness}
|
|
strokeDasharray={`${s.len} ${c - s.len}`}
|
|
strokeDashoffset={-s.offset}
|
|
strokeLinecap="butt"
|
|
/>
|
|
))}
|
|
</svg>
|
|
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className="sb-display tnum text-[20px] font-bold leading-none tracking-tight text-ink">
|
|
{centerValue}
|
|
</span>
|
|
<span className="mt-1 text-[10px] font-medium uppercase tracking-[0.1em] text-muted">
|
|
{centerLabel}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|