@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 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 };