@twentyfirstmedia/ui-core 0.5.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/admin.d.ts.map +1 -1
- package/dist/components/admin.js +31 -21
- package/dist/components/admin.js.map +1 -1
- package/dist/components/agents.d.ts.map +1 -1
- package/dist/components/agents.js +21 -18
- package/dist/components/agents.js.map +1 -1
- package/dist/components/app-chrome.d.ts.map +1 -1
- package/dist/components/app-chrome.js +5 -5
- package/dist/components/app-chrome.js.map +1 -1
- package/dist/components/approvals.d.ts.map +1 -1
- package/dist/components/approvals.js +2 -2
- package/dist/components/approvals.js.map +1 -1
- package/dist/components/building-blocks.d.ts.map +1 -1
- package/dist/components/building-blocks.js +13 -10
- package/dist/components/building-blocks.js.map +1 -1
- package/dist/components/charts.d.ts +2 -2
- package/dist/components/charts.d.ts.map +1 -1
- package/dist/components/charts.js +14 -11
- package/dist/components/charts.js.map +1 -1
- package/dist/components/chat.d.ts +2 -2
- package/dist/components/chat.d.ts.map +1 -1
- package/dist/components/chat.js +22 -18
- package/dist/components/chat.js.map +1 -1
- package/dist/components/commerce.d.ts.map +1 -1
- package/dist/components/commerce.js +20 -16
- package/dist/components/commerce.js.map +1 -1
- package/dist/components/data-display.d.ts +0 -5
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +41 -22
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/data-table.d.ts +2 -2
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +7 -6
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +5 -4
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +63 -21
- package/dist/components/forms.js.map +1 -1
- package/dist/components/foundation.d.ts.map +1 -1
- package/dist/components/foundation.js +13 -9
- package/dist/components/foundation.js.map +1 -1
- 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 +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/inputs.d.ts.map +1 -1
- package/dist/components/inputs.js +33 -8
- package/dist/components/inputs.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/mail.d.ts.map +1 -1
- package/dist/components/mail.js +5 -5
- package/dist/components/mail.js.map +1 -1
- package/dist/components/media.d.ts.map +1 -1
- package/dist/components/media.js +62 -11
- package/dist/components/media.js.map +1 -1
- package/dist/components/navigation.d.ts +3 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/navigation.js +287 -20
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/operations.d.ts.map +1 -1
- package/dist/components/operations.js +10 -10
- package/dist/components/operations.js.map +1 -1
- package/dist/components/overlays.d.ts.map +1 -1
- package/dist/components/overlays.js +2 -2
- package/dist/components/overlays.js.map +1 -1
- package/dist/components/previews.js +2 -2
- package/dist/components/previews.js.map +1 -1
- package/dist/components/primitives.js +2 -2
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/qr-code.d.ts +8 -0
- package/dist/components/qr-code.d.ts.map +1 -0
- package/dist/components/qr-code.js +18 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/real-estate.d.ts.map +1 -1
- package/dist/components/real-estate.js +5 -5
- package/dist/components/real-estate.js.map +1 -1
- package/dist/components/rich-text-editor.d.ts.map +1 -1
- package/dist/components/rich-text-editor.js +1 -1
- package/dist/components/rich-text-editor.js.map +1 -1
- package/dist/components/select-menu.js +1 -1
- package/dist/components/select-menu.js.map +1 -1
- package/dist/components/shell.d.ts +0 -4
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +100 -7
- package/dist/components/shell.js.map +1 -1
- package/dist/components/site.d.ts.map +1 -1
- package/dist/components/site.js +8 -6
- package/dist/components/site.js.map +1 -1
- package/dist/components/status.d.ts.map +1 -1
- package/dist/components/status.js +2 -2
- package/dist/components/status.js.map +1 -1
- package/dist/components/system.d.ts.map +1 -1
- package/dist/components/system.js +3 -2
- package/dist/components/system.js.map +1 -1
- package/dist/components/types.d.ts +589 -18
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +15 -12
- package/dist/components/workspace.js.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/qr.d.ts +14 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +298 -0
- package/dist/qr.js.map +1 -0
- package/dist/strings.d.ts +126 -4
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +88 -1
- package/dist/strings.js.map +1 -1
- package/dist/themes.css +15592 -10463
- package/package.json +2 -1
- package/src/components/admin.tsx +66 -23
- package/src/components/agents.tsx +22 -20
- package/src/components/app-chrome.tsx +9 -5
- package/src/components/approvals.tsx +3 -1
- package/src/components/building-blocks.tsx +24 -13
- package/src/components/charts.tsx +17 -12
- package/src/components/chat.tsx +54 -31
- package/src/components/commerce.tsx +58 -24
- package/src/components/data-display.tsx +106 -38
- package/src/components/data-table.tsx +11 -7
- package/src/components/developer.tsx +15 -6
- package/src/components/feedback.tsx +5 -3
- package/src/components/forms.tsx +101 -23
- package/src/components/foundation.tsx +9 -5
- package/src/components/household.tsx +489 -0
- package/src/components/index.ts +32 -1
- package/src/components/inputs.tsx +44 -7
- package/src/components/life.tsx +672 -0
- package/src/components/mail.tsx +7 -3
- package/src/components/media.tsx +88 -16
- package/src/components/navigation.tsx +651 -71
- package/src/components/operations.tsx +17 -8
- package/src/components/overlays.tsx +7 -2
- package/src/components/previews.tsx +2 -2
- package/src/components/primitives.tsx +2 -2
- package/src/components/qr-code.tsx +30 -0
- package/src/components/real-estate.tsx +7 -4
- package/src/components/rich-text-editor.tsx +7 -1
- package/src/components/select-menu.tsx +1 -1
- package/src/components/shell.tsx +237 -11
- package/src/components/site.tsx +18 -4
- package/src/components/status.tsx +2 -0
- package/src/components/system.tsx +3 -2
- package/src/components/types.ts +583 -18
- package/src/components/workspace.tsx +37 -15
- package/src/index.ts +2 -0
- package/src/qr.ts +300 -0
- package/src/strings.ts +297 -1
|
@@ -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
|
@@ -21,6 +21,29 @@ export {
|
|
|
21
21
|
TwoFactorSetup,
|
|
22
22
|
ColorField,
|
|
23
23
|
} from "./admin.js";
|
|
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";
|
|
24
47
|
export {
|
|
25
48
|
AgentCard,
|
|
26
49
|
AgentDetails,
|
|
@@ -157,7 +180,15 @@ export {
|
|
|
157
180
|
AttachmentPreview,
|
|
158
181
|
} from "./mail.js";
|
|
159
182
|
export { ImageVariants, Gallery, BeforeAfter } from "./media.js";
|
|
160
|
-
export {
|
|
183
|
+
export {
|
|
184
|
+
Tabs,
|
|
185
|
+
Breadcrumb,
|
|
186
|
+
Pagination,
|
|
187
|
+
Menubar,
|
|
188
|
+
NavigationMenu,
|
|
189
|
+
Sidebar,
|
|
190
|
+
UserMenu,
|
|
191
|
+
} from "./navigation.js";
|
|
161
192
|
export {
|
|
162
193
|
ConnectionCard,
|
|
163
194
|
ConnectionBanner,
|
|
@@ -129,24 +129,60 @@ export function Textarea(p: TextareaProps) {
|
|
|
129
129
|
|
|
130
130
|
export function SearchField(p: SearchFieldProps) {
|
|
131
131
|
const t = useStrings();
|
|
132
|
+
const input = React.useRef<HTMLInputElement>(null);
|
|
133
|
+
// ⌘K / Ctrl+K: the last character of the hint is the key
|
|
134
|
+
const key = p.bindShortcut && p.shortcut ? p.shortcut.trim().slice(-1).toLowerCase() : "";
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (!key) return;
|
|
137
|
+
const k = (e: KeyboardEvent) => {
|
|
138
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey || e.key.toLowerCase() !== key) return;
|
|
139
|
+
const el = e.target as HTMLElement | null;
|
|
140
|
+
const typing =
|
|
141
|
+
!!el &&
|
|
142
|
+
el !== input.current &&
|
|
143
|
+
(el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName));
|
|
144
|
+
if (typing) return;
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
input.current?.focus();
|
|
147
|
+
input.current?.select();
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("keydown", k);
|
|
150
|
+
return () => document.removeEventListener("keydown", k);
|
|
151
|
+
}, [key]);
|
|
132
152
|
return (
|
|
133
153
|
<label className={cx("tf-search", p.className)} style={p.style}>
|
|
134
154
|
<Icon name="search" size={15} />
|
|
135
155
|
<input
|
|
156
|
+
ref={input}
|
|
136
157
|
type="search"
|
|
158
|
+
id={p.id}
|
|
159
|
+
name={p.name}
|
|
160
|
+
autoFocus={p.autoFocus}
|
|
137
161
|
placeholder={p.placeholder || t.search}
|
|
138
162
|
aria-label={p.label || p.placeholder || t.search}
|
|
163
|
+
aria-keyshortcuts={
|
|
164
|
+
key ? `Meta+${key.toUpperCase()} Control+${key.toUpperCase()}` : undefined
|
|
165
|
+
}
|
|
139
166
|
value={p.value}
|
|
167
|
+
defaultValue={p.defaultValue}
|
|
140
168
|
onChange={p.onChange}
|
|
169
|
+
onKeyDown={p.onKeyDown}
|
|
170
|
+
onFocus={p.onFocus}
|
|
171
|
+
onBlur={p.onBlur}
|
|
172
|
+
{...p.inputProps}
|
|
141
173
|
/>
|
|
142
|
-
{p.shortcut &&
|
|
174
|
+
{p.shortcut && (
|
|
175
|
+
<span className="tf-kbd" aria-hidden>
|
|
176
|
+
{p.shortcut}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
143
179
|
</label>
|
|
144
180
|
);
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
export function Checkbox(p: CheckboxProps) {
|
|
148
184
|
return (
|
|
149
|
-
<label className="tf-check">
|
|
185
|
+
<label style={p.style} className={cx("tf-check", p.className)}>
|
|
150
186
|
<span className="tf-checkbox">
|
|
151
187
|
<input
|
|
152
188
|
type="checkbox"
|
|
@@ -182,15 +218,16 @@ export function Checkbox(p: CheckboxProps) {
|
|
|
182
218
|
export function Switch(p: SwitchProps) {
|
|
183
219
|
return (
|
|
184
220
|
<label
|
|
185
|
-
className="tf-check"
|
|
186
|
-
style={
|
|
187
|
-
p.spread
|
|
221
|
+
className={cx("tf-check", p.className)}
|
|
222
|
+
style={{
|
|
223
|
+
...(p.spread
|
|
188
224
|
? {
|
|
189
225
|
justifyContent: "space-between",
|
|
190
226
|
width: "100%",
|
|
191
227
|
}
|
|
192
|
-
: undefined
|
|
193
|
-
|
|
228
|
+
: undefined),
|
|
229
|
+
...p.style,
|
|
230
|
+
}}
|
|
194
231
|
>
|
|
195
232
|
{p.spread && p.children}
|
|
196
233
|
<input
|