@happyvertical/smrt-svelte 0.55.24 → 0.56.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.
Files changed (49) hide show
  1. package/agents/overview-surfaces.md +16 -4
  2. package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
  3. package/dist/components/overview/OverviewGrid.svelte +8 -4
  4. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
  5. package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
  6. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
  7. package/dist/components/overview/format.d.ts +3 -12
  8. package/dist/components/overview/format.d.ts.map +1 -1
  9. package/dist/components/overview/format.js +2 -45
  10. package/dist/components/overview/grid-types.d.ts +6 -11
  11. package/dist/components/overview/grid-types.d.ts.map +1 -1
  12. package/dist/components/overview/index.d.ts +1 -0
  13. package/dist/components/overview/index.d.ts.map +1 -1
  14. package/dist/components/overview/recipe-widgets.d.ts +6 -3
  15. package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
  16. package/dist/components/overview/recipe-widgets.js +9 -0
  17. package/dist/components/overview/registry.d.ts +3 -0
  18. package/dist/components/overview/registry.d.ts.map +1 -1
  19. package/dist/components/overview/registry.js +25 -0
  20. package/dist/components/overview/server.d.ts +1 -1
  21. package/dist/components/overview/server.d.ts.map +1 -1
  22. package/dist/components/overview/server.js +1 -1
  23. package/dist/components/overview/types.d.ts +17 -0
  24. package/dist/components/overview/types.d.ts.map +1 -1
  25. package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
  26. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
  27. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -1
  28. package/dist/components/overview/widgets/LineItemsWidget.svelte +44 -3
  29. package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -1
  30. package/dist/components/overview/widgets/data.d.ts +17 -0
  31. package/dist/components/overview/widgets/data.d.ts.map +1 -1
  32. package/dist/components/overview/widgets/line-totals.d.ts +35 -0
  33. package/dist/components/overview/widgets/line-totals.d.ts.map +1 -1
  34. package/dist/components/overview/widgets/line-totals.js +45 -0
  35. package/dist/components/overview/widgets/record-loaders.d.ts +3 -1
  36. package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -1
  37. package/dist/components/overview/widgets/record-loaders.js +8 -1
  38. package/dist/components/screens/RecordScreen.svelte +2 -0
  39. package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -1
  40. package/dist/components/screens/types.d.ts +8 -1
  41. package/dist/components/screens/types.d.ts.map +1 -1
  42. package/dist/components/workspace/admin-shell/AdminShell.svelte +4 -4
  43. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
  44. package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
  45. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
  46. package/dist/i18n/strings.overview.d.ts +5 -0
  47. package/dist/i18n/strings.overview.d.ts.map +1 -1
  48. package/dist/i18n/strings.overview.js +5 -0
  49. package/package.json +16 -16
@@ -1,272 +1,12 @@
1
1
  <script lang="ts">
2
- /**
3
- * Chart widget (#3727, #3888): bars, a line, a plain table, a donut or a single
4
- * headline value over labelled values, drawn from the loaded data. Every chart
5
- * carries its numbers as text (bar values inline, a visible table for `table`,
6
- * a visually hidden table for the line and the donut, the value itself for
7
- * `stat`) so it is readable without sight.
8
- */
9
- import { useI18n } from '@happyvertical/smrt-ui/i18n';
10
- import { M } from '../../../i18n/strings.overview.js';
11
- import { formatWidgetValue } from '../format.js';
2
+ import { Chart } from '@happyvertical/smrt-ui/charts';
12
3
  import type { WidgetComponentProps } from '../types.js';
13
- import { type ChartWidgetData, isRecord } from './data.js';
4
+ import type { ChartWidgetData } from './data.js';
14
5
 
15
6
  let { options, data, title, locale }: WidgetComponentProps<ChartWidgetData> =
16
7
  $props();
17
-
18
- const { t } = useI18n();
19
- const MAX_POINTS = 60;
20
-
21
- const chart = $derived(isRecord(data) ? data : null);
22
- const points = $derived.by(() => {
23
- if (!chart || !Array.isArray(chart.points)) return [];
24
- const out: { label: string; value: number }[] = [];
25
- for (const point of chart.points.slice(0, MAX_POINTS)) {
26
- if (
27
- isRecord(point) &&
28
- typeof point.label === 'string' &&
29
- typeof point.value === 'number' &&
30
- Number.isFinite(point.value)
31
- ) {
32
- out.push({ label: point.label, value: point.value });
33
- }
34
- }
35
- return out;
36
- });
37
- const STYLES = ['bar', 'line', 'table', 'pie', 'stat'] as const;
38
- const style = $derived(
39
- STYLES.find((candidate) => candidate === options.style) ?? 'bar',
40
- );
41
- const fmt = (value: number): string =>
42
- formatWidgetValue(value, {
43
- format: chart?.format ?? 'integer',
44
- currency: chart?.currency,
45
- locale,
46
- });
47
- const max = $derived(Math.max(0, ...points.map((point) => point.value)));
48
- const min = $derived(Math.min(0, ...points.map((point) => point.value)));
49
- const span = $derived(max - min || 1);
50
- const polyline = $derived(
51
- points
52
- .map((point, index) => {
53
- const x = points.length === 1 ? 50 : (index / (points.length - 1)) * 100;
54
- const y = 36 - ((point.value - min) / span) * 32;
55
- return `${x.toFixed(2)},${y.toFixed(2)}`;
56
- })
57
- .join(' '),
58
- );
59
-
60
- // Donut: slices are percentages of the positive total on a circle whose
61
- // path length is 100, so a segment's dash length is its share.
62
- const total = $derived(
63
- points.reduce((sum, point) => sum + Math.max(0, point.value), 0),
64
- );
65
- const slices = $derived.by(() => {
66
- let offset = 0;
67
- return points.map((point, index) => {
68
- const share = total > 0 ? (Math.max(0, point.value) / total) * 100 : 0;
69
- const slice = { ...point, index, share, offset };
70
- offset += share;
71
- return slice;
72
- });
73
- });
74
- const percent = (share: number): string =>
75
- formatWidgetValue(share / 100, { format: 'percent', locale });
76
-
77
- // Stat: the latest value, and its change against the one before.
78
- const latest = $derived(points.at(-1));
79
- const delta = $derived.by((): number | null => {
80
- if (
81
- chart &&
82
- typeof chart.delta === 'number' &&
83
- Number.isFinite(chart.delta)
84
- ) {
85
- return chart.delta;
86
- }
87
- const previous = points.at(-2);
88
- if (!latest || !previous || previous.value === 0) return null;
89
- return ((latest.value - previous.value) / Math.abs(previous.value)) * 100;
90
- });
91
- const deltaText = $derived(
92
- delta === null || delta === 0
93
- ? ''
94
- : t(
95
- M[
96
- delta > 0
97
- ? 'ui.overview.metric.change_up'
98
- : 'ui.overview.metric.change_down'
99
- ],
100
- {
101
- value: formatWidgetValue(Math.abs(delta) / 100, {
102
- format: 'percent',
103
- locale,
104
- }),
105
- },
106
- ),
107
- );
108
- const summary = $derived(
109
- `${title}. ${t(M['ui.overview.chart.summary'], { count: points.length })}`,
110
- );
111
8
  </script>
112
9
 
113
- {#snippet dataTable(hidden: boolean)}
114
- <table class={hidden ? 'smrt-chart__table' : 'smrt-chart__data'}>
115
- <caption>{t(M['ui.overview.chart.table'])}</caption>
116
- <thead>
117
- <tr>
118
- <th scope="col">{t(M['ui.overview.chart.label_column'])}</th>
119
- <th scope="col">{t(M['ui.overview.chart.value_column'])}</th>
120
- </tr>
121
- </thead>
122
- <tbody>
123
- {#each points as point, index (index)}
124
- <tr><th scope="row">{point.label}</th><td>{fmt(point.value)}</td></tr>
125
- {/each}
126
- </tbody>
127
- </table>
128
- {/snippet}
129
-
130
- {#if points.length === 0}
131
- <p class="smrt-chart__empty" data-smrt-component="ChartWidget">{t(M['ui.overview.no_data'])}</p>
132
- {:else if style === 'table'}
133
- <div class="smrt-chart smrt-chart--table" data-smrt-component="ChartWidget">
134
- {@render dataTable(false)}
135
- </div>
136
- {:else if style === 'stat'}
137
- {#if latest}
138
- <div class="smrt-chart smrt-chart--stat" data-smrt-component="ChartWidget">
139
- <p class="smrt-chart__headline">{fmt(latest.value)}</p>
140
- <p class="smrt-chart__caption">{latest.label}</p>
141
- {#if deltaText}
142
- <p class="smrt-chart__delta" data-direction={(delta ?? 0) > 0 ? 'up' : 'down'}>
143
- <span aria-hidden="true">{(delta ?? 0) > 0 ? '▲' : '▼'}</span>
144
- {deltaText}
145
- </p>
146
- {/if}
147
- </div>
148
- {/if}
149
- {:else if style === 'pie'}
150
- {#if total > 0}
151
- <div class="smrt-chart smrt-chart--pie" data-smrt-component="ChartWidget">
152
- <svg class="smrt-chart__donut" viewBox="0 0 42 42" role="img" aria-label={summary}>
153
- {#each slices as slice (slice.index)}
154
- {#if slice.share > 0}
155
- <circle
156
- class="smrt-chart__slice"
157
- data-slot={slice.index % 8}
158
- cx="21"
159
- cy="21"
160
- r="15.9155"
161
- fill="none"
162
- stroke-width="6"
163
- pathLength="100"
164
- stroke-dasharray="{slice.share} {100 - slice.share}"
165
- stroke-dashoffset={-slice.offset}
166
- transform="rotate(-90 21 21)"
167
- />
168
- {/if}
169
- {/each}
170
- </svg>
171
- <ul class="smrt-chart__legend" aria-hidden="true">
172
- {#each slices as slice (slice.index)}
173
- <li class="smrt-chart__legend-item">
174
- <span class="smrt-chart__swatch" data-slot={slice.index % 8}></span>
175
- <span class="smrt-chart__label">{slice.label}</span>
176
- <span class="smrt-chart__value">{fmt(slice.value)} · {percent(slice.share)}</span>
177
- </li>
178
- {/each}
179
- </ul>
180
- {@render dataTable(true)}
181
- </div>
182
- {:else}
183
- <p class="smrt-chart__empty" data-smrt-component="ChartWidget">{t(M['ui.overview.no_data'])}</p>
184
- {/if}
185
- {:else if style === 'line'}
186
- <div class="smrt-chart" data-smrt-component="ChartWidget">
187
- <svg class="smrt-chart__line" viewBox="0 0 100 40" preserveAspectRatio="none" role="img" aria-label={summary}>
188
- <polyline points={polyline} fill="none" stroke="currentColor" stroke-width="2" vector-effect="non-scaling-stroke" />
189
- </svg>
190
- <p class="smrt-chart__axis" aria-hidden="true">
191
- <span>{points[0].label}</span>
192
- <span>{points[points.length - 1].label}</span>
193
- </p>
194
- {@render dataTable(true)}
195
- </div>
196
- {:else}
197
- <ul class="smrt-chart__bars" aria-label={summary} data-smrt-component="ChartWidget">
198
- {#each points as point, index (index)}
199
- <li class="smrt-chart__row">
200
- <span class="smrt-chart__label">{point.label}</span>
201
- <span class="smrt-chart__track" aria-hidden="true">
202
- <span class="smrt-chart__bar" style:inline-size="{max > 0 ? (Math.max(0, point.value) / max) * 100 : 0}%"></span>
203
- </span>
204
- <span class="smrt-chart__value">{fmt(point.value)}</span>
205
- </li>
206
- {/each}
207
- </ul>
208
- {/if}
209
-
210
- <style>
211
- @layer smrt.reset, smrt.structure, smrt.theme;
212
- @layer smrt.theme {
213
- .smrt-chart { display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-primary); }
214
- .smrt-chart__line { inline-size: 100%; block-size: 7rem; overflow: visible; }
215
- .smrt-chart__axis { display: flex; justify-content: space-between; margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-large-size, 0.75rem); }
216
- .smrt-chart__bars { display: grid; gap: var(--smrt-spacing-2); margin: 0; padding: 0; list-style: none; }
217
- .smrt-chart__row { display: grid; grid-template-columns: minmax(4rem, 30%) minmax(0, 1fr) auto; align-items: center; gap: var(--smrt-spacing-2); font-size: var(--smrt-typography-body-medium-size, 0.875rem); color: var(--smrt-color-on-surface); }
218
- .smrt-chart__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
- .smrt-chart__track { display: block; block-size: 0.625rem; border-radius: var(--smrt-radius-full); background: var(--smrt-color-surface-container-high); overflow: hidden; }
220
- .smrt-chart__bar { display: block; block-size: 100%; border-radius: inherit; background: var(--smrt-color-primary); }
221
- .smrt-chart__value { font-variant-numeric: tabular-nums; color: var(--smrt-color-on-surface-variant); }
222
- .smrt-chart__empty { margin: 0; color: var(--smrt-color-on-surface-variant); }
223
- .smrt-chart--table { color: var(--smrt-color-on-surface); }
224
- .smrt-chart__data { inline-size: 100%; border-collapse: collapse; font-size: var(--smrt-typography-body-medium-size, 0.875rem); }
225
- .smrt-chart__data caption { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
226
- .smrt-chart__data th, .smrt-chart__data td { padding: var(--smrt-spacing-1) var(--smrt-spacing-2); border-block-end: var(--smrt-hairline); text-align: start; }
227
- .smrt-chart__data thead th { background: var(--smrt-surface-sunken); color: var(--smrt-color-on-surface-variant); font-weight: var(--smrt-typography-weight-medium, 500); }
228
- .smrt-chart__data tbody th { font-weight: inherit; overflow-wrap: anywhere; }
229
- .smrt-chart__data td, .smrt-chart__data thead th:last-child { text-align: end; font-variant-numeric: tabular-nums; }
230
- .smrt-chart--stat { gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); }
231
- .smrt-chart__headline { margin: 0; font-size: var(--smrt-typography-display-small-size, 2rem); font-weight: var(--smrt-typography-weight-medium, 500); line-height: 1.1; font-variant-numeric: tabular-nums; }
232
- .smrt-chart__caption { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-medium-size, 0.875rem); }
233
- .smrt-chart__delta { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-large-size, 0.875rem); }
234
- .smrt-chart--pie { grid-template-columns: minmax(6rem, 10rem) minmax(0, 1fr); align-items: center; gap: var(--smrt-spacing-4); }
235
- .smrt-chart__donut { inline-size: 100%; block-size: auto; }
236
- .smrt-chart__slice[data-slot='0'], .smrt-chart__swatch[data-slot='0'] { stroke: var(--smrt-color-primary); background: var(--smrt-color-primary); }
237
- .smrt-chart__slice[data-slot='1'], .smrt-chart__swatch[data-slot='1'] { stroke: var(--smrt-color-secondary); background: var(--smrt-color-secondary); }
238
- .smrt-chart__slice[data-slot='2'], .smrt-chart__swatch[data-slot='2'] { stroke: var(--smrt-color-tertiary); background: var(--smrt-color-tertiary); }
239
- .smrt-chart__slice[data-slot='3'], .smrt-chart__swatch[data-slot='3'] { stroke: var(--smrt-color-info); background: var(--smrt-color-info); }
240
- .smrt-chart__slice[data-slot='4'], .smrt-chart__swatch[data-slot='4'] { stroke: var(--smrt-color-primary-container); background: var(--smrt-color-primary-container); }
241
- .smrt-chart__slice[data-slot='5'], .smrt-chart__swatch[data-slot='5'] { stroke: var(--smrt-color-secondary-container); background: var(--smrt-color-secondary-container); }
242
- .smrt-chart__slice[data-slot='6'], .smrt-chart__swatch[data-slot='6'] { stroke: var(--smrt-color-tertiary-container); background: var(--smrt-color-tertiary-container); }
243
- .smrt-chart__slice[data-slot='7'], .smrt-chart__swatch[data-slot='7'] { stroke: var(--smrt-color-info-container); background: var(--smrt-color-info-container); }
244
- .smrt-chart__legend { display: grid; gap: var(--smrt-spacing-1); margin: 0; padding: 0; list-style: none; color: var(--smrt-color-on-surface); font-size: var(--smrt-typography-body-medium-size, 0.875rem); }
245
- .smrt-chart__legend-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--smrt-spacing-2); }
246
- .smrt-chart__swatch { inline-size: 0.75rem; block-size: 0.75rem; border-radius: var(--smrt-radius-small); }
247
- .smrt-chart__legend .smrt-chart__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
248
- @media (prefers-reduced-motion: no-preference) {
249
- .smrt-chart__slice { transition: stroke-dasharray var(--smrt-duration-normal) var(--smrt-easing-standard), stroke-dashoffset var(--smrt-duration-normal) var(--smrt-easing-standard); }
250
- .smrt-chart__bar { transition: inline-size var(--smrt-duration-normal) var(--smrt-easing-standard); }
251
- }
252
- .smrt-chart__table { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
253
- /* Forced colors (#3908): selected and open states keep a non-colour indicator. */
254
- @media (forced-colors: active) {
255
- /* Series keys: system colours cycle by slot (the user's own palette), so
256
- slices and legend swatches stay distinguishable without author colours.
257
- The legend always carries label, value and share as text. */
258
- .smrt-chart__slice[data-slot='0'], .smrt-chart__swatch[data-slot='0'] { stroke: CanvasText; background: CanvasText; }
259
- .smrt-chart__slice[data-slot='1'], .smrt-chart__swatch[data-slot='1'] { stroke: Highlight; background: Highlight; }
260
- .smrt-chart__slice[data-slot='2'], .smrt-chart__swatch[data-slot='2'] { stroke: LinkText; background: LinkText; }
261
- .smrt-chart__slice[data-slot='3'], .smrt-chart__swatch[data-slot='3'] { stroke: VisitedText; background: VisitedText; }
262
- .smrt-chart__slice[data-slot='4'], .smrt-chart__swatch[data-slot='4'] { stroke: ActiveText; background: ActiveText; }
263
- .smrt-chart__slice[data-slot='5'], .smrt-chart__swatch[data-slot='5'] { stroke: GrayText; background: GrayText; }
264
- .smrt-chart__slice[data-slot='6'], .smrt-chart__swatch[data-slot='6'] { stroke: ButtonText; background: ButtonText; }
265
- .smrt-chart__slice[data-slot='7'], .smrt-chart__swatch[data-slot='7'] { stroke: MarkText; background: MarkText; }
266
- .smrt-chart__swatch { border: var(--smrt-hairline-strong); }
267
- .smrt-chart__slice { stroke-linecap: butt; }
268
- .smrt-chart__track { background: Canvas; border: var(--smrt-hairline-strong); }
269
- .smrt-chart__bar { background: Highlight; }
270
- }
271
- }
272
- </style>
10
+ <div data-smrt-component="ChartWidget">
11
+ <Chart data={data ?? null} style={String(options.style ?? 'bar')} {title} {locale} />
12
+ </div>
@@ -1,5 +1,5 @@
1
1
  import type { WidgetComponentProps } from '../types.js';
2
- import { type ChartWidgetData } from './data.js';
2
+ import type { ChartWidgetData } from './data.js';
3
3
  declare const ChartWidget: import("svelte").Component<WidgetComponentProps<ChartWidgetData>, {}, "">;
4
4
  type ChartWidget = ReturnType<typeof ChartWidget>;
5
5
  export default ChartWidget;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/ChartWidget.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,KAAK,eAAe,EAAY,MAAM,WAAW,CAAC;AAqM3D,QAAA,MAAM,WAAW,2EAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ChartWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/ChartWidget.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAiBjD,QAAA,MAAM,WAAW,2EAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -12,7 +12,12 @@ import { M } from '../../../i18n/strings.overview.js';
12
12
  import { safeHref } from '../markdown.js';
13
13
  import type { WidgetComponentProps } from '../types.js';
14
14
  import { isRecord, type LineItemsWidgetData } from './data.js';
15
- import { lineItemTotals, sumLineAmounts } from './line-totals.js';
15
+ import {
16
+ groupStoredTaxComponents,
17
+ isStoredLineItemsTotals,
18
+ lineItemTotals,
19
+ sumLineAmounts,
20
+ } from './line-totals.js';
16
21
  import RecordTable from './RecordTable.svelte';
17
22
  import {
18
23
  formatRecordCell,
@@ -70,6 +75,19 @@ const result = $derived(
70
75
  lineSum,
71
76
  ),
72
77
  );
78
+ const storedTotals = $derived(
79
+ isRecord(data) && isStoredLineItemsTotals(data.storedTotals)
80
+ ? data.storedTotals
81
+ : undefined,
82
+ );
83
+ const storedTaxes = $derived(
84
+ storedTotals
85
+ ? groupStoredTaxComponents(storedTotals.taxes).filter(
86
+ (tax) => tax.tax !== 0,
87
+ )
88
+ : [],
89
+ );
90
+ const hasStoredTax = $derived(storedTaxes.length > 0);
73
91
  const viewAll = $derived(
74
92
  isRecord(data) && typeof data.href === 'string' ? safeHref(data.href) : null,
75
93
  );
@@ -101,7 +119,29 @@ const LABELS = {
101
119
  {:else}
102
120
  <p class="smrt-line-items__empty">{t(M['ui.overview.line_items.empty'])}</p>
103
121
  {/if}
104
- {#if result.lines.length > 0}
122
+ {#if storedTotals}
123
+ <dl class="smrt-line-items__totals" data-money-snapshot="stored">
124
+ {#if storedTotals.lineAmountType !== 'inclusive' || !hasStoredTax}
125
+ <div class="smrt-line-items__total"><dt>{t(M['ui.overview.line_items.subtotal'])}</dt><dd>{money(storedTotals.subtotal)}</dd></div>
126
+ {/if}
127
+ {#if storedTotals.discount !== 0}
128
+ <div class="smrt-line-items__total smrt-line-items__total--muted"><dt>{t(M['ui.overview.line_items.discount_included'])} {money(Math.abs(storedTotals.discount))}</dt></div>
129
+ {/if}
130
+ {#each storedTaxes as tax (`${tax.taxComponentId}-${tax.name}-${tax.percent}`)}
131
+ <div class="smrt-line-items__total smrt-line-items__total--muted">
132
+ <dt>{t(M[storedTotals.lineAmountType === 'inclusive' ? 'ui.overview.line_items.tax_included' : 'ui.overview.line_items.tax_on'], { name: tax.name, percent: tax.percent })} {money(storedTotals.lineAmountType === 'inclusive' ? tax.tax : tax.taxable)}</dt>
133
+ {#if storedTotals.lineAmountType !== 'inclusive'}<dd>{money(tax.tax)}</dd>{/if}
134
+ </div>
135
+ {/each}
136
+ {#if storedTotals.roundingScope === 'document'}
137
+ <div class="smrt-line-items__total smrt-line-items__total--muted"><dt>{t(M['ui.overview.line_items.rounded_document'])}</dt></div>
138
+ {/if}
139
+ <div class="smrt-line-items__total smrt-line-items__total--grand"><dt>{t(M['ui.overview.line_items.total'])}</dt><dd>{money(storedTotals.total)}</dd></div>
140
+ {#if storedTotals.lineAmountType === 'inclusive' && hasStoredTax && storedTotals.showNetSubtotal}
141
+ <div class="smrt-line-items__total"><dt>{t(M['ui.overview.line_items.subtotal_excluding_tax'])}</dt><dd>{money(storedTotals.subtotal)}</dd></div>
142
+ {/if}
143
+ </dl>
144
+ {:else if result.lines.length > 0}
105
145
  <dl class="smrt-line-items__totals">
106
146
  {#each result.lines as line (line.key)}
107
147
  <div class="smrt-line-items__total" class:smrt-line-items__total--grand={line.key === 'total'}>
@@ -114,7 +154,7 @@ const LABELS = {
114
154
  {/each}
115
155
  </dl>
116
156
  {/if}
117
- {#if result.mismatch}
157
+ {#if !storedTotals && result.mismatch}
118
158
  <p class="smrt-line-items__flag">
119
159
  <strong>{t(M['ui.overview.line_items.mismatch'], { amount: money(result.mismatch.lines) })}</strong>
120
160
  {t(M[result.mismatch.field === 'subtotal' ? 'ui.overview.line_items.mismatch_subtotal' : 'ui.overview.line_items.mismatch_total'], { amount: money(result.mismatch.recorded) })}
@@ -133,6 +173,7 @@ const LABELS = {
133
173
  .smrt-line-items__totals { display: grid; justify-self: end; gap: var(--smrt-spacing-1); min-inline-size: min(100%, 18rem); margin: 0; font-variant-numeric: tabular-nums; }
134
174
  .smrt-line-items__total { display: flex; justify-content: space-between; gap: var(--smrt-spacing-4); }
135
175
  .smrt-line-items__total dt { color: var(--smrt-color-on-surface-variant); }
176
+ .smrt-line-items__total--muted { color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-small-size); }
136
177
  .smrt-line-items__total dd { margin: 0; text-align: end; }
137
178
  .smrt-line-items__total--grand { padding-block-start: var(--smrt-spacing-2); border-block-start: var(--smrt-hairline); font-weight: var(--smrt-typography-weight-semibold); }
138
179
  .smrt-line-items__total--grand dt { color: var(--smrt-color-on-surface); }
@@ -1 +1 @@
1
- {"version":3,"file":"LineItemsWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/LineItemsWidget.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAY,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAwH/D,QAAA,MAAM,eAAe,+EAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"LineItemsWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/LineItemsWidget.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAY,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAgK/D,QAAA,MAAM,eAAe,+EAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -130,6 +130,23 @@ export interface LineItemsWidgetData {
130
130
  tax?: number | null;
131
131
  total?: number | null;
132
132
  };
133
+ /** Frozen document-money snapshot supplied by a domain host. */
134
+ storedTotals?: {
135
+ subtotal: number;
136
+ discount: number;
137
+ tax: number;
138
+ total: number;
139
+ lineAmountType: 'exclusive' | 'inclusive' | 'none';
140
+ roundingScope: 'line' | 'document';
141
+ showNetSubtotal?: boolean;
142
+ taxes: readonly {
143
+ taxComponentId: string;
144
+ name: string;
145
+ percent: string;
146
+ taxable: number;
147
+ tax: number;
148
+ }[];
149
+ };
133
150
  currency?: string;
134
151
  /** Footer values over every line (money in minor units). */
135
152
  summaries?: Record<string, number>;
@@ -1 +1 @@
1
- {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/data.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/data.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,41 @@ export interface LineItemsTotals {
16
16
  tax?: number | null;
17
17
  total?: number | null;
18
18
  }
19
+ /** A frozen per-component tax row supplied by a domain host. */
20
+ export interface StoredTaxComponent {
21
+ taxComponentId: string;
22
+ name: string;
23
+ percent: string;
24
+ taxable: number;
25
+ tax: number;
26
+ }
27
+ /**
28
+ * Authoritative document-money snapshot supplied by a domain host. The widget
29
+ * displays these persisted values only: it never derives tax or totals from
30
+ * the visible line page.
31
+ */
32
+ export interface StoredLineItemsTotals extends LineItemsTotals {
33
+ subtotal: number;
34
+ discount: number;
35
+ tax: number;
36
+ total: number;
37
+ lineAmountType: 'exclusive' | 'inclusive' | 'none';
38
+ roundingScope: 'line' | 'document';
39
+ /** Whether an inclusive taxed document should expose its net subtotal. */
40
+ showNetSubtotal?: boolean;
41
+ taxes: readonly StoredTaxComponent[];
42
+ }
43
+ /** A persisted component can occur through more than one tax code. */
44
+ export interface StoredTaxComponentGroup extends StoredTaxComponent {
45
+ }
46
+ /** Whether untrusted widget data is a complete persisted money snapshot. */
47
+ export declare function isStoredLineItemsTotals(value: unknown): value is StoredLineItemsTotals;
48
+ /**
49
+ * Group exact persisted rows only when they identify the same component,
50
+ * display name, and percent. Components sharing a display label/rate remain
51
+ * separate; this does not calculate or reallocate tax.
52
+ */
53
+ export declare function groupStoredTaxComponents(taxes: readonly StoredTaxComponent[]): StoredTaxComponentGroup[];
19
54
  export type LineTotalKey = 'subtotal' | 'discount' | 'tax' | 'total';
20
55
  export interface LineTotalLine {
21
56
  key: LineTotalKey;
@@ -1 +1 @@
1
- {"version":3,"file":"line-totals.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/line-totals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,GAAG,KAAK,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IAC5B,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACrC;AAQD,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAUzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,gBAAgB,CA2DlB"}
1
+ {"version":3,"file":"line-totals.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/line-totals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,WAAW,GAAG,WAAW,GAAG,MAAM,CAAC;IACnD,aAAa,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACtC;AAED,sEAAsE;AACtE,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;CAAG;AAEtE,4EAA4E;AAC5E,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,qBAAqB,CAchC;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,kBAAkB,EAAE,GACnC,uBAAuB,EAAE,CA0B3B;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,GAAG,KAAK,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IAC5B,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACrC;AAQD,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAUzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,gBAAgB,CA2DlB"}
@@ -9,6 +9,51 @@
9
9
  *
10
10
  * Money is integer minor units throughout.
11
11
  */
12
+ /** Whether untrusted widget data is a complete persisted money snapshot. */
13
+ export function isStoredLineItemsTotals(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value))
15
+ return false;
16
+ const record = value;
17
+ return ((record.lineAmountType === 'exclusive' ||
18
+ record.lineAmountType === 'inclusive' ||
19
+ record.lineAmountType === 'none') &&
20
+ (record.roundingScope === 'line' || record.roundingScope === 'document') &&
21
+ Number.isSafeInteger(record.subtotal) &&
22
+ Number.isSafeInteger(record.discount) &&
23
+ Number.isSafeInteger(record.tax) &&
24
+ Number.isSafeInteger(record.total) &&
25
+ Array.isArray(record.taxes));
26
+ }
27
+ /**
28
+ * Group exact persisted rows only when they identify the same component,
29
+ * display name, and percent. Components sharing a display label/rate remain
30
+ * separate; this does not calculate or reallocate tax.
31
+ */
32
+ export function groupStoredTaxComponents(taxes) {
33
+ const groups = new Map();
34
+ for (const tax of taxes) {
35
+ if (!tax ||
36
+ typeof tax.taxComponentId !== 'string' ||
37
+ typeof tax.name !== 'string' ||
38
+ typeof tax.percent !== 'string' ||
39
+ !Number.isSafeInteger(tax.taxable) ||
40
+ !Number.isSafeInteger(tax.tax))
41
+ continue;
42
+ const key = `${tax.taxComponentId}\u0000${tax.name}\u0000${tax.percent}`;
43
+ const existing = groups.get(key);
44
+ if (!existing) {
45
+ groups.set(key, { ...tax });
46
+ continue;
47
+ }
48
+ const taxable = existing.taxable + tax.taxable;
49
+ const amount = existing.tax + tax.tax;
50
+ if (!Number.isSafeInteger(taxable) || !Number.isSafeInteger(amount))
51
+ continue;
52
+ existing.taxable = taxable;
53
+ existing.tax = amount;
54
+ }
55
+ return [...groups.values()];
56
+ }
12
57
  function money(value) {
13
58
  return typeof value === 'number' && Number.isSafeInteger(value)
14
59
  ? value
@@ -18,7 +18,7 @@
18
18
  import type { ScreenRelation } from '../../screens/relations.js';
19
19
  import type { OverviewOptions, WidgetLoad } from '../types.js';
20
20
  import { type LineItemsWidgetData, type RecordListWidgetData, type RelatedWidgetData } from './data.js';
21
- import type { LineItemsTotals } from './line-totals.js';
21
+ import type { LineItemsTotals, StoredLineItemsTotals } from './line-totals.js';
22
22
  /** Rows per tab by default. */
23
23
  export declare const RELATED_WIDGET_LIMIT = 5;
24
24
  /** At most this many relations (tabs) one `related` widget lists. */
@@ -35,6 +35,8 @@ export interface RecordWidgetsOptions {
35
35
  * (read when the widget loads; `RecordScreen` reads them by role).
36
36
  */
37
37
  totals?: () => LineItemsTotals | undefined;
38
+ /** Frozen document-money snapshot. It is rendered without recalculation. */
39
+ storedTotals?: () => StoredLineItemsTotals | undefined;
38
40
  /** `line-items`: the currency of the parent's amounts. */
39
41
  currency?: () => string | undefined;
40
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"record-loaders.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/record-loaders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EAEX,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAQxD,+BAA+B;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IACrC,+EAA+E;IAC/E,OAAO,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAC1C,8DAA8D;IAC9D,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,cAAc,EAAE,CAMlB;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AA2CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,iBAAiB,CAAC,CAkC/B;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,GAAG,EAAE,OAAO,GACX,cAAc,GAAG,SAAS,CAM5B;AAED,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,mBAAmB,CAAC,CA6BjC"}
1
+ {"version":3,"file":"record-loaders.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/record-loaders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EAEX,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAQ/E,+BAA+B;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IACrC,+EAA+E;IAC/E,OAAO,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAC1C,8DAA8D;IAC9D,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;IAC3C,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,qBAAqB,GAAG,SAAS,CAAC;IACvD,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,cAAc,EAAE,CAMlB;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AA2CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,iBAAiB,CAAC,CAkC/B;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,GAAG,EAAE,OAAO,GACX,cAAc,GAAG,SAAS,CAM5B;AAED,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,mBAAmB,CAAC,CAoCjC"}
@@ -126,9 +126,15 @@ export function createLineItemsLoader(options) {
126
126
  return async (widgetOptions, ctx) => {
127
127
  const relation = lineItemsRelation(options.relations, widgetOptions.relation);
128
128
  const totals = options.totals?.();
129
+ const storedTotals = options.storedTotals?.();
129
130
  const currency = options.currency?.();
130
131
  if (!relation) {
131
- return { rows: [], columns: [], ...(totals ? { totals } : {}) };
132
+ return {
133
+ rows: [],
134
+ columns: [],
135
+ ...(totals ? { totals } : {}),
136
+ ...(storedTotals ? { storedTotals } : {}),
137
+ };
132
138
  }
133
139
  const limit = typeof widgetOptions.limit === 'number'
134
140
  ? widgetOptions.limit
@@ -140,6 +146,7 @@ export function createLineItemsLoader(options) {
140
146
  columns: tab.columns,
141
147
  ...(tab.total !== undefined ? { total: tab.total } : {}),
142
148
  ...(totals ? { totals } : {}),
149
+ ...(storedTotals ? { storedTotals } : {}),
143
150
  ...((tab.currency ?? currency)
144
151
  ? { currency: tab.currency ?? currency }
145
152
  : {}),
@@ -74,6 +74,7 @@ let {
74
74
  title,
75
75
  fields,
76
76
  currency,
77
+ lineItemStoredTotals,
77
78
  locale,
78
79
  backLabel,
79
80
  deleting = false,
@@ -152,6 +153,7 @@ const widgetRegistry = untrack(() => {
152
153
  records,
153
154
  viewAllHref: (relation) => viewAllHref?.(relation),
154
155
  totals: () => recordTotals(record, roles, viewFields),
156
+ storedTotals: () => lineItemStoredTotals?.(),
155
157
  currency: () => recordCurrency,
156
158
  });
157
159
  for (const def of recordWidgets.list()) {
@@ -1 +1 @@
1
- {"version":3,"file":"RecordScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecordScreen.svelte.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkVpD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"RecordScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecordScreen.svelte.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAoVpD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -3,6 +3,7 @@ import type { WidgetRegistry } from '../overview/registry.js';
3
3
  import type { OverviewDefinition, OverviewOverride, WidgetLoadContext } from '../overview/types.js';
4
4
  import type { CoreWidgetLoaders } from '../overview/widgets/core.js';
5
5
  import type { RecordStatusTone } from '../overview/widgets/data.js';
6
+ import type { StoredLineItemsTotals } from '../overview/widgets/line-totals.js';
6
7
  import type { ScreenRelation } from './relations.js';
7
8
  import type { FieldRole } from './roles.js';
8
9
  /**
@@ -317,6 +318,12 @@ export interface RecordScreenProps {
317
318
  fields?: readonly string[];
318
319
  /** ISO 4217 code used by money fields. */
319
320
  currency?: string;
321
+ /**
322
+ * Frozen document-money snapshot for the line-items widget. The host reads
323
+ * its own persisted rows (including per-component tax); this screen never
324
+ * recalculates them from visible lines.
325
+ */
326
+ lineItemStoredTotals?: () => StoredLineItemsTotals | undefined;
320
327
  /** BCP-47 locale for number and date formatting. */
321
328
  locale?: string;
322
329
  /** Label for the back action; defaults to the pluralized class name. */
@@ -391,6 +398,6 @@ export interface RecipeScreensProps {
391
398
  */
392
399
  definitions?: Readonly<Record<string, ScreenCollectionDefinition>>;
393
400
  /** Passed to `RecordScreen` for the record view and edit. */
394
- record?: Pick<RecordScreenProps, 'loaders' | 'registry' | 'widgetContext' | 'overview' | 'override' | 'onoverviewchange' | 'layoutEditing' | 'canCustomize' | 'hrefFor' | 'labels' | 'viewAllHref' | 'statusTones'>;
401
+ record?: Pick<RecordScreenProps, 'loaders' | 'registry' | 'widgetContext' | 'overview' | 'override' | 'onoverviewchange' | 'layoutEditing' | 'canCustomize' | 'hrefFor' | 'labels' | 'viewAllHref' | 'lineItemStoredTotals' | 'statusTones'>;
395
402
  }
396
403
  //# sourceMappingURL=types.d.ts.map