@umriss-ui/calculation 0.3.6 → 0.4.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/CHANGELOG.md +46 -0
- package/README.md +25 -0
- package/dist/calculation.css +1 -1
- package/dist/calculation.js +431 -303
- package/dist/index.d.ts +34 -5
- package/docs/llms-full.md +481 -7
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { JSX } from 'react';
|
|
|
4
4
|
import { Limit } from '@umriss-ui/core';
|
|
5
5
|
import { ReactNode } from 'react';
|
|
6
6
|
|
|
7
|
-
export declare function Calculation({ children, className, style, ...rest }: CalculationProps): JSX.Element;
|
|
7
|
+
export declare function Calculation({ children, className, style, metrics, ...rest }: CalculationProps): JSX.Element;
|
|
8
8
|
|
|
9
9
|
/** `className` and `style` size and place the surface; every other attribute -
|
|
10
10
|
`aria-label` first of all - lands on the list the surface holds. */
|
|
@@ -14,6 +14,12 @@ export declare interface CalculationProps extends Omit<HTMLAttributes<HTMLUListE
|
|
|
14
14
|
`<Chain>`, the two mixing freely; `<Ref>` for a quantity defined
|
|
15
15
|
elsewhere. */
|
|
16
16
|
children: ReactNode;
|
|
17
|
+
/** Several numbers per quantity, side by side - headcount and full-time
|
|
18
|
+
equivalents of the same teams (ADR-0038). Every `value` is then an
|
|
19
|
+
object with a number for each metric id; unit and places are the
|
|
20
|
+
metric's. A calculation with metrics only adds and subtracts, and
|
|
21
|
+
assesses no target or limits. */
|
|
22
|
+
metrics?: readonly Metric[];
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/** A calculation read top to bottom, as on paper. */
|
|
@@ -25,8 +31,9 @@ export declare interface ChainOperandProps extends Omit<GivenProps, "label" | "v
|
|
|
25
31
|
label?: string;
|
|
26
32
|
/** The operand's number, where the line is a given written in place.
|
|
27
33
|
Absent as for a `Given`: the line and every interim after it are
|
|
28
|
-
absent, with the reason.
|
|
29
|
-
|
|
34
|
+
absent, with the reason. With `metrics`, an object by metric id, as on
|
|
35
|
+
a `Given`. */
|
|
36
|
+
value?: number | null | MetricValues;
|
|
30
37
|
/** Instead of `label` and `value`: exactly one quantity - a tree, a
|
|
31
38
|
`<Ref>` or a `<Chain>`. */
|
|
32
39
|
children?: ReactNode;
|
|
@@ -53,8 +60,9 @@ export declare const Given: (props: GivenProps) => null;
|
|
|
53
60
|
export declare interface GivenProps extends QuantityProps {
|
|
54
61
|
/** The number the calculation starts from. Absent (`null`, `undefined`,
|
|
55
62
|
not finite) makes every quantity that depends on it absent, with the
|
|
56
|
-
reason - never zero.
|
|
57
|
-
|
|
63
|
+
reason - never zero. Where the calculation has `metrics`, an object with
|
|
64
|
+
a number for each metric id instead; absence then stays in its metric. */
|
|
65
|
+
value: number | null | undefined | MetricValues;
|
|
58
66
|
/** Where the number came from: a system, a report, a person. */
|
|
59
67
|
source?: string;
|
|
60
68
|
/** When the number was true - not when it was fetched. */
|
|
@@ -67,6 +75,27 @@ export declare interface GivenProps extends QuantityProps {
|
|
|
67
75
|
/** Names the value of a chain where it stands. */
|
|
68
76
|
export declare const Interim: (props: QuantityProps) => null;
|
|
69
77
|
|
|
78
|
+
/** One of several numbers every quantity carries side by side (ADR-0038):
|
|
79
|
+
headcount and full-time equivalents of the same teams. Each metric is
|
|
80
|
+
worked on its own, with the same derivation. */
|
|
81
|
+
export declare interface Metric {
|
|
82
|
+
/** The key of this metric's number in every `value` object. */
|
|
83
|
+
id: string;
|
|
84
|
+
/** The metric's name, in the head above its column and in each row's
|
|
85
|
+
sentence. */
|
|
86
|
+
label: string;
|
|
87
|
+
/** A label written after the number where a result closes, and beneath the
|
|
88
|
+
metric's name in the head. Never converted and never checked. */
|
|
89
|
+
unit?: string;
|
|
90
|
+
/** Fixed number of fraction digits as shown, for every quantity of this
|
|
91
|
+
metric. Without it: a given as given, at most two for a derived number. */
|
|
92
|
+
decimals?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A given's numbers where the calculation has metrics: one per metric id,
|
|
96
|
+
`null` where the number is absent. */
|
|
97
|
+
export declare type MetricValues = Readonly<Record<string, number | null>>;
|
|
98
|
+
|
|
70
99
|
/** Takes its operand from the value before it. */
|
|
71
100
|
export declare const Minus: (props: ChainOperandProps) => null;
|
|
72
101
|
|
package/docs/llms-full.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @umriss-ui/calculation 0.
|
|
1
|
+
# @umriss-ui/calculation 0.4.0
|
|
2
2
|
|
|
3
3
|
> React calculation view for data-dense applications - a derivation written as it is shown, evaluated by the library, folded and read line by line.
|
|
4
4
|
|
|
@@ -290,7 +290,7 @@ import type { InvoiceLine } from "./controlling";
|
|
|
290
290
|
const WAITING = INVOICES.filter((one) => one.status === "awaiting approval");
|
|
291
291
|
const DECISION_TONE = { approved: "success", rejected: "danger", pending: "neutral" } as const;
|
|
292
292
|
const date = (at: number) => new Date(at).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
293
|
-
const percent = (rate: number) => `${Math.round(rate * 100)}
|
|
293
|
+
const percent = (rate: number) => `${Math.round(rate * 100)}\u00a0%`;
|
|
294
294
|
|
|
295
295
|
/** One line's net amount: quantity × unit price, times the share paid after a
|
|
296
296
|
discount. A function and not a component: the calculation reads its
|
|
@@ -408,7 +408,7 @@ export default function CheckAnInvoice() {
|
|
|
408
408
|
))}
|
|
409
409
|
</Stack>
|
|
410
410
|
{decision === undefined ? (
|
|
411
|
-
<Stack direction="row" gap={2} data-callout="5">
|
|
411
|
+
<Stack direction="row" gap={2} wrap data-callout="5">
|
|
412
412
|
<Button variant="primary" onClick={() => setDecided({ ...decided, [invoice.id]: "approved" })}>
|
|
413
413
|
Approve as {next?.approver ?? "approver"}
|
|
414
414
|
</Button>
|
|
@@ -706,6 +706,7 @@ export default function Invoice() {
|
|
|
706
706
|
| Prop | Type | Default | Description |
|
|
707
707
|
|---|---|---|---|
|
|
708
708
|
| `children` (required) | `ReactNode` | — | The result: exactly one quantity - a tree (`<Sum>`, `<Difference>`, `<Product>`, `<Quotient>` with their operands, `<Given>` as leaves) or a `<Chain>`, the two mixing freely; `<Ref>` for a quantity defined elsewhere. |
|
|
709
|
+
| `metrics` | `readonly Metric[]` | — | Several numbers per quantity, side by side - headcount and full-time equivalents of the same teams (ADR-0038). Every `value` is then an object with a number for each metric id; unit and places are the metric's. A calculation with metrics only adds and subtracts, and assesses no target or limits. |
|
|
709
710
|
|
|
710
711
|
Also takes every attribute of the rendered element – without `children`.
|
|
711
712
|
|
|
@@ -1152,7 +1153,7 @@ export default function ErrorBudget() {
|
|
|
1152
1153
|
| Prop | Type | Default | Description |
|
|
1153
1154
|
|---|---|---|---|
|
|
1154
1155
|
| `label` | `string` | — | The operand's name, where the line is a given written in place. Left out where the line holds a quantity as its child. |
|
|
1155
|
-
| `value` | `number \| null` | — | The operand's number, where the line is a given written in place. Absent as for a `Given`: the line and every interim after it are absent, with the reason. |
|
|
1156
|
+
| `value` | `number \| null \| MetricValues` | — | The operand's number, where the line is a given written in place. Absent as for a `Given`: the line and every interim after it are absent, with the reason. With `metrics`, an object by metric id, as on a `Given`. |
|
|
1156
1157
|
| `children` | `ReactNode` | — | Instead of `label` and `value`: exactly one quantity - a tree, a `<Ref>` or a `<Chain>`. |
|
|
1157
1158
|
| `source` | `string` | — | Where the number came from: a system, a report, a person. From `GivenProps`. |
|
|
1158
1159
|
| `asOf` | `Date \| number` | — | When the number was true - not when it was fetched. From `GivenProps`. |
|
|
@@ -1287,7 +1288,7 @@ export default function InGerman() {
|
|
|
1287
1288
|
|
|
1288
1289
|
| Prop | Type | Default | Description |
|
|
1289
1290
|
|---|---|---|---|
|
|
1290
|
-
| `value` (required) | `number \| null \| undefined` | — | The number the calculation starts from. Absent (`null`, `undefined`, not finite) makes every quantity that depends on it absent, with the reason - never zero. |
|
|
1291
|
+
| `value` (required) | `number \| null \| undefined \| MetricValues` | — | The number the calculation starts from. Absent (`null`, `undefined`, not finite) makes every quantity that depends on it absent, with the reason - never zero. Where the calculation has `metrics`, an object with a number for each metric id instead; absence then stays in its metric. |
|
|
1291
1292
|
| `source` | `string` | — | Where the number came from: a system, a report, a person. |
|
|
1292
1293
|
| `asOf` | `Date \| number` | — | When the number was true - not when it was fetched. |
|
|
1293
1294
|
| `ages` | `FreshnessAges` | — | The ages at which `asOf` turns stale and lost. With both, the given carries a freshness through core; with `asOf` alone, only the time. |
|
|
@@ -1301,6 +1302,263 @@ export default function InGerman() {
|
|
|
1301
1302
|
| `explanation` | `string` | — | A sentence of plain text beside the line: why the quantity is here, or what it means. From `QuantityProps`. |
|
|
1302
1303
|
| `aside` | `ReactNode` | — | Content of the caller's own beside the line - a sparkline, a link. It never takes part in the calculation. From `QuantityProps`. |
|
|
1303
1304
|
|
|
1305
|
+
### Metrics
|
|
1306
|
+
|
|
1307
|
+
Several numbers on every line, side by side (columns, measures): headcount and full-time equivalents of the same teams, summed by the same derivation. Reach for it where one sheet has to answer in more than one unit.
|
|
1308
|
+
|
|
1309
|
+
```ts
|
|
1310
|
+
import { Calculation, Given, Sum, Chain, Plus, Minus, Interim } from "@umriss-ui/calculation";
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/calculation/metrics/
|
|
1314
|
+
|
|
1315
|
+
`metrics` on the calculation names each metric once, with its unit and places; every `value` is then an object with a number for each metric id. Each metric is worked on its own - nothing ever takes numbers from two of them (ADR-0038).
|
|
1316
|
+
|
|
1317
|
+
The head names each metric above its column. The unit stands again only where a result closes: the Result, an interim, the line that closes an open derivation.
|
|
1318
|
+
|
|
1319
|
+
A number one metric does not have is `null`, and makes absent only what depends on it in that metric; the badge says which metric, the sentence the full reason.
|
|
1320
|
+
|
|
1321
|
+
Where the figures leave the labels less room than they need, and less than 10 rem, each line puts them on a line beneath its label, still in their columns. The component measures this: it depends on the labels, the number of metrics and their digits, not on a breakpoint.
|
|
1322
|
+
|
|
1323
|
+
#### Examples
|
|
1324
|
+
|
|
1325
|
+
##### Headcount and full-time equivalents
|
|
1326
|
+
|
|
1327
|
+
`metrics` puts several numbers on every line, each in its own column; every `value` is then an object with a number per metric.
|
|
1328
|
+
|
|
1329
|
+
```tsx
|
|
1330
|
+
import { PEOPLE } from "./planning";
|
|
1331
|
+
import { Calculation, Given, Sum } from "@umriss-ui/calculation";
|
|
1332
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1333
|
+
|
|
1334
|
+
const METRICS: Metric[] = [
|
|
1335
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1336
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1337
|
+
];
|
|
1338
|
+
|
|
1339
|
+
/* A full-time week is 40 hours. */
|
|
1340
|
+
const staffOf = (team: "Web" | "Apps") => {
|
|
1341
|
+
const people = PEOPLE.filter((person) => person.team === team);
|
|
1342
|
+
return { heads: people.length, fte: people.reduce((sum, person) => sum + person.capacity, 0) / 40 };
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
export default function TwoTeams() {
|
|
1346
|
+
return (
|
|
1347
|
+
<Calculation aria-label="Staff of Tidewell" metrics={METRICS}>
|
|
1348
|
+
<Sum label="Tidewell">
|
|
1349
|
+
<Given label="Web team" value={staffOf("Web")} />
|
|
1350
|
+
<Given label="Apps team" value={staffOf("Apps")} />
|
|
1351
|
+
</Sum>
|
|
1352
|
+
</Calculation>
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1358
|
+
|
|
1359
|
+
##### A number one metric does not have
|
|
1360
|
+
|
|
1361
|
+
`null` for one metric makes absent only what depends on it in that metric, with the reason; the other metric sums on.
|
|
1362
|
+
|
|
1363
|
+
```tsx
|
|
1364
|
+
import { Calculation, Given, Sum } from "@umriss-ui/calculation";
|
|
1365
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1366
|
+
|
|
1367
|
+
const METRICS: Metric[] = [
|
|
1368
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1369
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1370
|
+
];
|
|
1371
|
+
|
|
1372
|
+
export default function NotRecorded() {
|
|
1373
|
+
return (
|
|
1374
|
+
<Calculation aria-label="Staff of customer service" metrics={METRICS}>
|
|
1375
|
+
<Sum label="Customer service">
|
|
1376
|
+
<Given label="Service desk" value={{ heads: 7, fte: 5.9 }} />
|
|
1377
|
+
<Given label="Returns" value={{ heads: 3, fte: 2.5 }} />
|
|
1378
|
+
<Given
|
|
1379
|
+
label="Working students"
|
|
1380
|
+
value={{ heads: 4, fte: null }}
|
|
1381
|
+
explanation="Paid by the hour; no contracted hours to count."
|
|
1382
|
+
/>
|
|
1383
|
+
</Sum>
|
|
1384
|
+
</Calculation>
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
##### Team, cost centre, business line
|
|
1390
|
+
|
|
1391
|
+
A tree of sums, built from data: every cost centre folds to its total and opens into its teams, each metric summed on its own.
|
|
1392
|
+
|
|
1393
|
+
```tsx
|
|
1394
|
+
import { COST_CENTRES, TEAMS } from "./controlling";
|
|
1395
|
+
import { Calculation, Given, Sum } from "@umriss-ui/calculation";
|
|
1396
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1397
|
+
|
|
1398
|
+
const METRICS: Metric[] = [
|
|
1399
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1400
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1401
|
+
];
|
|
1402
|
+
|
|
1403
|
+
const COMMERCIAL = ["CC-1100", "CC-1200", "CC-3100"];
|
|
1404
|
+
|
|
1405
|
+
export default function BusinessLine() {
|
|
1406
|
+
return (
|
|
1407
|
+
<Calculation aria-label="Staff of the commercial business line, 17 March" metrics={METRICS}>
|
|
1408
|
+
<Sum label="Commercial">
|
|
1409
|
+
{COMMERCIAL.map((id) => (
|
|
1410
|
+
<Sum key={id} label={COST_CENTRES.find((centre) => centre.id === id)!.name}>
|
|
1411
|
+
{TEAMS.filter((team) => team.costCentre === id).map((team) => (
|
|
1412
|
+
<Given key={team.name} label={team.name} value={{ heads: team.headcount, fte: team.fte }} />
|
|
1413
|
+
))}
|
|
1414
|
+
</Sum>
|
|
1415
|
+
))}
|
|
1416
|
+
</Sum>
|
|
1417
|
+
</Calculation>
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
It imports `controlling.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1423
|
+
|
|
1424
|
+
##### Staff movement as a chain
|
|
1425
|
+
|
|
1426
|
+
The chain works per metric too: joiners added, leavers taken away, and every interim closes with its units.
|
|
1427
|
+
|
|
1428
|
+
```tsx
|
|
1429
|
+
import { Calculation, Chain, Given, Interim, Minus, Plus } from "@umriss-ui/calculation";
|
|
1430
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1431
|
+
|
|
1432
|
+
const METRICS: Metric[] = [
|
|
1433
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1434
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1435
|
+
];
|
|
1436
|
+
|
|
1437
|
+
export default function StaffMovement() {
|
|
1438
|
+
return (
|
|
1439
|
+
<Calculation aria-label="Staff movement of Carrow & Lisle since 1 January" metrics={METRICS}>
|
|
1440
|
+
<Chain>
|
|
1441
|
+
<Given label="Staff on 1 January" value={{ heads: 100, fte: 89.9 }} source="Payroll" />
|
|
1442
|
+
<Plus label="Joiners" value={{ heads: 5, fte: 4.6 }} />
|
|
1443
|
+
<Minus label="Leavers" value={{ heads: 2, fte: 1.8 }} />
|
|
1444
|
+
<Interim label="Under contract" />
|
|
1445
|
+
<Minus label="Gone on parental leave" value={{ heads: 2, fte: 1.6 }} />
|
|
1446
|
+
<Plus label="Back from parental leave" value={{ heads: 1, fte: 0.4 }} explanation="Back part-time, 16 hours a week." />
|
|
1447
|
+
<Interim label="Staff on 17 March" />
|
|
1448
|
+
</Chain>
|
|
1449
|
+
</Calculation>
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
##### A third metric: staff cost
|
|
1455
|
+
|
|
1456
|
+
Any number of metrics, each with its unit and places. Narrow, where the figures leave the label too little room, each line puts them beneath its label.
|
|
1457
|
+
|
|
1458
|
+
```tsx
|
|
1459
|
+
import { COST_CENTRES, TEAMS } from "./controlling";
|
|
1460
|
+
import { Calculation, Given, Sum } from "@umriss-ui/calculation";
|
|
1461
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1462
|
+
|
|
1463
|
+
const METRICS: Metric[] = [
|
|
1464
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1465
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1466
|
+
{ id: "cost", label: "Staff cost a month", unit: "k€", decimals: 1 },
|
|
1467
|
+
];
|
|
1468
|
+
|
|
1469
|
+
const PRODUCT = ["CC-2100", "CC-2200"];
|
|
1470
|
+
|
|
1471
|
+
export default function StaffCost() {
|
|
1472
|
+
return (
|
|
1473
|
+
<Calculation aria-label="Staff and staff cost of the product business line" metrics={METRICS}>
|
|
1474
|
+
<Sum label="Product">
|
|
1475
|
+
{PRODUCT.map((id) => (
|
|
1476
|
+
<Sum key={id} label={COST_CENTRES.find((centre) => centre.id === id)!.name}>
|
|
1477
|
+
{TEAMS.filter((team) => team.costCentre === id).map((team) => (
|
|
1478
|
+
<Given
|
|
1479
|
+
key={team.name}
|
|
1480
|
+
label={team.name}
|
|
1481
|
+
value={{ heads: team.headcount, fte: team.fte, cost: team.monthlyCost }}
|
|
1482
|
+
/>
|
|
1483
|
+
))}
|
|
1484
|
+
</Sum>
|
|
1485
|
+
))}
|
|
1486
|
+
</Sum>
|
|
1487
|
+
</Calculation>
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
It imports `controlling.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1493
|
+
|
|
1494
|
+
##### The whole company
|
|
1495
|
+
|
|
1496
|
+
Three levels from data - business line, cost centre, team - and three metrics: 23 teams folded to three lines a reader opens one by one.
|
|
1497
|
+
|
|
1498
|
+
```tsx
|
|
1499
|
+
import { BUSINESS_LINES, COST_CENTRES, TEAMS } from "./controlling";
|
|
1500
|
+
import { Calculation, Given, Sum } from "@umriss-ui/calculation";
|
|
1501
|
+
import type { Metric } from "@umriss-ui/calculation";
|
|
1502
|
+
|
|
1503
|
+
const METRICS: Metric[] = [
|
|
1504
|
+
{ id: "heads", label: "Headcount", unit: "HC" },
|
|
1505
|
+
{ id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
|
|
1506
|
+
{ id: "cost", label: "Staff cost a month", unit: "k€", decimals: 1 },
|
|
1507
|
+
];
|
|
1508
|
+
|
|
1509
|
+
const centreName = (id: string) => COST_CENTRES.find((centre) => centre.id === id)!.name;
|
|
1510
|
+
|
|
1511
|
+
export default function WholeCompany() {
|
|
1512
|
+
return (
|
|
1513
|
+
<Calculation aria-label="Staff of Carrow & Lisle, 17 March" metrics={METRICS}>
|
|
1514
|
+
<Sum label="Carrow & Lisle">
|
|
1515
|
+
{BUSINESS_LINES.map((line) => (
|
|
1516
|
+
<Sum key={line.name} label={line.name}>
|
|
1517
|
+
{line.costCentres.map((id) => (
|
|
1518
|
+
<Sum key={id} label={centreName(id)}>
|
|
1519
|
+
{TEAMS.filter((team) => team.costCentre === id).map((team) => (
|
|
1520
|
+
<Given
|
|
1521
|
+
key={team.name}
|
|
1522
|
+
label={team.name}
|
|
1523
|
+
value={{ heads: team.headcount, fte: team.fte, cost: team.monthlyCost }}
|
|
1524
|
+
/>
|
|
1525
|
+
))}
|
|
1526
|
+
</Sum>
|
|
1527
|
+
))}
|
|
1528
|
+
</Sum>
|
|
1529
|
+
))}
|
|
1530
|
+
</Sum>
|
|
1531
|
+
</Calculation>
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
It imports `controlling.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1537
|
+
|
|
1538
|
+
#### When to use something else
|
|
1539
|
+
|
|
1540
|
+
- A ratio across two metrics, such as FTE per head → a calculation of its own, a `Quotient` of two givens
|
|
1541
|
+
- Many figures per row, sorted and filtered → `Table` from @umriss-ui/table
|
|
1542
|
+
|
|
1543
|
+
#### API
|
|
1544
|
+
|
|
1545
|
+
##### `Metric`
|
|
1546
|
+
|
|
1547
|
+
| Prop | Type | Default | Description |
|
|
1548
|
+
|---|---|---|---|
|
|
1549
|
+
| `id` (required) | `string` | — | The key of this metric's number in every `value` object. |
|
|
1550
|
+
| `label` (required) | `string` | — | The metric's name, in the head above its column and in each row's sentence. |
|
|
1551
|
+
| `unit` | `string` | — | A label written after the number where a result closes, and beneath the metric's name in the head. Never converted and never checked. |
|
|
1552
|
+
| `decimals` | `number` | — | Fixed number of fraction digits as shown, for every quantity of this metric. Without it: a given as given, at most two for a derived number. |
|
|
1553
|
+
|
|
1554
|
+
#### Known limits
|
|
1555
|
+
|
|
1556
|
+
- A calculation with metrics only adds and subtracts: `Product`, `Quotient`, `Times` and `DividedBy` fail on the first render, since a factor would need a unit in every metric.
|
|
1557
|
+
- Unit, format and places belong to the metric; on a quantity they fail on the first render, and so does a missing or unknown metric key in a `value`.
|
|
1558
|
+
- No target or limits with metrics: an assessment per cell is not there yet.
|
|
1559
|
+
|
|
1560
|
+
What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0032-what-umriss-is-not.md).
|
|
1561
|
+
|
|
1304
1562
|
## In practice
|
|
1305
1563
|
|
|
1306
1564
|
What a calculation does when the numbers are not what they should be, and whole figures worked in full.
|
|
@@ -1700,7 +1958,10 @@ The demos' shared data, which some examples import from beside themselves. Copie
|
|
|
1700
1958
|
* where the month is closed (January and February), the forecast for every
|
|
1701
1959
|
* month; Marketing overspends, IT runs under;
|
|
1702
1960
|
* - `INVOICES` - incoming invoices with their lines (quantity, unit price,
|
|
1703
|
-
* discount, VAT rate), and `APPROVALS`, who signed which, or has yet to
|
|
1961
|
+
* discount, VAT rate), and `APPROVALS`, who signed which, or has yet to;
|
|
1962
|
+
* - `TEAMS` - the teams of each cost centre with their headcount, full-time
|
|
1963
|
+
* equivalents and monthly staff cost, and `BUSINESS_LINES`, which cost
|
|
1964
|
+
* centres each of the three business lines holds.
|
|
1704
1965
|
*
|
|
1705
1966
|
* Amounts in euros. "Now" is Tuesday, 17 March 2026. Plain data and small pure
|
|
1706
1967
|
* functions, no imports: copy the file beside an example and it runs.
|
|
@@ -1852,6 +2113,58 @@ export const APPROVALS: readonly Approval[] = [
|
|
|
1852
2113
|
{ invoice: "INV-26-0226", step: "finance", approver: "Martina Vogel", decision: "approved", at: on(3, 2) },
|
|
1853
2114
|
{ invoice: "INV-26-0221", step: "cost centre", approver: "Anika Sørensen", decision: "rejected", at: on(2, 24), comment: "Not ordered - the review was cancelled in January." },
|
|
1854
2115
|
];
|
|
2116
|
+
|
|
2117
|
+
export interface Team {
|
|
2118
|
+
/** The cost centre the team books to. */
|
|
2119
|
+
costCentre: string;
|
|
2120
|
+
name: string;
|
|
2121
|
+
/** People employed, whatever their hours. */
|
|
2122
|
+
headcount: number;
|
|
2123
|
+
/** Full-time equivalents: contracted hours over 40 a week. */
|
|
2124
|
+
fte: number;
|
|
2125
|
+
/** Staff cost of a month, in thousand euros. */
|
|
2126
|
+
monthlyCost: number;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/** Every team, on 17 March: 102 people, 91.5 full-time equivalents. */
|
|
2130
|
+
export const TEAMS: readonly Team[] = [
|
|
2131
|
+
{ costCentre: "CC-1100", name: "Field sales North", headcount: 9, fte: 8.6, monthlyCost: 61.2 },
|
|
2132
|
+
{ costCentre: "CC-1100", name: "Field sales South", headcount: 8, fte: 7.5, monthlyCost: 53.9 },
|
|
2133
|
+
{ costCentre: "CC-1100", name: "Inside sales", headcount: 6, fte: 5.2, monthlyCost: 29.8 },
|
|
2134
|
+
{ costCentre: "CC-1100", name: "Key accounts", headcount: 4, fte: 4.0, monthlyCost: 33.6 },
|
|
2135
|
+
{ costCentre: "CC-1200", name: "Brand", headcount: 3, fte: 2.8, monthlyCost: 19.4 },
|
|
2136
|
+
{ costCentre: "CC-1200", name: "Online marketing", headcount: 4, fte: 3.5, monthlyCost: 24.1 },
|
|
2137
|
+
{ costCentre: "CC-1200", name: "Trade fairs", headcount: 2, fte: 1.5, monthlyCost: 10.2 },
|
|
2138
|
+
{ costCentre: "CC-3100", name: "Service desk", headcount: 7, fte: 5.9, monthlyCost: 31.3 },
|
|
2139
|
+
{ costCentre: "CC-3100", name: "Returns", headcount: 3, fte: 2.5, monthlyCost: 12.6 },
|
|
2140
|
+
{ costCentre: "CC-2100", name: "Seating", headcount: 6, fte: 5.8, monthlyCost: 43.5 },
|
|
2141
|
+
{ costCentre: "CC-2100", name: "Desks", headcount: 7, fte: 6.5, monthlyCost: 48.1 },
|
|
2142
|
+
{ costCentre: "CC-2100", name: "Storage", headcount: 5, fte: 4.6, monthlyCost: 33.9 },
|
|
2143
|
+
{ costCentre: "CC-2100", name: "Test lab", headcount: 3, fte: 2.5, monthlyCost: 16.8 },
|
|
2144
|
+
{ costCentre: "CC-2200", name: "Industrial design", headcount: 4, fte: 3.6, monthlyCost: 27.2 },
|
|
2145
|
+
{ costCentre: "CC-2200", name: "Colour and material", headcount: 2, fte: 1.8, monthlyCost: 12.9 },
|
|
2146
|
+
{ costCentre: "CC-4100", name: "Accounting", headcount: 5, fte: 4.4, monthlyCost: 27.5 },
|
|
2147
|
+
{ costCentre: "CC-4100", name: "Controlling", headcount: 3, fte: 3.0, monthlyCost: 21.3 },
|
|
2148
|
+
{ costCentre: "CC-4200", name: "Recruiting", headcount: 2, fte: 1.8, monthlyCost: 12.4 },
|
|
2149
|
+
{ costCentre: "CC-4200", name: "Payroll", headcount: 2, fte: 1.5, monthlyCost: 9.6 },
|
|
2150
|
+
{ costCentre: "CC-4300", name: "Workplace", headcount: 4, fte: 3.8, monthlyCost: 24.2 },
|
|
2151
|
+
{ costCentre: "CC-4300", name: "Applications", headcount: 5, fte: 4.5, monthlyCost: 34.8 },
|
|
2152
|
+
{ costCentre: "CC-4400", name: "Buildings", headcount: 3, fte: 3.0, monthlyCost: 16.9 },
|
|
2153
|
+
{ costCentre: "CC-4400", name: "Canteen", headcount: 5, fte: 3.2, monthlyCost: 14.7 },
|
|
2154
|
+
];
|
|
2155
|
+
|
|
2156
|
+
export interface BusinessLine {
|
|
2157
|
+
name: string;
|
|
2158
|
+
/** Its cost centres, in the order a report lists them. */
|
|
2159
|
+
costCentres: readonly string[];
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/** How the company reports: three business lines over the nine cost centres. */
|
|
2163
|
+
export const BUSINESS_LINES: readonly BusinessLine[] = [
|
|
2164
|
+
{ name: "Commercial", costCentres: ["CC-1100", "CC-1200", "CC-3100"] },
|
|
2165
|
+
{ name: "Product", costCentres: ["CC-2100", "CC-2200"] },
|
|
2166
|
+
{ name: "Corporate services", costCentres: ["CC-4100", "CC-4200", "CC-4300", "CC-4400"] },
|
|
2167
|
+
];
|
|
1855
2168
|
```
|
|
1856
2169
|
|
|
1857
2170
|
### `logistics.ts`
|
|
@@ -2388,7 +2701,7 @@ export interface AlertType {
|
|
|
2388
2701
|
|
|
2389
2702
|
export const ALERT_TYPES: readonly AlertType[] = [
|
|
2390
2703
|
{ id: "checkout-latency", label: "Checkout · p95 latency above 300 ms", priority: "high" },
|
|
2391
|
-
{ id: "checkout-errors", label: "Checkout · error rate above 2
|
|
2704
|
+
{ id: "checkout-errors", label: "Checkout · error rate above 2\u00a0%", priority: "high" },
|
|
2392
2705
|
{ id: "webhooks-queue", label: "Webhooks · queue older than 5 min", priority: "medium" },
|
|
2393
2706
|
{ id: "search-latency", label: "Search · p95 latency above 250 ms", priority: "medium" },
|
|
2394
2707
|
{ id: "images-disk", label: "Image service · disk 85 % full", priority: "low" },
|
|
@@ -2600,6 +2913,167 @@ export const SUCCESS_BY_HOUR: readonly SuccessCell[] = (() => {
|
|
|
2600
2913
|
})();
|
|
2601
2914
|
```
|
|
2602
2915
|
|
|
2916
|
+
### `planning.ts`
|
|
2917
|
+
|
|
2918
|
+
```ts
|
|
2919
|
+
/**
|
|
2920
|
+
* The planning world: Tidewell, a small studio that builds web and mobile
|
|
2921
|
+
* apps for its clients, in two teams.
|
|
2922
|
+
*
|
|
2923
|
+
* The standing cast:
|
|
2924
|
+
* - `PEOPLE` - ten people with their role, team and weekly capacity in hours;
|
|
2925
|
+
* - `PROJECTS` - four client projects, each with a colour;
|
|
2926
|
+
* - `SPRINTS` - two-week sprints; sprint 14 runs now, 9 to 20 March;
|
|
2927
|
+
* - `WORK` - the current sprint's work items with an estimate and a status,
|
|
2928
|
+
* placed on a person over time; structurally a `Subtask` of
|
|
2929
|
+
* `@umriss-ui/schedule` (`task` is the project, `lane` the person);
|
|
2930
|
+
* - `LEAVE` - holidays, sick days and training, to become blocked time;
|
|
2931
|
+
* - `BURNDOWN` - sprint 14's remaining hours at the end of each working day,
|
|
2932
|
+
* up to yesterday.
|
|
2933
|
+
*
|
|
2934
|
+
* "Now" is Tuesday, 17 March 2026, 10:30 local time. Plain data and small pure
|
|
2935
|
+
* functions, no imports: copy the file beside an example and it runs.
|
|
2936
|
+
*/
|
|
2937
|
+
|
|
2938
|
+
const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
|
|
2939
|
+
|
|
2940
|
+
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
|
|
2941
|
+
export const NOW = at(17, 10, 30);
|
|
2942
|
+
|
|
2943
|
+
export interface Person {
|
|
2944
|
+
id: string;
|
|
2945
|
+
name: string;
|
|
2946
|
+
role: "Developer" | "Designer" | "Product manager" | "QA engineer";
|
|
2947
|
+
team: "Web" | "Apps";
|
|
2948
|
+
/** Hours a week they can be planned for. */
|
|
2949
|
+
capacity: number;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
export const PEOPLE: readonly Person[] = [
|
|
2953
|
+
{ id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
|
|
2954
|
+
{ id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
|
|
2955
|
+
{ id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
|
|
2956
|
+
{ id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
|
|
2957
|
+
{ id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
|
|
2958
|
+
{ id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
|
|
2959
|
+
{ id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
|
|
2960
|
+
{ id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
|
|
2961
|
+
{ id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
|
|
2962
|
+
{ id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
|
|
2963
|
+
];
|
|
2964
|
+
|
|
2965
|
+
/** A project, shaped as `@umriss-ui/schedule`'s `Task`: an id, a name and a
|
|
2966
|
+
colour in both schemes. */
|
|
2967
|
+
export interface Project {
|
|
2968
|
+
id: string;
|
|
2969
|
+
name: string;
|
|
2970
|
+
client: string;
|
|
2971
|
+
color: string;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
export const PROJECTS: readonly Project[] = [
|
|
2975
|
+
{ id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
|
|
2976
|
+
{ id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
|
|
2977
|
+
{ id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
|
|
2978
|
+
{ id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
|
|
2979
|
+
];
|
|
2980
|
+
|
|
2981
|
+
export interface Sprint {
|
|
2982
|
+
id: string;
|
|
2983
|
+
name: string;
|
|
2984
|
+
from: number;
|
|
2985
|
+
to: number;
|
|
2986
|
+
goal: string;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/** Two weeks each, Monday 09:00 to the second Friday 17:00. */
|
|
2990
|
+
export const SPRINTS: readonly Sprint[] = [
|
|
2991
|
+
{ name: "Sprint 12", day: 9 - 28, goal: "Shop checkout on the new design" },
|
|
2992
|
+
{ name: "Sprint 13", day: 9 - 14, goal: "Booking app in the stores' beta" },
|
|
2993
|
+
{ name: "Sprint 14", day: 9, goal: "Member portal sign-in and profile" },
|
|
2994
|
+
{ name: "Sprint 15", day: 23, goal: "Shop search and filters" },
|
|
2995
|
+
{ name: "Sprint 16", day: 37, goal: "Booking reminders" },
|
|
2996
|
+
].map(({ name, day, goal }) => ({ id: name.toLowerCase().replace(" ", "-"), name, from: at(day), to: at(day + 11, 17), goal }));
|
|
2997
|
+
|
|
2998
|
+
/** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
|
|
2999
|
+
export interface WorkItem {
|
|
3000
|
+
id: string;
|
|
3001
|
+
/** The project. */
|
|
3002
|
+
task: string;
|
|
3003
|
+
/** The person. */
|
|
3004
|
+
lane: string;
|
|
3005
|
+
from: number;
|
|
3006
|
+
to: number;
|
|
3007
|
+
name: string;
|
|
3008
|
+
sprint: string;
|
|
3009
|
+
/** Hours, as estimated at planning. */
|
|
3010
|
+
estimate: number;
|
|
3011
|
+
status: "to do" | "in progress" | "in review" | "done";
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
|
|
3015
|
+
({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
|
|
3016
|
+
|
|
3017
|
+
export const WORK: readonly WorkItem[] = [
|
|
3018
|
+
item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
|
|
3019
|
+
item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
|
|
3020
|
+
item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
|
|
3021
|
+
item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
|
|
3022
|
+
item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
|
|
3023
|
+
item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
|
|
3024
|
+
item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
|
|
3025
|
+
item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
|
|
3026
|
+
item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
|
|
3027
|
+
item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
|
|
3028
|
+
item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
|
|
3029
|
+
item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
|
|
3030
|
+
item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
|
|
3031
|
+
item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
|
|
3032
|
+
item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
|
|
3033
|
+
item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
|
|
3034
|
+
];
|
|
3035
|
+
|
|
3036
|
+
export interface Leave {
|
|
3037
|
+
id: string;
|
|
3038
|
+
person: string;
|
|
3039
|
+
from: number;
|
|
3040
|
+
to: number;
|
|
3041
|
+
reason: "Holiday" | "Sick" | "Training";
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
/** When people are away: blocked time on their lane. */
|
|
3045
|
+
export const LEAVE: readonly Leave[] = [
|
|
3046
|
+
{ id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
|
|
3047
|
+
{ id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
|
|
3048
|
+
{ id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
|
|
3049
|
+
{ id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
|
|
3050
|
+
{ id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
|
|
3051
|
+
{ id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
|
|
3052
|
+
];
|
|
3053
|
+
|
|
3054
|
+
export interface BurndownPoint {
|
|
3055
|
+
/** The working day, at 17:00. */
|
|
3056
|
+
t: number;
|
|
3057
|
+
/** Where the line would be, burning evenly. */
|
|
3058
|
+
ideal: number;
|
|
3059
|
+
/** Hours left, `null` for the days still to come. */
|
|
3060
|
+
remaining: number | null;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/** Sprint 14's ten working days. The team fell behind in the first week, when
|
|
3064
|
+
two were away, and is catching up. */
|
|
3065
|
+
export const BURNDOWN: readonly BurndownPoint[] = (() => {
|
|
3066
|
+
const total = WORK.reduce((sum, one) => sum + one.estimate, 0);
|
|
3067
|
+
const days = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
|
|
3068
|
+
const burnt = [0, 22, 38, 60, 71, 98];
|
|
3069
|
+
return days.map((day, i) => ({
|
|
3070
|
+
t: at(day, 17),
|
|
3071
|
+
ideal: Math.round(total * (1 - i / (days.length - 1))),
|
|
3072
|
+
remaining: burnt[i] === undefined ? null : total - burnt[i]!,
|
|
3073
|
+
}));
|
|
3074
|
+
})();
|
|
3075
|
+
```
|
|
3076
|
+
|
|
2603
3077
|
### `plant.ts`
|
|
2604
3078
|
|
|
2605
3079
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umriss-ui/calculation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React calculation view for data-dense applications - a derivation written as it is shown, evaluated by the library, folded and read line by line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Roman Händler",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"react": ">=18",
|
|
49
49
|
"react-dom": ">=18",
|
|
50
|
-
"@umriss-ui/core": "^0.
|
|
50
|
+
"@umriss-ui/core": "^0.16.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@fontsource/geist-mono": "^5.3.0",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"typescript": "^5.6.3",
|
|
62
62
|
"vite": "^6.0.0",
|
|
63
63
|
"vite-plugin-dts": "^4.3.0",
|
|
64
|
-
"@umriss-ui/
|
|
65
|
-
"@umriss-ui/
|
|
64
|
+
"@umriss-ui/core": "0.16.0",
|
|
65
|
+
"@umriss-ui/demo": "0.0.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"props": "node --disable-warning=ExperimentalWarning --experimental-strip-types demo/props.ts",
|