@twentyfirstmedia/ui-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/household.d.ts +19 -0
- package/dist/components/household.d.ts.map +1 -0
- package/dist/components/household.js +137 -0
- package/dist/components/household.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/life.d.ts +26 -0
- package/dist/components/life.d.ts.map +1 -0
- package/dist/components/life.js +186 -0
- package/dist/components/life.js.map +1 -0
- package/dist/components/types.d.ts +332 -0
- package/dist/components/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.d.ts +96 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +73 -1
- package/dist/strings.js.map +1 -1
- package/dist/themes.css +14121 -11161
- package/package.json +1 -1
- package/src/components/household.tsx +489 -0
- package/src/components/index.ts +22 -0
- package/src/components/life.tsx +672 -0
- package/src/components/types.ts +322 -0
- package/src/index.ts +2 -0
- package/src/strings.ts +241 -0
package/package.json
CHANGED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cx } from "../cx.js";
|
|
4
|
+
import { useFormat, useStrings } from "../context.js";
|
|
5
|
+
import { HOUSEHOLD_STRINGS } from "../strings.js";
|
|
6
|
+
import { Icon } from "../Icon.js";
|
|
7
|
+
import { registerIcons } from "../icons.js";
|
|
8
|
+
import { landmark, receipt, repeat, umbrella } from "../icon-library.js";
|
|
9
|
+
import { Badge, IconButton, TintIcon } from "./foundation.js";
|
|
10
|
+
import { Sparkline } from "./charts.js";
|
|
11
|
+
import { StatusIndicator } from "./status.js";
|
|
12
|
+
import type {
|
|
13
|
+
BudgetListProps,
|
|
14
|
+
ClaimTimelineProps,
|
|
15
|
+
CoverageMatrixProps,
|
|
16
|
+
DeadlineListProps,
|
|
17
|
+
DocumentListProps,
|
|
18
|
+
NetWorthCardProps,
|
|
19
|
+
PolicyCardProps,
|
|
20
|
+
SubscriptionListProps,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
// default icons of these components come from the icon library
|
|
24
|
+
registerIcons({ landmark, receipt, repeat, umbrella });
|
|
25
|
+
|
|
26
|
+
const DAY = 86400000;
|
|
27
|
+
/** Whole days from `from` to `to`, counted on the calendar (negative when `to` is earlier). */
|
|
28
|
+
function daysBetween(from: Date | string | number, to: Date | string | number): number {
|
|
29
|
+
const a = new Date(from),
|
|
30
|
+
b = new Date(to);
|
|
31
|
+
const ua = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()),
|
|
32
|
+
ub = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
|
33
|
+
return Math.round((ub - ua) / DAY);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── BudgetList ── */
|
|
37
|
+
|
|
38
|
+
/** Spending per category against its monthly limit, with what is left or over in words. */
|
|
39
|
+
export function BudgetList(p: BudgetListProps) {
|
|
40
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
41
|
+
const { formatMoney } = useFormat();
|
|
42
|
+
const money = (n: number) => formatMoney(n, p.currency, { maximumFractionDigits: 0 });
|
|
43
|
+
return (
|
|
44
|
+
<ul style={p.style} className={cx("tf-budgets", p.className)} aria-label={p.label || t.budgets}>
|
|
45
|
+
{p.categories.map((c) => {
|
|
46
|
+
const pct = c.limit ? (c.spent / c.limit) * 100 : 0;
|
|
47
|
+
const over = c.spent > c.limit;
|
|
48
|
+
const tone = over ? "blush" : pct >= 85 ? "butter" : c.tint || "mint";
|
|
49
|
+
const body = (
|
|
50
|
+
<>
|
|
51
|
+
<TintIcon icon={c.icon || "wallet"} tint={c.tint || "sky"} size={32} />
|
|
52
|
+
<span className="tf-budget-main">
|
|
53
|
+
<span className="tf-budget-top">
|
|
54
|
+
<b>{c.label}</b>
|
|
55
|
+
<span className="tf-budget-nums">
|
|
56
|
+
{money(c.spent)} <small>/ {money(c.limit)}</small>
|
|
57
|
+
</span>
|
|
58
|
+
</span>
|
|
59
|
+
<span
|
|
60
|
+
className="tf-budget-bar"
|
|
61
|
+
role="progressbar"
|
|
62
|
+
aria-label={c.label}
|
|
63
|
+
aria-valuemin={0}
|
|
64
|
+
aria-valuemax={100}
|
|
65
|
+
aria-valuenow={Math.min(100, Math.round(pct))}
|
|
66
|
+
aria-valuetext={`${money(c.spent)} / ${money(c.limit)}`}
|
|
67
|
+
>
|
|
68
|
+
<span className={"tf-sf-" + tone} style={{ width: Math.min(100, pct) + "%" }} />
|
|
69
|
+
</span>
|
|
70
|
+
<small className={cx("tf-budget-left", over && "over")}>
|
|
71
|
+
{over
|
|
72
|
+
? t.budgetOver(money(c.spent - c.limit))
|
|
73
|
+
: t.budgetLeft(money(c.limit - c.spent))}
|
|
74
|
+
</small>
|
|
75
|
+
</span>
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
return (
|
|
79
|
+
<li key={c.id}>
|
|
80
|
+
{p.onSelect ? (
|
|
81
|
+
<button type="button" className="tf-budget" onClick={() => p.onSelect?.(c.id)}>
|
|
82
|
+
{body}
|
|
83
|
+
</button>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="tf-budget">{body}</div>
|
|
86
|
+
)}
|
|
87
|
+
</li>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</ul>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── SubscriptionList ── */
|
|
95
|
+
|
|
96
|
+
const PER_YEAR = { week: 52, month: 12, quarter: 4, year: 1 } as const;
|
|
97
|
+
|
|
98
|
+
/** Recurring payments with their next charge, and what they cost a month and a year together. */
|
|
99
|
+
export function SubscriptionList(p: SubscriptionListProps) {
|
|
100
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
101
|
+
const { formatMoney, formatDate } = useFormat();
|
|
102
|
+
const money = (n: number, d = 2) =>
|
|
103
|
+
formatMoney(n, p.currency, { minimumFractionDigits: d, maximumFractionDigits: d });
|
|
104
|
+
const active = p.items.filter((s) => s.status !== "cancelled");
|
|
105
|
+
const year = active.reduce((sum, s) => sum + s.amount * PER_YEAR[s.period], 0);
|
|
106
|
+
return (
|
|
107
|
+
<div style={p.style} className={cx("tf-subs", p.className)}>
|
|
108
|
+
<ul aria-label={p.label || t.subscriptions}>
|
|
109
|
+
{p.items.map((s) => (
|
|
110
|
+
<li key={s.id} className={cx("tf-sub", s.status === "cancelled" && "is-cancelled")}>
|
|
111
|
+
{s.logo || <TintIcon icon={s.icon || "repeat"} tint="lilac" size={32} />}
|
|
112
|
+
<span className="tf-sub-text">
|
|
113
|
+
<b>{s.name}</b>
|
|
114
|
+
<small>
|
|
115
|
+
{[
|
|
116
|
+
s.category,
|
|
117
|
+
s.status === "cancelled"
|
|
118
|
+
? t.cancelledLabel
|
|
119
|
+
: s.next != null &&
|
|
120
|
+
t.nextCharge(formatDate(s.next, { day: "numeric", month: "short" })),
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(" · ")}
|
|
124
|
+
</small>
|
|
125
|
+
</span>
|
|
126
|
+
{s.status === "trial" && <Badge tone="info">{t.trialEnds}</Badge>}
|
|
127
|
+
<span className="tf-sub-amount">
|
|
128
|
+
{money(s.amount)}
|
|
129
|
+
<small>/ {t.periodWord[s.period]}</small>
|
|
130
|
+
</span>
|
|
131
|
+
{p.onManage && (
|
|
132
|
+
<IconButton
|
|
133
|
+
icon="more"
|
|
134
|
+
label={t.manageItem(s.name)}
|
|
135
|
+
size="sm"
|
|
136
|
+
variant="ghost"
|
|
137
|
+
onClick={() => p.onManage?.(s.id)}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
{p.totals !== false && (
|
|
144
|
+
<div className="tf-subs-total">
|
|
145
|
+
<b>{t.perMonthTotal(money(year / 12))}</b>
|
|
146
|
+
<span>{t.perYearTotal(money(year, 0))}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── NetWorthCard ── */
|
|
154
|
+
|
|
155
|
+
/** What you own minus what you owe, its trend, and the parts on both sides. */
|
|
156
|
+
export function NetWorthCard(p: NetWorthCardProps) {
|
|
157
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
158
|
+
const { formatMoney } = useFormat();
|
|
159
|
+
const money = (n: number) => formatMoney(n, p.currency, { maximumFractionDigits: 0 });
|
|
160
|
+
const assets = p.assets.reduce((s, a) => s + a.value, 0);
|
|
161
|
+
const debts = (p.liabilities || []).reduce((s, a) => s + a.value, 0);
|
|
162
|
+
const side = (title: string, list: NetWorthCardProps["assets"], total: number) => (
|
|
163
|
+
<div className="tf-nw-side">
|
|
164
|
+
<div className="tf-nw-sidehead">
|
|
165
|
+
<span>{title}</span>
|
|
166
|
+
<b>{money(total)}</b>
|
|
167
|
+
</div>
|
|
168
|
+
<ul>
|
|
169
|
+
{list.map((a) => (
|
|
170
|
+
<li key={a.label}>
|
|
171
|
+
<span>
|
|
172
|
+
{a.icon && <Icon name={a.icon} size={14} />}
|
|
173
|
+
{a.label}
|
|
174
|
+
</span>
|
|
175
|
+
<span>{money(a.value)}</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
return (
|
|
182
|
+
<div style={p.style} className={cx("tf-nw", p.surface && "tf-" + p.surface, p.className)}>
|
|
183
|
+
<div className="tf-nw-head">
|
|
184
|
+
<div className="tf-col" style={{ gap: 2, minWidth: 0 }}>
|
|
185
|
+
<span className="tf-nw-title">{p.title || t.netWorth}</span>
|
|
186
|
+
<span className="tf-nw-total">{money(assets - debts)}</span>
|
|
187
|
+
{p.change != null && (
|
|
188
|
+
<span className={cx("tf-nw-change", p.change < 0 ? "down" : "up")}>
|
|
189
|
+
<Icon name="arrowUp" size={12} />
|
|
190
|
+
{(p.change > 0 ? "+" : "") + money(p.change)}
|
|
191
|
+
{p.changeLabel && <span> {p.changeLabel}</span>}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
{p.history && p.history.length > 1 && (
|
|
196
|
+
<Sparkline
|
|
197
|
+
values={p.history}
|
|
198
|
+
width={140}
|
|
199
|
+
height={48}
|
|
200
|
+
tint={(p.change ?? 0) < 0 ? "blush" : "mint"}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
<div className="tf-nw-sides">
|
|
205
|
+
{side(t.assetsLabel, p.assets, assets)}
|
|
206
|
+
{p.liabilities &&
|
|
207
|
+
p.liabilities.length > 0 &&
|
|
208
|
+
side(t.liabilitiesLabel, p.liabilities, debts)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── DeadlineList ── */
|
|
215
|
+
|
|
216
|
+
/** Bills, renewals, cancellation dates and expiring documents in date order, the urgent first. */
|
|
217
|
+
export function DeadlineList(p: DeadlineListProps) {
|
|
218
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
219
|
+
const { formatMoney, formatDate } = useFormat();
|
|
220
|
+
const today = p.today ?? Date.now();
|
|
221
|
+
const soon = p.soon ?? 14;
|
|
222
|
+
const items = [...p.items].sort(
|
|
223
|
+
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
|
224
|
+
);
|
|
225
|
+
return (
|
|
226
|
+
<ul
|
|
227
|
+
style={p.style}
|
|
228
|
+
className={cx("tf-deadlines", p.className)}
|
|
229
|
+
aria-label={p.label || t.deadlines}
|
|
230
|
+
>
|
|
231
|
+
{items.map((d) => {
|
|
232
|
+
const days = daysBetween(today, d.date);
|
|
233
|
+
const st = d.done ? "done" : days < 0 ? "overdue" : days <= soon ? "soon" : "later";
|
|
234
|
+
const when = days < 0 ? t.overdueBy(-days) : days === 0 ? t.dueToday : t.dueIn(days);
|
|
235
|
+
return (
|
|
236
|
+
<li key={d.id} className={cx("tf-deadline", "is-" + st)}>
|
|
237
|
+
<span className="tf-deadline-date">
|
|
238
|
+
<b>{formatDate(d.date, { day: "numeric" })}</b>
|
|
239
|
+
<small>{formatDate(d.date, { month: "short" })}</small>
|
|
240
|
+
</span>
|
|
241
|
+
<span className="tf-deadline-text">
|
|
242
|
+
<b>{d.title}</b>
|
|
243
|
+
<small>{[d.kind, d.note].filter(Boolean).join(" · ")}</small>
|
|
244
|
+
</span>
|
|
245
|
+
{d.amount != null && (
|
|
246
|
+
<span className="tf-deadline-amount">{formatMoney(d.amount, p.currency)}</span>
|
|
247
|
+
)}
|
|
248
|
+
{!d.done && (
|
|
249
|
+
<Badge tone={st === "overdue" ? "danger" : st === "soon" ? "warning" : "neutral"}>
|
|
250
|
+
{when}
|
|
251
|
+
</Badge>
|
|
252
|
+
)}
|
|
253
|
+
{p.onDone && !d.done && (
|
|
254
|
+
<IconButton
|
|
255
|
+
icon="check"
|
|
256
|
+
label={t.markDone(d.title)}
|
|
257
|
+
size="sm"
|
|
258
|
+
onClick={() => p.onDone?.(d.id)}
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
261
|
+
{d.done && <Icon name="check" size={16} className="tf-deadline-done" />}
|
|
262
|
+
</li>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</ul>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ── PolicyCard ── */
|
|
270
|
+
|
|
271
|
+
const POLICY_STATUS = {
|
|
272
|
+
active: "ok",
|
|
273
|
+
pending: "running",
|
|
274
|
+
expiring: "warn",
|
|
275
|
+
lapsed: "danger",
|
|
276
|
+
} as const;
|
|
277
|
+
|
|
278
|
+
/** One insurance policy: type, insurer, sum insured, premium, deductible, renewal and status. */
|
|
279
|
+
export function PolicyCard(p: PolicyCardProps) {
|
|
280
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
281
|
+
const { formatMoney, formatDate } = useFormat();
|
|
282
|
+
const money = (n: number) => formatMoney(n, p.currency, { maximumFractionDigits: 0 });
|
|
283
|
+
const st = p.status || "active";
|
|
284
|
+
const no = p.policyNumber && p.maskNumber ? "•••• " + p.policyNumber.slice(-4) : p.policyNumber;
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
style={p.style}
|
|
288
|
+
className={cx("tf-policy", p.surface && "tf-" + p.surface, "is-" + st, p.className)}
|
|
289
|
+
>
|
|
290
|
+
<div className="tf-policy-head">
|
|
291
|
+
<TintIcon icon={p.icon || "umbrella"} tint={p.tint || "sky"} size={40} />
|
|
292
|
+
<div className="tf-option-text">
|
|
293
|
+
<b>{p.type}</b>
|
|
294
|
+
<small>{p.insurer}</small>
|
|
295
|
+
</div>
|
|
296
|
+
<StatusIndicator status={POLICY_STATUS[st]} label={t.policyStatus[st]} size="sm" />
|
|
297
|
+
</div>
|
|
298
|
+
<dl className="tf-policy-facts">
|
|
299
|
+
{p.coverage != null && (
|
|
300
|
+
<div>
|
|
301
|
+
<dt>{t.sumInsured}</dt>
|
|
302
|
+
<dd>{typeof p.coverage === "number" ? money(p.coverage) : p.coverage}</dd>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
{p.premium && (
|
|
306
|
+
<div>
|
|
307
|
+
<dt>{t.periodWord[p.premium.period]}</dt>
|
|
308
|
+
<dd>{formatMoney(p.premium.amount, p.currency)}</dd>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
{p.deductible != null && (
|
|
312
|
+
<div>
|
|
313
|
+
<dt>{t.deductibleLabel}</dt>
|
|
314
|
+
<dd>{money(p.deductible)}</dd>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
{no && (
|
|
318
|
+
<div>
|
|
319
|
+
<dt>{t.policyNo}</dt>
|
|
320
|
+
<dd className="tf-mono">{no}</dd>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</dl>
|
|
324
|
+
{(p.renews != null || p.actions) && (
|
|
325
|
+
<div className="tf-policy-foot">
|
|
326
|
+
{p.renews != null && (
|
|
327
|
+
<span>
|
|
328
|
+
{t.renewsOn(
|
|
329
|
+
formatDate(p.renews, { day: "numeric", month: "short", year: "numeric" }),
|
|
330
|
+
)}
|
|
331
|
+
</span>
|
|
332
|
+
)}
|
|
333
|
+
{p.actions && <span className="tf-policy-actions">{p.actions}</span>}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* ── CoverageMatrix ── */
|
|
341
|
+
|
|
342
|
+
const COVER_STATUS = { covered: "ok", partial: "warn", none: "danger" } as const;
|
|
343
|
+
|
|
344
|
+
/** Life risks and whether a policy covers them: covered, partly, not covered, with notes. */
|
|
345
|
+
export function CoverageMatrix(p: CoverageMatrixProps) {
|
|
346
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
347
|
+
const words = { covered: t.covered, partial: t.partlyCovered, none: t.notCovered };
|
|
348
|
+
const counts = { covered: 0, partial: 0, none: 0 };
|
|
349
|
+
p.rows.forEach((r) => counts[r.status]++);
|
|
350
|
+
return (
|
|
351
|
+
<div style={p.style} className={cx("tf-coverage", p.className)}>
|
|
352
|
+
<div className="tf-coverage-sum" aria-hidden>
|
|
353
|
+
{(["covered", "partial", "none"] as const).map((k) => (
|
|
354
|
+
<span key={k}>
|
|
355
|
+
<StatusIndicator
|
|
356
|
+
status={COVER_STATUS[k]}
|
|
357
|
+
label={`${counts[k]} ${words[k]}`}
|
|
358
|
+
size="sm"
|
|
359
|
+
/>
|
|
360
|
+
</span>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
<div className="tf-coverage-box">
|
|
364
|
+
<table className="tf-coverage-table">
|
|
365
|
+
<caption className="tf-sidebar-sr">{p.label || t.coverageLabel}</caption>
|
|
366
|
+
<tbody>
|
|
367
|
+
{p.rows.map((r) => (
|
|
368
|
+
<tr key={r.id} className={"is-" + r.status}>
|
|
369
|
+
<th scope="row">
|
|
370
|
+
<b>{r.risk}</b>
|
|
371
|
+
{r.policy && <small>{r.policy}</small>}
|
|
372
|
+
</th>
|
|
373
|
+
<td>
|
|
374
|
+
<StatusIndicator
|
|
375
|
+
status={COVER_STATUS[r.status]}
|
|
376
|
+
label={words[r.status]}
|
|
377
|
+
size="sm"
|
|
378
|
+
/>
|
|
379
|
+
</td>
|
|
380
|
+
<td className="tf-coverage-note">{r.note}</td>
|
|
381
|
+
</tr>
|
|
382
|
+
))}
|
|
383
|
+
</tbody>
|
|
384
|
+
</table>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── ClaimTimeline ── */
|
|
391
|
+
|
|
392
|
+
/** An insurance claim from report to payout as steps with dates. */
|
|
393
|
+
export function ClaimTimeline(p: ClaimTimelineProps) {
|
|
394
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
395
|
+
const { formatMoney, formatDate } = useFormat();
|
|
396
|
+
return (
|
|
397
|
+
<div style={p.style} className={cx("tf-claim", p.className)}>
|
|
398
|
+
<div className="tf-claim-head">
|
|
399
|
+
<div className="tf-option-text">
|
|
400
|
+
<b>{p.title}</b>
|
|
401
|
+
{p.meta && <small>{p.meta}</small>}
|
|
402
|
+
</div>
|
|
403
|
+
{p.amount != null && (
|
|
404
|
+
<span className="tf-claim-amount">
|
|
405
|
+
<small>{t.claimAmount}</small>
|
|
406
|
+
{formatMoney(p.amount, p.currency)}
|
|
407
|
+
</span>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
<ol className="tf-claim-steps">
|
|
411
|
+
{p.steps.map((s, i) => (
|
|
412
|
+
<li
|
|
413
|
+
key={i}
|
|
414
|
+
className={"is-" + s.state}
|
|
415
|
+
aria-current={s.state === "current" ? "step" : undefined}
|
|
416
|
+
>
|
|
417
|
+
<span className="tf-claim-dot" aria-hidden>
|
|
418
|
+
{s.state === "done" && <Icon name="check" size={10} />}
|
|
419
|
+
{s.state === "failed" && <Icon name="x" size={10} />}
|
|
420
|
+
</span>
|
|
421
|
+
<span className="tf-claim-text">
|
|
422
|
+
<b>{s.label}</b>
|
|
423
|
+
{(s.date != null || s.note) && (
|
|
424
|
+
<small>
|
|
425
|
+
{[
|
|
426
|
+
s.date != null && formatDate(s.date, { day: "numeric", month: "short" }),
|
|
427
|
+
s.note,
|
|
428
|
+
]
|
|
429
|
+
.filter(Boolean)
|
|
430
|
+
.map((x, k) => (
|
|
431
|
+
<React.Fragment key={k}>
|
|
432
|
+
{k > 0 && " · "}
|
|
433
|
+
{x}
|
|
434
|
+
</React.Fragment>
|
|
435
|
+
))}
|
|
436
|
+
</small>
|
|
437
|
+
)}
|
|
438
|
+
</span>
|
|
439
|
+
</li>
|
|
440
|
+
))}
|
|
441
|
+
</ol>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* ── DocumentList ── */
|
|
447
|
+
|
|
448
|
+
/** Important papers (IDs, contracts, policies, certificates) with holder and expiry. */
|
|
449
|
+
export function DocumentList(p: DocumentListProps) {
|
|
450
|
+
const t = useStrings(HOUSEHOLD_STRINGS);
|
|
451
|
+
const { formatDate } = useFormat();
|
|
452
|
+
const today = p.today ?? Date.now();
|
|
453
|
+
const soon = p.soon ?? 60;
|
|
454
|
+
return (
|
|
455
|
+
<ul style={p.style} className={cx("tf-docs", p.className)} aria-label={p.label || t.documents}>
|
|
456
|
+
{p.documents.map((d) => {
|
|
457
|
+
const days = d.expires != null ? daysBetween(today, d.expires) : null;
|
|
458
|
+
const st = days == null ? null : days < 0 ? "expired" : days <= soon ? "soon" : "ok";
|
|
459
|
+
const body = (
|
|
460
|
+
<>
|
|
461
|
+
<TintIcon icon={d.icon || "file"} tint={st === "expired" ? "blush" : "sky"} size={36} />
|
|
462
|
+
<span className="tf-doc-text">
|
|
463
|
+
<b>{d.title}</b>
|
|
464
|
+
<small>{[d.category, d.holder, d.meta].filter(Boolean).join(" · ")}</small>
|
|
465
|
+
</span>
|
|
466
|
+
{st === "expired" && <Badge tone="danger">{t.expired}</Badge>}
|
|
467
|
+
{st === "soon" && days != null && <Badge tone="warning">{t.expiresIn(days)}</Badge>}
|
|
468
|
+
{st === "ok" && d.expires != null && (
|
|
469
|
+
<span className="tf-doc-valid">
|
|
470
|
+
{t.expiresOn(formatDate(d.expires, { month: "short", year: "numeric" }))}
|
|
471
|
+
</span>
|
|
472
|
+
)}
|
|
473
|
+
</>
|
|
474
|
+
);
|
|
475
|
+
return (
|
|
476
|
+
<li key={d.id}>
|
|
477
|
+
{p.onOpen ? (
|
|
478
|
+
<button type="button" className="tf-doc" onClick={() => p.onOpen?.(d.id)}>
|
|
479
|
+
{body}
|
|
480
|
+
</button>
|
|
481
|
+
) : (
|
|
482
|
+
<div className="tf-doc">{body}</div>
|
|
483
|
+
)}
|
|
484
|
+
</li>
|
|
485
|
+
);
|
|
486
|
+
})}
|
|
487
|
+
</ul>
|
|
488
|
+
);
|
|
489
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -22,6 +22,28 @@ export {
|
|
|
22
22
|
ColorField,
|
|
23
23
|
} from "./admin.js";
|
|
24
24
|
export { QrCode } from "./qr-code.js";
|
|
25
|
+
export {
|
|
26
|
+
HabitTracker,
|
|
27
|
+
MoodPicker,
|
|
28
|
+
VitalTile,
|
|
29
|
+
ActivityRings,
|
|
30
|
+
SleepChart,
|
|
31
|
+
MedicationList,
|
|
32
|
+
JournalEntry,
|
|
33
|
+
GoalCard,
|
|
34
|
+
InsightCard,
|
|
35
|
+
ConsentList,
|
|
36
|
+
} from "./life.js";
|
|
37
|
+
export {
|
|
38
|
+
BudgetList,
|
|
39
|
+
SubscriptionList,
|
|
40
|
+
NetWorthCard,
|
|
41
|
+
DeadlineList,
|
|
42
|
+
PolicyCard,
|
|
43
|
+
CoverageMatrix,
|
|
44
|
+
ClaimTimeline,
|
|
45
|
+
DocumentList,
|
|
46
|
+
} from "./household.js";
|
|
25
47
|
export {
|
|
26
48
|
AgentCard,
|
|
27
49
|
AgentDetails,
|