@chelone/core 0.5.0 → 0.6.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.
Files changed (38) hide show
  1. package/dist/assistant/assistant-session.d.ts +22 -0
  2. package/dist/assistant/assistant-session.js +185 -0
  3. package/dist/assistant/echo-transport.d.ts +7 -0
  4. package/dist/assistant/echo-transport.js +31 -0
  5. package/dist/assistant/types.d.ts +60 -0
  6. package/dist/chart/chart-data.d.ts +25 -0
  7. package/dist/chart/chart-data.js +59 -0
  8. package/dist/controller/infinite-list-controller.js +1 -1
  9. package/dist/controller/job-monitor.d.ts +42 -0
  10. package/dist/controller/job-monitor.js +59 -0
  11. package/dist/controller/pivot-grid.d.ts +36 -0
  12. package/dist/controller/pivot-grid.js +105 -0
  13. package/dist/controller/record-timeline.d.ts +35 -0
  14. package/dist/controller/record-timeline.js +36 -0
  15. package/dist/data/cache-ops.js +1 -3
  16. package/dist/form/edit-create-form.js +7 -7
  17. package/dist/i18n/i18n.d.ts +6 -0
  18. package/dist/i18n/i18n.js +13 -1
  19. package/dist/i18n/missing-keys.d.ts +19 -0
  20. package/dist/i18n/missing-keys.js +64 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +43 -0
  23. package/dist/onboarding-tour/placement.d.ts +34 -0
  24. package/dist/onboarding-tour/placement.js +82 -0
  25. package/dist/onboarding-tour/tour-controller.d.ts +25 -0
  26. package/dist/onboarding-tour/tour-controller.js +58 -0
  27. package/dist/realtime/announce-lock.d.ts +7 -0
  28. package/dist/realtime/announce-lock.js +15 -0
  29. package/dist/realtime/broadcast-channel-transport.d.ts +14 -0
  30. package/dist/realtime/broadcast-channel-transport.js +63 -0
  31. package/dist/realtime/in-memory-lock-provider.js +3 -14
  32. package/dist/realtime/memory-transport.js +10 -51
  33. package/dist/realtime/reconnect-backoff.d.ts +15 -0
  34. package/dist/realtime/topic-hub.d.ts +24 -0
  35. package/dist/realtime/topic-hub.js +75 -0
  36. package/dist/rich-text/rich-text.d.ts +56 -0
  37. package/dist/rich-text/rich-text.js +105 -0
  38. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ import { type MicroStore } from "../store/micro-store.js";
2
+ import type { AssistantMessage, AssistantTool, AssistantToolCall, AssistantTransport } from "./types.js";
3
+ export interface AssistantSessionState {
4
+ messages: AssistantMessage[];
5
+ status: "idle" | "streaming" | "error";
6
+ error: Error | null;
7
+ }
8
+ export interface CreateAssistantSessionOptions {
9
+ transport: AssistantTransport;
10
+ tools: Record<string, AssistantTool>;
11
+ /** Follow-up turns after tool calls before the session gives up. Default 5. */
12
+ maxToolRounds?: number;
13
+ /** Answers for tools that set `confirm`. Without it they never run. */
14
+ confirmTool?: (call: AssistantToolCall) => boolean | Promise<boolean>;
15
+ }
16
+ export interface AssistantSession extends MicroStore<AssistantSessionState> {
17
+ /** Resolves when the turn, with its tool rounds, ends. */
18
+ send(text: string): Promise<void>;
19
+ stop(): void;
20
+ reset(): void;
21
+ }
22
+ export declare function createAssistantSession({ transport, tools, maxToolRounds, confirmTool, }: CreateAssistantSessionOptions): AssistantSession;
@@ -0,0 +1,185 @@
1
+ import { createMicroStore } from "../store/micro-store.js";
2
+ const toError = (error) => error instanceof Error ? error : new Error(String(error));
3
+ const issuesText = (issues) => issues.map((issue) => issue.message).join("; ");
4
+ function raceAbort(promise, signal) {
5
+ return new Promise((resolve, reject) => {
6
+ const onAbort = () => resolve(void 0);
7
+ if (signal.aborted) return onAbort();
8
+ signal.addEventListener("abort", onAbort, { once: true });
9
+ promise.then(resolve, reject).finally(() => {
10
+ signal.removeEventListener("abort", onAbort);
11
+ });
12
+ });
13
+ }
14
+ function createAssistantSession({
15
+ transport,
16
+ tools,
17
+ maxToolRounds = 5,
18
+ confirmTool
19
+ }) {
20
+ const store = createMicroStore({
21
+ messages: [],
22
+ status: "idle",
23
+ error: null
24
+ });
25
+ const declarations = Object.fromEntries(
26
+ Object.entries(tools).map(([name, { description, parameters }]) => [
27
+ name,
28
+ { description, parameters }
29
+ ])
30
+ );
31
+ let active = null;
32
+ let destroyed = false;
33
+ store.onDestroy(() => {
34
+ destroyed = true;
35
+ active?.abort();
36
+ active = null;
37
+ });
38
+ async function runTool(call, signal) {
39
+ const base = {
40
+ role: "tool",
41
+ id: call.id,
42
+ toolName: call.toolName
43
+ };
44
+ const definition = Object.hasOwn(tools, call.toolName) ? tools[call.toolName] : void 0;
45
+ if (!definition) {
46
+ return { ...base, error: `Unknown tool: ${call.toolName}` };
47
+ }
48
+ const stopped = { ...base, error: "Stopped" };
49
+ try {
50
+ let args = call.args;
51
+ if (definition.schema) {
52
+ const outcome = await definition.schema["~standard"].validate(args);
53
+ if (signal.aborted) return stopped;
54
+ if (outcome.issues) {
55
+ return {
56
+ ...base,
57
+ error: `Invalid arguments: ${issuesText(outcome.issues)}`
58
+ };
59
+ }
60
+ args = outcome.value;
61
+ }
62
+ if (definition.confirm) {
63
+ const answer = await confirmTool?.(call);
64
+ if (signal.aborted) return stopped;
65
+ if (!answer) {
66
+ return { ...base, error: "The user declined to run this tool" };
67
+ }
68
+ }
69
+ return { ...base, result: await definition.handler(args) };
70
+ } catch (error) {
71
+ return { ...base, error: toError(error).message };
72
+ }
73
+ }
74
+ async function run(controller) {
75
+ const { signal } = controller;
76
+ const update = (patch) => {
77
+ if (!signal.aborted) store.setState(patch);
78
+ };
79
+ for (let round = 0; ; round++) {
80
+ let content = "";
81
+ let index = -1;
82
+ const calls = [];
83
+ let iterator;
84
+ try {
85
+ iterator = transport.send(store.getSnapshot().messages, declarations, { signal })[Symbol.asyncIterator]();
86
+ for (; ; ) {
87
+ const step = await raceAbort(iterator.next(), signal);
88
+ if (!step || step.done || signal.aborted) break;
89
+ const chunk = step.value;
90
+ if (chunk.type === "done") break;
91
+ if (chunk.type === "tool-call") {
92
+ const { id, toolName, args } = chunk;
93
+ calls.push({ id, toolName, args });
94
+ continue;
95
+ }
96
+ content += chunk.delta;
97
+ const text = content;
98
+ if (index < 0) {
99
+ index = store.getSnapshot().messages.length;
100
+ update((s) => ({
101
+ messages: [...s.messages, { role: "assistant", content: text }]
102
+ }));
103
+ } else {
104
+ update((s) => ({
105
+ messages: s.messages.map(
106
+ (m, i) => i === index ? { role: "assistant", content: text } : m
107
+ )
108
+ }));
109
+ }
110
+ }
111
+ } catch (error) {
112
+ if (!signal.aborted) {
113
+ update(() => ({ status: "error", error: toError(error) }));
114
+ }
115
+ return;
116
+ } finally {
117
+ iterator?.return?.().catch(() => {
118
+ });
119
+ }
120
+ if (signal.aborted) return;
121
+ if (calls.length === 0) break;
122
+ if (round >= maxToolRounds) {
123
+ update(() => ({
124
+ status: "error",
125
+ error: new Error(
126
+ `The assistant kept calling tools after ${maxToolRounds} rounds`
127
+ )
128
+ }));
129
+ return;
130
+ }
131
+ const results = [];
132
+ for (const call of calls) {
133
+ const result = await raceAbort(runTool(call, signal), signal);
134
+ if (!result) return;
135
+ results.push(result);
136
+ if (signal.aborted) return;
137
+ }
138
+ const assistant = {
139
+ role: "assistant",
140
+ content,
141
+ toolCalls: calls
142
+ };
143
+ update((s) => ({
144
+ messages: index < 0 ? [...s.messages, assistant, ...results] : [
145
+ ...s.messages.map((m, i) => i === index ? assistant : m),
146
+ ...results
147
+ ]
148
+ }));
149
+ }
150
+ update(() => ({ status: "idle" }));
151
+ }
152
+ return {
153
+ ...store,
154
+ async send(text) {
155
+ if (destroyed) return;
156
+ if (!text.trim() || store.getSnapshot().status === "streaming") return;
157
+ const controller = new AbortController();
158
+ active = controller;
159
+ store.setState((s) => ({
160
+ messages: [...s.messages, { role: "user", content: text }],
161
+ status: "streaming",
162
+ error: null
163
+ }));
164
+ try {
165
+ await run(controller);
166
+ } finally {
167
+ if (active === controller) active = null;
168
+ }
169
+ },
170
+ stop() {
171
+ if (!active) return;
172
+ active.abort();
173
+ active = null;
174
+ store.setState({ status: "idle" });
175
+ },
176
+ reset() {
177
+ active?.abort();
178
+ active = null;
179
+ store.setState({ messages: [], status: "idle", error: null });
180
+ }
181
+ };
182
+ }
183
+ export {
184
+ createAssistantSession
185
+ };
@@ -0,0 +1,7 @@
1
+ import type { AssistantTransport } from "./types.js";
2
+ export interface EchoAssistantTransportOptions {
3
+ /** Waits this long before each character. */
4
+ delayMs?: number;
5
+ }
6
+ /** Replies `Echo: <last user text>` one character at a time, with no model behind it. */
7
+ export declare function echoAssistantTransport({ delayMs, }?: EchoAssistantTransportOptions): AssistantTransport;
@@ -0,0 +1,31 @@
1
+ const wait = (ms, signal) => new Promise((resolve) => {
2
+ const timer = setTimeout(done, ms);
3
+ function done() {
4
+ clearTimeout(timer);
5
+ signal.removeEventListener("abort", done);
6
+ resolve();
7
+ }
8
+ signal.addEventListener("abort", done, { once: true });
9
+ });
10
+ function echoAssistantTransport({
11
+ delayMs = 0
12
+ } = {}) {
13
+ return {
14
+ async *send(messages, _tools, { signal }) {
15
+ let said = "";
16
+ for (const message of messages) {
17
+ if (message.role === "user") said = message.content;
18
+ }
19
+ const reply = `Echo: ${said}`;
20
+ for (const delta of reply) {
21
+ if (delayMs > 0) await wait(delayMs, signal);
22
+ if (signal.aborted) return;
23
+ yield { type: "text", delta };
24
+ }
25
+ yield { type: "done" };
26
+ }
27
+ };
28
+ }
29
+ export {
30
+ echoAssistantTransport
31
+ };
@@ -0,0 +1,60 @@
1
+ import type { StandardSchemaV1 } from "@tanstack/form-core";
2
+ /** What crosses the model boundary: arguments in, results out. */
3
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
4
+ [key: string]: JsonValue;
5
+ };
6
+ export type JsonObject = {
7
+ [key: string]: JsonValue;
8
+ };
9
+ /** A tool call the model asked for. `id` ties the result back to it. */
10
+ export interface AssistantToolCall {
11
+ id: string;
12
+ toolName: string;
13
+ args: JsonObject;
14
+ }
15
+ export type AssistantMessage = {
16
+ role: "user";
17
+ content: string;
18
+ } | {
19
+ role: "assistant";
20
+ content: string;
21
+ /** Present once the calls ran and their results follow as tool messages. */
22
+ toolCalls?: AssistantToolCall[];
23
+ } | {
24
+ role: "tool";
25
+ /** The `id` of the call this answers. */
26
+ id: string;
27
+ toolName: string;
28
+ /** What the handler returned; absent when `error` is set. */
29
+ result?: JsonValue;
30
+ error?: string;
31
+ };
32
+ export type AssistantChunk = {
33
+ type: "text";
34
+ delta: string;
35
+ } | ({
36
+ type: "tool-call";
37
+ } & AssistantToolCall) | {
38
+ type: "done";
39
+ };
40
+ export interface AssistantTool {
41
+ description: string;
42
+ /** A JSON Schema object, handed to the transport as is. */
43
+ parameters: JsonObject;
44
+ /** Method syntax keeps a handler with narrower `args` assignable. */
45
+ handler(args: JsonObject): JsonValue | undefined | Promise<JsonValue | undefined>;
46
+ /** Checks the arguments before `handler` runs; its output reaches `handler`. */
47
+ schema?: StandardSchemaV1<JsonObject, JsonObject>;
48
+ /** Ask the user through the session's `confirmTool` before this tool runs. */
49
+ confirm?: boolean;
50
+ }
51
+ /** What a transport learns about a tool: no handler, schema or confirm. */
52
+ export interface AssistantToolDeclaration {
53
+ description: string;
54
+ parameters: JsonObject;
55
+ }
56
+ export interface AssistantTransport {
57
+ send(messages: readonly AssistantMessage[], tools: Readonly<Record<string, AssistantToolDeclaration>>, options: {
58
+ signal: AbortSignal;
59
+ }): AsyncIterable<AssistantChunk>;
60
+ }
@@ -0,0 +1,25 @@
1
+ /** Theme variables the registry ships, in series order. */
2
+ export declare const DEFAULT_CHART_COLORS: readonly string[];
3
+ export declare function chartColor(index: number, colors?: readonly string[]): string;
4
+ /** One point of a series: `x` is the row's value as given, `y` a finite number or a `null` gap. */
5
+ export type CartesianRow<TX> = {
6
+ index: number;
7
+ x: TX;
8
+ y: number | null;
9
+ };
10
+ export declare function toCartesianRows<TRow extends object, TX extends keyof TRow & string, TY extends keyof TRow & string>(data: readonly TRow[], xField: TX, yField: TY): CartesianRow<TRow[TX]>[];
11
+ export declare function hasChartValues(rows: readonly {
12
+ y: number | null;
13
+ }[]): boolean;
14
+ export type DonutSlice = {
15
+ label: string;
16
+ value: number;
17
+ share: number;
18
+ };
19
+ export declare function toDonutSlices<TRow extends object, TLabel extends keyof TRow & string, TValue extends keyof TRow & string>(data: readonly TRow[], labelField: TLabel, valueField: TValue): DonutSlice[];
20
+ export type MetricDelta = {
21
+ direction: "up" | "down" | "flat";
22
+ text: string;
23
+ };
24
+ /** `delta` is a ratio (0.05 is 5.0%). Direction follows the sign of `delta`, not of the rounded text. */
25
+ export declare function getMetricDelta(delta: number | undefined): MetricDelta | null;
@@ -0,0 +1,59 @@
1
+ const DEFAULT_CHART_COLORS = [
2
+ "var(--chart-1)",
3
+ "var(--chart-2)",
4
+ "var(--chart-3)",
5
+ "var(--chart-4)",
6
+ "var(--chart-5)"
7
+ ];
8
+ const FALLBACK_COLOR = "var(--chart-1)";
9
+ function chartColor(index, colors = DEFAULT_CHART_COLORS) {
10
+ const list = colors.length > 0 ? colors : DEFAULT_CHART_COLORS;
11
+ const length = list.length;
12
+ return list[(index % length + length) % length] ?? FALLBACK_COLOR;
13
+ }
14
+ function toFiniteNumber(value) {
15
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
16
+ if (typeof value === "string" && value.trim() !== "") {
17
+ const parsed = Number(value);
18
+ return Number.isFinite(parsed) ? parsed : null;
19
+ }
20
+ return null;
21
+ }
22
+ function toCartesianRows(data, xField, yField) {
23
+ return data.map((row, index) => ({
24
+ index,
25
+ x: row[xField],
26
+ y: toFiniteNumber(row[yField])
27
+ }));
28
+ }
29
+ function hasChartValues(rows) {
30
+ return rows.some((row) => row.y !== null);
31
+ }
32
+ function toLabel(value) {
33
+ return value === void 0 || value === null ? "" : String(value);
34
+ }
35
+ function toDonutSlices(data, labelField, valueField) {
36
+ const kept = [];
37
+ for (const row of data) {
38
+ const value = toFiniteNumber(row[valueField]);
39
+ if (value !== null && value > 0) {
40
+ kept.push({ label: toLabel(row[labelField]), value });
41
+ }
42
+ }
43
+ const total = kept.reduce((sum, slice) => sum + slice.value, 0);
44
+ if (total <= 0) return [];
45
+ return kept.map((slice) => ({ ...slice, share: slice.value / total }));
46
+ }
47
+ function getMetricDelta(delta) {
48
+ if (delta === void 0 || !Number.isFinite(delta)) return null;
49
+ const direction = delta > 0 ? "up" : delta < 0 ? "down" : "flat";
50
+ return { direction, text: `${(delta * 100).toFixed(1)}%` };
51
+ }
52
+ export {
53
+ DEFAULT_CHART_COLORS,
54
+ chartColor,
55
+ getMetricDelta,
56
+ hasChartValues,
57
+ toCartesianRows,
58
+ toDonutSlices
59
+ };
@@ -1,5 +1,5 @@
1
1
  import { QueryClient, InfiniteQueryObserver } from "@tanstack/query-core";
2
- import { getListOptions, getInfiniteListOptions } from "../data/query-options.js";
2
+ import { getInfiniteListOptions, getListOptions } from "../data/query-options.js";
3
3
  import { createMicroStore } from "../store/micro-store.js";
4
4
  import { createListController, createErrorAnnouncer, runSelectAll } from "./list-controller.js";
5
5
  function createInfiniteListController(ctx, resource, options = {}) {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure rules behind a job monitor's Retry and Cancel buttons, so every adapter
3
+ * offers them on the same jobs and writes the same patch.
4
+ *
5
+ * These are UI rules, not security ones: the server has to refuse a retry of a
6
+ * job that is not failed. Retry writes only the status; the worker counts
7
+ * attempts.
8
+ */
9
+ import type { Identifier } from "../resource/identifier.js";
10
+ import type { ResourceRecord } from "../resource/record.js";
11
+ import type { SourceKey } from "../resource/source-path.js";
12
+ /** The monitor's tabs, in order. One `buildKanbanBuckets` column per status. */
13
+ export declare const DEFAULT_JOB_TABS: readonly ["running", "queued", "failed", "done"];
14
+ export interface JobActionOptions<TRecord = Record<string, unknown>> {
15
+ /** Record field holding the job status, a top-level key. Default `"status"`. */
16
+ statusSource?: SourceKey<TRecord>;
17
+ /** Statuses a job can be retried from. Default `["failed"]`. */
18
+ retryFrom?: readonly string[];
19
+ /** Statuses a job can be cancelled from. Default `["running", "queued"]`. */
20
+ cancelFrom?: readonly string[];
21
+ /** Status Retry writes. Default `"queued"`. */
22
+ retryValue?: string;
23
+ /** Status Cancel writes. Default `"cancelled"`. */
24
+ cancelValue?: string;
25
+ }
26
+ export interface JobActions {
27
+ /** The job's status as a string; `""` when it has none. */
28
+ status: string;
29
+ canRetry: boolean;
30
+ canCancel: boolean;
31
+ }
32
+ /** The arguments of one `update` call, ready to hand to a data provider. */
33
+ export interface JobPlan<TRecord> {
34
+ id: Identifier;
35
+ data: Record<string, string>;
36
+ previousData: TRecord;
37
+ }
38
+ export declare function getJobActions<TRecord = Record<string, unknown>>(record: TRecord & object, options?: JobActionOptions<TRecord>): JobActions;
39
+ /** The `update` that retries the job, or `null` when it cannot be retried. */
40
+ export declare function planJobRetry<TRecord extends ResourceRecord = ResourceRecord>(record: TRecord, options?: JobActionOptions<TRecord>): JobPlan<TRecord> | null;
41
+ /** The `update` that cancels the job, or `null` when it cannot be cancelled. */
42
+ export declare function planJobCancel<TRecord extends ResourceRecord = ResourceRecord>(record: TRecord, options?: JobActionOptions<TRecord>): JobPlan<TRecord> | null;
@@ -0,0 +1,59 @@
1
+ import { getAllowedTransitions } from "./record-workflow.js";
2
+ const DEFAULT_JOB_TABS = [
3
+ "running",
4
+ "queued",
5
+ "failed",
6
+ "done"
7
+ ];
8
+ function canMove(record, source, from, to) {
9
+ const transitions = Object.fromEntries(from.map((status) => [status, [to]]));
10
+ const { current, allowed } = getAllowedTransitions(record, {
11
+ source,
12
+ transitions
13
+ });
14
+ return { status: current, allowed: allowed.includes(to) };
15
+ }
16
+ function getJobActions(record, options = {}) {
17
+ const {
18
+ statusSource = "status",
19
+ retryFrom = ["failed"],
20
+ cancelFrom = ["running", "queued"],
21
+ retryValue = "queued",
22
+ cancelValue = "cancelled"
23
+ } = options;
24
+ const retry = canMove(record, statusSource, retryFrom, retryValue);
25
+ const cancel = canMove(record, statusSource, cancelFrom, cancelValue);
26
+ return {
27
+ status: retry.status,
28
+ canRetry: retry.allowed,
29
+ canCancel: cancel.allowed
30
+ };
31
+ }
32
+ function plan(record, source, value, allowed) {
33
+ if (!allowed) return null;
34
+ return { id: record.id, data: { [source]: value }, previousData: record };
35
+ }
36
+ function planJobRetry(record, options = {}) {
37
+ const { statusSource = "status", retryValue = "queued" } = options;
38
+ return plan(
39
+ record,
40
+ statusSource,
41
+ retryValue,
42
+ getJobActions(record, options).canRetry
43
+ );
44
+ }
45
+ function planJobCancel(record, options = {}) {
46
+ const { statusSource = "status", cancelValue = "cancelled" } = options;
47
+ return plan(
48
+ record,
49
+ statusSource,
50
+ cancelValue,
51
+ getJobActions(record, options).canCancel
52
+ );
53
+ }
54
+ export {
55
+ DEFAULT_JOB_TABS,
56
+ getJobActions,
57
+ planJobCancel,
58
+ planJobRetry
59
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure rule for pivoting a flat record list into a row by column table. No
3
+ * react, no vue: both PivotGrid adapters build the same table from the same
4
+ * records and print numbers the same way.
5
+ */
6
+ import type { SourcePath } from "../resource/source-path.js";
7
+ export type PivotAggregator = "count" | "sum" | "avg" | "min" | "max";
8
+ export interface PivotOptions<R> {
9
+ rowSource: SourcePath<R>;
10
+ columnSource: SourcePath<R>;
11
+ /** Read by every aggregator but `count`. */
12
+ valueSource?: SourcePath<R>;
13
+ /** Defaults to `count`. */
14
+ aggregator?: PivotAggregator;
15
+ }
16
+ export interface PivotTable {
17
+ rowKeys: string[];
18
+ columnKeys: string[];
19
+ /** `cells[row][column]` follows the order of `rowKeys` and `columnKeys`. */
20
+ cells: (number | null)[][];
21
+ rowTotals: (number | null)[];
22
+ columnTotals: (number | null)[];
23
+ grandTotal: number | null;
24
+ }
25
+ /**
26
+ * A row or column key is `String(value)`, and a null, undefined or empty value
27
+ * is the key `""`. Keys sort numerically (`10` after `9`) with `""` last.
28
+ * `count` counts records and ignores `valueSource`. The other aggregators use
29
+ * finite numbers and numeric strings, and skip anything else. A cell with no
30
+ * records is `null`, and so is a cell with no usable value, unless it counts.
31
+ * A total aggregates the raw values of its row, column or table, never the
32
+ * cell results, so an average total is not a sum of averages.
33
+ */
34
+ export declare function buildPivotTable<R>(records: readonly R[], options: PivotOptions<R>): PivotTable;
35
+ /** Integers print bare and fractions round to two digits. */
36
+ export declare function formatPivotValue(value: number, locale?: string): string;
@@ -0,0 +1,105 @@
1
+ import { getAtPath } from "../resource/get-at-path.js";
2
+ const collator = new Intl.Collator(void 0, { numeric: true });
3
+ const NUMERIC_KEY = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
4
+ const compareKeys = (a, b) => {
5
+ if (a === b) return 0;
6
+ if (a === "") return 1;
7
+ if (b === "") return -1;
8
+ if (NUMERIC_KEY.test(a) && NUMERIC_KEY.test(b)) {
9
+ const difference = Number(a) - Number(b);
10
+ if (difference !== 0 && !Number.isNaN(difference)) return difference;
11
+ }
12
+ return collator.compare(a, b) || (a < b ? -1 : 1);
13
+ };
14
+ const toKey = (value) => value == null || value === "" ? "" : String(value);
15
+ const toNumber = (value) => {
16
+ if (typeof value === "number")
17
+ return Number.isFinite(value) ? value : void 0;
18
+ if (typeof value === "string" && value !== "") {
19
+ const parsed = Number(value);
20
+ return Number.isFinite(parsed) ? parsed : void 0;
21
+ }
22
+ return void 0;
23
+ };
24
+ const aggregate = (group, aggregator) => {
25
+ if (group.count === 0) return null;
26
+ if (aggregator === "count") return group.count;
27
+ const { values } = group;
28
+ if (values.length === 0) return null;
29
+ switch (aggregator) {
30
+ case "sum":
31
+ return values.reduce((total, value) => total + value, 0);
32
+ case "avg":
33
+ return values.reduce((total, value) => total + value, 0) / values.length;
34
+ case "min":
35
+ return values.reduce((a, b) => b < a ? b : a);
36
+ case "max":
37
+ return values.reduce((a, b) => b > a ? b : a);
38
+ }
39
+ };
40
+ const newGroup = () => ({ count: 0, values: [] });
41
+ const getOrCreate = (map, key, create) => {
42
+ const existing = map.get(key);
43
+ if (existing !== void 0) return existing;
44
+ const created = create();
45
+ map.set(key, created);
46
+ return created;
47
+ };
48
+ const add = (group, value) => {
49
+ group.count += 1;
50
+ if (value !== void 0) group.values.push(value);
51
+ };
52
+ function buildPivotTable(records, options) {
53
+ const {
54
+ rowSource,
55
+ columnSource,
56
+ valueSource,
57
+ aggregator = "count"
58
+ } = options;
59
+ const readsValue = aggregator !== "count" && valueSource !== void 0;
60
+ const cellGroups = /* @__PURE__ */ new Map();
61
+ const rowGroups = /* @__PURE__ */ new Map();
62
+ const columnGroups = /* @__PURE__ */ new Map();
63
+ const tableGroup = newGroup();
64
+ for (const record of records) {
65
+ const row = toKey(getAtPath(record, rowSource));
66
+ const column = toKey(getAtPath(record, columnSource));
67
+ const value = readsValue ? toNumber(getAtPath(record, valueSource)) : void 0;
68
+ const cellRow = getOrCreate(cellGroups, row, () => /* @__PURE__ */ new Map());
69
+ const cell = getOrCreate(cellRow, column, newGroup);
70
+ const rowGroup = getOrCreate(rowGroups, row, newGroup);
71
+ const columnGroup = getOrCreate(columnGroups, column, newGroup);
72
+ add(cell, value);
73
+ add(rowGroup, value);
74
+ add(columnGroup, value);
75
+ add(tableGroup, value);
76
+ }
77
+ const rowKeys = [...rowGroups.keys()].sort(compareKeys);
78
+ const columnKeys = [...columnGroups.keys()].sort(compareKeys);
79
+ const empty = newGroup();
80
+ return {
81
+ rowKeys,
82
+ columnKeys,
83
+ cells: rowKeys.map(
84
+ (row) => columnKeys.map(
85
+ (column) => aggregate(cellGroups.get(row)?.get(column) ?? empty, aggregator)
86
+ )
87
+ ),
88
+ rowTotals: rowKeys.map(
89
+ (row) => aggregate(rowGroups.get(row), aggregator)
90
+ ),
91
+ columnTotals: columnKeys.map(
92
+ (column) => aggregate(columnGroups.get(column), aggregator)
93
+ ),
94
+ grandTotal: aggregate(tableGroup, aggregator)
95
+ };
96
+ }
97
+ function formatPivotValue(value, locale) {
98
+ return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(
99
+ value
100
+ );
101
+ }
102
+ export {
103
+ buildPivotTable,
104
+ formatPivotValue
105
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure rules for a record timeline. No react, no vue: both RecordTimeline
3
+ * adapters read the same entries from the same records and print the same
4
+ * relative time text.
5
+ */
6
+ import type { Identifier } from "../resource/identifier.js";
7
+ import type { SourcePath } from "../resource/source-path.js";
8
+ export interface RecordTimelineEntry<R> {
9
+ id: Identifier;
10
+ record: R;
11
+ message: string;
12
+ /** `undefined` when the timestamp is missing or not a valid date. */
13
+ date: Date | undefined;
14
+ user: string | undefined;
15
+ icon: string | undefined;
16
+ }
17
+ export interface ToTimelineEntriesOptions<R> {
18
+ messageSource: SourcePath<R>;
19
+ timestampSource: SourcePath<R>;
20
+ userSource?: SourcePath<R>;
21
+ iconSource?: SourcePath<R>;
22
+ }
23
+ /**
24
+ * One entry per record, in list order: the data provider's sort decides the
25
+ * order, and the records are not changed.
26
+ */
27
+ export declare function toTimelineEntries<R extends {
28
+ id: Identifier;
29
+ }>(records: readonly R[], options: ToTimelineEntriesOptions<R>): RecordTimelineEntry<R>[];
30
+ /**
31
+ * "2 hours ago" or "in 3 days", from the distance between `date` and `now`
32
+ * in whole seconds, minutes, hours, days or 30-day months. It takes `now` so
33
+ * a caller decides what the present is.
34
+ */
35
+ export declare function formatRelativeTime(date: Date, now: Date, locale?: string): string;