@stll/workspace-ui 0.0.1-placeholder.0 → 0.2.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/README.md +59 -0
- package/dist/calculation-format.d.ts +54 -0
- package/dist/calculation-format.js +72 -0
- package/dist/calculation-selection.d.ts +15 -0
- package/dist/calculation-selection.js +14 -0
- package/dist/calculations.d.ts +67 -0
- package/dist/calculations.js +108 -0
- package/dist/colors.d.ts +2 -0
- package/dist/colors.js +2 -0
- package/dist/conditions/builder.d.ts +98 -0
- package/dist/conditions/builder.js +447 -0
- package/dist/conditions/index.d.ts +3 -0
- package/dist/conditions/index.js +3 -0
- package/dist/conditions/logic.d.ts +70 -0
- package/dist/conditions/logic.js +248 -0
- package/dist/field-value-logic.d.ts +7 -0
- package/dist/field-value-logic.js +8 -0
- package/dist/field-value.d.ts +30 -0
- package/dist/field-value.js +308 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/property-icon.d.ts +12 -0
- package/dist/property-icon.js +56 -0
- package/dist/sorts.d.ts +41 -0
- package/dist/sorts.js +99 -0
- package/dist/table-skeleton-rows.d.ts +18 -0
- package/dist/table-skeleton-rows.js +34 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +17 -0
- package/dist/view-switcher.d.ts +33 -0
- package/dist/view-switcher.js +159 -0
- package/dist/view-switcher.logic.d.ts +14 -0
- package/dist/view-switcher.logic.js +14 -0
- package/package.json +135 -3
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @stll/workspace-ui
|
|
2
|
+
|
|
3
|
+
Reusable workspace presentation components and view helpers for Stella.
|
|
4
|
+
|
|
5
|
+
## Saved views
|
|
6
|
+
|
|
7
|
+
`@stll/workspace-ui/view-switcher` exports a controlled saved-view strip with
|
|
8
|
+
keyboard selection, horizontal overflow, inline editing slots, and bidi-aware
|
|
9
|
+
Pragmatic Drag and Drop reordering. Hosts retain ownership of persistence,
|
|
10
|
+
permissions, translations, menus, and layout rendering.
|
|
11
|
+
|
|
12
|
+
Reusable React presentation modules for Stella workspaces: field values,
|
|
13
|
+
property icons, sorting chips, condition builders, table loading rows, and
|
|
14
|
+
column calculations.
|
|
15
|
+
|
|
16
|
+
The package owns view-level rendering contracts while the surrounding workspace
|
|
17
|
+
supplies its data, labels, and locale. The condition builder resolves no
|
|
18
|
+
strings itself; labels and field options arrive through its props.
|
|
19
|
+
|
|
20
|
+
## Imports
|
|
21
|
+
|
|
22
|
+
Use the explicit subpaths for focused modules:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { ConditionBuilder } from "@stll/workspace-ui/conditions";
|
|
26
|
+
import { FieldValue } from "@stll/workspace-ui/field-value";
|
|
27
|
+
import { SortChips } from "@stll/workspace-ui/sorts";
|
|
28
|
+
import { WorkspaceViewSwitcher } from "@stll/workspace-ui/view-switcher";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package also exposes a root entry for code that uses several workspace
|
|
32
|
+
surfaces together. The subpaths remain the clearest way to declare a module's
|
|
33
|
+
boundary.
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
bun run build # tsdown, one output module per source module
|
|
39
|
+
bun run test # bun test src
|
|
40
|
+
bun run typecheck
|
|
41
|
+
bun run lint
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Peer dependencies: `react`, `react-dom`, `@base-ui/react`, Pragmatic Drag and
|
|
45
|
+
Drop v3 with its hitbox package, TanStack React Table, and Tailwind CSS v4. The
|
|
46
|
+
package's components use Tailwind utility classes; the host owns the Tailwind
|
|
47
|
+
entry point and token configuration.
|
|
48
|
+
|
|
49
|
+
Include both published component packages in that entry point so Tailwind scans
|
|
50
|
+
their shipped class names:
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
@import "tailwindcss";
|
|
54
|
+
@import "@stll/ui/theme.css";
|
|
55
|
+
@source "../node_modules/@stll/ui/dist";
|
|
56
|
+
@source "../node_modules/@stll/workspace-ui/dist";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This package is published, so public API changes need a changeset.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CalculationResult } from "@stll/calculations";
|
|
2
|
+
import { CentsAmount } from "@stll/money";
|
|
3
|
+
//#region src/calculation-format.d.ts
|
|
4
|
+
type CalculationFormatters = {
|
|
5
|
+
number: (value: number) => string;
|
|
6
|
+
money: (amountCents: CentsAmount, currency: string) => string;
|
|
7
|
+
percent: (ratio: number) => string;
|
|
8
|
+
};
|
|
9
|
+
type CalculationLabels = {
|
|
10
|
+
/** The reduction's short name, e.g. "Sum". */
|
|
11
|
+
kind: string;
|
|
12
|
+
/** Stands in for a value the reduction cannot produce. */
|
|
13
|
+
unavailable: string;
|
|
14
|
+
};
|
|
15
|
+
type FormattedCalculation = {
|
|
16
|
+
/** One compact line, for the column header. */
|
|
17
|
+
summary: string;
|
|
18
|
+
/**
|
|
19
|
+
* One line per currency, for the tooltip. Empty when the summary already
|
|
20
|
+
* says everything: there is nothing to expand for a single unit.
|
|
21
|
+
*/
|
|
22
|
+
breakdown: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
type FormatCalculationParams = {
|
|
25
|
+
result: CalculationResult;
|
|
26
|
+
formatters: CalculationFormatters;
|
|
27
|
+
labels: CalculationLabels;
|
|
28
|
+
};
|
|
29
|
+
declare const formatCalculationResult: ({ result, formatters, labels }: FormatCalculationParams) => FormattedCalculation;
|
|
30
|
+
/**
|
|
31
|
+
* How many minor units make a major one, for this currency: 100 for CZK, 1 for
|
|
32
|
+
* JPY, 1000 for KWD. It is a property of the currency and not of the reader, so
|
|
33
|
+
* the lookup is deliberately locale-independent.
|
|
34
|
+
*
|
|
35
|
+
* A malformed code makes the `Intl.NumberFormat` constructor throw, before any
|
|
36
|
+
* `?? 2` on its result could help, so the fallback has to wrap the call.
|
|
37
|
+
*/
|
|
38
|
+
declare const currencyMinorUnitDigits: (currency: string) => number;
|
|
39
|
+
type FormatMoneyCentsParams = {
|
|
40
|
+
amountCents: number;
|
|
41
|
+
currency: string;
|
|
42
|
+
locale: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Money is stored in minor units, and how many of them make a major one is a
|
|
46
|
+
* property of the currency. Ask the currency rather than assuming a hundred.
|
|
47
|
+
*
|
|
48
|
+
* A code `Intl` rejects falls back to the amount beside the raw code: a column
|
|
49
|
+
* showing "1500 A1C" is wrong-looking data, which is the truth, where a thrown
|
|
50
|
+
* RangeError would take the whole board down with it.
|
|
51
|
+
*/
|
|
52
|
+
declare const formatMoneyCents: ({ amountCents, currency, locale }: FormatMoneyCentsParams) => string;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { CalculationFormatters, CalculationLabels, FormatCalculationParams, FormatMoneyCentsParams, FormattedCalculation, currencyMinorUnitDigits, formatCalculationResult, formatMoneyCents };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
//#region src/calculation-format.ts
|
|
3
|
+
/**
|
|
4
|
+
* Turning a calculation result into the two strings a view shows: the compact
|
|
5
|
+
* line in the header, and the breakdown behind it.
|
|
6
|
+
*
|
|
7
|
+
* Formatting is separate from reducing because the reduction is the same
|
|
8
|
+
* everywhere and the formatting is not: it needs the reader's locale, and a
|
|
9
|
+
* monetary reduction has one line per currency to lay out.
|
|
10
|
+
*/
|
|
11
|
+
const formatCalculationResult = ({ result, formatters, labels }) => {
|
|
12
|
+
switch (result.type) {
|
|
13
|
+
case "count":
|
|
14
|
+
case "number": return line(labels.kind, formatters.number(result.value));
|
|
15
|
+
case "ratio": return line(labels.kind, formatters.percent(result.value));
|
|
16
|
+
case "money": {
|
|
17
|
+
const amounts = result.totals.map((total) => formatters.money(total.amountCents, total.currency));
|
|
18
|
+
if (amounts.length === 0) return line(labels.kind, labels.unavailable);
|
|
19
|
+
return {
|
|
20
|
+
summary: `${labels.kind} ${amounts.join(SEPARATOR)}`,
|
|
21
|
+
breakdown: amounts.length > 1 ? amounts : []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
case "unsupported": return line(labels.kind, labels.unavailable);
|
|
25
|
+
default: return result;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Two, the ISO 4217 default, used when a stored code is one `Intl` will not
|
|
30
|
+
* accept. Validation at the API boundary keeps those out, but a row written
|
|
31
|
+
* before that constraint existed must still render rather than throw.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_MINOR_UNIT_DIGITS = 2;
|
|
34
|
+
/**
|
|
35
|
+
* How many minor units make a major one, for this currency: 100 for CZK, 1 for
|
|
36
|
+
* JPY, 1000 for KWD. It is a property of the currency and not of the reader, so
|
|
37
|
+
* the lookup is deliberately locale-independent.
|
|
38
|
+
*
|
|
39
|
+
* A malformed code makes the `Intl.NumberFormat` constructor throw, before any
|
|
40
|
+
* `?? 2` on its result could help, so the fallback has to wrap the call.
|
|
41
|
+
*/
|
|
42
|
+
const currencyMinorUnitDigits = (currency) => {
|
|
43
|
+
const resolved = Result.try(() => new Intl.NumberFormat("en", {
|
|
44
|
+
style: "currency",
|
|
45
|
+
currency
|
|
46
|
+
}).resolvedOptions().maximumFractionDigits);
|
|
47
|
+
if (resolved.isErr()) return DEFAULT_MINOR_UNIT_DIGITS;
|
|
48
|
+
return resolved.value ?? DEFAULT_MINOR_UNIT_DIGITS;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Money is stored in minor units, and how many of them make a major one is a
|
|
52
|
+
* property of the currency. Ask the currency rather than assuming a hundred.
|
|
53
|
+
*
|
|
54
|
+
* A code `Intl` rejects falls back to the amount beside the raw code: a column
|
|
55
|
+
* showing "1500 A1C" is wrong-looking data, which is the truth, where a thrown
|
|
56
|
+
* RangeError would take the whole board down with it.
|
|
57
|
+
*/
|
|
58
|
+
const formatMoneyCents = ({ amountCents, currency, locale }) => {
|
|
59
|
+
const major = amountCents / 10 ** currencyMinorUnitDigits(currency);
|
|
60
|
+
const formatted = Result.try(() => new Intl.NumberFormat(locale, {
|
|
61
|
+
style: "currency",
|
|
62
|
+
currency
|
|
63
|
+
}).format(major));
|
|
64
|
+
return formatted.isErr() ? `${major} ${currency}` : formatted.value;
|
|
65
|
+
};
|
|
66
|
+
const SEPARATOR = " · ";
|
|
67
|
+
const line = (kind, value) => ({
|
|
68
|
+
summary: `${kind} ${value}`,
|
|
69
|
+
breakdown: []
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
72
|
+
export { currencyMinorUnitDigits, formatCalculationResult, formatMoneyCents };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CalculationKind } from "@stll/calculations";
|
|
2
|
+
//#region src/calculation-selection.d.ts
|
|
3
|
+
type CalculationSelection = {
|
|
4
|
+
propertyId: string;
|
|
5
|
+
kind: CalculationKind;
|
|
6
|
+
};
|
|
7
|
+
type ApplyCalculationSelectionParams = {
|
|
8
|
+
selections: readonly CalculationSelection[];
|
|
9
|
+
propertyId: string;
|
|
10
|
+
/** The reduction to show, or null to stop showing one. */
|
|
11
|
+
kind: CalculationKind | null;
|
|
12
|
+
};
|
|
13
|
+
declare const applyCalculationSelection: ({ selections, propertyId, kind }: ApplyCalculationSelectionParams) => CalculationSelection[];
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ApplyCalculationSelectionParams, CalculationSelection, applyCalculationSelection };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/calculation-selection.ts
|
|
2
|
+
const applyCalculationSelection = ({ selections, propertyId, kind }) => {
|
|
3
|
+
if (kind === null) return selections.filter((selection) => selection.propertyId !== propertyId);
|
|
4
|
+
if (selections.some((selection) => selection.propertyId === propertyId)) return selections.map((selection) => selection.propertyId === propertyId ? {
|
|
5
|
+
propertyId,
|
|
6
|
+
kind
|
|
7
|
+
} : selection);
|
|
8
|
+
return [...selections, {
|
|
9
|
+
propertyId,
|
|
10
|
+
kind
|
|
11
|
+
}];
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { applyCalculationSelection };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { FormattedCalculation } from "./calculation-format.js";
|
|
2
|
+
import { CalculationSelection } from "./calculation-selection.js";
|
|
3
|
+
import { CalculationKind, CalculationResult, CalculationValue } from "@stll/calculations";
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
//#region src/calculations.d.ts
|
|
6
|
+
/** A property a view can calculate over, with the reductions its values allow. */
|
|
7
|
+
type CalculationProperty = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kinds: readonly CalculationKind[];
|
|
11
|
+
};
|
|
12
|
+
type WorkspaceCalculationLabels = {
|
|
13
|
+
choose: string;
|
|
14
|
+
kinds: Record<CalculationKind, string>;
|
|
15
|
+
noProperties: string;
|
|
16
|
+
none: string;
|
|
17
|
+
unavailable: string;
|
|
18
|
+
};
|
|
19
|
+
type CalculationPickerProps = {
|
|
20
|
+
labels: WorkspaceCalculationLabels;
|
|
21
|
+
properties: readonly CalculationProperty[];
|
|
22
|
+
selections: readonly CalculationSelection[];
|
|
23
|
+
onChange: (selections: CalculationSelection[]) => void;
|
|
24
|
+
/** Trigger content. Defaults to the calculation glyph. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Choose what a view calculates: a property, then a reduction. Every step is a
|
|
29
|
+
* menu item, so the whole choice is reachable from the keyboard.
|
|
30
|
+
*/
|
|
31
|
+
declare const CalculationPicker: ({ labels, properties, selections, onChange, children }: CalculationPickerProps) => import("react").JSX.Element;
|
|
32
|
+
type CalculationKindPickerProps = {
|
|
33
|
+
labels: WorkspaceCalculationLabels;
|
|
34
|
+
/** Reductions this column's values allow. */
|
|
35
|
+
kinds: readonly CalculationKind[];
|
|
36
|
+
/** The reduction currently shown, or null for none. */
|
|
37
|
+
value: CalculationKind | null;
|
|
38
|
+
onChange: (kind: CalculationKind | null) => void;
|
|
39
|
+
/** Trigger content. Defaults to the calculation glyph. */
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Choose one column's reduction. The board picks a property first (it shows one
|
|
44
|
+
* line for the whole board); a table column already is the property.
|
|
45
|
+
*/
|
|
46
|
+
declare const CalculationKindPicker: ({ labels, kinds, value, onChange, children }: CalculationKindPickerProps) => import("react").JSX.Element;
|
|
47
|
+
type CalculationSummaryProps = {
|
|
48
|
+
calculation: FormattedCalculation;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The calculation as one compact line. When it reduces to several currencies
|
|
52
|
+
* the line stays short and the full set sits behind a tooltip.
|
|
53
|
+
*/
|
|
54
|
+
declare const CalculationSummary: ({ calculation }: CalculationSummaryProps) => import("react").JSX.Element;
|
|
55
|
+
type UseCalculationParams = {
|
|
56
|
+
kind: CalculationKind;
|
|
57
|
+
labels: WorkspaceCalculationLabels;
|
|
58
|
+
values: readonly CalculationValue[];
|
|
59
|
+
/** Every value in the view, for a reduction relative to the whole. */
|
|
60
|
+
scopeValues?: readonly CalculationValue[] | undefined;
|
|
61
|
+
};
|
|
62
|
+
/** Reduce a column's values and format the answer for the reader's locale. */
|
|
63
|
+
declare const useCalculation: ({ kind, labels, values, scopeValues }: UseCalculationParams) => FormattedCalculation;
|
|
64
|
+
/** One configured calculation, reduced over a column and rendered compactly. */
|
|
65
|
+
declare const ColumnCalculation: (params: UseCalculationParams) => import("react").JSX.Element;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { CalculationKindPicker, CalculationKindPickerProps, CalculationPicker, CalculationPickerProps, CalculationProperty, type CalculationResult, type CalculationSelection, CalculationSummary, CalculationSummaryProps, ColumnCalculation, UseCalculationParams, WorkspaceCalculationLabels, useCalculation };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { formatCalculationResult, formatMoneyCents } from "./calculation-format.js";
|
|
2
|
+
import { applyCalculationSelection } from "./calculation-selection.js";
|
|
3
|
+
import { CheckIcon, SigmaIcon } from "lucide-react";
|
|
4
|
+
import { useFormatter, useLocale } from "use-intl";
|
|
5
|
+
import { runCalculation } from "@stll/calculations";
|
|
6
|
+
import { Button } from "@stll/ui/button";
|
|
7
|
+
import { Menu, MenuItem, MenuPopup, MenuSub, MenuSubPopup, MenuSubTrigger, MenuTrigger } from "@stll/ui/menu";
|
|
8
|
+
import { Tooltip, TooltipPopup, TooltipTrigger } from "@stll/ui/tooltip";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/calculations.tsx
|
|
11
|
+
/**
|
|
12
|
+
* Choose what a view calculates: a property, then a reduction. Every step is a
|
|
13
|
+
* menu item, so the whole choice is reachable from the keyboard.
|
|
14
|
+
*/
|
|
15
|
+
const CalculationPicker = ({ labels, properties, selections, onChange, children }) => {
|
|
16
|
+
const select = (propertyId, kind) => {
|
|
17
|
+
onChange(applyCalculationSelection({
|
|
18
|
+
selections,
|
|
19
|
+
propertyId,
|
|
20
|
+
kind
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
return /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
|
|
24
|
+
"aria-label": labels.choose,
|
|
25
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
26
|
+
size: "icon-xs",
|
|
27
|
+
variant: "ghost"
|
|
28
|
+
}),
|
|
29
|
+
children: children ?? /* @__PURE__ */ jsx(SigmaIcon, {})
|
|
30
|
+
}), /* @__PURE__ */ jsxs(MenuPopup, { children: [properties.length === 0 && /* @__PURE__ */ jsx(MenuItem, {
|
|
31
|
+
disabled: true,
|
|
32
|
+
children: labels.noProperties
|
|
33
|
+
}), properties.map((property) => {
|
|
34
|
+
const selected = selections.find((selection) => selection.propertyId === property.id);
|
|
35
|
+
return /* @__PURE__ */ jsxs(MenuSub, { children: [/* @__PURE__ */ jsx(MenuSubTrigger, { children: property.name }), /* @__PURE__ */ jsxs(MenuSubPopup, { children: [/* @__PURE__ */ jsxs(MenuItem, {
|
|
36
|
+
onClick: () => select(property.id, null),
|
|
37
|
+
children: [selected === void 0 ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", {}), labels.none]
|
|
38
|
+
}), property.kinds.map((kind) => /* @__PURE__ */ jsxs(MenuItem, {
|
|
39
|
+
onClick: () => select(property.id, kind),
|
|
40
|
+
children: [selected?.kind === kind ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", {}), labels.kinds[kind]]
|
|
41
|
+
}, kind))] })] }, property.id);
|
|
42
|
+
})] })] });
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Choose one column's reduction. The board picks a property first (it shows one
|
|
46
|
+
* line for the whole board); a table column already is the property.
|
|
47
|
+
*/
|
|
48
|
+
const CalculationKindPicker = ({ labels, kinds, value, onChange, children }) => /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
|
|
49
|
+
"aria-label": labels.choose,
|
|
50
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
51
|
+
size: "icon-xs",
|
|
52
|
+
variant: "ghost"
|
|
53
|
+
}),
|
|
54
|
+
children: children ?? /* @__PURE__ */ jsx(SigmaIcon, {})
|
|
55
|
+
}), /* @__PURE__ */ jsxs(MenuPopup, { children: [/* @__PURE__ */ jsxs(MenuItem, {
|
|
56
|
+
onClick: () => onChange(null),
|
|
57
|
+
children: [value === null ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", {}), labels.none]
|
|
58
|
+
}), kinds.map((kind) => /* @__PURE__ */ jsxs(MenuItem, {
|
|
59
|
+
onClick: () => onChange(kind),
|
|
60
|
+
children: [value === kind ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", {}), labels.kinds[kind]]
|
|
61
|
+
}, kind))] })] });
|
|
62
|
+
/**
|
|
63
|
+
* The calculation as one compact line. When it reduces to several currencies
|
|
64
|
+
* the line stays short and the full set sits behind a tooltip.
|
|
65
|
+
*/
|
|
66
|
+
const CalculationSummary = ({ calculation }) => {
|
|
67
|
+
if (calculation.breakdown.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
68
|
+
className: "text-muted-foreground shrink-0 text-xs tabular-nums",
|
|
69
|
+
children: calculation.summary
|
|
70
|
+
});
|
|
71
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
72
|
+
render: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground shrink-0 text-xs tabular-nums" }),
|
|
73
|
+
children: calculation.summary
|
|
74
|
+
}), /* @__PURE__ */ jsx(TooltipPopup, { children: /* @__PURE__ */ jsx("ul", { children: calculation.breakdown.map((entry) => /* @__PURE__ */ jsx("li", { children: entry }, entry)) }) })] });
|
|
75
|
+
};
|
|
76
|
+
/** Reduce a column's values and format the answer for the reader's locale. */
|
|
77
|
+
const useCalculation = ({ kind, labels, values, scopeValues }) => {
|
|
78
|
+
const formatters = useCalculationFormatters();
|
|
79
|
+
return formatCalculationResult({
|
|
80
|
+
result: runCalculation({
|
|
81
|
+
kind,
|
|
82
|
+
values,
|
|
83
|
+
scopeValues
|
|
84
|
+
}),
|
|
85
|
+
formatters,
|
|
86
|
+
labels: {
|
|
87
|
+
kind: labels.kinds[kind],
|
|
88
|
+
unavailable: labels.unavailable
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
/** One configured calculation, reduced over a column and rendered compactly. */
|
|
93
|
+
const ColumnCalculation = (params) => /* @__PURE__ */ jsx(CalculationSummary, { calculation: useCalculation(params) });
|
|
94
|
+
const useCalculationFormatters = () => {
|
|
95
|
+
const format = useFormatter();
|
|
96
|
+
const locale = useLocale();
|
|
97
|
+
return {
|
|
98
|
+
number: (value) => format.number(value),
|
|
99
|
+
money: (amountCents, currency) => formatMoneyCents({
|
|
100
|
+
amountCents,
|
|
101
|
+
currency,
|
|
102
|
+
locale
|
|
103
|
+
}),
|
|
104
|
+
percent: (ratio) => format.number(ratio, { style: "percent" })
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
export { CalculationKindPicker, CalculationPicker, CalculationSummary, ColumnCalculation, useCalculation };
|
package/dist/colors.d.ts
ADDED
package/dist/colors.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ConditionOperator, FieldOption, FieldValueType, ValueEditorKind } from "./logic.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import { ConditionNode, GroupNode } from "@stll/conditions";
|
|
4
|
+
//#region src/conditions/builder.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Every string the builder draws. The module resolves none of them: a host
|
|
7
|
+
* brings its own catalogue, and an exported module that reached into one would
|
|
8
|
+
* only work inside the app whose messages it happened to know.
|
|
9
|
+
*/
|
|
10
|
+
type ConditionBuilderLabels = {
|
|
11
|
+
/** "+ Add condition". */
|
|
12
|
+
addCondition: string;
|
|
13
|
+
/** "+ Add group". */
|
|
14
|
+
addGroup: string;
|
|
15
|
+
/** Leads the first row: "When". */
|
|
16
|
+
when: string;
|
|
17
|
+
/** Accessible name of the and/or select. */
|
|
18
|
+
match: string;
|
|
19
|
+
and: string;
|
|
20
|
+
or: string;
|
|
21
|
+
/** Removes one row or group. */
|
|
22
|
+
remove: string;
|
|
23
|
+
/** Placeholder in the field select. */
|
|
24
|
+
fieldPlaceholder: string;
|
|
25
|
+
/** Placeholder in the value editor. */
|
|
26
|
+
valuePlaceholder: string;
|
|
27
|
+
/** The "ƒ Calculated value…" item. */
|
|
28
|
+
useFormula: string;
|
|
29
|
+
/** The date editor's own three. */
|
|
30
|
+
clearDate: string;
|
|
31
|
+
selectDate: string;
|
|
32
|
+
today: string;
|
|
33
|
+
/** How an operator reads for a given value type. */
|
|
34
|
+
operator: (valueType: FieldValueType, op: ConditionOperator) => string;
|
|
35
|
+
};
|
|
36
|
+
/** Capability flags and host injections that let one recursive builder serve
|
|
37
|
+
* both the View filter surface and the template rule surface. The host owns
|
|
38
|
+
* the field list and (optionally) the value editors; the builder owns the
|
|
39
|
+
* tree shape, the gutter, nesting, and the formula escape hatch. */
|
|
40
|
+
type ConditionCapabilities = {
|
|
41
|
+
/** Operands the picker may target (property / builtin / kind / path). */
|
|
42
|
+
fields: FieldOption[];
|
|
43
|
+
/** Allow nested groups (the "+ Add group" affordance + bordered subgroups). */
|
|
44
|
+
allowNesting?: boolean;
|
|
45
|
+
/** Offer a "ƒ Calculated value…" item that switches a leaf's left operand to
|
|
46
|
+
* a formula edited via `FormulaCell`. */
|
|
47
|
+
allowFormula?: boolean;
|
|
48
|
+
/** Numeric operands a formula leaf may reference; required for `allowFormula`. */
|
|
49
|
+
formulaNumberFields?: readonly {
|
|
50
|
+
path: string;
|
|
51
|
+
label: string;
|
|
52
|
+
}[];
|
|
53
|
+
/** Host-injected value editor (e.g. faceted selects). Return null to fall back
|
|
54
|
+
* to the builder's built-in editor for that kind. */
|
|
55
|
+
renderValueEditor?: (ctx: ValueEditorRenderCtx) => React.ReactNode | null;
|
|
56
|
+
/** Draws the formula cell for a formula leaf; required for `allowFormula`. */
|
|
57
|
+
renderFormulaCell?: (ctx: FormulaCellRenderCtx) => ReactNode;
|
|
58
|
+
/** Restrict the operator set per value type (e.g. the template surface only
|
|
59
|
+
* exposes operators its serializer can render). Defaults to the logic's
|
|
60
|
+
* `operatorsFor`. */
|
|
61
|
+
operatorsFor?: (valueType: FieldValueType) => readonly ConditionOperator[];
|
|
62
|
+
/** Override how a value type renders its value editor. Defaults to the logic's
|
|
63
|
+
* `valueEditorFor`. */
|
|
64
|
+
valueEditorFor?: (valueType: FieldValueType, op: ConditionOperator) => ValueEditorKind;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The formula editor is a slot: its expression language belongs to the host,
|
|
68
|
+
* so the builder hands it the expression and takes back either a new one or a
|
|
69
|
+
* request to go back to an ordinary field. Rebuilding the leaf stays here,
|
|
70
|
+
* because that is AST work.
|
|
71
|
+
*/
|
|
72
|
+
type FormulaCellRenderCtx = {
|
|
73
|
+
expr: string;
|
|
74
|
+
numberFields: readonly {
|
|
75
|
+
path: string;
|
|
76
|
+
label: string;
|
|
77
|
+
}[];
|
|
78
|
+
onChangeExpr: (expr: string) => void;
|
|
79
|
+
onUseField: () => void;
|
|
80
|
+
};
|
|
81
|
+
type ValueEditorRenderCtx = {
|
|
82
|
+
editorKind: ValueEditorKind;
|
|
83
|
+
field: FieldOption;
|
|
84
|
+
node: ConditionNode;
|
|
85
|
+
operator: ConditionOperator;
|
|
86
|
+
emit: (value: string | string[]) => void;
|
|
87
|
+
};
|
|
88
|
+
type ConditionBuilderProps = {
|
|
89
|
+
value: ConditionNode | null;
|
|
90
|
+
onChange: (next: GroupNode) => void;
|
|
91
|
+
capabilities: ConditionCapabilities;
|
|
92
|
+
labels: ConditionBuilderLabels;
|
|
93
|
+
/** Formatting locale for the date editor. */
|
|
94
|
+
locale: string;
|
|
95
|
+
};
|
|
96
|
+
declare const ConditionBuilder: ({ value, onChange, capabilities, labels, locale }: ConditionBuilderProps) => import("react").JSX.Element;
|
|
97
|
+
//#endregion
|
|
98
|
+
export { ConditionBuilder, ConditionBuilderLabels, ConditionCapabilities, FormulaCellRenderCtx, ValueEditorRenderCtx };
|