Rental
Approval Card
One thing waiting on the owner — a repair quote, a fee waiver, a refund, a new tenancy — with what it costs, who asked, and the decision to make.
Approving is one press. Rejecting asks why, because the person who asked
will read the answer; require-reason="all" asks on approval too, for a
portfolio that wants every decision explained. Once decided, the card
becomes the record of the decision: who made it, when, and what they said.
The card does not decide anything itself. It fires the decision; the app
saves it and sets status. Set busy while the save is in flight.
Preview
Section titled “Preview”<nana-approval-card
kind="Repair quote"
heading="Replace the geyser in Unit 4B"
amount="18500"
currency="KES"
requester="Otieno, caretaker"
requested-at="2026-10-07T08:30:00Z"
>
The element has failed twice this month. Quote from Jua Plumbers, fitted
Thursday.
</nana-approval-card> Rejecting
Section titled “Rejecting”Reject opens a field for the reason and will not send without one: the
caretaker who asked will read it. Escape or Cancel puts the buttons back.
allow-changes adds a third answer, for a request that is nearly right.
<nana-approval-card
kind="Fee waiver"
heading="Waive the October late fee for Unit 1B"
amount="1500"
requester="Otieno S., tenant"
requested-at="2026-10-06T12:10:00Z"
due="2026-10-09"
allow-changes
style="max-width: 32rem"
>
Paid on the 6th; the M-Pesa reversal on the 5th was the bank's error.
Statement attached.
</nana-approval-card> Once decided
Section titled “Once decided”The app saves the decision and sets status, decided-by, decided-at and
decision-note. The buttons give way to the record.
<nana-approval-card
kind="Repair quote"
heading="Replace the geyser in Unit 4B"
amount="18500"
requester="Otieno, caretaker"
requested-at="2026-10-07T08:30:00Z"
status="rejected"
decided-by="Mary N."
decided-at="2026-10-07T18:02:00Z"
decision-note="Get a second quote. Last year's was 12,000 for the same unit."
style="max-width: 32rem"
></nana-approval-card> Wiring it up
Section titled “Wiring it up”const card = document.querySelector("nana-approval-card");
card.addEventListener("nana-approve", async (e) => { card.busy = true; await api.decide(id, "approved", e.detail.reason); Object.assign(card, { busy: false, status: "approved", decidedBy: me.name, decidedAt: new Date().toISOString() });});card.addEventListener("nana-reject", (e) => api.decide(id, "rejected", e.detail.reason));require-reason decides which answers need words: reject (the default),
all, or none. Asking for changes always does.
Installation
Section titled “Installation”npm install @nana-tec/ui-componentsOr copy the source in and own it:
npx @nana-tec/ui add approval-cardimport "@nana-tec/ui-components/approval-card";<nana-approval-card kind="Repair quote" heading="Replace the geyser in Unit 4B" amount="18500" currency="KES" requester="Otieno, caretaker" requested-at="2026-10-07T08:30:00Z"> The element has failed twice this month. Quote from Jua Plumbers, fitted Thursday.</nana-approval-card>const el = document.querySelector("nana-approval-card");el.addEventListener("nana-approve", (e) => console.log(e.detail));el.addEventListener("nana-reject", (e) => console.log(e.detail));el.addEventListener("nana-request-changes", (e) => console.log(e.detail));Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
actionsAlign | actions-align | 'start' | 'end' | 'end' | Which side the decision buttons sit on |
allowChanges | allow-changes | boolean | false | Offer to send the request back with changes, as well as approve and reject |
amount | amount | number | undefined | — | What it costs; leave empty for a request with no amount |
busy | busy | boolean | false | A decision is being saved |
currency | currency | string | 'KES' | |
decidedAt | decided-at | string | '' | When the decision was made, as an ISO date |
decidedBy | decided-by | string | '' | Who made the decision |
decisionNote | decision-note | string | '' | What was said with the decision |
due | due | string | '' | When a decision is needed by, as an ISO date |
heading | heading | string | '' | The request in a line |
headingLevel | heading-level | number | 3 | Level of the heading in the page's outline |
kind | kind | string | '' | What kind of request this is |
requestedAt | requested-at | string | '' | When they asked, as an ISO date |
requester | requester | string | '' | Who asked |
requireReason | require-reason | ReasonRule | 'reject' | Which decisions need a reason |
status | status | ApprovalStatus | 'pending' | Where the request stands |
Events
| Event | Description |
|---|---|
nana-approve | The owner approved (detail.reason) |
nana-reject | The owner rejected (detail.reason) |
nana-request-changes | The owner sent it back with changes to make (detail.reason) |
Slots
| Slot | Description |
|---|---|
(default) | What is being asked for, in detail |
attachments | Quotes, photographs, invoices that support the request |
actions | Further controls beside the decision buttons |
CSS parts
| Part | Description |
|---|---|
::part(base) | The card |
::part(header) | The kind, heading and status row |
::part(kind) | The kind of request |
::part(heading) | The request's heading |
::part(status) | The status chip |
::part(amount) | The amount being asked for |
::part(requester) | Who asked, and when |
::part(body) | The request's detail |
::part(attachments) | The supporting documents |
::part(actions) | The decision buttons |
::part(approve) | The approve button |
::part(reject) | The reject button |
::part(changes) | The ask-for-changes button |
::part(composer) | The reason form |
::part(reason) | The reason field |
::part(error) | The message when a reason is missing |
::part(decision) | The record of the decision once made |
CSS custom properties
| Property | Description |
|---|---|
--nana-approval-card-bg | Card background |
--nana-approval-card-border | Card border colour |
--nana-approval-card-accent | The stripe that marks a request awaiting a decision |
--nana-press-scale | How far the buttons depress on press, shared with the other pressable controls |