@terpjs/react-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/dataview/DataView.test.tsx +1 -1
- package/src/dataview/DataViewTable.tsx +4 -1
- package/src/dataview/DataViewToolbar.tsx +23 -12
- package/src/dataview/internal.tsx +4 -0
- package/src/styles.test.ts +9 -0
- package/src/styles.ts +33 -0
- package/src/tokens.guard.test.ts +10 -0
- package/src/ui/DatePicker.test.tsx +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/react-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Terp React stack core — typed @terpjs/contract client provider, auth session, capability gates, TanStack Router adapter, app shell, page archetypes, DataView and token-styled UI primitives. First frontend stack; see README.md for the component catalog.",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@tanstack/react-router": "^1.170.16",
|
|
16
|
-
"@terpjs/contract": "^0.
|
|
16
|
+
"@terpjs/contract": "^0.2.0"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -374,7 +374,7 @@ describe("DataView embedded variant", () => {
|
|
|
374
374
|
expect(screen.getByRole("searchbox")).toBeInTheDocument();
|
|
375
375
|
expect(screen.queryByText(/of 4 results/)).not.toBeInTheDocument();
|
|
376
376
|
expect(screen.queryByRole("button", { name: "Card view" })).not.toBeInTheDocument();
|
|
377
|
-
expect(screen.queryByRole("
|
|
377
|
+
expect(screen.queryByRole("button", { name: "Rows per page" })).not.toBeInTheDocument();
|
|
378
378
|
// All rows rendered — the parent owns paging.
|
|
379
379
|
expect(screen.getAllByRole("row")).toHaveLength(5);
|
|
380
380
|
});
|
|
@@ -235,7 +235,10 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
|
|
|
235
235
|
style={{
|
|
236
236
|
position: "absolute",
|
|
237
237
|
top: 0,
|
|
238
|
-
right
|
|
238
|
+
// Sit flush with the cell's right edge — a negative offset would
|
|
239
|
+
// let the last column's handle spill past the table and trip the
|
|
240
|
+
// scroll container's overflow-x, adding a spurious scrollbar.
|
|
241
|
+
right: 0,
|
|
239
242
|
width: 7,
|
|
240
243
|
height: "100%",
|
|
241
244
|
cursor: "col-resize",
|
|
@@ -2,10 +2,9 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { Button } from "../ui/Button";
|
|
4
4
|
import { Input } from "../ui/Input";
|
|
5
|
-
import { Select } from "../ui/Select";
|
|
6
5
|
import { DataViewColumnSettings } from "./DataViewColumnSettings";
|
|
7
6
|
import type { DataViewColumnSettingsProps } from "./DataViewColumnSettings";
|
|
8
|
-
import { CardsGlyph, CloseGlyph, EllipsisGlyph, SearchGlyph, TableGlyph } from "./glyphs";
|
|
7
|
+
import { CardsGlyph, ChevronDownGlyph, CloseGlyph, EllipsisGlyph, SearchGlyph, TableGlyph } from "./glyphs";
|
|
9
8
|
import { DataViewMenu, DataViewMenuItem, useDataViewText } from "./internal";
|
|
10
9
|
import { useViewSearch } from "./hooks/useViewSearch";
|
|
11
10
|
import type { DataViewBatchAction, DataViewSearchScope } from "./types";
|
|
@@ -204,17 +203,29 @@ export function DataViewToolbar<T>(props: DataViewToolbarProps<T>) {
|
|
|
204
203
|
)}
|
|
205
204
|
<span style={{ flex: 1 }} />
|
|
206
205
|
{props.onPageSizeChange !== undefined && props.pageSize !== undefined && (
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
<DataViewMenu
|
|
207
|
+
triggerLabel={resolve(strings.pageSize)}
|
|
208
|
+
trigger={
|
|
209
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-1)" }}>
|
|
210
|
+
{props.pageSize}
|
|
211
|
+
<ChevronDownGlyph size={14} />
|
|
212
|
+
</span>
|
|
213
|
+
}
|
|
211
214
|
>
|
|
212
|
-
{(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
{(close) =>
|
|
216
|
+
(props.pageSizeOptions ?? [10, 25, 50, 100]).map((option) => (
|
|
217
|
+
<DataViewMenuItem
|
|
218
|
+
key={option}
|
|
219
|
+
label={String(option)}
|
|
220
|
+
selected={option === props.pageSize}
|
|
221
|
+
onSelect={() => {
|
|
222
|
+
props.onPageSizeChange?.(option);
|
|
223
|
+
close();
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
))
|
|
227
|
+
}
|
|
228
|
+
</DataViewMenu>
|
|
218
229
|
)}
|
|
219
230
|
{props.onLayoutChange !== undefined && (
|
|
220
231
|
<span style={{ display: "inline-flex", gap: "var(--space-1)" }}>
|
|
@@ -75,12 +75,15 @@ export function DataViewMenuItem({
|
|
|
75
75
|
label,
|
|
76
76
|
destructive = false,
|
|
77
77
|
disabled = false,
|
|
78
|
+
selected,
|
|
78
79
|
icon,
|
|
79
80
|
onSelect,
|
|
80
81
|
}: {
|
|
81
82
|
label: string;
|
|
82
83
|
destructive?: boolean;
|
|
83
84
|
disabled?: boolean;
|
|
85
|
+
/** Marks one choice in a mutually exclusive menu (renders `menuitemradio`). */
|
|
86
|
+
selected?: boolean;
|
|
84
87
|
icon?: ReactNode;
|
|
85
88
|
onSelect: () => void;
|
|
86
89
|
}) {
|
|
@@ -88,6 +91,7 @@ export function DataViewMenuItem({
|
|
|
88
91
|
<MenuItem
|
|
89
92
|
label={label}
|
|
90
93
|
icon={icon}
|
|
94
|
+
selected={selected}
|
|
91
95
|
destructive={destructive}
|
|
92
96
|
disabled={disabled}
|
|
93
97
|
onSelect={onSelect}
|
package/src/styles.test.ts
CHANGED
|
@@ -18,4 +18,13 @@ describe("injectTerpStyles", () => {
|
|
|
18
18
|
expect(nodes[0]?.textContent ?? "").toContain('[data-terp="input"][type="number"]');
|
|
19
19
|
expect(nodes[0]?.textContent ?? "").toContain("::-webkit-inner-spin-button");
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
it("themes scrollbars against the token palette (thin, not the OS default)", () => {
|
|
23
|
+
injectTerpStyles();
|
|
24
|
+
const css = document.querySelector(`style#${TERP_STYLES_ID}`)?.textContent ?? "";
|
|
25
|
+
expect(css).toContain("scrollbar-width: thin");
|
|
26
|
+
expect(css).toContain("scrollbar-color: var(--color-neutral-300) transparent");
|
|
27
|
+
expect(css).toContain("::-webkit-scrollbar");
|
|
28
|
+
expect(css).toContain("::-webkit-scrollbar-thumb");
|
|
29
|
+
});
|
|
21
30
|
});
|
package/src/styles.ts
CHANGED
|
@@ -48,6 +48,39 @@ body {
|
|
|
48
48
|
box-sizing: border-box;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/* Themed scrollbars: the OS default chrome (thick, grey, light-only) ignores
|
|
52
|
+
the app theme and looks foreign against a token-styled surface — most
|
|
53
|
+
obvious on the horizontal overflow of a DataView table. These rules give
|
|
54
|
+
every scroll container a thin, token-coloured bar that tracks light/dark.
|
|
55
|
+
Firefox uses the inheritable scrollbar-* properties (set once on the root);
|
|
56
|
+
WebKit/Blink use the ::-webkit-scrollbar pseudo-elements (not inherited, so
|
|
57
|
+
they match every scrollable element globally). Paired with the color-scheme
|
|
58
|
+
declaration on the token roots so any native chrome we do not restyle here
|
|
59
|
+
(notably the native <select> option popup) also follows the theme. */
|
|
60
|
+
html {
|
|
61
|
+
scrollbar-width: thin;
|
|
62
|
+
scrollbar-color: var(--color-neutral-300) transparent;
|
|
63
|
+
}
|
|
64
|
+
::-webkit-scrollbar {
|
|
65
|
+
width: 10px;
|
|
66
|
+
height: 10px;
|
|
67
|
+
}
|
|
68
|
+
::-webkit-scrollbar-track {
|
|
69
|
+
background: transparent;
|
|
70
|
+
}
|
|
71
|
+
::-webkit-scrollbar-thumb {
|
|
72
|
+
background-color: var(--color-neutral-300);
|
|
73
|
+
border: 2px solid transparent;
|
|
74
|
+
background-clip: padding-box;
|
|
75
|
+
border-radius: var(--radius-full);
|
|
76
|
+
}
|
|
77
|
+
::-webkit-scrollbar-thumb:hover {
|
|
78
|
+
background-color: var(--color-neutral-400);
|
|
79
|
+
}
|
|
80
|
+
::-webkit-scrollbar-corner {
|
|
81
|
+
background: transparent;
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
/* Shared focus-visible ring: every interactive element that opts in via
|
|
52
85
|
[data-terp] shows a soft outline ring. !important lets the ring beat
|
|
53
86
|
inline base box-shadows (e.g. the primary button's resting shadow) so
|
package/src/tokens.guard.test.ts
CHANGED
|
@@ -48,4 +48,14 @@ describe("design tokens", () => {
|
|
|
48
48
|
}
|
|
49
49
|
expect(offenders).toEqual([]);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
it("opts native chrome into the palette via color-scheme (light + dark)", () => {
|
|
53
|
+
// Without `color-scheme`, native chrome the framework cannot restyle — the
|
|
54
|
+
// <select> option popup, and any scrollbar a browser draws natively — stays
|
|
55
|
+
// in OS-light rendering even under the dark theme. The light root declares
|
|
56
|
+
// it and both dark blocks (explicit [data-theme='dark'] and the
|
|
57
|
+
// prefers-color-scheme media query) flip it.
|
|
58
|
+
expect(tokensCss).toContain("color-scheme: light");
|
|
59
|
+
expect(tokensCss.match(/color-scheme: dark/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
|
|
60
|
+
});
|
|
51
61
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
|
-
import { LOCALE_NL, LocaleProvider } from "../locale";
|
|
5
|
+
import { LOCALE_EN, LOCALE_NL, LocaleProvider } from "../locale";
|
|
6
6
|
import { DatePicker, DateRangePicker } from "./DatePicker";
|
|
7
7
|
|
|
8
8
|
afterEach(cleanup);
|
|
@@ -10,7 +10,15 @@ afterEach(cleanup);
|
|
|
10
10
|
describe("DatePicker", () => {
|
|
11
11
|
it("selects a date and supports keyboard navigation", () => {
|
|
12
12
|
const onChange = vi.fn();
|
|
13
|
-
|
|
13
|
+
// Pinned to `en`: without a LocaleProvider the picker formats with
|
|
14
|
+
// `Intl.DateTimeFormat(undefined, …)`, i.e. whatever locale the machine
|
|
15
|
+
// running the suite happens to use — the month name below would then be
|
|
16
|
+
// English only on an English host.
|
|
17
|
+
render(
|
|
18
|
+
<LocaleProvider locales={{ en: LOCALE_EN }}>
|
|
19
|
+
<DatePicker aria-label="Due date" defaultValue={new Date(2026, 6, 7)} onChange={onChange} />
|
|
20
|
+
</LocaleProvider>,
|
|
21
|
+
);
|
|
14
22
|
fireEvent.click(screen.getByRole("button", { name: "Due date" }));
|
|
15
23
|
expect(screen.getByRole("grid", { name: /July 2026/ })).toBeInTheDocument();
|
|
16
24
|
fireEvent.keyDown(screen.getByRole("grid"), { key: "ArrowRight" });
|