@happyvertical/smrt-svelte 0.55.23 → 0.56.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/AGENTS.md +2 -2
- package/README.md +2 -2
- package/agents/overview-surfaces.md +117 -6
- package/agents/screens.md +171 -5
- package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
- package/dist/components/overview/OverviewGrid.svelte +8 -4
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
- package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
- package/dist/components/overview/format.d.ts +3 -12
- package/dist/components/overview/format.d.ts.map +1 -1
- package/dist/components/overview/format.js +2 -45
- package/dist/components/overview/grid-types.d.ts +6 -11
- package/dist/components/overview/grid-types.d.ts.map +1 -1
- package/dist/components/overview/index.d.ts +5 -1
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +3 -0
- package/dist/components/overview/recipe-widgets.d.ts +6 -3
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
- package/dist/components/overview/recipe-widgets.js +9 -0
- package/dist/components/overview/record.d.ts +51 -0
- package/dist/components/overview/record.d.ts.map +1 -0
- package/dist/components/overview/record.js +146 -0
- package/dist/components/overview/registry.d.ts +3 -0
- package/dist/components/overview/registry.d.ts.map +1 -1
- package/dist/components/overview/registry.js +25 -0
- package/dist/components/overview/server.d.ts +5 -1
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +5 -1
- package/dist/components/overview/types.d.ts +29 -0
- package/dist/components/overview/types.d.ts.map +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/RecordTable.svelte +167 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +13 -2
- package/dist/components/overview/widgets/core.d.ts.map +1 -1
- package/dist/components/overview/widgets/core.js +43 -2
- package/dist/components/overview/widgets/data.d.ts +97 -8
- package/dist/components/overview/widgets/data.d.ts.map +1 -1
- package/dist/components/overview/widgets/line-totals.d.ts +48 -0
- package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
- package/dist/components/overview/widgets/line-totals.js +91 -0
- package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
- package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-loaders.js +150 -0
- package/dist/components/overview/widgets/record-table.d.ts +45 -0
- package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-table.js +201 -0
- package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
- package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-widgets.js +100 -0
- package/dist/components/screens/DetailScreen.svelte +105 -6
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
- package/dist/components/screens/EditForm.svelte +39 -3
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecipeScreens.svelte +18 -18
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecordHeader.svelte +238 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecordScreen.svelte +511 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/columns.d.ts +29 -0
- package/dist/components/screens/columns.d.ts.map +1 -0
- package/dist/components/screens/columns.js +115 -0
- package/dist/components/screens/fields.d.ts +12 -2
- package/dist/components/screens/fields.d.ts.map +1 -1
- package/dist/components/screens/fields.js +37 -5
- package/dist/components/screens/index.d.ts +7 -2
- package/dist/components/screens/index.d.ts.map +1 -1
- package/dist/components/screens/index.js +6 -1
- package/dist/components/screens/record-header.d.ts +51 -0
- package/dist/components/screens/record-header.d.ts.map +1 -0
- package/dist/components/screens/record-header.js +118 -0
- package/dist/components/screens/relations.d.ts +49 -0
- package/dist/components/screens/relations.d.ts.map +1 -0
- package/dist/components/screens/relations.js +98 -0
- package/dist/components/screens/roles.d.ts +56 -0
- package/dist/components/screens/roles.d.ts.map +1 -0
- package/dist/components/screens/roles.js +174 -0
- package/dist/components/screens/types.d.ts +182 -1
- package/dist/components/screens/types.d.ts.map +1 -1
- package/dist/components/screens/values.d.ts +23 -1
- package/dist/components/screens/values.d.ts.map +1 -1
- package/dist/components/screens/values.js +80 -4
- package/dist/components/workspace/admin-shell/AdminShell.svelte +4 -4
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
- package/dist/i18n/strings.overview.d.ts +25 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +25 -0
- package/dist/i18n/strings.screens.d.ts +8 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -1
- package/dist/i18n/strings.screens.js +8 -0
- 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 {
|
|
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
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
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 {
|
|
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":"
|
|
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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Line items widget (#3718 record page): a document's lines as a table
|
|
4
|
+
* (item, quantity, unit price, amount) with a footer, then the totals block
|
|
5
|
+
* aligned under the amounts: subtotal, discount and tax when present, total.
|
|
6
|
+
* The record's own figures are shown as they are; a missing one is computed
|
|
7
|
+
* from the lines and says so; when the record and the lines disagree, a
|
|
8
|
+
* visible flag says what the lines add up to (`lineItemTotals`).
|
|
9
|
+
*/
|
|
10
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
11
|
+
import { M } from '../../../i18n/strings.overview.js';
|
|
12
|
+
import { safeHref } from '../markdown.js';
|
|
13
|
+
import type { WidgetComponentProps } from '../types.js';
|
|
14
|
+
import { isRecord, type LineItemsWidgetData } from './data.js';
|
|
15
|
+
import { lineItemTotals, sumLineAmounts } from './line-totals.js';
|
|
16
|
+
import RecordTable from './RecordTable.svelte';
|
|
17
|
+
import {
|
|
18
|
+
formatRecordCell,
|
|
19
|
+
readRecordRows,
|
|
20
|
+
resolveRecordColumns,
|
|
21
|
+
} from './record-table.js';
|
|
22
|
+
|
|
23
|
+
let { data, locale, title }: WidgetComponentProps<LineItemsWidgetData> =
|
|
24
|
+
$props();
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
|
|
28
|
+
const rows = $derived(readRecordRows(data));
|
|
29
|
+
const columns = $derived(
|
|
30
|
+
resolveRecordColumns([], isRecord(data) ? data.columns : undefined),
|
|
31
|
+
);
|
|
32
|
+
const total = $derived(
|
|
33
|
+
isRecord(data) &&
|
|
34
|
+
typeof data.total === 'number' &&
|
|
35
|
+
Number.isFinite(data.total)
|
|
36
|
+
? data.total
|
|
37
|
+
: rows.length,
|
|
38
|
+
);
|
|
39
|
+
const currency = $derived(
|
|
40
|
+
isRecord(data) && typeof data.currency === 'string'
|
|
41
|
+
? data.currency
|
|
42
|
+
: undefined,
|
|
43
|
+
);
|
|
44
|
+
const summaryValues = $derived.by(() => {
|
|
45
|
+
if (!isRecord(data) || !isRecord(data.summaries)) return undefined;
|
|
46
|
+
const out: Record<string, number> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(data.summaries)) {
|
|
48
|
+
if (typeof value === 'number' && Number.isFinite(value)) out[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
});
|
|
52
|
+
/** The line amount column: the last money column that is summed. */
|
|
53
|
+
const amountKey = $derived(
|
|
54
|
+
[...columns]
|
|
55
|
+
.reverse()
|
|
56
|
+
.find((c) => c.role === 'money' && c.summarize !== false)?.key,
|
|
57
|
+
);
|
|
58
|
+
const lineSum = $derived.by((): number | null => {
|
|
59
|
+
if (!amountKey) return null;
|
|
60
|
+
const hosted = summaryValues?.[amountKey];
|
|
61
|
+
if (typeof hosted === 'number') return hosted;
|
|
62
|
+
if (total > rows.length) return null;
|
|
63
|
+
return sumLineAmounts(rows.map((row) => row.cells?.[amountKey]));
|
|
64
|
+
});
|
|
65
|
+
const result = $derived(
|
|
66
|
+
lineItemTotals(
|
|
67
|
+
isRecord(data) && isRecord(data.totals)
|
|
68
|
+
? (data.totals as LineItemsWidgetData['totals'])
|
|
69
|
+
: undefined,
|
|
70
|
+
lineSum,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
const viewAll = $derived(
|
|
74
|
+
isRecord(data) && typeof data.href === 'string' ? safeHref(data.href) : null,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
function money(value: number): string {
|
|
78
|
+
return formatRecordCell({ role: 'money' }, value, { currency, locale });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const LABELS = {
|
|
82
|
+
subtotal: M['ui.overview.line_items.subtotal'],
|
|
83
|
+
discount: M['ui.overview.line_items.discount'],
|
|
84
|
+
tax: M['ui.overview.line_items.tax'],
|
|
85
|
+
total: M['ui.overview.line_items.total'],
|
|
86
|
+
} as const;
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="smrt-line-items" data-smrt-component="LineItemsWidget">
|
|
90
|
+
{#if rows.length > 0 && columns.length > 0}
|
|
91
|
+
<RecordTable
|
|
92
|
+
{rows}
|
|
93
|
+
{columns}
|
|
94
|
+
{locale}
|
|
95
|
+
label={title}
|
|
96
|
+
{currency}
|
|
97
|
+
summaries
|
|
98
|
+
{summaryValues}
|
|
99
|
+
{total}
|
|
100
|
+
/>
|
|
101
|
+
{:else}
|
|
102
|
+
<p class="smrt-line-items__empty">{t(M['ui.overview.line_items.empty'])}</p>
|
|
103
|
+
{/if}
|
|
104
|
+
{#if result.lines.length > 0}
|
|
105
|
+
<dl class="smrt-line-items__totals">
|
|
106
|
+
{#each result.lines as line (line.key)}
|
|
107
|
+
<div class="smrt-line-items__total" class:smrt-line-items__total--grand={line.key === 'total'}>
|
|
108
|
+
<dt>
|
|
109
|
+
{t(LABELS[line.key])}
|
|
110
|
+
{#if line.computed}<span class="smrt-line-items__computed">{t(M['ui.overview.line_items.computed'])}</span>{/if}
|
|
111
|
+
</dt>
|
|
112
|
+
<dd>{line.key === 'discount' ? `−${money(line.value)}` : money(line.value)}</dd>
|
|
113
|
+
</div>
|
|
114
|
+
{/each}
|
|
115
|
+
</dl>
|
|
116
|
+
{/if}
|
|
117
|
+
{#if result.mismatch}
|
|
118
|
+
<p class="smrt-line-items__flag">
|
|
119
|
+
<strong>{t(M['ui.overview.line_items.mismatch'], { amount: money(result.mismatch.lines) })}</strong>
|
|
120
|
+
{t(M[result.mismatch.field === 'subtotal' ? 'ui.overview.line_items.mismatch_subtotal' : 'ui.overview.line_items.mismatch_total'], { amount: money(result.mismatch.recorded) })}
|
|
121
|
+
</p>
|
|
122
|
+
{/if}
|
|
123
|
+
{#if viewAll}
|
|
124
|
+
<p class="smrt-line-items__foot"><a href={viewAll}>{t(M['ui.overview.records.view_all'])}</a></p>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<style>
|
|
129
|
+
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
130
|
+
@layer smrt.theme {
|
|
131
|
+
.smrt-line-items { display: grid; gap: var(--smrt-spacing-3); min-inline-size: 0; }
|
|
132
|
+
.smrt-line-items__empty { margin: 0; color: var(--smrt-color-on-surface-variant); }
|
|
133
|
+
.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
|
+
.smrt-line-items__total { display: flex; justify-content: space-between; gap: var(--smrt-spacing-4); }
|
|
135
|
+
.smrt-line-items__total dt { color: var(--smrt-color-on-surface-variant); }
|
|
136
|
+
.smrt-line-items__total dd { margin: 0; text-align: end; }
|
|
137
|
+
.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
|
+
.smrt-line-items__total--grand dt { color: var(--smrt-color-on-surface); }
|
|
139
|
+
.smrt-line-items__computed { margin-inline-start: var(--smrt-spacing-1); font-size: var(--smrt-typography-body-small-size, 0.75rem); }
|
|
140
|
+
.smrt-line-items__flag { margin: 0; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border-inline-start: var(--smrt-spacing-1) solid var(--smrt-color-warning); border-radius: var(--smrt-radius-sm); background: var(--smrt-color-warning-container); color: var(--smrt-color-on-warning-container); }
|
|
141
|
+
.smrt-line-items__foot { display: flex; justify-content: flex-end; margin: 0; }
|
|
142
|
+
.smrt-line-items__foot a { color: var(--smrt-color-primary); }
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WidgetComponentProps } from '../types.js';
|
|
2
|
+
import { type LineItemsWidgetData } from './data.js';
|
|
3
|
+
declare const LineItemsWidget: import("svelte").Component<WidgetComponentProps<LineItemsWidgetData>, {}, "">;
|
|
4
|
+
type LineItemsWidget = ReturnType<typeof LineItemsWidget>;
|
|
5
|
+
export default LineItemsWidget;
|
|
6
|
+
//# sourceMappingURL=LineItemsWidget.svelte.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
* Record list widget (#3727): the rows a loader chose, each optionally a
|
|
4
4
|
* link. The loader owns which field is the title and which records the viewer
|
|
5
5
|
* may see; the widget only draws what it is given.
|
|
6
|
+
*
|
|
7
|
+
* With columns (the `columns` option, else the data's `columns`) the rows
|
|
8
|
+
* render as a compact table of their `cells` (`RecordTable`): identifier,
|
|
9
|
+
* status badge, date and amount by role, with a totals footer (on by
|
|
10
|
+
* default when a money or quantity column exists; the `summaries` option
|
|
11
|
+
* overrides). Without columns, the title list.
|
|
6
12
|
*/
|
|
7
13
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
8
14
|
import { M } from '../../../i18n/strings.overview.js';
|
|
9
15
|
import { safeHref } from '../markdown.js';
|
|
10
16
|
import type { WidgetComponentProps } from '../types.js';
|
|
11
17
|
import { isRecord, type RecordListWidgetData } from './data.js';
|
|
18
|
+
import RecordTable from './RecordTable.svelte';
|
|
19
|
+
import {
|
|
20
|
+
isSummedColumn,
|
|
21
|
+
parseColumnsOption,
|
|
22
|
+
readRecordRows,
|
|
23
|
+
resolveRecordColumns,
|
|
24
|
+
} from './record-table.js';
|
|
12
25
|
|
|
13
|
-
let {
|
|
26
|
+
let {
|
|
27
|
+
data,
|
|
28
|
+
locale,
|
|
29
|
+
options,
|
|
30
|
+
title,
|
|
31
|
+
}: WidgetComponentProps<RecordListWidgetData> = $props();
|
|
14
32
|
|
|
15
33
|
const { t } = useI18n();
|
|
16
34
|
const MAX_ROWS = 50;
|
|
@@ -46,9 +64,54 @@ const total = $derived(
|
|
|
46
64
|
const viewAll = $derived(
|
|
47
65
|
isRecord(data) && typeof data.href === 'string' ? safeHref(data.href) : null,
|
|
48
66
|
);
|
|
67
|
+
|
|
68
|
+
const columns = $derived(
|
|
69
|
+
resolveRecordColumns(
|
|
70
|
+
parseColumnsOption(options?.columns),
|
|
71
|
+
isRecord(data) ? data.columns : undefined,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
const tableRows = $derived(readRecordRows(data));
|
|
75
|
+
const showSummaries = $derived(
|
|
76
|
+
typeof options?.summaries === 'boolean'
|
|
77
|
+
? options.summaries
|
|
78
|
+
: columns.some((c) => isSummedColumn(c)),
|
|
79
|
+
);
|
|
80
|
+
const dataTotal = $derived(
|
|
81
|
+
isRecord(data) &&
|
|
82
|
+
typeof data.total === 'number' &&
|
|
83
|
+
Number.isFinite(data.total)
|
|
84
|
+
? data.total
|
|
85
|
+
: null,
|
|
86
|
+
);
|
|
87
|
+
const summaryValues = $derived.by(() => {
|
|
88
|
+
if (!isRecord(data) || !isRecord(data.summaries)) return undefined;
|
|
89
|
+
const out: Record<string, number> = {};
|
|
90
|
+
for (const [key, value] of Object.entries(data.summaries)) {
|
|
91
|
+
if (typeof value === 'number' && Number.isFinite(value)) out[key] = value;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
});
|
|
49
95
|
</script>
|
|
50
96
|
|
|
51
|
-
{#if
|
|
97
|
+
{#if columns.length > 0 && tableRows.length > 0}
|
|
98
|
+
<RecordTable
|
|
99
|
+
rows={tableRows}
|
|
100
|
+
{columns}
|
|
101
|
+
{locale}
|
|
102
|
+
label={title}
|
|
103
|
+
currency={isRecord(data) && typeof data.currency === 'string' ? data.currency : undefined}
|
|
104
|
+
timeZone={isRecord(data) && typeof data.timeZone === 'string' ? data.timeZone : undefined}
|
|
105
|
+
summaries={showSummaries}
|
|
106
|
+
{summaryValues}
|
|
107
|
+
total={dataTotal}
|
|
108
|
+
/>
|
|
109
|
+
{#if viewAll}
|
|
110
|
+
<p class="smrt-records__foot smrt-records__foot--end" data-smrt-component="RecordListWidget">
|
|
111
|
+
<a href={viewAll}>{t(M['ui.overview.records.view_all'])}</a>
|
|
112
|
+
</p>
|
|
113
|
+
{/if}
|
|
114
|
+
{:else if rows.length === 0}
|
|
52
115
|
<p class="smrt-records__empty" data-smrt-component="RecordListWidget">{t(M['ui.overview.records.empty'])}</p>
|
|
53
116
|
{:else}
|
|
54
117
|
<ul class="smrt-records" data-smrt-component="RecordListWidget">
|
|
@@ -67,7 +130,7 @@ const viewAll = $derived(
|
|
|
67
130
|
{/each}
|
|
68
131
|
</ul>
|
|
69
132
|
{/if}
|
|
70
|
-
{#if total !== null || viewAll}
|
|
133
|
+
{#if columns.length === 0 && (total !== null || viewAll)}
|
|
71
134
|
<p class="smrt-records__foot" data-smrt-component="RecordListWidget">
|
|
72
135
|
{#if total !== null}
|
|
73
136
|
<span>{t(M['ui.overview.records.total'], { count: new Intl.NumberFormat(locale).format(total) })}</span>
|
|
@@ -88,6 +151,7 @@ const viewAll = $derived(
|
|
|
88
151
|
.smrt-records__sub, .smrt-records__meta { color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-medium-size, 0.875rem); }
|
|
89
152
|
.smrt-records__meta { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
90
153
|
.smrt-records__foot { display: flex; justify-content: space-between; gap: var(--smrt-spacing-3); margin: var(--smrt-spacing-2) 0 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-medium-size, 0.875rem); }
|
|
154
|
+
.smrt-records__foot--end { justify-content: flex-end; }
|
|
91
155
|
.smrt-records__foot a { color: var(--smrt-color-primary); }
|
|
92
156
|
.smrt-records__empty { margin: 0; color: var(--smrt-color-on-surface-variant); }
|
|
93
157
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordListWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/RecordListWidget.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RecordListWidget.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/RecordListWidget.svelte.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAY,KAAK,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAiIhE,QAAA,MAAM,gBAAgB,gFAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|