add clinic-console live-artifact template (#795)

* add clinic-console live-artifact template

Adds a built-in clinic-console template under
skills/live-artifact/assets/templates/, fulfilling the
assets/templates/<name> directory shape that
specs/2026-04-29-live-artifacts/spec.md §5.1 already plans for but
has not yet populated.

Three files, no other changes:
- template.html — html_template_v1 source, only DOM, CSS tokens, and {{data.*}} bindings
- data.json    — canonical default sample (~6.7 KB, well within bounded JSON limits)
- README.md    — data contract + telemedicine/pharmacy/pediatric variants

Verified locally against apps/daemon/src/live-artifacts/render.ts:
all paths match the html_template_v1 grammar, all bindings resolve
to scalars, no script/iframe/srcdoc/on*=/javascript:/raw HTML
directives, no forbidden JSON keys, JSON within bounded envelope.

* clinic-console: hardcode icon refs, drop icon_href bindings

Address @mrcfps's review on PR #795: stop interpolating {{data.*}} into
<use href="..."> attributes. The html_template_v1 binding contract
forbids interpolation inside URL-bearing attributes, and the security
validator runs *before* {{data.*}} substitution — so even a benign
icon_href today could be replaced with javascript:alert(1) tomorrow
without the validator ever seeing it.

Fix:

- template.html: every <use href="..."> is now a hardcoded literal
  (#icon-dashboard, #icon-message, …). 14 nav slots + 4 KPI tiles
  converted; total <use href="..."> count unchanged at 28, all
  fragment-id literals, zero {{data.*}} inside URL-bearing attrs.
- data.json: icon_href removed from each nav_main[] item, each
  nav_management[] item, and from kpi_a / kpi_b / kpi_c / kpi_d
  (14 keys removed). Sample is now 6,333 bytes (was ~6,840).
- README.md: data-contract tables no longer list icon_href; new
  "Icons are template-locked" section explains the binding-contract
  rationale, lists the hardcoded id per slot, and gives guidance for
  future runtime-configurable icons (route through a constrained,
  non-URL mechanism such as enumerated CSS classes — never interpolate
  into <use href> directly).

Verified locally against apps/daemon/src/live-artifacts/render.ts:
298 bindings, 297 unique paths, all resolve to scalars; zero
{{...}} inside any URL-bearing attribute (href / src / action /
formaction / srcset / xlink:href / ping / background / poster /
cite); none of the 6 forbidden security patterns present; bounded
JSON envelope: 6.3 KiB / depth 5 / max 22 keys / max 35 array items
/ max 45-char string — all well within limits.

---------

Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
This commit is contained in:
Joey-nexu 2026-05-07 18:22:09 +08:00 committed by GitHub
parent 55aa24167b
commit 2bbd677ea2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1055 additions and 0 deletions

View file

@ -0,0 +1,342 @@
# Clinic Console — live artifact template
A `html_template_v1` template for a friendly clinic / hospital / telemedicine
operations console. Soft-mint healthcare aesthetic: cool off-white canvas,
single mint accent, generous 18px card radii, signature diagonal-stripe
pattern fills inside KPI tiles and bar-chart bars, illustrated CSS-gradient
avatars, and one dark surface (the calendar activity popover).
## Files
```text
clinic-console/
├── template.html # html_template_v1 source — only DOM, CSS tokens, and {{data.*}} bindings
├── data.json # canonical default sample (renders straight out of the box)
└── README.md # this file — data contract + customization notes
```
## How an agent uses this template
This template is intended to be copied (or referenced) when the
[`live-artifact`](../../../SKILL.md) skill is invoked with a healthcare
operations brief such as *"clinic dashboard", "doctors schedule", "hospital
admin", "appointment console", "telemedicine ops", "诊所后台", "医院管理"*.
The agent should:
1. Copy `template.html` and `data.json` into the project's live-artifact
workspace directory as `template.html` and `data.json`.
2. Edit `data.json` to reflect the actual brand, names, schedules, and
numbers from the brief or connector source. The shape of the JSON must
match what `template.html` references (every `{{data.path}}` interpolation
below).
3. Author `artifact.json` and `provenance.json` per the live-artifact
protocol, then register the artifact through the daemon wrapper:
```bash
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json
```
4. The daemon renders `template.html + data.json` into the preview
`index.html` automatically. The agent does **not** author `index.html`.
When the user clicks **Refresh**, the daemon re-runs the registered source,
maps results back into `data.json`, re-renders the preview, and snapshots
the change — the layout never changes; only the numbers, names, and pill
states do.
## Default sample renders out of the box
If you create a live artifact using the default `data.json` shipped here,
you get the canonical "St. Lukes Wellness" demo screen:
- Greeting: `Hey Lukmon, glad to have you back! 🙌`
- Four KPI tiles: Total doctors / Total bookings / Available rooms / Total
visitors, with mixed amber- and blue-stripe pattern footers and an inline
General/Private rooms mini-list.
- Patient overview chart with paired diagonal-stripe bars across JanJul,
Mar 2025 highlighted with a mint outline.
- March 2025 mini-calendar with day 8 active (mint circle + dot) and a dark
Activity Detail popover floating below it.
- Top requested clinics donut: Dental 120 / Cardiology 249 / Surgery 165.
- Doctor schedule with three pastel pills (Available / Unavailable / Leave).
- Today's appointments list with five illustrated avatars + venue / mode
hints (`room 204`, `video call`, …).
## Default sample provenance.json
If you ship the default sample without re-sourcing the data, use:
```json
{
"generatedAt": "2026-04-29T12:00:00.000Z",
"generatedBy": "agent",
"notes": "Default sample data shipped with the clinic-console template. Replace with real clinic data before sharing externally.",
"sources": [
{ "label": "Template default sample", "type": "user_input" }
]
}
```
## Data contract
The shape below is the contract between `template.html` and `data.json`.
Every key listed is referenced by at least one `{{data.path}}` interpolation
in `template.html`. All values are scalars (string or number); the template
does not invoke any expression / helper / conditional logic — it is a
straight `html_template_v1` substitution.
### Top-level scalars
| Key | Example | Notes |
|---|---|---|
| `brand_name` | `"ST. LUKES"` | Sidebar wordmark. Keep ≤14 characters. |
| `greeting` | `"Hey Lukmon, glad to have you back! 🙌"` | Single emoji allowed at the end; no other emoji anywhere in the artifact. |
| `search_placeholder` | `"Search doctors, patients, rooms…"` | Greeting-row search input ghost text. |
| `search_shortcut` | `"⌘K"` | Right-side keycap label. |
| `secondary_action_label` | `"Export CSV"` | Greeting-row secondary button text. |
| `primary_action_label` | `"Add new"` | Greeting-row primary mint CTA text. |
### `user`
| Key | Example | Notes |
|---|---|---|
| `name` | `"Lukmon Olabode"` | Sidebar bottom row. |
| `role` | `"Admin"` | One-word role; longer roles wrap. |
| `av_class` | `"av-orange"` | One of `av-orange`, `av-pink`, `av-mint`, `av-blue`, `av-violet`, `av-amber`, `av-rose`. |
| `initial` | `"L"` | Single uppercase letter. |
### `nav_main` and `nav_management` (5 items each)
Each item shape:
| Key | Example | Notes |
|---|---|---|
| `label` | `"Dashboard"` | Nav text. |
| `active_class` | `""` or `"active"` | Set to `"active"` on exactly one nav item across both groups. |
| `count` | `""` or `"10"` | Empty string hides the count badge (CSS `:empty { display: none }`). |
> **Icons are template-locked.** Each nav slot's icon is hardcoded inside
> `template.html` (see [Icons are template-locked](#icons-are-template-locked)
> below) and is not exposed through `data.json`. The `html_template_v1`
> security validator forbids `{{data.*}}` interpolation inside URL-bearing
> attributes (`<use href>`, `<a href>`, `<img src>`, …) — and even if it
> didn't, the validator runs *before* substitution, so a malformed `data.json`
> could smuggle a `javascript:` URL past it. The reorder rule is therefore:
> if you change the meaning of a nav slot, also edit the corresponding
> `<use href="#icon-…">` literal in `template.html`.
### `pro_card`
| Key | Example | Notes |
|---|---|---|
| `tag` | `"Pro"` | Black pill in the upgrade card. Keep ≤6 characters. |
| `title` | `"Pssst!"` | Display title. |
| `body` | `"Your subscription expires in 9 days."` | One-sentence nudge. |
| `primary_label` | `"Renew"` | Mint primary action. |
| `secondary_label` | `"Cancel"` | Outlined secondary action. |
### KPI tiles `kpi_a` `kpi_b` `kpi_c` `kpi_d`
Tiles A, B, D share the **caption + pattern strip** layout. Tile C uses a
**2-row mini-list** layout instead. Every tile must have either a strip or a
mini-list — never bare.
Common keys:
| Key | Example | Notes |
|---|---|---|
| `label` | `"Total doctors"` | Tile label. |
| `value` | `"1,089"` | Big number (Plus Jakarta Sans 700). Use commas for thousands. |
| `trend_class` | `"up"` or `"down"` | Pill grammar — `up` = mint, `down` = rose. |
| `trend_label` | `"↑ 5.5%"` | Always include the arrow glyph. |
> KPI icons are also template-locked — see
> [Icons are template-locked](#icons-are-template-locked) below.
A / B / D additional keys:
| Key | Example | Notes |
|---|---|---|
| `caption` | `"An increase of 20 doctors in the last 7 days."` | One sentence answering "compared to what". |
| `strip_class` | `"stripe-amber"` | One of `stripe-amber`, `stripe-blue`, `stripe-mint`. Adjacent tiles should alternate hues. |
| `mini_stat` (B / D only) | `"1,635 today"` | Right-aligned tiny caption below the strip. |
C (`kpi_c`) additional keys:
| Key | Example |
|---|---|
| `rows` | array of 2 objects: `{ "label": "General room", "value": "100" }` |
### `chart`
| Key | Example | Notes |
|---|---|---|
| `title` | `"Patient overview"` | Card title. |
| `dropdown_label` | `"Last 6 months"` | Time-range chip text. |
| `legend_a` `legend_b` `legend_c` | `"Total patients"` etc. | Three legend captions. |
| `bars` | array of 14 objects: `{ "x": "34", "y": "148", "h": "92" }` | 7 month pairs (mint back, blue front). Bar 5 (index 5) is the highlighted month — the template adds a 2px mint stroke to bar 5 only. |
| `x_labels` | `["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]` | Seven month labels matching the seven bar pairs. |
### `calendar`
| Key | Example | Notes |
|---|---|---|
| `month_label` | `"March 2025"` | Header. |
| `dow` | `["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]` | Always 7 items. |
| `days` | array of exactly **35** objects: `{ "label": "1", "modifier": "" }` | 5 weeks × 7 days. `modifier` = `""`, `"muted"` (leading/trailing month), or `"active"` (single highlighted day, mint circle). |
### `activity`
| Key | Example | Notes |
|---|---|---|
| `title` | `"Activity Detail · Mar 8"` | Popover header — should reference the active calendar day. |
| `events` | array of exactly 3 objects: `{ "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" }` | Three events. `av_class``blue`, `pink`, `violet`, `mint`, `amber`. |
| `add_label` | `" Add item"` | Footer link. |
### `donut`
| Key | Example | Notes |
|---|---|---|
| `title` | `"Top 3 most requested clinics"` | Card title. |
| `center_label` | `"Total patients"` | Above the center number. |
| `center_num` | `"534"` | The big tabular number in the donut hole. |
| `segment_b` | `{ "dasharray": "120 240", "dashoffset": "0" }` | SVG `stroke-dasharray` + `stroke-dashoffset` for the pink slice. Sum of arc lengths over a r=38 circle equals `2π × 38 ≈ 239`. |
| `segment_c` | `{ "dasharray": "35 240", "dashoffset": "-120" }` | Same for the mint slice. The blue background ring is the full circumference — no per-segment math needed. |
| `legend` | array of exactly 3 objects: `{ "value": "120", "label": "Dental" }` | Three entries in blue / pink / mint order. |
### `schedule`
| Key | Example | Notes |
|---|---|---|
| `title` | `"Doctors's schedule"` | Card title. |
| `stats` | array of exactly 3 objects: `{ "value": "51", "small": "Total", "label": "Available" }` | Available / Unavailable / Leave counts in a 3-column grid. |
| `list_header_label` | `"List of Doctor"` | Sortable list header label. |
| `doctors` | array of exactly 4 objects | See below. |
Each `doctors` row:
| Key | Example | Notes |
|---|---|---|
| `av_class` | `"av-blue"` | Avatar gradient. |
| `initial` | `"P"` | Single uppercase letter. |
| `name` | `"Peter Bashir"` | Real-feeling name. |
| `role` | `"Anesthesiologist"` | Specialty. |
| `status_class` | `"avail"` `"unav"` `"leave"` | Pill grammar. |
| `status_label` | `"Available"` `"Unavailable"` `"Leave"` | Pill text. |
### `appointments`
| Key | Example |
|---|---|
| `title` | `"Today's appointments"` |
| `list` | array of exactly 5 objects |
Each `list` row:
| Key | Example | Notes |
|---|---|---|
| `av_class` | `"av-pink"` | Avatar gradient. |
| `initial` | `"R"` | Single uppercase letter. |
| `name` | `"Ruth Tubonimi"` | Real-feeling name. |
| `role` | `"Gastroenterology · room 204"` | Specialty + venue / mode hint (`room N`, `video call`, `telemedicine`). |
| `date` | `"Today"` | Short date label. |
| `time` | `"09:40"` | 24h or 12h, pick one and stay consistent. |
## Icons are template-locked
`html_template_v1` forbids `{{data.*}}` interpolation inside URL-bearing
attributes such as `<use href>`, `<a href>`, `<img src>`,
`<form action>`, etc. (see
[`skills/live-artifact/references/artifact-schema.md`](../../../references/artifact-schema.md#html-template-v1-binding-rules)).
The renderer's security validator runs *before* `{{data.*}}` substitution, so
even a well-formed validator pass would not protect a future `data.json`
that put `javascript:alert(1)` (or any other URL value) into one of these
attributes.
This template therefore hardcodes every `<use href="#icon-…">` reference in
`template.html` itself. Each slot has a fixed icon id:
| Slot | Hardcoded icon id |
|---|---|
| Sidebar brand mark | `#icon-leaf` |
| Sidebar collapse toggle | `#icon-collapse` |
| `nav_main[0]` Dashboard | `#icon-dashboard` |
| `nav_main[1]` Message | `#icon-message` |
| `nav_main[2]` Schedule | `#icon-schedule` |
| `nav_main[3]` Notification | `#icon-bell` |
| `nav_main[4]` Transaction | `#icon-card` |
| `nav_management[0]` Doctor | `#icon-user` |
| `nav_management[1]` Medicine | `#icon-pill` |
| `nav_management[2]` Bedroom | `#icon-bed` |
| `nav_management[3]` Appointment | `#icon-check-square` |
| `nav_management[4]` Patient | `#icon-people` |
| Sidebar logout | `#icon-logout` |
| Greeting-row search | `#icon-search` |
| Greeting-row secondary CTA | `#icon-download` |
| Greeting-row primary CTA | `#icon-plus` |
| `kpi_a` glyph | `#icon-user` |
| `kpi_b` glyph | `#icon-schedule` |
| `kpi_c` glyph | `#icon-bed` |
| `kpi_d` glyph | `#icon-people` |
| Patient-overview card | `#icon-clock` |
| Time-range dropdown chevron | `#icon-chev-down` |
| Calendar prev / next | `#icon-chev-left`, `#icon-chev-right` |
| Top-clinics card | `#icon-stethoscope` |
| Doctor-schedule card | `#icon-schedule` |
| List header chevron | `#icon-chev-down` |
| Today's-appointments card | `#icon-check-square` |
If you re-purpose a slot (e.g. swap `nav_main[2] Schedule` for
`nav_main[2] Reports`), edit the corresponding `<use href="#icon-…">` literal
in `template.html` to match — the icon set inside the inline `<symbol>`
defs at the top of `template.html` already includes 21 icons covering the
common clinic / hospital / pharmacy / telemedicine vocabulary
(`#icon-dashboard`, `#icon-message`, `#icon-schedule`, `#icon-bell`,
`#icon-card`, `#icon-user`, `#icon-pill`, `#icon-bed`, `#icon-check-square`,
`#icon-people`, `#icon-leaf`, `#icon-clock`, `#icon-stethoscope`,
`#icon-search`, `#icon-download`, `#icon-plus`, `#icon-chev-left`,
`#icon-chev-right`, `#icon-chev-down`, `#icon-collapse`, `#icon-logout`).
If you need a runtime-configurable icon, add a new constrained,
non-URL-bearing mechanism (for example a `data.kpi_a.icon_class` that toggles
between a fixed list of CSS classes the template enumerates) — never
interpolate into `<use href>` directly.
## Style guarantees
The template enforces, in CSS only (no JavaScript):
- Cool off-white canvas (`#EEF2F6`), bright white surfaces, 18px card radii, 1px hairline borders.
- Mint accent (`#10B981`) restricted to five places: active sidebar nav row, primary CTA, KPI icon glyphs, success metric pill, active calendar date.
- Diagonal-stripe pattern fills (135°, 8px line + 8px gap) on KPI footer strips and inside bar-chart bars.
- Pastel-only status pills (mint / rose / amber).
- Tabular lining numerals on every numeric value (`font-feature-settings: "tnum","lnum"`).
- The dark calendar activity popover is the only dark surface in the artifact.
- Mobile reflow at ≤920px: sidebar stacks above main, KPI strip becomes 2 cols then 1 col, mid and bottom rows stack.
- No external CDN imports. Fonts use system fallback (`Plus Jakarta Sans, Inter, system-ui, sans-serif`).
## Customization tips
- **Telemedicine** variant: replace `kpi_c` (Available rooms) with `Live sessions`, swap the donut to `Top consultation types` (Video / Audio / Chat), and add `· video call` / `· audio call` venue hints in appointment rows.
- **Pharmacy** variant: replace the doctor schedule with stock levels — keep the same shape, just rename the columns to SKU / drug / stock pill.
- **Pediatric** variant: tilt the avatar palette toward `av-pink`, `av-amber`, `av-orange`, keep the active calendar day on a children's milestone.
For all variants, **do not** introduce new colors, fonts, or radii. Every visual lever is already a token in `:root{}`.
## Bounded JSON envelope
This default `data.json` is well within the live-artifact bounded JSON
constraints:
| Constraint | Limit | This sample |
|---|---|---|
| Object/array depth | 8 | 4 |
| Object keys | 100 / object | ≤20 |
| Array length | 500 | 35 (calendar.days) |
| String length | 16 KiB | <100 chars |
| Serialized size | 256 KiB | ~7 KiB |
If you scale up the bar count, calendar density, or list rows, stay well
under these limits. Refresh writes go through the same validation, so
oversized data will be rejected before persistence.

View file

@ -0,0 +1,165 @@
{
"brand_name": "ST. LUKES",
"greeting": "Hey Lukmon, glad to have you back! 🙌",
"search_placeholder": "Search doctors, patients, rooms…",
"search_shortcut": "⌘K",
"secondary_action_label": "Export CSV",
"primary_action_label": "Add new",
"user": {
"name": "Lukmon Olabode",
"role": "Admin",
"av_class": "av-orange",
"initial": "L"
},
"nav_main_label": "Main Menu",
"nav_main": [
{ "label": "Dashboard", "active_class": "active", "count": "" },
{ "label": "Message", "active_class": "", "count": "10" },
{ "label": "Schedule", "active_class": "", "count": "" },
{ "label": "Notification", "active_class": "", "count": "12" },
{ "label": "Transaction", "active_class": "", "count": "" }
],
"nav_management_label": "Management",
"nav_management": [
{ "label": "Doctor", "active_class": "", "count": "" },
{ "label": "Medicine", "active_class": "", "count": "" },
{ "label": "Bedroom", "active_class": "", "count": "" },
{ "label": "Appointment", "active_class": "", "count": "" },
{ "label": "Patient", "active_class": "", "count": "" }
],
"pro_card": {
"tag": "Pro",
"title": "Pssst!",
"body": "Your subscription expires in 9 days.",
"primary_label": "Renew",
"secondary_label": "Cancel"
},
"kpi_a": {
"label": "Total doctors",
"value": "1,089",
"trend_class": "down",
"trend_label": "↓ 4.2%",
"caption": "An increase of 20 doctors in the last 7 days.",
"strip_class": "stripe-amber"
},
"kpi_b": {
"label": "Total bookings",
"value": "17,610",
"trend_class": "up",
"trend_label": "↑ 5.5%",
"caption": "Last 7 days: 5,231 → 8,323 visitors.",
"strip_class": "stripe-blue",
"mini_stat": "1,635 today"
},
"kpi_c": {
"label": "Available rooms",
"value": "8,450",
"trend_class": "up",
"trend_label": "↑ 462",
"rows": [
{ "label": "General room", "value": "100" },
{ "label": "Private room", "value": "75" }
]
},
"kpi_d": {
"label": "Total visitors",
"value": "29,709",
"trend_class": "up",
"trend_label": "↑ 3.5%",
"caption": "Top 3 in-demand clinics this month.",
"strip_class": "stripe-amber",
"mini_stat": "1,070 today"
},
"chart": {
"title": "Patient overview",
"dropdown_label": "Last 6 months",
"legend_a": "Total patients",
"legend_b": "Avg. hospitalized",
"legend_c": "Avg. outpatient care",
"bars": [
{ "x": "34", "y": "148", "h": "92" },
{ "x": "58", "y": "108", "h": "132" },
{ "x": "120", "y": "124", "h": "116" },
{ "x": "144", "y": "92", "h": "148" },
{ "x": "206", "y": "96", "h": "144" },
{ "x": "230", "y": "56", "h": "184" },
{ "x": "292", "y": "156", "h": "84" },
{ "x": "316", "y": "116", "h": "124" },
{ "x": "378", "y": "140", "h": "100" },
{ "x": "402", "y": "100", "h": "140" },
{ "x": "464", "y": "120", "h": "120" },
{ "x": "488", "y": "80", "h": "160" },
{ "x": "550", "y": "160", "h": "80" },
{ "x": "574", "y": "128", "h": "112" }
],
"x_labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]
},
"calendar": {
"month_label": "March 2025",
"dow": ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"],
"days": [
{ "label": "23", "modifier": "muted" }, { "label": "24", "modifier": "muted" }, { "label": "25", "modifier": "muted" }, { "label": "26", "modifier": "muted" }, { "label": "27", "modifier": "muted" }, { "label": "28", "modifier": "muted" }, { "label": "1", "modifier": "" },
{ "label": "2", "modifier": "" }, { "label": "3", "modifier": "" }, { "label": "4", "modifier": "" }, { "label": "5", "modifier": "" }, { "label": "6", "modifier": "" }, { "label": "7", "modifier": "" }, { "label": "8", "modifier": "active" },
{ "label": "9", "modifier": "" }, { "label": "10", "modifier": "" }, { "label": "11", "modifier": "" }, { "label": "12", "modifier": "" }, { "label": "13", "modifier": "" }, { "label": "14", "modifier": "" }, { "label": "15", "modifier": "" },
{ "label": "16", "modifier": "" }, { "label": "17", "modifier": "" }, { "label": "18", "modifier": "" }, { "label": "19", "modifier": "" }, { "label": "20", "modifier": "" }, { "label": "21", "modifier": "" }, { "label": "22", "modifier": "" },
{ "label": "23", "modifier": "" }, { "label": "24", "modifier": "" }, { "label": "25", "modifier": "" }, { "label": "26", "modifier": "" }, { "label": "27", "modifier": "" }, { "label": "28", "modifier": "" }, { "label": "29", "modifier": "" }
]
},
"activity": {
"title": "Activity Detail · Mar 8",
"events": [
{ "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" },
{ "av_class": "pink", "name": "Dental staff meetup", "time": "3:00 pm" },
{ "av_class": "violet", "name": "Ola Muhammad intake", "time": "4:00 pm" }
],
"add_label": " Add item"
},
"donut": {
"title": "Top 3 most requested clinics",
"center_label": "Total patients",
"center_num": "534",
"segment_b": { "dasharray": "120 240", "dashoffset": "0" },
"segment_c": { "dasharray": "35 240", "dashoffset": "-120" },
"legend": [
{ "value": "120", "label": "Dental" },
{ "value": "249", "label": "Cardiology" },
{ "value": "165", "label": "Surgery" }
]
},
"schedule": {
"title": "Doctors's schedule",
"stats": [
{ "value": "51", "small": "Total", "label": "Available" },
{ "value": "23", "small": "Total", "label": "Unavailable" },
{ "value": "09", "small": "Total", "label": "Leave" }
],
"list_header_label": "List of Doctor",
"doctors": [
{ "av_class": "av-blue", "initial": "P", "name": "Peter Bashir", "role": "Anesthesiologist", "status_class": "avail", "status_label": "Available" },
{ "av_class": "av-violet", "initial": "D", "name": "Deborah Fagbemi", "role": "Cardiologist", "status_class": "unav", "status_label": "Unavailable" },
{ "av_class": "av-rose", "initial": "H", "name": "Hannah Diongoli", "role": "Dermatologist", "status_class": "avail", "status_label": "Available" },
{ "av_class": "av-amber", "initial": "A", "name": "Aisha Bello", "role": "Pediatrician", "status_class": "leave", "status_label": "Leave" }
]
},
"appointments": {
"title": "Today's appointments",
"list": [
{ "av_class": "av-pink", "initial": "R", "name": "Ruth Tubonimi", "role": "Gastroenterology · room 204", "date": "Today", "time": "09:40" },
{ "av_class": "av-amber", "initial": "J", "name": "Joseph Obiano", "role": "Psychiatry · video call", "date": "Today", "time": "10:25" },
{ "av_class": "av-mint", "initial": "T", "name": "Timothy Jibrin", "role": "Hematology · room 117", "date": "Today", "time": "11:00" },
{ "av_class": "av-violet", "initial": "E", "name": "Elizabeth Kanu", "role": "Ophthalmology · room 09", "date": "Today", "time": "02:15" },
{ "av_class": "av-blue", "initial": "S", "name": "Simon Garba", "role": "Otolaryngology · room 21", "date": "Today", "time": "03:40" }
]
}
}

View file

@ -0,0 +1,548 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{data.brand_name}} — Clinic Operations Console</title>
<style>
:root {
--canvas: #eef2f6;
--surface: #ffffff;
--surface-muted: #f8fafc;
--surface-inverse: #0f172a;
--border: #e5e7eb;
--border-strong: #d6dbe3;
--fg: #0f172a;
--fg-muted: #64748b;
--fg-tertiary: #94a3b8;
--fg-on-inverse: #f8fafc;
--accent: #10b981;
--accent-strong: #059669;
--accent-soft: #d1fae5;
--pill-up-bg: #d1fae5; --pill-up-fg: #059669;
--pill-down-bg: #fee2e2; --pill-down-fg: #dc2626;
--pill-avail-bg: #d1fae5; --pill-avail-fg: #047857;
--pill-unav-bg: #fee2e2; --pill-unav-fg: #b91c1c;
--pill-leave-bg: #fef3c7; --pill-leave-fg: #b45309;
--stripe-blue: #bfdbfe; --stripe-amber: #fde68a; --stripe-mint: #a7f3d0;
--donut-blue: #bfdbfe; --donut-pink: #fbcfe8; --donut-mint: #ccfbf1;
--legend-blue: #3b82f6; --legend-amber: #f59e0b; --legend-pink: #ec4899; --legend-mint: #2dd4bf;
--av-orange-1: #fed7aa; --av-orange-2: #f97316; --av-orange-fg: #7c2d12;
--av-pink-1: #fbcfe8; --av-pink-2: #ec4899;
--av-mint-1: #a7f3d0; --av-mint-2: #10b981;
--av-blue-1: #bfdbfe; --av-blue-2: #3b82f6;
--av-violet-1: #ddd6fe; --av-violet-2: #7c3aed;
--av-amber-1: #fde68a; --av-amber-2: #f59e0b; --av-amber-fg: #78350f;
--av-rose-1: #fecaca; --av-rose-2: #ef4444;
--av-fg-light: #ffffff;
--r-card: 18px; --r-pill: 999px; --r-md: 14px; --r-sm: 10px; --r-xs: 8px;
--shadow-card: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.04);
--shadow-popover: 0 12px 28px rgba(2,6,23,.32);
--font-display: 'Plus Jakarta Sans', 'SF Pro Display', system-ui, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 20px;
background: var(--canvas); color: var(--fg);
font: 14px/1.5 var(--font-body);
font-feature-settings: "tnum", "lnum";
display: grid; grid-template-columns: 240px 1fr; gap: 20px;
min-height: 100vh;
}
@media (max-width: 920px) {
body { grid-template-columns: 1fr; }
aside.sidebar { position: static !important; height: auto !important; }
}
.card, aside.sidebar {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--r-card); box-shadow: var(--shadow-card);
}
aside.sidebar {
padding: 18px; display: flex; flex-direction: column; gap: 16px;
position: sticky; top: 20px; height: calc(100vh - 40px);
}
.brand { display: flex; align-items: center; justify-content: space-between; padding: 0 6px; }
.brand-mark { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -0.01em; }
.brand-leaf { width: 22px; height: 22px; border-radius: var(--r-pill); background: var(--accent); color: var(--surface); display: inline-flex; align-items: center; justify-content: center; }
.brand-toggle { color: var(--fg-tertiary); }
.nav-section { display: flex; flex-direction: column; gap: 2px; }
.nav-label { color: var(--fg-tertiary); font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; padding: 8px 10px 4px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm); color: var(--fg-muted); font-size: 13px; }
.nav-item .ico { color: var(--fg-muted); }
.nav-item .count { margin-left: auto; background: var(--surface-muted); color: var(--fg-muted); border-radius: var(--r-pill); padding: 1px 8px; font-size: 11px; font-weight: 500; }
.nav-item .count:empty { display: none; }
.nav-item.active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }
.nav-item.active .ico { color: var(--accent-strong); }
.pro-card { margin-top: auto; background: var(--surface-muted); border-radius: var(--r-md); padding: 14px; display: flex; flex-direction: column; gap: 8px; }
.pro-tag { display: inline-flex; background: var(--fg); color: var(--surface); border-radius: var(--r-pill); padding: 2px 8px; font-size: 10px; font-weight: 600; width: fit-content; }
.pro-card h4 { margin: 0; font-size: 13px; font-weight: 600; font-family: var(--font-display); }
.pro-card p { margin: 0; font-size: 11px; color: var(--fg-muted); }
.pro-actions { display: flex; gap: 8px; margin-top: 4px; }
.pro-actions button { font-size: 11px; padding: 6px 12px; border-radius: var(--r-sm); border: 1px solid var(--border); background: var(--surface); color: var(--fg-muted); cursor: pointer; }
.pro-actions .primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
.user-row { display: flex; align-items: center; gap: 10px; padding: 10px 6px 0; border-top: 1px solid var(--border); }
.user-row .meta { display: flex; flex-direction: column; }
.user-row .name { font-size: 13px; font-weight: 600; }
.user-row .role { font-size: 11px; color: var(--fg-tertiary); }
.user-row .out { margin-left: auto; color: var(--fg-tertiary); }
.av { border-radius: var(--r-pill); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--av-fg-light); font-weight: 600; }
.av-32 { width: 32px; height: 32px; font-size: 12px; }
.av-40 { width: 40px; height: 40px; font-size: 14px; }
.av-orange { background: linear-gradient(135deg, var(--av-orange-1), var(--av-orange-2)); color: var(--av-orange-fg); }
.av-pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
.av-mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
.av-blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
.av-violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
.av-amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); color: var(--av-amber-fg); }
.av-rose { background: linear-gradient(135deg, var(--av-rose-1), var(--av-rose-2)); }
main { display: flex; flex-direction: column; gap: 24px; }
.greeting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.greeting { font-family: var(--font-display); font-size: 26px; font-weight: 700; letter-spacing: -0.01em; }
.actions { display: flex; align-items: center; gap: 12px; }
.search { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 9px 14px; width: 260px; color: var(--fg-tertiary); font-size: 13px; }
.search .kbd { margin-left: auto; font-size: 11px; border: 1px solid var(--border); padding: 1px 6px; border-radius: 4px; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; border-radius: var(--r-sm); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-family: var(--font-body); }
.btn.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
.kpi-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
@media (max-width: 1100px) { .kpi-strip { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .kpi-strip { grid-template-columns: 1fr; } }
.kpi { padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 150px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-card); box-shadow: var(--shadow-card); }
.kpi-head { display: flex; align-items: center; gap: 10px; }
.kpi-glyph { width: 32px; height: 32px; border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent-strong); display: inline-flex; align-items: center; justify-content: center; }
.kpi-label { font-size: 13px; color: var(--fg-muted); font-weight: 500; flex: 1; }
.kpi-menu { color: var(--fg-tertiary); }
.kpi-num-row { display: flex; align-items: baseline; gap: 10px; }
.kpi-num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1.1; letter-spacing: -0.015em; }
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--r-pill); padding: 2px 8px; font-size: 11px; font-weight: 600; }
.pill.up { background: var(--pill-up-bg); color: var(--pill-up-fg); }
.pill.down { background: var(--pill-down-bg); color: var(--pill-down-fg); }
.pill.avail { background: var(--pill-avail-bg); color: var(--pill-avail-fg); font-weight: 500; }
.pill.unav { background: var(--pill-unav-bg); color: var(--pill-unav-fg); font-weight: 500; }
.pill.leave { background: var(--pill-leave-bg); color: var(--pill-leave-fg); font-weight: 500; }
.kpi-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; }
.kpi-cap { font-size: 12px; color: var(--fg-tertiary); line-height: 1.4; max-width: 60%; }
.kpi-strip-pat { height: 32px; flex-shrink: 0; border-radius: var(--r-xs); }
.stripe-amber { background: repeating-linear-gradient(135deg, var(--stripe-amber) 0 8px, var(--surface) 8px 16px); width: 96px; }
.stripe-blue { background: repeating-linear-gradient(135deg, var(--stripe-blue) 0 8px, var(--surface) 8px 16px); width: 120px; }
.stripe-mint { background: repeating-linear-gradient(135deg, var(--stripe-mint) 0 8px, var(--surface) 8px 16px); width: 96px; }
.kpi-mini-stat { font-size: 11px; color: var(--fg-tertiary); margin-top: 4px; text-align: right; }
.kpi-rooms-list { display: flex; flex-direction: column; gap: 6px; margin-top: auto; }
.kpi-rooms-list .row { display: flex; justify-content: space-between; font-size: 12px; }
.kpi-rooms-list .row .lbl { color: var(--fg-muted); display: flex; align-items: center; gap: 6px; }
.kpi-rooms-list .row .lbl::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--fg-tertiary); }
.kpi-rooms-list .row .val { color: var(--fg); font-weight: 600; }
.mid-row { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
@media (max-width: 1100px) { .mid-row { grid-template-columns: 1fr; } }
.card-pad { padding: 20px; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.card-title { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-size: 15px; font-weight: 600; }
.card-title .ico-tile { width: 26px; height: 26px; border-radius: var(--r-sm); background: var(--surface-muted); color: var(--fg-muted); display: inline-flex; align-items: center; justify-content: center; }
.dropdown { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12px; color: var(--fg-muted); background: var(--surface); }
.legend { display: flex; gap: 16px; margin-bottom: 8px; flex-wrap: wrap; }
.legend .lg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-muted); }
.legend .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
.cal-head { display: flex; align-items: center; justify-content: space-between; }
.cal-month { font-family: var(--font-display); font-size: 16px; font-weight: 600; }
.cal-nav { color: var(--fg-muted); display: inline-flex; gap: 6px; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; font-size: 12px; margin-top: 10px; position: relative; }
.cal-grid .dow { color: var(--fg-tertiary); font-weight: 500; padding: 6px 0; font-size: 11px; }
.cal-grid .day { height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-pill); }
.cal-grid .day.muted { color: var(--fg-tertiary); }
.cal-grid .day.active { background: var(--accent); color: var(--surface); font-weight: 700; position: relative; }
.cal-grid .day.active::after { content: ""; position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: var(--r-pill); background: var(--accent); }
.activity-popover { background: var(--surface-inverse); color: var(--fg-on-inverse); border-radius: var(--r-md); padding: 14px; box-shadow: var(--shadow-popover); margin-top: 14px; }
.activity-popover h6 { margin: 0 0 10px; font-size: 12px; font-weight: 600; }
.activity-popover .ev { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
.activity-popover .ev:last-of-type { border-bottom: none; }
.activity-popover .ev .av-pop { width: 22px; height: 22px; border-radius: var(--r-pill); flex-shrink: 0; }
.activity-popover .ev .av-pop.blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
.activity-popover .ev .av-pop.pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
.activity-popover .ev .av-pop.violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
.activity-popover .ev .av-pop.mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
.activity-popover .ev .av-pop.amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); }
.activity-popover .ev .name { flex: 1; font-size: 11px; }
.activity-popover .ev .time { font-size: 10px; color: rgba(248,250,252,0.6); }
.activity-popover .add { margin-top: 8px; font-size: 11px; color: rgba(248,250,252,0.7); }
.bottom-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
@media (max-width: 1100px) { .bottom-row { grid-template-columns: 1fr; } }
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 0; }
.donut { position: relative; width: 180px; height: 180px; }
.donut .center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.donut .center .lbl { font-size: 12px; color: var(--fg-tertiary); }
.donut .center .num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1; margin-top: 2px; }
.donut-legend { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; font-size: 12px; color: var(--fg-muted); }
.donut-legend .lg { display: inline-flex; align-items: center; gap: 6px; }
.donut-legend .lg .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
.donut-legend .lg .v { color: var(--fg); font-weight: 600; margin-right: 2px; }
.sched-stats { display: grid; grid-template-columns: 1fr 1fr 1fr; margin-bottom: 12px; }
.sched-stats .col { padding: 4px 12px; border-left: 1px solid var(--border); }
.sched-stats .col:first-child { border-left: none; padding-left: 0; }
.sched-stats .col .v { font-family: var(--font-display); font-size: 18px; font-weight: 700; line-height: 1.1; }
.sched-stats .col .v small { font-size: 11px; color: var(--fg-tertiary); font-weight: 400; margin-left: 2px; }
.sched-stats .col .lbl { font-size: 11px; color: var(--fg-tertiary); margin-top: 2px; }
.list-head { display: flex; align-items: center; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 11px; color: var(--fg-tertiary); font-weight: 500; }
.list-row { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.list-row:last-child { border-bottom: none; }
.person { flex: 1; display: flex; flex-direction: column; }
.person .n { font-size: 13px; font-weight: 600; }
.person .r { font-size: 11px; color: var(--fg-tertiary); }
.pill.list { padding: 3px 10px; }
.appt-time { display: flex; flex-direction: column; align-items: flex-end; }
.appt-time .d { font-size: 11px; color: var(--fg-tertiary); }
.appt-time .t { font-size: 13px; font-weight: 600; }
.ic { width: 18px; height: 18px; stroke-width: 1.6; }
.ic-sm { width: 14px; height: 14px; stroke-width: 1.6; }
</style>
</head>
<body data-od-id="clinic-console-template">
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="icon-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></symbol>
<symbol id="icon-message" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 1 1 21 11.5z"/></symbol>
<symbol id="icon-schedule" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
<symbol id="icon-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></symbol>
<symbol id="icon-card" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></symbol>
<symbol id="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a8 8 0 0 1 16 0v1"/></symbol>
<symbol id="icon-pill" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10 2v4M14 2v4M5 10h14M6 6h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM12 13v6M9 16h6"/></symbol>
<symbol id="icon-bed" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 17h18M5 17v-5a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5"/></symbol>
<symbol id="icon-check-square" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M8 14l3 3 5-5"/></symbol>
<symbol id="icon-people" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="9" cy="8" r="3"/><path d="M3 21v-1a6 6 0 0 1 12 0v1"/><circle cx="17" cy="6" r="2.5"/><path d="M14 14a4 4 0 0 1 7 4"/></symbol>
<symbol id="icon-leaf" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12c4-8 14-8 14 0 0 6-7 8-7 8s-7-2-7-8z"/></symbol>
<symbol id="icon-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/></symbol>
<symbol id="icon-stethoscope" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 1 1 8 0v4"/></symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></symbol>
<symbol id="icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="icon-chev-left" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 18l-6-6 6-6"/></symbol>
<symbol id="icon-chev-right" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 18l6-6-6-6"/></symbol>
<symbol id="icon-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 9l6 6 6-6"/></symbol>
<symbol id="icon-collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 6l-6 6 6 6M21 6l-6 6 6 6"/></symbol>
<symbol id="icon-logout" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
</defs>
</svg>
<aside class="sidebar" data-od-id="sidebar">
<div class="brand">
<div class="brand-mark">
<span class="brand-leaf"><svg class="ic-sm"><use href="#icon-leaf"/></svg></span>
{{data.brand_name}}
</div>
<span class="brand-toggle"><svg class="ic"><use href="#icon-collapse"/></svg></span>
</div>
<div class="nav-section">
<div class="nav-label">{{data.nav_main_label}}</div>
<a class="nav-item {{data.nav_main.0.active_class}}"><svg class="ic"><use href="#icon-dashboard"/></svg>{{data.nav_main.0.label}}<span class="count">{{data.nav_main.0.count}}</span></a>
<a class="nav-item {{data.nav_main.1.active_class}}"><svg class="ic"><use href="#icon-message"/></svg>{{data.nav_main.1.label}}<span class="count">{{data.nav_main.1.count}}</span></a>
<a class="nav-item {{data.nav_main.2.active_class}}"><svg class="ic"><use href="#icon-schedule"/></svg>{{data.nav_main.2.label}}<span class="count">{{data.nav_main.2.count}}</span></a>
<a class="nav-item {{data.nav_main.3.active_class}}"><svg class="ic"><use href="#icon-bell"/></svg>{{data.nav_main.3.label}}<span class="count">{{data.nav_main.3.count}}</span></a>
<a class="nav-item {{data.nav_main.4.active_class}}"><svg class="ic"><use href="#icon-card"/></svg>{{data.nav_main.4.label}}<span class="count">{{data.nav_main.4.count}}</span></a>
</div>
<div class="nav-section">
<div class="nav-label">{{data.nav_management_label}}</div>
<a class="nav-item {{data.nav_management.0.active_class}}"><svg class="ic"><use href="#icon-user"/></svg>{{data.nav_management.0.label}}<span class="count">{{data.nav_management.0.count}}</span></a>
<a class="nav-item {{data.nav_management.1.active_class}}"><svg class="ic"><use href="#icon-pill"/></svg>{{data.nav_management.1.label}}<span class="count">{{data.nav_management.1.count}}</span></a>
<a class="nav-item {{data.nav_management.2.active_class}}"><svg class="ic"><use href="#icon-bed"/></svg>{{data.nav_management.2.label}}<span class="count">{{data.nav_management.2.count}}</span></a>
<a class="nav-item {{data.nav_management.3.active_class}}"><svg class="ic"><use href="#icon-check-square"/></svg>{{data.nav_management.3.label}}<span class="count">{{data.nav_management.3.count}}</span></a>
<a class="nav-item {{data.nav_management.4.active_class}}"><svg class="ic"><use href="#icon-people"/></svg>{{data.nav_management.4.label}}<span class="count">{{data.nav_management.4.count}}</span></a>
</div>
<div class="pro-card">
<span class="pro-tag">{{data.pro_card.tag}}</span>
<h4>{{data.pro_card.title}}</h4>
<p>{{data.pro_card.body}}</p>
<div class="pro-actions">
<button class="primary">{{data.pro_card.primary_label}}</button>
<button>{{data.pro_card.secondary_label}}</button>
</div>
</div>
<div class="user-row">
<span class="av av-32 {{data.user.av_class}}">{{data.user.initial}}</span>
<div class="meta">
<span class="name">{{data.user.name}}</span>
<span class="role">{{data.user.role}}</span>
</div>
<span class="out"><svg class="ic"><use href="#icon-logout"/></svg></span>
</div>
</aside>
<main>
<header class="greeting-row" data-od-id="greeting">
<div class="greeting">{{data.greeting}}</div>
<div class="actions">
<div class="search">
<svg class="ic-sm"><use href="#icon-search"/></svg>
{{data.search_placeholder}}
<span class="kbd">{{data.search_shortcut}}</span>
</div>
<button class="btn"><svg class="ic-sm"><use href="#icon-download"/></svg>{{data.secondary_action_label}}</button>
<button class="btn primary"><svg class="ic-sm"><use href="#icon-plus"/></svg>{{data.primary_action_label}}</button>
</div>
</header>
<section class="kpi-strip" data-od-id="kpi-strip">
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-user"/></svg></span>
<span class="kpi-label">{{data.kpi_a.label}}</span>
<span class="kpi-menu"></span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_a.value}}</div><span class="pill {{data.kpi_a.trend_class}}">{{data.kpi_a.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_a.caption}}</div>
<div class="kpi-strip-pat {{data.kpi_a.strip_class}}"></div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
<span class="kpi-label">{{data.kpi_b.label}}</span>
<span class="kpi-menu"></span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_b.value}}</div><span class="pill {{data.kpi_b.trend_class}}">{{data.kpi_b.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_b.caption}}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div class="kpi-strip-pat {{data.kpi_b.strip_class}}"></div>
<div class="kpi-mini-stat">{{data.kpi_b.mini_stat}}</div>
</div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-bed"/></svg></span>
<span class="kpi-label">{{data.kpi_c.label}}</span>
<span class="kpi-menu"></span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_c.value}}</div><span class="pill {{data.kpi_c.trend_class}}">{{data.kpi_c.trend_label}}</span></div>
<div class="kpi-rooms-list">
<div class="row"><span class="lbl">{{data.kpi_c.rows.0.label}}</span><span class="val">{{data.kpi_c.rows.0.value}}</span></div>
<div class="row"><span class="lbl">{{data.kpi_c.rows.1.label}}</span><span class="val">{{data.kpi_c.rows.1.value}}</span></div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-people"/></svg></span>
<span class="kpi-label">{{data.kpi_d.label}}</span>
<span class="kpi-menu"></span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_d.value}}</div><span class="pill {{data.kpi_d.trend_class}}">{{data.kpi_d.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_d.caption}}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div class="kpi-strip-pat {{data.kpi_d.strip_class}}"></div>
<div class="kpi-mini-stat">{{data.kpi_d.mini_stat}}</div>
</div>
</div>
</div>
</section>
<section class="mid-row" data-od-id="patient-overview-and-calendar">
<div class="card card-pad">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-clock"/></svg></span>
{{data.chart.title}}
</div>
<div class="dropdown">{{data.chart.dropdown_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
</div>
<div class="legend">
<span class="lg"><span class="dot" style="background: var(--accent);"></span>{{data.chart.legend_a}}</span>
<span class="lg"><span class="dot" style="background: var(--legend-blue);"></span>{{data.chart.legend_b}}</span>
<span class="lg"><span class="dot" style="background: var(--legend-amber);"></span>{{data.chart.legend_c}}</span>
</div>
<svg viewBox="0 0 600 240" preserveAspectRatio="none" style="width:100%; height:240px; display:block;">
<defs>
<pattern id="stripeBlue" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
<rect width="12" height="12" style="fill: var(--surface);" />
<rect width="6" height="12" style="fill: var(--stripe-blue);" />
</pattern>
<pattern id="stripeMint" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
<rect width="12" height="12" style="fill: var(--surface);" />
<rect width="6" height="12" style="fill: var(--stripe-mint);" opacity="0.7" />
</pattern>
</defs>
<g style="stroke: var(--border);" stroke-width="1">
<line x1="0" y1="48" x2="600" y2="48" />
<line x1="0" y1="96" x2="600" y2="96" />
<line x1="0" y1="144" x2="600" y2="144" />
<line x1="0" y1="192" x2="600" y2="192" />
</g>
<g>
<rect x="{{data.chart.bars.0.x}}" y="{{data.chart.bars.0.y}}" width="22" height="{{data.chart.bars.0.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.1.x}}" y="{{data.chart.bars.1.y}}" width="22" height="{{data.chart.bars.1.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.2.x}}" y="{{data.chart.bars.2.y}}" width="22" height="{{data.chart.bars.2.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.3.x}}" y="{{data.chart.bars.3.y}}" width="22" height="{{data.chart.bars.3.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.4.x}}" y="{{data.chart.bars.4.y}}" width="22" height="{{data.chart.bars.4.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.5.x}}" y="{{data.chart.bars.5.y}}" width="22" height="{{data.chart.bars.5.h}}" rx="6" fill="url(#stripeBlue)" style="stroke: var(--accent);" stroke-width="2" />
<rect x="{{data.chart.bars.6.x}}" y="{{data.chart.bars.6.y}}" width="22" height="{{data.chart.bars.6.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.7.x}}" y="{{data.chart.bars.7.y}}" width="22" height="{{data.chart.bars.7.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.8.x}}" y="{{data.chart.bars.8.y}}" width="22" height="{{data.chart.bars.8.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.9.x}}" y="{{data.chart.bars.9.y}}" width="22" height="{{data.chart.bars.9.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.10.x}}" y="{{data.chart.bars.10.y}}" width="22" height="{{data.chart.bars.10.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.11.x}}" y="{{data.chart.bars.11.y}}" width="22" height="{{data.chart.bars.11.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.12.x}}" y="{{data.chart.bars.12.y}}" width="22" height="{{data.chart.bars.12.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.13.x}}" y="{{data.chart.bars.13.y}}" width="22" height="{{data.chart.bars.13.h}}" rx="6" fill="url(#stripeBlue)" />
</g>
<g style="fill: var(--fg-tertiary);" font-size="11" text-anchor="middle">
<text x="46" y="232">{{data.chart.x_labels.0}}</text>
<text x="132" y="232">{{data.chart.x_labels.1}}</text>
<text x="218" y="232">{{data.chart.x_labels.2}}</text>
<text x="304" y="232">{{data.chart.x_labels.3}}</text>
<text x="390" y="232">{{data.chart.x_labels.4}}</text>
<text x="476" y="232">{{data.chart.x_labels.5}}</text>
<text x="562" y="232">{{data.chart.x_labels.6}}</text>
</g>
</svg>
</div>
<div class="card card-pad" data-od-id="calendar">
<div class="cal-head">
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-left"/></svg></span>
<div class="cal-month">{{data.calendar.month_label}}</div>
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-right"/></svg></span>
</div>
<div class="cal-grid">
<div class="dow">{{data.calendar.dow.0}}</div><div class="dow">{{data.calendar.dow.1}}</div><div class="dow">{{data.calendar.dow.2}}</div><div class="dow">{{data.calendar.dow.3}}</div><div class="dow">{{data.calendar.dow.4}}</div><div class="dow">{{data.calendar.dow.5}}</div><div class="dow">{{data.calendar.dow.6}}</div>
<div class="day {{data.calendar.days.0.modifier}}">{{data.calendar.days.0.label}}</div><div class="day {{data.calendar.days.1.modifier}}">{{data.calendar.days.1.label}}</div><div class="day {{data.calendar.days.2.modifier}}">{{data.calendar.days.2.label}}</div><div class="day {{data.calendar.days.3.modifier}}">{{data.calendar.days.3.label}}</div><div class="day {{data.calendar.days.4.modifier}}">{{data.calendar.days.4.label}}</div><div class="day {{data.calendar.days.5.modifier}}">{{data.calendar.days.5.label}}</div><div class="day {{data.calendar.days.6.modifier}}">{{data.calendar.days.6.label}}</div>
<div class="day {{data.calendar.days.7.modifier}}">{{data.calendar.days.7.label}}</div><div class="day {{data.calendar.days.8.modifier}}">{{data.calendar.days.8.label}}</div><div class="day {{data.calendar.days.9.modifier}}">{{data.calendar.days.9.label}}</div><div class="day {{data.calendar.days.10.modifier}}">{{data.calendar.days.10.label}}</div><div class="day {{data.calendar.days.11.modifier}}">{{data.calendar.days.11.label}}</div><div class="day {{data.calendar.days.12.modifier}}">{{data.calendar.days.12.label}}</div><div class="day {{data.calendar.days.13.modifier}}">{{data.calendar.days.13.label}}</div>
<div class="day {{data.calendar.days.14.modifier}}">{{data.calendar.days.14.label}}</div><div class="day {{data.calendar.days.15.modifier}}">{{data.calendar.days.15.label}}</div><div class="day {{data.calendar.days.16.modifier}}">{{data.calendar.days.16.label}}</div><div class="day {{data.calendar.days.17.modifier}}">{{data.calendar.days.17.label}}</div><div class="day {{data.calendar.days.18.modifier}}">{{data.calendar.days.18.label}}</div><div class="day {{data.calendar.days.19.modifier}}">{{data.calendar.days.19.label}}</div><div class="day {{data.calendar.days.20.modifier}}">{{data.calendar.days.20.label}}</div>
<div class="day {{data.calendar.days.21.modifier}}">{{data.calendar.days.21.label}}</div><div class="day {{data.calendar.days.22.modifier}}">{{data.calendar.days.22.label}}</div><div class="day {{data.calendar.days.23.modifier}}">{{data.calendar.days.23.label}}</div><div class="day {{data.calendar.days.24.modifier}}">{{data.calendar.days.24.label}}</div><div class="day {{data.calendar.days.25.modifier}}">{{data.calendar.days.25.label}}</div><div class="day {{data.calendar.days.26.modifier}}">{{data.calendar.days.26.label}}</div><div class="day {{data.calendar.days.27.modifier}}">{{data.calendar.days.27.label}}</div>
<div class="day {{data.calendar.days.28.modifier}}">{{data.calendar.days.28.label}}</div><div class="day {{data.calendar.days.29.modifier}}">{{data.calendar.days.29.label}}</div><div class="day {{data.calendar.days.30.modifier}}">{{data.calendar.days.30.label}}</div><div class="day {{data.calendar.days.31.modifier}}">{{data.calendar.days.31.label}}</div><div class="day {{data.calendar.days.32.modifier}}">{{data.calendar.days.32.label}}</div><div class="day {{data.calendar.days.33.modifier}}">{{data.calendar.days.33.label}}</div><div class="day {{data.calendar.days.34.modifier}}">{{data.calendar.days.34.label}}</div>
</div>
<div class="activity-popover" data-od-id="activity-popover">
<h6>{{data.activity.title}}</h6>
<div class="ev"><span class="av-pop {{data.activity.events.0.av_class}}"></span><span class="name">{{data.activity.events.0.name}}</span><span class="time">{{data.activity.events.0.time}}</span></div>
<div class="ev"><span class="av-pop {{data.activity.events.1.av_class}}"></span><span class="name">{{data.activity.events.1.name}}</span><span class="time">{{data.activity.events.1.time}}</span></div>
<div class="ev"><span class="av-pop {{data.activity.events.2.av_class}}"></span><span class="name">{{data.activity.events.2.name}}</span><span class="time">{{data.activity.events.2.time}}</span></div>
<div class="add">{{data.activity.add_label}}</div>
</div>
</div>
</section>
<section class="bottom-row">
<div class="card card-pad" data-od-id="top-clinics">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-stethoscope"/></svg></span>
{{data.donut.title}}
</div>
<span class="kpi-menu"></span>
</div>
<div class="donut-wrap">
<div class="donut">
<svg viewBox="0 0 100 100" style="width:100%; height:100%; transform: rotate(-90deg);">
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-blue);" />
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-pink);" stroke-dasharray="{{data.donut.segment_b.dasharray}}" stroke-dashoffset="{{data.donut.segment_b.dashoffset}}" />
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-mint);" stroke-dasharray="{{data.donut.segment_c.dasharray}}" stroke-dashoffset="{{data.donut.segment_c.dashoffset}}" />
</svg>
<div class="center">
<span class="lbl">{{data.donut.center_label}}</span>
<span class="num">{{data.donut.center_num}}</span>
</div>
</div>
<div class="donut-legend">
<div class="lg"><span class="dot" style="background: var(--legend-blue);"></span><span class="v">{{data.donut.legend.0.value}}</span> {{data.donut.legend.0.label}}</div>
<div class="lg"><span class="dot" style="background: var(--legend-pink);"></span><span class="v">{{data.donut.legend.1.value}}</span> {{data.donut.legend.1.label}}</div>
<div class="lg"><span class="dot" style="background: var(--legend-mint);"></span><span class="v">{{data.donut.legend.2.value}}</span> {{data.donut.legend.2.label}}</div>
</div>
</div>
</div>
<div class="card card-pad" data-od-id="doctor-schedule">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
{{data.schedule.title}}
</div>
<span class="kpi-menu"></span>
</div>
<div class="sched-stats">
<div class="col"><div class="v">{{data.schedule.stats.0.value}} <small>{{data.schedule.stats.0.small}}</small></div><div class="lbl">{{data.schedule.stats.0.label}}</div></div>
<div class="col"><div class="v">{{data.schedule.stats.1.value}} <small>{{data.schedule.stats.1.small}}</small></div><div class="lbl">{{data.schedule.stats.1.label}}</div></div>
<div class="col"><div class="v">{{data.schedule.stats.2.value}} <small>{{data.schedule.stats.2.small}}</small></div><div class="lbl">{{data.schedule.stats.2.label}}</div></div>
</div>
<div class="list-head">{{data.schedule.list_header_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.0.av_class}}">{{data.schedule.doctors.0.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.0.name}}</span><span class="r">{{data.schedule.doctors.0.role}}</span></div>
<span class="pill list {{data.schedule.doctors.0.status_class}}">{{data.schedule.doctors.0.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.1.av_class}}">{{data.schedule.doctors.1.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.1.name}}</span><span class="r">{{data.schedule.doctors.1.role}}</span></div>
<span class="pill list {{data.schedule.doctors.1.status_class}}">{{data.schedule.doctors.1.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.2.av_class}}">{{data.schedule.doctors.2.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.2.name}}</span><span class="r">{{data.schedule.doctors.2.role}}</span></div>
<span class="pill list {{data.schedule.doctors.2.status_class}}">{{data.schedule.doctors.2.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.3.av_class}}">{{data.schedule.doctors.3.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.3.name}}</span><span class="r">{{data.schedule.doctors.3.role}}</span></div>
<span class="pill list {{data.schedule.doctors.3.status_class}}">{{data.schedule.doctors.3.status_label}}</span>
</div>
</div>
<div class="card card-pad" data-od-id="appointments">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-check-square"/></svg></span>
{{data.appointments.title}}
</div>
<span class="kpi-menu"></span>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.0.av_class}}">{{data.appointments.list.0.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.0.name}}</span><span class="r">{{data.appointments.list.0.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.0.date}}</span><span class="t">{{data.appointments.list.0.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.1.av_class}}">{{data.appointments.list.1.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.1.name}}</span><span class="r">{{data.appointments.list.1.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.1.date}}</span><span class="t">{{data.appointments.list.1.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.2.av_class}}">{{data.appointments.list.2.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.2.name}}</span><span class="r">{{data.appointments.list.2.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.2.date}}</span><span class="t">{{data.appointments.list.2.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.3.av_class}}">{{data.appointments.list.3.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.3.name}}</span><span class="r">{{data.appointments.list.3.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.3.date}}</span><span class="t">{{data.appointments.list.3.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.4.av_class}}">{{data.appointments.list.4.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.4.name}}</span><span class="r">{{data.appointments.list.4.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.4.date}}</span><span class="t">{{data.appointments.list.4.time}}</span></div>
</div>
</div>
</section>
</main>
</body>
</html>