@olwiba/ui 0.2.24 → 0.2.25
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/dist/index.d.ts +51 -3
- package/dist/index.js +40 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Chart.tsx +38 -7
- package/src/components/DataTable.tsx +56 -8
- package/src/components/Notify.tsx +32 -1
- package/src/marketing/Footer.tsx +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olwiba/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
42
42
|
"@dnd-kit/sortable": "^10.0.0",
|
|
43
43
|
"@dnd-kit/utilities": "^3.2.2",
|
|
44
|
-
"@olwiba/dx": "0.0.23",
|
|
45
44
|
"@tanstack/react-table": "^8.21.3",
|
|
46
45
|
"clsx": "^2.1.1",
|
|
47
46
|
"lucide-react": "^0.562.0",
|
|
@@ -65,6 +64,7 @@
|
|
|
65
64
|
"@content-collections/mdx": "^0.2.2",
|
|
66
65
|
"@olwiba/cn": "0.1.35",
|
|
67
66
|
"@olwiba/docs": "0.1.40",
|
|
67
|
+
"@olwiba/dx": "0.0.23",
|
|
68
68
|
"@tailwindcss/vite": "^4.1.18",
|
|
69
69
|
"@tanstack/react-router": "1.154.8",
|
|
70
70
|
"@tanstack/react-router-devtools": "1.154.8",
|
package/src/components/Chart.tsx
CHANGED
|
@@ -42,6 +42,27 @@ export interface ChartProps {
|
|
|
42
42
|
grid?: boolean;
|
|
43
43
|
/** Legend. @default true for multiple series or donut, false for one series */
|
|
44
44
|
legend?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Stack series instead of overlaying them. Area and bar only — a stacked
|
|
47
|
+
* line is a shape people misread as absolute values.
|
|
48
|
+
*
|
|
49
|
+
* Off by default: stacking answers "what does the total split into", and
|
|
50
|
+
* silently switching a two-series comparison to it would change what an
|
|
51
|
+
* existing chart claims.
|
|
52
|
+
*/
|
|
53
|
+
stacked?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Formats x-axis ticks. Separate from `valueFormatter`, which formats the
|
|
56
|
+
* measured value — an axis of ISO dates wants shortening without changing
|
|
57
|
+
* what the tooltip reports.
|
|
58
|
+
*/
|
|
59
|
+
xTickFormatter?: (value: string) => string;
|
|
60
|
+
/**
|
|
61
|
+
* Fixes the y-axis range. Without it recharts scales to the data, which for
|
|
62
|
+
* a percentage series makes a 98-100%% band fill the plot and read as
|
|
63
|
+
* volatility.
|
|
64
|
+
*/
|
|
65
|
+
yDomain?: [number, number];
|
|
45
66
|
/** Formats values in tooltips and the y-axis, e.g. `(v) => \`$${v}\``. */
|
|
46
67
|
valueFormatter?: (value: number) => string;
|
|
47
68
|
className?: string;
|
|
@@ -121,6 +142,9 @@ export function Chart({
|
|
|
121
142
|
height = 300,
|
|
122
143
|
grid = true,
|
|
123
144
|
legend,
|
|
145
|
+
stacked = false,
|
|
146
|
+
xTickFormatter,
|
|
147
|
+
yDomain,
|
|
124
148
|
valueFormatter = (v) => String(v),
|
|
125
149
|
className,
|
|
126
150
|
}: ChartProps) {
|
|
@@ -170,11 +194,12 @@ export function Chart({
|
|
|
170
194
|
chart = (
|
|
171
195
|
<BarChart data={data} barCategoryGap="25%">
|
|
172
196
|
{gridEl}
|
|
173
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
174
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
197
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
198
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
175
199
|
{tooltip}
|
|
176
200
|
{series.map((s, i) => (
|
|
177
201
|
<Bar
|
|
202
|
+
stackId={stacked ? 'stack' : undefined}
|
|
178
203
|
key={s.key}
|
|
179
204
|
dataKey={s.key}
|
|
180
205
|
name={s.label ?? s.key}
|
|
@@ -189,8 +214,8 @@ export function Chart({
|
|
|
189
214
|
chart = (
|
|
190
215
|
<AreaChart data={data}>
|
|
191
216
|
{gridEl}
|
|
192
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
193
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
217
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
218
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
194
219
|
{tooltip}
|
|
195
220
|
{series.map((s, i) => (
|
|
196
221
|
<Area
|
|
@@ -198,10 +223,16 @@ export function Chart({
|
|
|
198
223
|
type="monotone"
|
|
199
224
|
dataKey={s.key}
|
|
200
225
|
name={s.label ?? s.key}
|
|
226
|
+
// One shared id is what makes recharts stack rather than
|
|
227
|
+
// overlay; undefined leaves the existing behaviour untouched.
|
|
228
|
+
stackId={stacked ? 'stack' : undefined}
|
|
201
229
|
stroke={seriesColor(s, i)}
|
|
202
230
|
strokeWidth={2}
|
|
203
231
|
fill={seriesColor(s, i)}
|
|
204
|
-
|
|
232
|
+
// Overlaid areas must stay translucent so the ones behind show
|
|
233
|
+
// through. Stacked bands never overlap, and at 0.12 they are too
|
|
234
|
+
// faint to tell apart.
|
|
235
|
+
fillOpacity={stacked ? 0.35 : 0.12}
|
|
205
236
|
dot={false}
|
|
206
237
|
activeDot={{ r: 4 }}
|
|
207
238
|
/>
|
|
@@ -212,8 +243,8 @@ export function Chart({
|
|
|
212
243
|
chart = (
|
|
213
244
|
<LineChart data={data}>
|
|
214
245
|
{gridEl}
|
|
215
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
216
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
246
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
247
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
217
248
|
{tooltip}
|
|
218
249
|
{series.map((s, i) => (
|
|
219
250
|
<Line
|
|
@@ -26,6 +26,23 @@ export interface DataTableProps<TData> {
|
|
|
26
26
|
onSelectionChange?: (rows: TData[]) => void;
|
|
27
27
|
/** Rows per page. Set to `0` to disable pagination entirely. @default 10 */
|
|
28
28
|
pageSize?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Hands pagination to the caller, for data the client does not hold all of.
|
|
31
|
+
*
|
|
32
|
+
* With this set, `data` is treated as the current page rather than the whole
|
|
33
|
+
* set: the table stops slicing, and the footer reports and drives the
|
|
34
|
+
* caller's page instead of its own. Without it nothing changes — a table
|
|
35
|
+
* given every row keeps paginating locally, which is right for the common
|
|
36
|
+
* case and avoids making every consumer implement paging to get a table.
|
|
37
|
+
*/
|
|
38
|
+
pagination?: {
|
|
39
|
+
pageIndex: number;
|
|
40
|
+
/** Total pages. Omit when unknown — the footer then relies on `hasNext`. */
|
|
41
|
+
pageCount?: number;
|
|
42
|
+
hasNext?: boolean;
|
|
43
|
+
hasPrevious?: boolean;
|
|
44
|
+
onPageChange: (pageIndex: number) => void;
|
|
45
|
+
};
|
|
29
46
|
/** Slot rendered top-right of the toolbar — e.g. an "Add" button. */
|
|
30
47
|
toolbar?: React.ReactNode;
|
|
31
48
|
onRowClick?: (row: TData) => void;
|
|
@@ -66,6 +83,7 @@ export function DataTable<TData>({
|
|
|
66
83
|
selectable = false,
|
|
67
84
|
onSelectionChange,
|
|
68
85
|
pageSize = 10,
|
|
86
|
+
pagination,
|
|
69
87
|
toolbar,
|
|
70
88
|
onRowClick,
|
|
71
89
|
emptyMessage = 'No results.',
|
|
@@ -94,8 +112,13 @@ export function DataTable<TData>({
|
|
|
94
112
|
},
|
|
95
113
|
getCoreRowModel: getCoreRowModel(),
|
|
96
114
|
getSortedRowModel: getSortedRowModel(),
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// Server-paginated tables must not slice: `data` is already the page, and
|
|
116
|
+
// running the client model over it would paginate a single page again.
|
|
117
|
+
...(pageSize > 0 && !pagination
|
|
118
|
+
? { getPaginationRowModel: getPaginationRowModel() }
|
|
119
|
+
: {}),
|
|
120
|
+
...(pagination ? { manualPagination: true } : {}),
|
|
121
|
+
initialState: pageSize > 0 && !pagination ? { pagination: { pageSize } } : undefined,
|
|
99
122
|
enableRowSelection: selectable,
|
|
100
123
|
});
|
|
101
124
|
|
|
@@ -107,6 +130,31 @@ export function DataTable<TData>({
|
|
|
107
130
|
|
|
108
131
|
const rows = table.getRowModel().rows;
|
|
109
132
|
|
|
133
|
+
// One set of derivations so the footer does not branch on `pagination`
|
|
134
|
+
// three separate times and drift between them.
|
|
135
|
+
const canPrevious = pagination
|
|
136
|
+
? (pagination.hasPrevious ?? pagination.pageIndex > 0)
|
|
137
|
+
: table.getCanPreviousPage();
|
|
138
|
+
const canNext = pagination
|
|
139
|
+
? (pagination.hasNext ??
|
|
140
|
+
(pagination.pageCount !== undefined && pagination.pageIndex < pagination.pageCount - 1))
|
|
141
|
+
: table.getCanNextPage();
|
|
142
|
+
const goPrevious = () =>
|
|
143
|
+
pagination ? pagination.onPageChange(pagination.pageIndex - 1) : table.previousPage();
|
|
144
|
+
const goNext = () =>
|
|
145
|
+
pagination ? pagination.onPageChange(pagination.pageIndex + 1) : table.nextPage();
|
|
146
|
+
// A server-paginated table shows its pager whenever paging is possible.
|
|
147
|
+
// Total pages are often unknown, so "more than one page" is not a question
|
|
148
|
+
// it can answer up front the way the local model can.
|
|
149
|
+
const showPager = pagination
|
|
150
|
+
? canPrevious || canNext
|
|
151
|
+
: pageSize > 0 && table.getPageCount() > 1;
|
|
152
|
+
const pageLabel = pagination
|
|
153
|
+
? pagination.pageCount !== undefined
|
|
154
|
+
? `Page ${pagination.pageIndex + 1} of ${pagination.pageCount}`
|
|
155
|
+
: `Page ${pagination.pageIndex + 1}`
|
|
156
|
+
: `Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`;
|
|
157
|
+
|
|
110
158
|
return (
|
|
111
159
|
<div className={cn('space-y-4', className)}>
|
|
112
160
|
{(searchKey || toolbar) && (
|
|
@@ -186,19 +234,19 @@ export function DataTable<TData>({
|
|
|
186
234
|
</Table>
|
|
187
235
|
</div>
|
|
188
236
|
|
|
189
|
-
{(selectable ||
|
|
237
|
+
{(selectable || showPager) && (
|
|
190
238
|
<div className="flex items-center justify-between">
|
|
191
239
|
<p className="text-sm text-muted-foreground">
|
|
192
240
|
{selectable && `${table.getFilteredSelectedRowModel().rows.length} of ${table.getFilteredRowModel().rows.length} selected`}
|
|
193
|
-
{selectable &&
|
|
194
|
-
{
|
|
241
|
+
{selectable && showPager && ' · '}
|
|
242
|
+
{showPager && pageLabel}
|
|
195
243
|
</p>
|
|
196
|
-
{
|
|
244
|
+
{showPager && (
|
|
197
245
|
<div className="flex gap-2">
|
|
198
|
-
<Button variant="outline" size="sm" onClick={
|
|
246
|
+
<Button variant="outline" size="sm" onClick={goPrevious} disabled={!canPrevious}>
|
|
199
247
|
<ChevronLeft className="size-4" /> Previous
|
|
200
248
|
</Button>
|
|
201
|
-
<Button variant="outline" size="sm" onClick={
|
|
249
|
+
<Button variant="outline" size="sm" onClick={goNext} disabled={!canNext}>
|
|
202
250
|
Next <ChevronRight className="size-4" />
|
|
203
251
|
</Button>
|
|
204
252
|
</div>
|
|
@@ -86,11 +86,42 @@ export function NotificationToast({
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export interface NotifyOptions extends Omit<NotificationToastProps, 'onDismiss'> {
|
|
89
|
+
/**
|
|
90
|
+
* Overrides the per-variant default below. Explicit always wins — a caller
|
|
91
|
+
* that has thought about the timing knows more than this heuristic does.
|
|
92
|
+
*/
|
|
89
93
|
duration?: number;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/**
|
|
97
|
+
* How long a toast stays, when the caller has not said.
|
|
98
|
+
*
|
|
99
|
+
* The Toaster's own default suits a confirmation: long enough to read "Saved",
|
|
100
|
+
* short enough not to linger. Two cases need longer, and both were previously
|
|
101
|
+
* left to whoever remembered to pass a number.
|
|
102
|
+
*
|
|
103
|
+
* A failure has to be *read* — often it is the only account of what went
|
|
104
|
+
* wrong, and a message that vanishes before it is understood is barely better
|
|
105
|
+
* than none. A toast carrying an action has to be read *and* decided on, and a
|
|
106
|
+
* button that disappears mid-reach is worse than no button, because the reader
|
|
107
|
+
* knows they missed something.
|
|
108
|
+
*
|
|
109
|
+
* Returning undefined defers to the Toaster, so a product that has set its own
|
|
110
|
+
* default keeps it rather than being silently overridden here.
|
|
111
|
+
*/
|
|
112
|
+
function defaultDuration(variant: NotificationToastProps['variant'], hasAction: boolean) {
|
|
113
|
+
if (variant === 'error') return 10_000;
|
|
114
|
+
if (variant === 'warning') return 8_000;
|
|
115
|
+
if (hasAction) return 8_000;
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
/** Fires a `NotificationToast` through sonner. Requires `<Toaster />` from `@olwiba/cn` mounted once in your app. */
|
|
93
120
|
export function notify(options: NotifyOptions) {
|
|
94
121
|
const { duration, ...toastProps } = options;
|
|
95
|
-
|
|
122
|
+
const resolved =
|
|
123
|
+
duration ?? defaultDuration(toastProps.variant, Boolean(toastProps.action || toastProps.secondaryAction));
|
|
124
|
+
return toast.custom((id) => <NotificationToast {...toastProps} onDismiss={() => toast.dismiss(id)} />, {
|
|
125
|
+
duration: resolved,
|
|
126
|
+
});
|
|
96
127
|
}
|
package/src/marketing/Footer.tsx
CHANGED
|
@@ -9,7 +9,13 @@ export interface FooterProps {
|
|
|
9
9
|
navLinks?: Array<{ label: string; href: string }>;
|
|
10
10
|
socialLinks?: Array<{ label: string; href: string; icon: ReactNode }>;
|
|
11
11
|
legal?: string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Live status pill. `label` is a ReactNode, not a string: a real status
|
|
14
|
+
* pill often carries per-service detail in a tooltip, and typing it as
|
|
15
|
+
* string forced consumers to smuggle that through `as unknown as string`.
|
|
16
|
+
* A plain string still works and is the common case.
|
|
17
|
+
*/
|
|
18
|
+
status?: { label: ReactNode; operational?: boolean };
|
|
13
19
|
renderLink?: AppShellRenderLink;
|
|
14
20
|
}
|
|
15
21
|
|