@stll/workspace-ui 0.12.7 → 0.13.1
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/README.md +139 -0
- package/dist/feature-previews/billing-preview.d.ts +38 -0
- package/dist/feature-previews/billing-preview.js +160 -0
- package/dist/feature-previews/contacts-preview.d.ts +35 -0
- package/dist/feature-previews/contacts-preview.js +109 -0
- package/dist/feature-previews/documents-preview.d.ts +44 -0
- package/dist/feature-previews/documents-preview.js +105 -0
- package/dist/feature-previews/format.d.ts +16 -0
- package/dist/feature-previews/format.js +20 -0
- package/dist/feature-previews/index.d.ts +9 -0
- package/dist/feature-previews/index.js +7 -0
- package/dist/feature-previews/matters-preview.d.ts +30 -0
- package/dist/feature-previews/matters-preview.js +153 -0
- package/dist/feature-previews/mock-data.d.ts +41 -0
- package/dist/feature-previews/mock-data.js +25 -0
- package/dist/feature-previews/research-preview.d.ts +33 -0
- package/dist/feature-previews/research-preview.js +115 -0
- package/dist/feature-previews/surface.d.ts +34 -0
- package/dist/feature-previews/surface.js +42 -0
- package/dist/feature-previews/time-tracking-preview.d.ts +35 -0
- package/dist/feature-previews/time-tracking-preview.js +135 -0
- package/dist/field-value.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/responsive-action-toolbar.js +4 -4
- package/dist/task-status.d.ts +21 -0
- package/dist/task-status.js +27 -0
- package/dist/types.d.ts +3 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -38,6 +38,145 @@ are intentionally available through explicit subpaths; use those subpaths to
|
|
|
38
38
|
declare a module's
|
|
39
39
|
boundary.
|
|
40
40
|
|
|
41
|
+
## Feature previews
|
|
42
|
+
|
|
43
|
+
`@stll/workspace-ui/feature-previews` exports small, static product scenes for
|
|
44
|
+
marketing pages, menus and slides. They are composed from the components the
|
|
45
|
+
app renders (kanban card shell, comment cards, tracked-change marks, court
|
|
46
|
+
badges, matter icons, task status glyphs, money formatting), so they follow
|
|
47
|
+
the product as it changes.
|
|
48
|
+
|
|
49
|
+
| Component | Shows | `labels` | `data` |
|
|
50
|
+
| --------------------- | -------------------------------------------------- | ------------------------------------------------------------- | ------------------------- |
|
|
51
|
+
| `MattersPreview` | A matter's task board | `open`, `inProgress`, `done` | `MattersPreviewData` |
|
|
52
|
+
| `DocumentsPreview` | A Word document with a tracked change and comments | `comments` | `DocumentsPreviewData` |
|
|
53
|
+
| `ResearchPreview` | Case-law search results with matched passages | `caseLaw` | `ResearchPreviewData` |
|
|
54
|
+
| `BillingPreview` | An invoice's time entries and total | `date`, `description`, `billedTime`, `rate`, `fee`, `summary` | `BillingPreviewData` |
|
|
55
|
+
| `ContactsPreview` | A company contact filled from a business registry | `businessRegistry`, `matters` | `ContactsPreviewData` |
|
|
56
|
+
| `TimeTrackingPreview` | A running timer above the day's entries | `time`, `summary` | `TimeTrackingPreviewData` |
|
|
57
|
+
|
|
58
|
+
Every scene takes the same props:
|
|
59
|
+
|
|
60
|
+
- `labels`: the interface strings it draws, typed per scene
|
|
61
|
+
(`MattersPreviewLabels`, …). The package carries no catalogs.
|
|
62
|
+
- `locale`: the formatting locale for dates, times, durations and money
|
|
63
|
+
(`en-GB`, `cs-CZ`).
|
|
64
|
+
- `data` (optional): what the scene depicts, typed per scene
|
|
65
|
+
(`MattersPreviewData`, …). Without it the scene shows the package's one
|
|
66
|
+
fictional English set; supply your own for another jurisdiction, language
|
|
67
|
+
or audience.
|
|
68
|
+
- `className` (optional): classes for the outer surface.
|
|
69
|
+
|
|
70
|
+
A scene fills the box it is given (`size-full`) and clips what does not fit;
|
|
71
|
+
size it with the parent. It reads well from about 540 to 900 px wide at a
|
|
72
|
+
16:10 to 16:9 aspect; keep custom content about as long as the default so it
|
|
73
|
+
fits as well. Scenes are decorative: the surface is `aria-hidden` and takes
|
|
74
|
+
no pointer input, so name what it illustrates beside it.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { MattersPreview } from "@stll/workspace-ui/feature-previews";
|
|
78
|
+
|
|
79
|
+
<div className="aspect-[16/10] w-[720px]">
|
|
80
|
+
<MattersPreview
|
|
81
|
+
labels={{ open: "Open", inProgress: "In progress", done: "Done" }}
|
|
82
|
+
locale="en-GB"
|
|
83
|
+
/>
|
|
84
|
+
</div>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom content
|
|
88
|
+
|
|
89
|
+
`data` is plain data: the scene formats it with `locale` and computes what it
|
|
90
|
+
can (column counts, invoice amounts and totals, the day's total time). Dates
|
|
91
|
+
are `YYYY-MM-DD`, times `HH:MM`, durations whole minutes; text is drawn as
|
|
92
|
+
given. Two shapes are shared:
|
|
93
|
+
|
|
94
|
+
- `PreviewPerson`: `{ name }`, drawn as initials.
|
|
95
|
+
- `PreviewMatter`: `{ id, color, name }`; `color` is a matter colour token
|
|
96
|
+
(`--option-blue`, `--option-violet`, …).
|
|
97
|
+
|
|
98
|
+
Per scene:
|
|
99
|
+
|
|
100
|
+
- `MattersPreviewData`: `{ matter, team, tasks }`. Each `MattersPreviewTask`
|
|
101
|
+
has `name`, `status` (`MattersPreviewTaskStatus`: `open`, `in_progress`,
|
|
102
|
+
`done`), `due`, `assignee` and `attachment` (a file name or `null`).
|
|
103
|
+
- `DocumentsPreviewData`: `fileName`, `reviewers`, `title`, `heading`,
|
|
104
|
+
`trackedChange` (`before`, `deleted`, `inserted`, `after`), `commented`
|
|
105
|
+
(`before`, `marked`, `after`), `nextHeading`, `clauses` and `comments`.
|
|
106
|
+
Each `DocumentsPreviewComment` has `author`, `time`, `anchor` (the quoted
|
|
107
|
+
passage) and `body`.
|
|
108
|
+
- `ResearchPreviewData`: `{ query, decisions }`. Each
|
|
109
|
+
`ResearchPreviewDecision` has `court` (`name`, `abbreviation`, and `tier`:
|
|
110
|
+
`constitutional`, `supreme`, `regional` or `other`), `date`, `fileNumber`
|
|
111
|
+
and the matched passage as `before`, `hit`, `after`.
|
|
112
|
+
- `BillingPreviewData`: `invoiceNumber`, `client`, `matter`, `currency` (ISO
|
|
113
|
+
4217), `hourlyRate` (minor units, minted with `cents` from `@stll/money`)
|
|
114
|
+
and `entries`. Each `BillingPreviewEntry` has `date`, `description` and
|
|
115
|
+
`billedMinutes`.
|
|
116
|
+
- `ContactsPreviewData`: `company` (`name`, `legalForm`, `status`,
|
|
117
|
+
`registry`, `details` as `{ label, value }` pairs, `incorporatedOn` as
|
|
118
|
+
`{ label, date }`) and `linkedMatters`. Registry field titles are the
|
|
119
|
+
registry's own, as the app shows them, so they travel with the data rather
|
|
120
|
+
than `labels`.
|
|
121
|
+
- `TimeTrackingPreviewData`: `day`, `running` (`description`, `matter`,
|
|
122
|
+
`elapsedMinutes`) and `entries`. Each `TimeTrackingPreviewEntry` has
|
|
123
|
+
`start`, `end`, `minutes`, `description` and `matter`.
|
|
124
|
+
|
|
125
|
+
Declare content with `satisfies`, so a missing or misspelt field fails
|
|
126
|
+
typecheck:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { cents } from "@stll/money";
|
|
130
|
+
import { BillingPreview } from "@stll/workspace-ui/feature-previews";
|
|
131
|
+
import type { BillingPreviewData } from "@stll/workspace-ui/feature-previews";
|
|
132
|
+
|
|
133
|
+
const invoice = {
|
|
134
|
+
invoiceNumber: "INV-0042",
|
|
135
|
+
client: "Northwind Shipping Ltd",
|
|
136
|
+
matter: {
|
|
137
|
+
id: "northwind",
|
|
138
|
+
color: "--option-blue",
|
|
139
|
+
name: "Northwind charter dispute",
|
|
140
|
+
},
|
|
141
|
+
currency: "GBP",
|
|
142
|
+
hourlyRate: cents(30_000),
|
|
143
|
+
entries: [
|
|
144
|
+
{
|
|
145
|
+
date: "2026-10-05",
|
|
146
|
+
description: "Review of the charterparty",
|
|
147
|
+
billedMinutes: 120,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
date: "2026-10-07",
|
|
151
|
+
description: "Draft letter to owners",
|
|
152
|
+
billedMinutes: 90,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
} as const satisfies BillingPreviewData;
|
|
156
|
+
|
|
157
|
+
<BillingPreview
|
|
158
|
+
data={invoice}
|
|
159
|
+
labels={{
|
|
160
|
+
date: "Date",
|
|
161
|
+
description: "Description",
|
|
162
|
+
billedTime: "Hours",
|
|
163
|
+
rate: "Rate",
|
|
164
|
+
fee: "Amount",
|
|
165
|
+
summary: "Total",
|
|
166
|
+
}}
|
|
167
|
+
locale="en-GB"
|
|
168
|
+
/>;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Styling: the scenes use Tailwind utilities against the `@stll/ui` theme
|
|
172
|
+
tokens, so the host's Tailwind entry needs the theme and both packages'
|
|
173
|
+
shipped sources (the CSS block under Development below). Light and dark follow
|
|
174
|
+
the theme: put `dark` on the root element (`<html class="dark">`); the theme's
|
|
175
|
+
derived tokens resolve there, so a `dark` class lower in the tree leaves them
|
|
176
|
+
on their light values. The theme's
|
|
177
|
+
`--font-sans` names DM Sans; load it (for example from Google Fonts) or the
|
|
178
|
+
system sans serif stands in. The document page uses Tailwind's `font-serif`.
|
|
179
|
+
|
|
41
180
|
## Development
|
|
42
181
|
|
|
43
182
|
```sh
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PreviewMatter } from "./mock-data.js";
|
|
2
|
+
import { FeaturePreviewProps } from "./surface.js";
|
|
3
|
+
import { CentsAmount } from "@stll/money";
|
|
4
|
+
//#region src/feature-previews/billing-preview.d.ts
|
|
5
|
+
export type BillingPreviewLabels = {
|
|
6
|
+
/** Column headings of the invoice's time entries: date, description,
|
|
7
|
+
* billed time ("Hours"), rate and fee ("Amount"). */
|
|
8
|
+
date: string;
|
|
9
|
+
description: string;
|
|
10
|
+
billedTime: string;
|
|
11
|
+
rate: string;
|
|
12
|
+
fee: string;
|
|
13
|
+
/** Label of the invoice's total row. */
|
|
14
|
+
summary: string;
|
|
15
|
+
};
|
|
16
|
+
export type BillingPreviewEntry = {
|
|
17
|
+
/** `YYYY-MM-DD`; unique within the invoice. */
|
|
18
|
+
date: string;
|
|
19
|
+
description: string;
|
|
20
|
+
billedMinutes: number;
|
|
21
|
+
};
|
|
22
|
+
/** The invoice's content. Every entry bills at `hourlyRate` (minor units,
|
|
23
|
+
* minted with `cents` from `@stll/money`) in `currency` (ISO 4217); the
|
|
24
|
+
* amounts and total are computed. */
|
|
25
|
+
export type BillingPreviewData = {
|
|
26
|
+
invoiceNumber: string;
|
|
27
|
+
client: string;
|
|
28
|
+
matter: PreviewMatter;
|
|
29
|
+
currency: string;
|
|
30
|
+
hourlyRate: CentsAmount;
|
|
31
|
+
/** In the order the invoice lists them, usually by date. */
|
|
32
|
+
entries: readonly BillingPreviewEntry[];
|
|
33
|
+
};
|
|
34
|
+
/** An invoice draft: the matter's time entries in date order with hours,
|
|
35
|
+
* rate and amount, and the total. Amounts come from the same money helpers
|
|
36
|
+
* the app bills with. */
|
|
37
|
+
export declare const BillingPreview: ({ labels, locale, data, className }: FeaturePreviewProps<BillingPreviewLabels, BillingPreviewData>) => import("react").JSX.Element;
|
|
38
|
+
//#endregion
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { formatPreviewDate, formatPreviewHours } from "./format.js";
|
|
2
|
+
import { PREVIEW_MATTERS } from "./mock-data.js";
|
|
3
|
+
import { FeaturePreviewSurface } from "./surface.js";
|
|
4
|
+
import { cents, formatMoneyCents, timeEntryAmount } from "@stll/money";
|
|
5
|
+
import { BanknoteIcon } from "@stll/ui/icons";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { MatterIcon } from "@stll/ui/matter-icon";
|
|
8
|
+
//#region src/feature-previews/billing-preview.tsx
|
|
9
|
+
/** An invoice draft: the matter's time entries in date order with hours,
|
|
10
|
+
* rate and amount, and the total. Amounts come from the same money helpers
|
|
11
|
+
* the app bills with. */
|
|
12
|
+
const BillingPreview = ({ labels, locale, data = DEFAULT_DATA, className }) => {
|
|
13
|
+
const money = (amountCents) => formatMoneyCents({
|
|
14
|
+
amountCents,
|
|
15
|
+
currency: data.currency,
|
|
16
|
+
locale
|
|
17
|
+
});
|
|
18
|
+
const rows = data.entries.map((entry) => ({
|
|
19
|
+
...entry,
|
|
20
|
+
amount: timeEntryAmount({
|
|
21
|
+
billedMinutes: entry.billedMinutes,
|
|
22
|
+
rateAtEntry: data.hourlyRate,
|
|
23
|
+
noCharge: false
|
|
24
|
+
})
|
|
25
|
+
}));
|
|
26
|
+
const totalMinutes = rows.reduce((sum, row) => sum + row.billedMinutes, 0);
|
|
27
|
+
const totalCents = rows.reduce((sum, row) => sum + row.amount, 0);
|
|
28
|
+
return /* @__PURE__ */ jsx(FeaturePreviewSurface, {
|
|
29
|
+
className,
|
|
30
|
+
icon: /* @__PURE__ */ jsx(BanknoteIcon, { className: "text-muted-foreground size-4 shrink-0" }),
|
|
31
|
+
meta: /* @__PURE__ */ jsx("span", {
|
|
32
|
+
className: "tabular-nums",
|
|
33
|
+
children: data.invoiceNumber
|
|
34
|
+
}),
|
|
35
|
+
title: data.client,
|
|
36
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
37
|
+
className: "flex h-full min-h-0 flex-col p-3",
|
|
38
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
39
|
+
className: "text-muted-foreground mb-2 flex min-w-0 items-center gap-1.5",
|
|
40
|
+
children: [/* @__PURE__ */ jsx(MatterIcon, {
|
|
41
|
+
className: "size-3.5 shrink-0",
|
|
42
|
+
matter: data.matter
|
|
43
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
44
|
+
className: "truncate",
|
|
45
|
+
children: data.matter.name
|
|
46
|
+
})]
|
|
47
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
48
|
+
className: "grid grid-cols-[auto_minmax(0,1fr)_auto_auto_auto] gap-x-4",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsxs("div", {
|
|
51
|
+
className: "text-muted-foreground col-span-full grid grid-cols-subgrid border-b pb-1.5 font-medium",
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", { children: labels.date }),
|
|
54
|
+
/* @__PURE__ */ jsx("span", { children: labels.description }),
|
|
55
|
+
/* @__PURE__ */ jsx("span", {
|
|
56
|
+
className: "text-end",
|
|
57
|
+
children: labels.billedTime
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: "text-end",
|
|
61
|
+
children: labels.rate
|
|
62
|
+
}),
|
|
63
|
+
/* @__PURE__ */ jsx("span", {
|
|
64
|
+
className: "text-end",
|
|
65
|
+
children: labels.fee
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
}),
|
|
69
|
+
rows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
className: "col-span-full grid grid-cols-subgrid items-baseline border-b py-2",
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsx("span", {
|
|
73
|
+
className: "text-muted-foreground whitespace-nowrap tabular-nums",
|
|
74
|
+
children: formatPreviewDate({
|
|
75
|
+
locale,
|
|
76
|
+
date: row.date,
|
|
77
|
+
options: {
|
|
78
|
+
day: "numeric",
|
|
79
|
+
month: "short",
|
|
80
|
+
year: "numeric"
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: "truncate",
|
|
86
|
+
children: row.description
|
|
87
|
+
}),
|
|
88
|
+
/* @__PURE__ */ jsx("span", {
|
|
89
|
+
className: "text-end tabular-nums",
|
|
90
|
+
children: formatPreviewHours(locale, row.billedMinutes)
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ jsx("span", {
|
|
93
|
+
className: "text-muted-foreground text-end whitespace-nowrap tabular-nums",
|
|
94
|
+
children: money(data.hourlyRate)
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsx("span", {
|
|
97
|
+
className: "text-end whitespace-nowrap tabular-nums",
|
|
98
|
+
children: money(row.amount)
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
}, row.date)),
|
|
102
|
+
/* @__PURE__ */ jsxs("div", {
|
|
103
|
+
className: "col-span-full grid grid-cols-subgrid pt-2.5 text-sm font-semibold",
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx("span", {
|
|
106
|
+
className: "col-span-2",
|
|
107
|
+
children: labels.summary
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "text-end tabular-nums",
|
|
111
|
+
children: formatPreviewHours(locale, totalMinutes)
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
114
|
+
/* @__PURE__ */ jsx("span", {
|
|
115
|
+
className: "text-end whitespace-nowrap tabular-nums",
|
|
116
|
+
children: money(totalCents)
|
|
117
|
+
})
|
|
118
|
+
]
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
})]
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const DEFAULT_DATA = {
|
|
126
|
+
invoiceNumber: "INV-2026-0187",
|
|
127
|
+
client: "Harbour Logistics Ltd",
|
|
128
|
+
matter: PREVIEW_MATTERS.cargo,
|
|
129
|
+
currency: "EUR",
|
|
130
|
+
hourlyRate: cents(22e3),
|
|
131
|
+
entries: [
|
|
132
|
+
{
|
|
133
|
+
date: "2026-10-02",
|
|
134
|
+
description: "Conflict check and engagement letter",
|
|
135
|
+
billedMinutes: 30
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
date: "2026-10-06",
|
|
139
|
+
description: "Review of carriage contract and bills of lading",
|
|
140
|
+
billedMinutes: 144
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
date: "2026-10-09",
|
|
144
|
+
description: "Call with client on the cargo damage claim",
|
|
145
|
+
billedMinutes: 90
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
date: "2026-10-14",
|
|
149
|
+
description: "Draft letter before action",
|
|
150
|
+
billedMinutes: 210
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
date: "2026-10-16",
|
|
154
|
+
description: "Research on carrier liability limits",
|
|
155
|
+
billedMinutes: 120
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
export { BillingPreview };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PreviewMatter } from "./mock-data.js";
|
|
2
|
+
import { FeaturePreviewProps } from "./surface.js";
|
|
3
|
+
//#region src/feature-previews/contacts-preview.d.ts
|
|
4
|
+
export type ContactsPreviewLabels = {
|
|
5
|
+
/** Heading of the section filled from the company registry. */
|
|
6
|
+
businessRegistry: string;
|
|
7
|
+
/** Heading of the matters the contact is linked to. */
|
|
8
|
+
matters: string;
|
|
9
|
+
};
|
|
10
|
+
/** The contact's content. Registry field titles (`details[].label`,
|
|
11
|
+
* `incorporatedOn.label`) are the registry's own, as the app shows them, so
|
|
12
|
+
* they come with the data rather than the labels. */
|
|
13
|
+
export type ContactsPreviewData = {
|
|
14
|
+
company: {
|
|
15
|
+
name: string;
|
|
16
|
+
legalForm: string;
|
|
17
|
+
status: string;
|
|
18
|
+
/** The registry's name, beside the section heading. */
|
|
19
|
+
registry: string;
|
|
20
|
+
details: readonly {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}[];
|
|
24
|
+
/** `date` is `YYYY-MM-DD`. */
|
|
25
|
+
incorporatedOn: {
|
|
26
|
+
label: string;
|
|
27
|
+
date: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
linkedMatters: readonly PreviewMatter[];
|
|
31
|
+
};
|
|
32
|
+
/** A company contact filled from its business registry: legal form, status,
|
|
33
|
+
* registered details and the matters it appears in. */
|
|
34
|
+
export declare const ContactsPreview: ({ labels, locale, data: { company, linkedMatters }, className }: FeaturePreviewProps<ContactsPreviewLabels, ContactsPreviewData>) => import("react").JSX.Element;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { formatPreviewDate } from "./format.js";
|
|
2
|
+
import { PREVIEW_MATTERS } from "./mock-data.js";
|
|
3
|
+
import { FeaturePreviewSurface } from "./surface.js";
|
|
4
|
+
import { Building2Icon, ShieldCheckIcon } from "@stll/ui/icons";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { MatterIcon } from "@stll/ui/matter-icon";
|
|
7
|
+
import { DetailsGrid, DetailsItem } from "@stll/ui/details-grid";
|
|
8
|
+
//#region src/feature-previews/contacts-preview.tsx
|
|
9
|
+
/** A company contact filled from its business registry: legal form, status,
|
|
10
|
+
* registered details and the matters it appears in. */
|
|
11
|
+
const ContactsPreview = ({ labels, locale, data: { company, linkedMatters } = DEFAULT_DATA, className }) => /* @__PURE__ */ jsx(FeaturePreviewSurface, {
|
|
12
|
+
className,
|
|
13
|
+
icon: /* @__PURE__ */ jsx(Building2Icon, { className: "text-muted-foreground size-4 shrink-0" }),
|
|
14
|
+
title: company.name,
|
|
15
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "flex h-full min-h-0 flex-col gap-4 p-4",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "flex min-w-0 items-center gap-2 text-sm",
|
|
20
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
21
|
+
className: "truncate",
|
|
22
|
+
children: company.legalForm
|
|
23
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
24
|
+
className: "text-success flex shrink-0 items-center gap-1 font-medium",
|
|
25
|
+
children: [/* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-current" }), company.status]
|
|
26
|
+
})]
|
|
27
|
+
}),
|
|
28
|
+
/* @__PURE__ */ jsxs("section", {
|
|
29
|
+
className: "flex min-w-0 flex-col gap-2",
|
|
30
|
+
children: [/* @__PURE__ */ jsxs("h3", {
|
|
31
|
+
className: "text-muted-foreground flex items-center gap-1.5 font-medium",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(ShieldCheckIcon, { className: "size-3.5 shrink-0" }),
|
|
34
|
+
labels.businessRegistry,
|
|
35
|
+
/* @__PURE__ */ jsxs("span", {
|
|
36
|
+
className: "text-foreground-subtle font-normal",
|
|
37
|
+
children: ["· ", company.registry]
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
}), /* @__PURE__ */ jsxs(DetailsGrid, {
|
|
41
|
+
className: "gap-y-1.5",
|
|
42
|
+
children: [company.details.map((detail) => /* @__PURE__ */ jsx(DetailsItem, {
|
|
43
|
+
label: detail.label,
|
|
44
|
+
children: detail.value
|
|
45
|
+
}, detail.label)), /* @__PURE__ */ jsx(DetailsItem, {
|
|
46
|
+
label: company.incorporatedOn.label,
|
|
47
|
+
children: formatPreviewDate({
|
|
48
|
+
locale,
|
|
49
|
+
date: company.incorporatedOn.date,
|
|
50
|
+
options: {
|
|
51
|
+
day: "numeric",
|
|
52
|
+
month: "long",
|
|
53
|
+
year: "numeric"
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
})]
|
|
57
|
+
})]
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ jsxs("section", {
|
|
60
|
+
className: "flex min-w-0 flex-col gap-1.5",
|
|
61
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
62
|
+
className: "text-muted-foreground font-medium",
|
|
63
|
+
children: labels.matters
|
|
64
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
65
|
+
className: "flex flex-col gap-1",
|
|
66
|
+
children: linkedMatters.map((matter) => /* @__PURE__ */ jsxs("li", {
|
|
67
|
+
className: "flex min-w-0 items-center gap-2",
|
|
68
|
+
children: [/* @__PURE__ */ jsx(MatterIcon, {
|
|
69
|
+
className: "size-3.5 shrink-0",
|
|
70
|
+
matter
|
|
71
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
72
|
+
className: "truncate",
|
|
73
|
+
children: matter.name
|
|
74
|
+
})]
|
|
75
|
+
}, matter.id))
|
|
76
|
+
})]
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
const DEFAULT_DATA = {
|
|
82
|
+
company: {
|
|
83
|
+
name: "Meridian Freight Ltd",
|
|
84
|
+
legalForm: "Private limited company",
|
|
85
|
+
status: "Active",
|
|
86
|
+
registry: "Companies House",
|
|
87
|
+
details: [
|
|
88
|
+
{
|
|
89
|
+
label: "Company number",
|
|
90
|
+
value: "09876543"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "Registered office",
|
|
94
|
+
value: "Unit 4, Quayside Park, Bristol BS1 6XX"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "SIC code",
|
|
98
|
+
value: "52290 – Other transportation support activities"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
incorporatedOn: {
|
|
102
|
+
label: "Incorporated on",
|
|
103
|
+
date: "2015-03-04"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
linkedMatters: [PREVIEW_MATTERS.cargo, PREVIEW_MATTERS.lease]
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { ContactsPreview };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PreviewPerson } from "./mock-data.js";
|
|
2
|
+
import { FeaturePreviewProps } from "./surface.js";
|
|
3
|
+
//#region src/feature-previews/documents-preview.d.ts
|
|
4
|
+
export type DocumentsPreviewLabels = {
|
|
5
|
+
/** Heading of the comment rail beside the page. */
|
|
6
|
+
comments: string;
|
|
7
|
+
};
|
|
8
|
+
export type DocumentsPreviewComment = {
|
|
9
|
+
author: PreviewPerson;
|
|
10
|
+
/** `HH:MM`; unique within the document. */
|
|
11
|
+
time: string;
|
|
12
|
+
/** The commented passage, quoted under the comment. */
|
|
13
|
+
anchor: string;
|
|
14
|
+
body: string;
|
|
15
|
+
};
|
|
16
|
+
/** The page's content, top to bottom: a title, a heading, a clause with one
|
|
17
|
+
* tracked change (`deleted` replaced by `inserted`), a clause with a
|
|
18
|
+
* commented passage (`marked`), another heading and plain clauses; then
|
|
19
|
+
* the comment rail. Text is literal: number each clause in its text. */
|
|
20
|
+
export type DocumentsPreviewData = {
|
|
21
|
+
fileName: string;
|
|
22
|
+
/** People shown in the title row. */
|
|
23
|
+
reviewers: readonly PreviewPerson[];
|
|
24
|
+
title: string;
|
|
25
|
+
heading: string;
|
|
26
|
+
trackedChange: {
|
|
27
|
+
before: string;
|
|
28
|
+
deleted: string;
|
|
29
|
+
inserted: string;
|
|
30
|
+
after: string;
|
|
31
|
+
};
|
|
32
|
+
commented: {
|
|
33
|
+
before: string;
|
|
34
|
+
marked: string;
|
|
35
|
+
after: string;
|
|
36
|
+
};
|
|
37
|
+
nextHeading: string;
|
|
38
|
+
clauses: readonly string[];
|
|
39
|
+
comments: readonly DocumentsPreviewComment[];
|
|
40
|
+
};
|
|
41
|
+
/** A Word document edited in place: a page with a tracked change and a
|
|
42
|
+
* commented passage, and the comment rail beside it. */
|
|
43
|
+
export declare const DocumentsPreview: ({ labels, locale, data, className }: FeaturePreviewProps<DocumentsPreviewLabels, DocumentsPreviewData>) => import("react").JSX.Element;
|
|
44
|
+
//#endregion
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { formatPreviewCount, formatPreviewTime } from "./format.js";
|
|
2
|
+
import { PREVIEW_PEOPLE } from "./mock-data.js";
|
|
3
|
+
import { FeaturePreviewSurface, PreviewAvatarStack } from "./surface.js";
|
|
4
|
+
import { FileTextIcon } from "@stll/ui/icons";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { ReviewCommentCard } from "@stll/ui/review-comment-card";
|
|
7
|
+
import { ReviewDiffDeletion, ReviewDiffInsertion } from "@stll/ui/review-diff-text";
|
|
8
|
+
import { TextMark } from "@stll/ui/text-mark";
|
|
9
|
+
//#region src/feature-previews/documents-preview.tsx
|
|
10
|
+
/** A Word document edited in place: a page with a tracked change and a
|
|
11
|
+
* commented passage, and the comment rail beside it. */
|
|
12
|
+
const DocumentsPreview = ({ labels, locale, data = DEFAULT_DATA, className }) => /* @__PURE__ */ jsx(FeaturePreviewSurface, {
|
|
13
|
+
className,
|
|
14
|
+
icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "text-info size-4 shrink-0" }),
|
|
15
|
+
meta: /* @__PURE__ */ jsx(PreviewAvatarStack, { people: data.reviewers }),
|
|
16
|
+
title: data.fileName,
|
|
17
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
18
|
+
className: "flex h-full min-h-0",
|
|
19
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
20
|
+
className: "bg-muted/50 flex min-w-0 flex-1 justify-center overflow-hidden px-4 pt-4",
|
|
21
|
+
children: /* @__PURE__ */ jsxs("article", {
|
|
22
|
+
className: "bg-card text-foreground text-2xs flex w-full max-w-md flex-col gap-2.5 rounded-t-sm border border-b-0 px-6 pt-6 font-serif leading-relaxed shadow-sm",
|
|
23
|
+
dir: "ltr",
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx("h3", {
|
|
26
|
+
className: "text-center text-xs font-semibold tracking-widest",
|
|
27
|
+
children: data.title
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ jsx("p", {
|
|
30
|
+
className: "font-semibold",
|
|
31
|
+
children: data.heading
|
|
32
|
+
}),
|
|
33
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
34
|
+
data.trackedChange.before,
|
|
35
|
+
/* @__PURE__ */ jsx(ReviewDiffDeletion, { children: data.trackedChange.deleted }),
|
|
36
|
+
/* @__PURE__ */ jsx(ReviewDiffInsertion, { children: data.trackedChange.inserted }),
|
|
37
|
+
data.trackedChange.after
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
40
|
+
data.commented.before,
|
|
41
|
+
/* @__PURE__ */ jsx(TextMark, {
|
|
42
|
+
tone: "option-yellow",
|
|
43
|
+
variant: "fill",
|
|
44
|
+
children: data.commented.marked
|
|
45
|
+
}),
|
|
46
|
+
data.commented.after
|
|
47
|
+
] }),
|
|
48
|
+
/* @__PURE__ */ jsx("p", {
|
|
49
|
+
className: "font-semibold",
|
|
50
|
+
children: data.nextHeading
|
|
51
|
+
}),
|
|
52
|
+
data.clauses.map((clause) => /* @__PURE__ */ jsx("p", { children: clause }, clause))
|
|
53
|
+
]
|
|
54
|
+
})
|
|
55
|
+
}), /* @__PURE__ */ jsxs("aside", {
|
|
56
|
+
className: "flex w-[38%] max-w-64 min-w-0 shrink-0 flex-col border-s",
|
|
57
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
58
|
+
className: "text-muted-foreground flex h-8 shrink-0 items-center gap-1.5 px-3 font-medium",
|
|
59
|
+
children: [labels.comments, /* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: "tabular-nums",
|
|
61
|
+
children: formatPreviewCount(locale, data.comments.length)
|
|
62
|
+
})]
|
|
63
|
+
}), data.comments.map((comment) => /* @__PURE__ */ jsx(ReviewCommentCard, {
|
|
64
|
+
anchorText: comment.anchor,
|
|
65
|
+
author: comment.author,
|
|
66
|
+
body: comment.body,
|
|
67
|
+
className: "border-t",
|
|
68
|
+
formattedTime: formatPreviewTime(locale, comment.time),
|
|
69
|
+
timestamp: `2026-10-19T${comment.time}:00Z`
|
|
70
|
+
}, comment.time))]
|
|
71
|
+
})]
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
const DEFAULT_DATA = {
|
|
75
|
+
fileName: "Share Purchase Agreement.docx",
|
|
76
|
+
reviewers: [PREVIEW_PEOPLE.laura, PREVIEW_PEOPLE.amira],
|
|
77
|
+
title: "SHARE PURCHASE AGREEMENT",
|
|
78
|
+
heading: "4. Purchase price",
|
|
79
|
+
trackedChange: {
|
|
80
|
+
before: "4.1 The Purchase Price shall be EUR 4,200,000, payable to the Seller's account within ",
|
|
81
|
+
deleted: "30",
|
|
82
|
+
inserted: "45",
|
|
83
|
+
after: " Business Days of Completion."
|
|
84
|
+
},
|
|
85
|
+
commented: {
|
|
86
|
+
before: "4.2 An amount equal to 10% of the Purchase Price shall be held in escrow ",
|
|
87
|
+
marked: "until the expiry of the Warranty Period",
|
|
88
|
+
after: " in accordance with Schedule 6."
|
|
89
|
+
},
|
|
90
|
+
nextHeading: "5. Warranties",
|
|
91
|
+
clauses: ["5.1 The Seller warrants to the Buyer that each of the Warranties is true, accurate and not misleading at the date of this Agreement and will be so at Completion.", "5.2 Each Warranty is a separate and independent warranty and shall not be limited by reference to any other Warranty."],
|
|
92
|
+
comments: [{
|
|
93
|
+
author: PREVIEW_PEOPLE.laura,
|
|
94
|
+
time: "09:12",
|
|
95
|
+
anchor: "45 Business Days",
|
|
96
|
+
body: "Buyer's counsel will push for 60 days. 45 is our floor."
|
|
97
|
+
}, {
|
|
98
|
+
author: PREVIEW_PEOPLE.amira,
|
|
99
|
+
time: "10:47",
|
|
100
|
+
anchor: "until the expiry of the Warranty Period",
|
|
101
|
+
body: "Tie the escrow release to the claims deadline in clause 7.3."
|
|
102
|
+
}]
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { DocumentsPreview };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/feature-previews/format.d.ts
|
|
2
|
+
type FormatPreviewDateParams = {
|
|
3
|
+
locale: string;
|
|
4
|
+
/** `YYYY-MM-DD`. */
|
|
5
|
+
date: string;
|
|
6
|
+
options: Intl.DateTimeFormatOptions;
|
|
7
|
+
};
|
|
8
|
+
export declare const formatPreviewDate: ({ locale, date, options }: FormatPreviewDateParams) => string;
|
|
9
|
+
/** A wall-clock time (`HH:MM`) in the locale's own hour cycle. */
|
|
10
|
+
export declare const formatPreviewTime: (locale: string, time: string) => string;
|
|
11
|
+
/** Minutes as `h:mm`, the way a timesheet shows a duration. */
|
|
12
|
+
export declare const formatPreviewDuration: (locale: string, totalMinutes: number) => string;
|
|
13
|
+
/** Minutes as decimal hours, the way an invoice bills them. */
|
|
14
|
+
export declare const formatPreviewHours: (locale: string, totalMinutes: number) => string;
|
|
15
|
+
export declare const formatPreviewCount: (locale: string, count: number) => string;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill/full";
|
|
2
|
+
//#region src/feature-previews/format.ts
|
|
3
|
+
const formatPreviewDate = ({ locale, date, options }) => Temporal.PlainDate.from(date).toLocaleString(locale, options);
|
|
4
|
+
/** A wall-clock time (`HH:MM`) in the locale's own hour cycle. */
|
|
5
|
+
const formatPreviewTime = (locale, time) => Temporal.PlainTime.from(time).toLocaleString(locale, {
|
|
6
|
+
hour: "numeric",
|
|
7
|
+
minute: "2-digit"
|
|
8
|
+
});
|
|
9
|
+
/** Minutes as `h:mm`, the way a timesheet shows a duration. */
|
|
10
|
+
const formatPreviewDuration = (locale, totalMinutes) => {
|
|
11
|
+
return `${new Intl.NumberFormat(locale).format(Math.floor(totalMinutes / 60))}:${new Intl.NumberFormat(locale, { minimumIntegerDigits: 2 }).format(totalMinutes % 60)}`;
|
|
12
|
+
};
|
|
13
|
+
/** Minutes as decimal hours, the way an invoice bills them. */
|
|
14
|
+
const formatPreviewHours = (locale, totalMinutes) => new Intl.NumberFormat(locale, {
|
|
15
|
+
minimumFractionDigits: 1,
|
|
16
|
+
maximumFractionDigits: 2
|
|
17
|
+
}).format(totalMinutes / 60);
|
|
18
|
+
const formatPreviewCount = (locale, count) => new Intl.NumberFormat(locale).format(count);
|
|
19
|
+
//#endregion
|
|
20
|
+
export { formatPreviewCount, formatPreviewDate, formatPreviewDuration, formatPreviewHours, formatPreviewTime };
|