@terpjs/react-core 0.6.1 → 0.8.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/README.md +12 -2
- package/package.json +2 -2
- package/src/AppShell.test.tsx +33 -12
- package/src/AppShell.tsx +69 -249
- package/src/Breadcrumbs.test.tsx +24 -0
- package/src/Breadcrumbs.tsx +9 -32
- package/src/ConfirmDialog.tsx +13 -44
- package/src/EmptyState.tsx +8 -36
- package/src/ErrorState.tsx +8 -36
- package/src/Field.test.tsx +57 -0
- package/src/Field.tsx +46 -22
- package/src/HubPage.test.tsx +22 -13
- package/src/HubPage.tsx +25 -97
- package/src/LoadingState.tsx +3 -24
- package/src/ModuleNav.tsx +1 -1
- package/src/PageActions.tsx +5 -10
- package/src/UserMenu.test.tsx +12 -5
- package/src/UserMenu.tsx +33 -62
- package/src/dataview/DataView.test.tsx +109 -5
- package/src/dataview/DataView.tsx +41 -23
- package/src/dataview/DataViewCardList.tsx +14 -60
- package/src/dataview/DataViewColumnSettings.tsx +46 -51
- package/src/dataview/DataViewExpandableRow.tsx +2 -17
- package/src/dataview/DataViewPagination.tsx +2 -32
- package/src/dataview/DataViewRowActions.tsx +13 -33
- package/src/dataview/DataViewTable.tsx +16 -103
- package/src/dataview/DataViewToolbar.tsx +53 -76
- package/src/dataview/README.md +6 -0
- package/src/dataview/index.ts +1 -0
- package/src/dataview/internal.tsx +4 -1
- package/src/dataview/types.ts +13 -0
- package/src/feedback.test.tsx +26 -0
- package/src/files.test.tsx +18 -0
- package/src/files.tsx +13 -4
- package/src/icons.test.tsx +10 -6
- package/src/icons.tsx +33 -37
- package/src/index.ts +0 -3
- package/src/layout.test.tsx +24 -9
- package/src/layout.tsx +24 -21
- package/src/layoutContract.test.tsx +95 -0
- package/src/locale.tsx +27 -4
- package/src/markers.test.ts +468 -0
- package/src/raw.d.ts +15 -1
- package/src/router.tsx +6 -9
- package/src/ssr.test.tsx +1 -3
- package/src/styles.test.ts +823 -6
- package/src/styles.ts +2699 -153
- package/src/theme.test.tsx +39 -0
- package/src/theme.themes.test.ts +124 -0
- package/src/theme.tsx +62 -14
- package/src/toast.tsx +35 -71
- package/src/tokens.guard.test.ts +3 -12
- package/src/ui/Alert.test.tsx +12 -0
- package/src/ui/Alert.tsx +15 -43
- package/src/ui/Badge.test.tsx +14 -3
- package/src/ui/Badge.tsx +13 -25
- package/src/ui/Button.test.tsx +17 -4
- package/src/ui/Button.tsx +10 -63
- package/src/ui/Card.test.tsx +6 -2
- package/src/ui/Card.tsx +11 -39
- package/src/ui/Checkbox.tsx +2 -19
- package/src/ui/Combobox.test.tsx +22 -0
- package/src/ui/Combobox.tsx +31 -80
- package/src/ui/DatePicker.test.tsx +131 -4
- package/src/ui/DatePicker.tsx +158 -106
- package/src/ui/Input.tsx +6 -19
- package/src/ui/Markdown.test.tsx +26 -0
- package/src/ui/Markdown.tsx +28 -2
- package/src/ui/Menu.test.tsx +38 -4
- package/src/ui/Menu.tsx +50 -52
- package/src/ui/Popover.tsx +53 -19
- package/src/ui/Radio.tsx +5 -30
- package/src/ui/Select.tsx +7 -30
- package/src/ui/Switch.tsx +2 -20
- package/src/ui/Tabs.tsx +4 -28
- package/src/ui/Textarea.tsx +6 -17
- package/src/ui/Tooltip.tsx +9 -21
- package/src/uiText.tsx +9 -0
- package/src/ui/controlStyles.ts +0 -9
package/src/icons.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { injectTerpStyles } from "./styles";
|
|
4
|
+
|
|
5
|
+
injectTerpStyles();
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* The dependency-free icon layer: a small set of inline SVG glyphs the shell's
|
|
@@ -77,6 +81,30 @@ export const ICON_GLYPHS: Record<string, ReactNode> = {
|
|
|
77
81
|
<path d="M8 21h8M12 17v4" />
|
|
78
82
|
</svg>
|
|
79
83
|
),
|
|
84
|
+
// The three glyphs below exist for the named themes. `moon` and `sun` were already
|
|
85
|
+
// taken by dark and light, and a theme picker where two entries share a glyph is a
|
|
86
|
+
// picker whose icons carry no information.
|
|
87
|
+
"moon-stars": (
|
|
88
|
+
<svg {...svgProps}>
|
|
89
|
+
<path d="M20.5 15.6A8 8 0 0 1 9.4 4.5 8.4 8.4 0 1 0 20.5 15.6Z" />
|
|
90
|
+
<path d="M17 3v3M15.5 4.5h3M20.5 8v2M19.5 9h2" />
|
|
91
|
+
</svg>
|
|
92
|
+
),
|
|
93
|
+
sunset: (
|
|
94
|
+
<svg {...svgProps}>
|
|
95
|
+
<path d="M12 4v2M5.6 7.6l1.4 1.4M17 9l1.4-1.4M3 15h1.5M19.5 15H21" />
|
|
96
|
+
<path d="M8 15a4 4 0 0 1 8 0" />
|
|
97
|
+
<path d="M3 19h18" />
|
|
98
|
+
</svg>
|
|
99
|
+
),
|
|
100
|
+
contrast: (
|
|
101
|
+
<svg {...svgProps}>
|
|
102
|
+
<circle cx="12" cy="12" r="9" />
|
|
103
|
+
{/* Filled half, so the glyph reads as contrast rather than as a plain circle.
|
|
104
|
+
Stroked shapes elsewhere use `currentColor`; so does this fill. */}
|
|
105
|
+
<path d="M12 3a9 9 0 0 0 0 18Z" fill="currentColor" stroke="none" />
|
|
106
|
+
</svg>
|
|
107
|
+
),
|
|
80
108
|
document: (
|
|
81
109
|
<svg {...svgProps}>
|
|
82
110
|
<path d="M6 3h8l4 4v14H6Z" />
|
|
@@ -407,31 +435,6 @@ export const ICON_GLYPHS: Record<string, ReactNode> = {
|
|
|
407
435
|
),
|
|
408
436
|
};
|
|
409
437
|
|
|
410
|
-
const navIconStyle: CSSProperties = {
|
|
411
|
-
display: "inline-flex",
|
|
412
|
-
alignItems: "center",
|
|
413
|
-
justifyContent: "center",
|
|
414
|
-
width: "1.25rem",
|
|
415
|
-
height: "1.25rem",
|
|
416
|
-
flex: "0 0 1.25rem",
|
|
417
|
-
fontSize: "1rem",
|
|
418
|
-
lineHeight: 1,
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const fallbackStyle: CSSProperties = {
|
|
422
|
-
display: "inline-flex",
|
|
423
|
-
alignItems: "center",
|
|
424
|
-
justifyContent: "center",
|
|
425
|
-
width: "100%",
|
|
426
|
-
height: "100%",
|
|
427
|
-
borderRadius: "var(--radius-sm, 4px)",
|
|
428
|
-
background: "var(--color-brand-primary-soft, var(--color-neutral-200))",
|
|
429
|
-
color: "var(--color-brand-primary, var(--color-neutral-700))",
|
|
430
|
-
fontSize: "0.7em",
|
|
431
|
-
fontWeight: "var(--font-weight-medium)" as CSSProperties["fontWeight"],
|
|
432
|
-
lineHeight: 1,
|
|
433
|
-
};
|
|
434
|
-
|
|
435
438
|
export interface NavIconProps {
|
|
436
439
|
/** Glyph name (a `NavItem.icon` value); unknown / missing falls back to the initial. */
|
|
437
440
|
name?: string;
|
|
@@ -446,9 +449,9 @@ export interface NavIconProps {
|
|
|
446
449
|
export function NavIcon({ name, label }: NavIconProps) {
|
|
447
450
|
const glyph = name !== undefined ? ICON_GLYPHS[name] : undefined;
|
|
448
451
|
return (
|
|
449
|
-
<span aria-hidden="true" data-terp="nav-icon"
|
|
452
|
+
<span aria-hidden="true" data-terp="nav-icon">
|
|
450
453
|
{glyph ?? (
|
|
451
|
-
<span
|
|
454
|
+
<span data-terp="nav-icon-fallback">
|
|
452
455
|
{(label[0] ?? "?").toUpperCase()}
|
|
453
456
|
</span>
|
|
454
457
|
)}
|
|
@@ -487,15 +490,8 @@ export function Icon({ name, size = "1em", title }: IconProps) {
|
|
|
487
490
|
aria-hidden={labelled ? undefined : true}
|
|
488
491
|
role={labelled ? "img" : undefined}
|
|
489
492
|
aria-label={labelled ? title : undefined}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
alignItems: "center",
|
|
493
|
-
justifyContent: "center",
|
|
494
|
-
width: px,
|
|
495
|
-
height: px,
|
|
496
|
-
lineHeight: 1,
|
|
497
|
-
color: "inherit",
|
|
498
|
-
}}
|
|
493
|
+
data-terp="icon"
|
|
494
|
+
style={{ width: px, height: px }}
|
|
499
495
|
>
|
|
500
496
|
{glyph}
|
|
501
497
|
</span>
|
package/src/index.ts
CHANGED
package/src/layout.test.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { DetailList, Stack } from "./layout";
|
|
|
7
7
|
afterEach(cleanup);
|
|
8
8
|
|
|
9
9
|
describe("Stack", () => {
|
|
10
|
-
it("
|
|
10
|
+
it("names a column at the default gap, with no inline styling", () => {
|
|
11
11
|
render(
|
|
12
12
|
<Stack data-testid="stack">
|
|
13
13
|
<span>a</span>
|
|
@@ -16,24 +16,39 @@ describe("Stack", () => {
|
|
|
16
16
|
);
|
|
17
17
|
const el = screen.getByTestId("stack");
|
|
18
18
|
expect(el.tagName).toBe("DIV");
|
|
19
|
-
expect(el
|
|
20
|
-
expect(el
|
|
21
|
-
expect(el.
|
|
19
|
+
expect(el).toHaveAttribute("data-direction", "column");
|
|
20
|
+
expect(el).toHaveAttribute("data-gap", "2");
|
|
21
|
+
expect(el).not.toHaveAttribute("data-wrap");
|
|
22
|
+
expect(el.getAttribute("style")).toBeNull();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
it("renders the requested element
|
|
25
|
+
it("renders the requested element and names direction, gap and wrap", () => {
|
|
25
26
|
render(
|
|
26
|
-
<Stack data-testid="row" as="section" direction="row" gap={4}
|
|
27
|
+
<Stack data-testid="row" as="section" direction="row" gap={4} wrap>
|
|
27
28
|
<span>a</span>
|
|
28
29
|
</Stack>,
|
|
29
30
|
);
|
|
30
31
|
const el = screen.getByTestId("row");
|
|
31
32
|
expect(el.tagName).toBe("SECTION");
|
|
32
|
-
expect(el
|
|
33
|
-
expect(el
|
|
33
|
+
expect(el).toHaveAttribute("data-direction", "row");
|
|
34
|
+
expect(el).toHaveAttribute("data-gap", "4");
|
|
35
|
+
expect(el).toHaveAttribute("data-wrap", "true");
|
|
36
|
+
expect(el.getAttribute("style")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps alignment inline, because it is an open set of CSS keywords", () => {
|
|
40
|
+
// align/justify accept any alignment keyword, so they cannot become a rule per value
|
|
41
|
+
// without inventing a vocabulary CSS already has (ADR 0094). They stay inline, and
|
|
42
|
+
// the geometry attributes stay attributes — the two halves of the same element.
|
|
43
|
+
render(
|
|
44
|
+
<Stack data-testid="aligned" align="center" justify="space-between">
|
|
45
|
+
<span>a</span>
|
|
46
|
+
</Stack>,
|
|
47
|
+
);
|
|
48
|
+
const el = screen.getByTestId("aligned");
|
|
34
49
|
expect(el.style.alignItems).toBe("center");
|
|
35
50
|
expect(el.style.justifyContent).toBe("space-between");
|
|
36
|
-
expect(el.style.
|
|
51
|
+
expect(el.style.display).toBe("");
|
|
37
52
|
});
|
|
38
53
|
|
|
39
54
|
it("works as a form (submit handler fires)", () => {
|
package/src/layout.tsx
CHANGED
|
@@ -38,16 +38,27 @@ export function Stack({
|
|
|
38
38
|
wrap = false,
|
|
39
39
|
...rest
|
|
40
40
|
}: StackProps) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// `direction`, `gap` and `wrap` are closed sets, so they are attributes the sheet keys
|
|
42
|
+
// on. `align` and `justify` take any alignment keyword CSS accepts, so they stay inline
|
|
43
|
+
// rather than turning an open vocabulary into a rule per value (ADR 0094). Undefined on
|
|
44
|
+
// both means no style attribute at all.
|
|
45
|
+
const alignment: CSSProperties | undefined =
|
|
46
|
+
align === undefined && justify === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: {
|
|
49
|
+
...(align !== undefined ? { alignItems: align } : undefined),
|
|
50
|
+
...(justify !== undefined ? { justifyContent: justify } : undefined),
|
|
51
|
+
};
|
|
52
|
+
return (
|
|
53
|
+
<Component
|
|
54
|
+
{...rest}
|
|
55
|
+
data-terp="stack"
|
|
56
|
+
data-direction={direction}
|
|
57
|
+
data-gap={String(gap)}
|
|
58
|
+
data-wrap={wrap ? "true" : undefined}
|
|
59
|
+
style={alignment}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export interface DetailItem {
|
|
@@ -62,14 +73,6 @@ export interface DetailListProps extends Omit<HTMLAttributes<HTMLDListElement>,
|
|
|
62
73
|
items: readonly DetailItem[];
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
const detailListStyle: CSSProperties = {
|
|
66
|
-
margin: 0,
|
|
67
|
-
display: "grid",
|
|
68
|
-
gap: "var(--space-1)",
|
|
69
|
-
};
|
|
70
|
-
const detailTermStyle: CSSProperties = { display: "inline", fontWeight: "var(--font-weight-medium)" };
|
|
71
|
-
const detailValueStyle: CSSProperties = { display: "inline", margin: 0 };
|
|
72
|
-
|
|
73
76
|
/**
|
|
74
77
|
* Token-styled label/value pairs as a semantic `<dl>` — record metadata on a detail page,
|
|
75
78
|
* an expanded row's summary. Centralizes the "Label: value" pattern so modules never
|
|
@@ -78,11 +81,11 @@ const detailValueStyle: CSSProperties = { display: "inline", margin: 0 };
|
|
|
78
81
|
export function DetailList({ items, ...rest }: DetailListProps) {
|
|
79
82
|
const text = useUiText();
|
|
80
83
|
return (
|
|
81
|
-
<dl {...rest} data-terp="detail-list"
|
|
84
|
+
<dl {...rest} data-terp="detail-list">
|
|
82
85
|
{items.map((item, index) => (
|
|
83
86
|
<div key={index}>
|
|
84
|
-
<dt
|
|
85
|
-
<dd
|
|
87
|
+
<dt data-terp="detail-list-term">{text(item.label)}: </dt>
|
|
88
|
+
<dd data-terp="detail-list-value">{item.value}</dd>
|
|
86
89
|
</div>
|
|
87
90
|
))}
|
|
88
91
|
</dl>
|
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
import { OverviewPage } from "./OverviewPage";
|
|
22
22
|
import { Page } from "./Page";
|
|
23
23
|
import { DetailList, Stack } from "./layout";
|
|
24
|
+
import { PageActions } from "./PageActions";
|
|
25
|
+
import { ThemeProvider, ThemeToggle } from "./theme";
|
|
26
|
+
import { Button } from "./ui/Button";
|
|
24
27
|
import { Card } from "./ui/Card";
|
|
25
28
|
|
|
26
29
|
afterEach(cleanup);
|
|
@@ -179,3 +182,95 @@ describe("runtime slot enforcement", () => {
|
|
|
179
182
|
expect(verifySlotChildren("standard", "Page", [])).toBeNull();
|
|
180
183
|
});
|
|
181
184
|
});
|
|
185
|
+
|
|
186
|
+
// The styling migration (ADR 0094) is renaming rendered roots: components that used to emit
|
|
187
|
+
// an unmarked <div>, or to borrow Popover's `popover`, now name themselves. Every one of those
|
|
188
|
+
// names is read by this check, because the check IS the marker join — so the question is not
|
|
189
|
+
// whether the names look sensible but whether any slot's verdict moved.
|
|
190
|
+
//
|
|
191
|
+
// It did not, and the reason is worth stating rather than re-derived: verifySlotChildren
|
|
192
|
+
// refuses a direct body-slot child whose data-terp is not in the slot's allow table, and a
|
|
193
|
+
// missing attribute is refused too (`marker === null`). None of the new names is in any table,
|
|
194
|
+
// and neither was the unmarked element each replaced. So each of these was refused before the
|
|
195
|
+
// migration and is refused after it. What changed is the MESSAGE — describeElement now reports
|
|
196
|
+
// a named component instead of a bare tag, which is strictly more useful to the person reading
|
|
197
|
+
// the refusal, and is also a string a test could have pinned.
|
|
198
|
+
describe("layout contract survives the roots the styling migration renames", () => {
|
|
199
|
+
it("still refuses a chrome menu in a governed body, and now names it", async () => {
|
|
200
|
+
underContract(
|
|
201
|
+
// The provider renders no element of its own, so the slot's direct child is still the
|
|
202
|
+
// toggle. Without it ThemeToggle returns null and the body is empty — which passes for
|
|
203
|
+
// no reason at all.
|
|
204
|
+
<ThemeProvider>
|
|
205
|
+
<OverviewPage title="Records">
|
|
206
|
+
<ThemeToggle variant="stacked" />
|
|
207
|
+
</OverviewPage>
|
|
208
|
+
</ThemeProvider>,
|
|
209
|
+
);
|
|
210
|
+
await waitFor(() => {
|
|
211
|
+
expect(screen.getByTestId("refused").textContent).toBe(
|
|
212
|
+
slotViolationMessage("standard", "OverviewPage", '<div data-terp="theme-toggle">'),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("still refuses an action cluster in a governed body, and now names it", async () => {
|
|
218
|
+
underContract(
|
|
219
|
+
<DetailPage title="Record 1" parents={[{ label: "Records", to: "/records" }]}>
|
|
220
|
+
<PageActions primary={<Button>Publish</Button>} />
|
|
221
|
+
</DetailPage>,
|
|
222
|
+
);
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(screen.getByTestId("refused").textContent).toBe(
|
|
225
|
+
slotViolationMessage("standard", "DetailPage", '<div data-terp="page-actions">'),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("keeps every marker the allow tables name out of the migration's way", () => {
|
|
231
|
+
// The tables are the audit surface: a rename of one of THESE would widen or close a slot
|
|
232
|
+
// silently, so they are the markers the migration may not touch without changing both
|
|
233
|
+
// halves of the contract. Asserted as a set so an addition has to be deliberate.
|
|
234
|
+
const named = new Set(
|
|
235
|
+
Object.values(LAYOUT_CONTRACTS.standard!.slots).flatMap((slot) =>
|
|
236
|
+
Object.values(slot.components),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
expect([...named].sort()).toEqual([
|
|
240
|
+
"alert",
|
|
241
|
+
"card",
|
|
242
|
+
"dataview",
|
|
243
|
+
"detail-list",
|
|
244
|
+
"dialog",
|
|
245
|
+
"empty-state",
|
|
246
|
+
"error-state",
|
|
247
|
+
"hubcard",
|
|
248
|
+
"loading-state",
|
|
249
|
+
"module-nav",
|
|
250
|
+
"resource-list",
|
|
251
|
+
"stack",
|
|
252
|
+
"tabs",
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("passes a governed body whose children are still allowed components", async () => {
|
|
257
|
+
// The other direction: the migration must not have widened anything either. A Stack and a
|
|
258
|
+
// Card are allowed, and they stay allowed with their markers unchanged.
|
|
259
|
+
underContract(
|
|
260
|
+
<ThemeProvider>
|
|
261
|
+
<DetailPage title="Record 1" parents={[{ label: "Records", to: "/records" }]}>
|
|
262
|
+
<Card title="A section">body</Card>
|
|
263
|
+
<Stack>
|
|
264
|
+
<ThemeToggle variant="stacked" />
|
|
265
|
+
</Stack>
|
|
266
|
+
</DetailPage>
|
|
267
|
+
</ThemeProvider>,
|
|
268
|
+
);
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
270
|
+
// And the toggle really did render, or this asserts nothing.
|
|
271
|
+
expect(document.querySelector('[data-terp="theme-toggle"]')).not.toBeNull();
|
|
272
|
+
// Nested inside an allowed container is sanctioned composition — the check reads direct
|
|
273
|
+
// children only, which is what makes a marked root safe to put anywhere below one.
|
|
274
|
+
expect(screen.queryByTestId("refused")).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/locale.tsx
CHANGED
|
@@ -67,6 +67,9 @@ export const LOCALE_NL: LocaleCatalog = {
|
|
|
67
67
|
theme: "Thema",
|
|
68
68
|
themeLight: "Licht",
|
|
69
69
|
themeDark: "Donker",
|
|
70
|
+
themeMidnight: "Middernacht",
|
|
71
|
+
themeTwilight: "Schemering",
|
|
72
|
+
themeContrast: "Hoog contrast",
|
|
70
73
|
themeSystem: "Systeem",
|
|
71
74
|
language: "Taal",
|
|
72
75
|
admin: "Beheer",
|
|
@@ -215,8 +218,28 @@ export function LanguageSwitcher({ variant = "stacked" }: LanguageSwitcherProps)
|
|
|
215
218
|
if (context === null || context.locales.length < 2) {
|
|
216
219
|
return null;
|
|
217
220
|
}
|
|
221
|
+
// Hoisted out of the attribute rather than inlined as `variant === "inline"`, and not for
|
|
222
|
+
// readability: the marker inventory scanner reads every string literal inside a
|
|
223
|
+
// `data-terp={…}` expression as a marker, so the comparison's own "inline" was picked up as
|
|
224
|
+
// a component marker that nothing styles. Keep marker expressions to marker literals.
|
|
225
|
+
const isInline = variant === "inline";
|
|
218
226
|
const menu = (
|
|
219
|
-
<Menu
|
|
227
|
+
<Menu
|
|
228
|
+
// Claim the root ONLY when this Menu is the root, which is the inline variant. The
|
|
229
|
+
// stacked variant renders its own div and puts the menu inside it, so stamping the
|
|
230
|
+
// same marker unconditionally put it on BOTH elements — and the inner wrapper then
|
|
231
|
+
// matched the stacked grid rule instead of the popover wrapper's geometry. The
|
|
232
|
+
// baselines did not catch that: a one-child grid and a one-child inline-flex box
|
|
233
|
+
// shrink-wrap to the same pixels, so it was wrong and invisible at the same time.
|
|
234
|
+
data-terp={isInline ? "language-switcher" : undefined}
|
|
235
|
+
data-variant={isInline ? "inline" : undefined}
|
|
236
|
+
// Unconditional, unlike the root marker: the panel is the same panel in both variants, so
|
|
237
|
+
// a rule for it must reach both. Deriving the owner from the conditional root marker made
|
|
238
|
+
// this panel "language-switcher" when inline and "popover" when stacked.
|
|
239
|
+
data-owner="language-switcher"
|
|
240
|
+
trigger={<Icon name="globe" size="1.15rem" />}
|
|
241
|
+
triggerLabel={strings.language}
|
|
242
|
+
>
|
|
220
243
|
{({ close }) => (
|
|
221
244
|
<>
|
|
222
245
|
{context.locales.map((code) => (
|
|
@@ -234,12 +257,12 @@ export function LanguageSwitcher({ variant = "stacked" }: LanguageSwitcherProps)
|
|
|
234
257
|
)}
|
|
235
258
|
</Menu>
|
|
236
259
|
);
|
|
237
|
-
if (
|
|
260
|
+
if (isInline) {
|
|
238
261
|
return menu;
|
|
239
262
|
}
|
|
240
263
|
return (
|
|
241
|
-
<div
|
|
242
|
-
<span
|
|
264
|
+
<div data-terp="language-switcher" data-variant="stacked">
|
|
265
|
+
<span data-terp="language-switcher-label">{strings.language}</span>
|
|
243
266
|
{menu}
|
|
244
267
|
</div>
|
|
245
268
|
);
|