@multiplatform.one/components 7.81.1 → 7.82.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/dist/cjs/charts/BarChart.cjs +1 -0
- package/dist/cjs/charts/BarChart.native.js +1 -0
- package/dist/cjs/charts/BarChart.native.js.map +1 -1
- package/dist/cjs/charts/ChartDataView.cjs +89 -9
- package/dist/cjs/charts/ChartDataView.native.js +94 -9
- package/dist/cjs/charts/ChartDataView.native.js.map +1 -1
- package/dist/cjs/charts/ChartSurface.cjs +26 -8
- package/dist/cjs/charts/ChartSurface.native.js +27 -9
- package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
- package/dist/cjs/charts/chartHeight.cjs +1 -1
- package/dist/cjs/charts/chartHeight.native.js +1 -1
- package/dist/cjs/charts/chartHeight.native.js.map +1 -1
- package/dist/cjs/charts/composeChart.cjs +1 -1
- package/dist/cjs/charts/composeChart.native.js +1 -1
- package/dist/cjs/charts/composeChart.native.js.map +1 -1
- package/dist/cjs/code/CodeBlock/index.cjs +38 -31
- package/dist/cjs/code/CodeBlock/index.native.js +38 -31
- package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
- package/dist/cjs/code/LogView.cjs +142 -0
- package/dist/cjs/code/LogView.native.js +151 -0
- package/dist/cjs/code/LogView.native.js.map +1 -0
- package/dist/cjs/code/index.cjs +2 -1
- package/dist/cjs/code/index.native.js +1 -0
- package/dist/cjs/code/index.native.js.map +1 -1
- package/dist/cjs/layouts/EmptyState.cjs +9 -43
- package/dist/cjs/layouts/EmptyState.native.js +9 -45
- package/dist/cjs/layouts/EmptyState.native.js.map +1 -1
- package/dist/cjs/layouts/PageTemplates.cjs +61 -0
- package/dist/cjs/layouts/PageTemplates.native.js +62 -0
- package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
- package/dist/cjs/layouts/barTitleSpan.cjs +54 -0
- package/dist/cjs/layouts/barTitleSpan.native.js +78 -0
- package/dist/cjs/layouts/barTitleSpan.native.js.map +1 -0
- package/dist/cjs/layouts/page.cjs +130 -41
- package/dist/cjs/layouts/page.native.js +143 -45
- package/dist/cjs/layouts/page.native.js.map +1 -1
- package/dist/cjs/views/ImageGrid.cjs +9 -0
- package/dist/cjs/views/ImageGrid.native.js +12 -1
- package/dist/cjs/views/ImageGrid.native.js.map +1 -1
- package/dist/esm/charts/BarChart.mjs +1 -0
- package/dist/esm/charts/BarChart.mjs.map +1 -1
- package/dist/esm/charts/BarChart.native.js +1 -0
- package/dist/esm/charts/BarChart.native.js.map +1 -1
- package/dist/esm/charts/ChartDataView.mjs +83 -3
- package/dist/esm/charts/ChartDataView.mjs.map +1 -1
- package/dist/esm/charts/ChartDataView.native.js +88 -3
- package/dist/esm/charts/ChartDataView.native.js.map +1 -1
- package/dist/esm/charts/ChartSurface.mjs +26 -8
- package/dist/esm/charts/ChartSurface.mjs.map +1 -1
- package/dist/esm/charts/ChartSurface.native.js +27 -9
- package/dist/esm/charts/ChartSurface.native.js.map +1 -1
- package/dist/esm/charts/chartHeight.mjs +1 -1
- package/dist/esm/charts/chartHeight.mjs.map +1 -1
- package/dist/esm/charts/chartHeight.native.js +1 -1
- package/dist/esm/charts/chartHeight.native.js.map +1 -1
- package/dist/esm/charts/composeChart.mjs +1 -1
- package/dist/esm/charts/composeChart.mjs.map +1 -1
- package/dist/esm/charts/composeChart.native.js +1 -1
- package/dist/esm/charts/composeChart.native.js.map +1 -1
- package/dist/esm/code/CodeBlock/index.mjs +39 -32
- package/dist/esm/code/CodeBlock/index.mjs.map +1 -1
- package/dist/esm/code/CodeBlock/index.native.js +39 -32
- package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
- package/dist/esm/code/LogView.mjs +117 -0
- package/dist/esm/code/LogView.mjs.map +1 -0
- package/dist/esm/code/LogView.native.js +123 -0
- package/dist/esm/code/LogView.native.js.map +1 -0
- package/dist/esm/code/index.mjs +1 -0
- package/dist/esm/code/index.mjs.map +1 -1
- package/dist/esm/code/index.native.js +1 -0
- package/dist/esm/code/index.native.js.map +1 -1
- package/dist/esm/layouts/EmptyState.mjs +9 -43
- package/dist/esm/layouts/EmptyState.mjs.map +1 -1
- package/dist/esm/layouts/EmptyState.native.js +9 -45
- package/dist/esm/layouts/EmptyState.native.js.map +1 -1
- package/dist/esm/layouts/PageTemplates.mjs +61 -1
- package/dist/esm/layouts/PageTemplates.mjs.map +1 -1
- package/dist/esm/layouts/PageTemplates.native.js +62 -1
- package/dist/esm/layouts/PageTemplates.native.js.map +1 -1
- package/dist/esm/layouts/barTitleSpan.mjs +27 -0
- package/dist/esm/layouts/barTitleSpan.mjs.map +1 -0
- package/dist/esm/layouts/barTitleSpan.native.js +48 -0
- package/dist/esm/layouts/barTitleSpan.native.js.map +1 -0
- package/dist/esm/layouts/page.mjs +132 -43
- package/dist/esm/layouts/page.mjs.map +1 -1
- package/dist/esm/layouts/page.native.js +145 -47
- package/dist/esm/layouts/page.native.js.map +1 -1
- package/dist/esm/views/ImageGrid.mjs +10 -1
- package/dist/esm/views/ImageGrid.mjs.map +1 -1
- package/dist/esm/views/ImageGrid.native.js +13 -2
- package/dist/esm/views/ImageGrid.native.js.map +1 -1
- package/dist/jsx/charts/BarChart.mjs +1 -0
- package/dist/jsx/charts/BarChart.mjs.map +1 -1
- package/dist/jsx/charts/BarChart.native.js +1 -0
- package/dist/jsx/charts/BarChart.native.js.map +1 -1
- package/dist/jsx/charts/ChartDataView.mjs +83 -3
- package/dist/jsx/charts/ChartDataView.mjs.map +1 -1
- package/dist/jsx/charts/ChartDataView.native.js +94 -9
- package/dist/jsx/charts/ChartDataView.native.js.map +1 -1
- package/dist/jsx/charts/ChartSurface.mjs +26 -8
- package/dist/jsx/charts/ChartSurface.mjs.map +1 -1
- package/dist/jsx/charts/ChartSurface.native.js +27 -9
- package/dist/jsx/charts/ChartSurface.native.js.map +1 -1
- package/dist/jsx/charts/chartHeight.mjs +1 -1
- package/dist/jsx/charts/chartHeight.mjs.map +1 -1
- package/dist/jsx/charts/chartHeight.native.js +1 -1
- package/dist/jsx/charts/chartHeight.native.js.map +1 -1
- package/dist/jsx/charts/composeChart.mjs +1 -1
- package/dist/jsx/charts/composeChart.mjs.map +1 -1
- package/dist/jsx/charts/composeChart.native.js +1 -1
- package/dist/jsx/charts/composeChart.native.js.map +1 -1
- package/dist/jsx/code/CodeBlock/index.mjs +39 -32
- package/dist/jsx/code/CodeBlock/index.mjs.map +1 -1
- package/dist/jsx/code/CodeBlock/index.native.js +38 -31
- package/dist/jsx/code/CodeBlock/index.native.js.map +1 -1
- package/dist/jsx/code/LogView.mjs +117 -0
- package/dist/jsx/code/LogView.mjs.map +1 -0
- package/dist/jsx/code/LogView.native.js +151 -0
- package/dist/jsx/code/LogView.native.js.map +1 -0
- package/dist/jsx/code/index.mjs +1 -0
- package/dist/jsx/code/index.mjs.map +1 -1
- package/dist/jsx/code/index.native.js +1 -0
- package/dist/jsx/code/index.native.js.map +1 -1
- package/dist/jsx/layouts/EmptyState.mjs +9 -43
- package/dist/jsx/layouts/EmptyState.mjs.map +1 -1
- package/dist/jsx/layouts/EmptyState.native.js +9 -45
- package/dist/jsx/layouts/EmptyState.native.js.map +1 -1
- package/dist/jsx/layouts/PageTemplates.mjs +61 -1
- package/dist/jsx/layouts/PageTemplates.mjs.map +1 -1
- package/dist/jsx/layouts/PageTemplates.native.js +62 -0
- package/dist/jsx/layouts/PageTemplates.native.js.map +1 -1
- package/dist/jsx/layouts/barTitleSpan.mjs +27 -0
- package/dist/jsx/layouts/barTitleSpan.mjs.map +1 -0
- package/dist/jsx/layouts/barTitleSpan.native.js +78 -0
- package/dist/jsx/layouts/barTitleSpan.native.js.map +1 -0
- package/dist/jsx/layouts/page.mjs +132 -43
- package/dist/jsx/layouts/page.mjs.map +1 -1
- package/dist/jsx/layouts/page.native.js +143 -45
- package/dist/jsx/layouts/page.native.js.map +1 -1
- package/dist/jsx/views/ImageGrid.mjs +10 -1
- package/dist/jsx/views/ImageGrid.mjs.map +1 -1
- package/dist/jsx/views/ImageGrid.native.js +12 -1
- package/dist/jsx/views/ImageGrid.native.js.map +1 -1
- package/package.json +9 -9
- package/src/charts/BarChart.tsx +8 -2
- package/src/charts/ChartDataView.spec.tsx +23 -2
- package/src/charts/ChartDataView.tsx +87 -6
- package/src/charts/ChartSurface.tsx +17 -8
- package/src/charts/chartHeight.ts +8 -2
- package/src/charts/composeChart.ts +3 -2
- package/src/code/CodeBlock/CodeBlock.copy.spec.tsx +13 -0
- package/src/code/CodeBlock/index.tsx +46 -30
- package/src/code/LogView.tsx +127 -0
- package/src/code/index.ts +1 -0
- package/src/layouts/DenseContent.stories.tsx +93 -0
- package/src/layouts/EmptyState.spec.tsx +2 -0
- package/src/layouts/EmptyState.tsx +6 -39
- package/src/layouts/PageHeader.stories.tsx +238 -38
- package/src/layouts/PageTemplates.tsx +90 -0
- package/src/layouts/barTitleSpan.spec.ts +35 -0
- package/src/layouts/barTitleSpan.ts +44 -0
- package/src/layouts/glyphCentre.spec.tsx +218 -0
- package/src/layouts/navBar.spec.tsx +58 -2
- package/src/layouts/page.tsx +126 -34
- package/src/views/ImageGrid.tsx +19 -2
- package/types/charts/BarChart.d.ts +1 -1
- package/types/charts/BarChart.d.ts.map +1 -1
- package/types/charts/ChartDataView.d.ts +3 -1
- package/types/charts/ChartDataView.d.ts.map +1 -1
- package/types/charts/ChartSurface.d.ts.map +1 -1
- package/types/charts/chartHeight.d.ts +2 -1
- package/types/charts/chartHeight.d.ts.map +1 -1
- package/types/charts/composeChart.d.ts +2 -1
- package/types/charts/composeChart.d.ts.map +1 -1
- package/types/code/CodeBlock/index.d.ts.map +1 -1
- package/types/code/LogView.d.ts +31 -0
- package/types/code/LogView.d.ts.map +1 -0
- package/types/code/index.d.ts +1 -0
- package/types/code/index.d.ts.map +1 -1
- package/types/layouts/EmptyState.d.ts.map +1 -1
- package/types/layouts/PageTemplates.d.ts +26 -0
- package/types/layouts/PageTemplates.d.ts.map +1 -1
- package/types/layouts/barTitleSpan.d.ts +29 -0
- package/types/layouts/barTitleSpan.d.ts.map +1 -0
- package/types/layouts/page.d.ts +6 -1
- package/types/layouts/page.d.ts.map +1 -1
- package/types/views/ImageGrid.d.ts.map +1 -1
package/src/charts/BarChart.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import { useChartTheme } from "./useChartTheme";
|
|
|
20
20
|
export interface BarChartProps
|
|
21
21
|
extends ChartDatumEvents, Omit<YStackProps, "children" | "width" | "height"> {
|
|
22
22
|
data: ChartDatum[];
|
|
23
|
-
/** Drawing height as a theme size token or
|
|
23
|
+
/** Drawing height as a theme size token, pixels, or "fill" (default 200). */
|
|
24
24
|
height?: ChartHeight;
|
|
25
25
|
/** Explicit width bypasses responsive measurement. */
|
|
26
26
|
width?: number;
|
|
@@ -85,7 +85,13 @@ export function BarChart({
|
|
|
85
85
|
const interactive = Boolean(onDatumPress || onDatumHoverIn || onDatumHoverOut);
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<ChartDataFrame
|
|
88
|
+
<ChartDataFrame
|
|
89
|
+
data={data}
|
|
90
|
+
title={title}
|
|
91
|
+
valueFormatter={valueFormatter}
|
|
92
|
+
width={width}
|
|
93
|
+
fill={height === "fill"}
|
|
94
|
+
>
|
|
89
95
|
<ChartSurface
|
|
90
96
|
label={ariaLabel}
|
|
91
97
|
proof={{
|
|
@@ -2,6 +2,7 @@ import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
|
2
2
|
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { afterEach, expect, it, vi } from "vitest";
|
|
4
4
|
import { XStack, YStack } from "tamagui";
|
|
5
|
+
import { BarChart } from "./BarChart";
|
|
5
6
|
import { ChartDataView } from "./ChartDataView";
|
|
6
7
|
import { Sparkline } from "./Sparkline";
|
|
7
8
|
import { PieChart } from "./PieChart";
|
|
@@ -29,7 +30,7 @@ it("opens the complete formatted dataset without expanding the concise chart nam
|
|
|
29
30
|
)}
|
|
30
31
|
</ChartDataView>,
|
|
31
32
|
);
|
|
32
|
-
const image = document.querySelector("svg")!;
|
|
33
|
+
const image = document.querySelector("svg[data-mpo-chart]")!;
|
|
33
34
|
expect(image.getAttribute("aria-label")).toBe("Revenue trend");
|
|
34
35
|
expect(image.getAttribute("height")).toBe("36");
|
|
35
36
|
expect(image.querySelector("button")).toBeNull();
|
|
@@ -71,7 +72,9 @@ it("keeps a visible pie legend complete without a dangling data-action associati
|
|
|
71
72
|
);
|
|
72
73
|
expect(screen.getByText("Region 13")).toBeInTheDocument();
|
|
73
74
|
expect(screen.queryByRole("button", { name: "View data for Revenue" })).toBeNull();
|
|
74
|
-
expect(
|
|
75
|
+
expect(
|
|
76
|
+
document.querySelector("svg[data-mpo-chart]")!.getAttribute("aria-describedby"),
|
|
77
|
+
).toBeNull();
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
it("keeps a consumer metric press override separate from the data action", async () => {
|
|
@@ -102,3 +105,21 @@ it("keeps a consumer metric press override separate from the data action", async
|
|
|
102
105
|
expect(consumer).toHaveBeenCalledTimes(2);
|
|
103
106
|
expect(configured).not.toHaveBeenCalled();
|
|
104
107
|
});
|
|
108
|
+
|
|
109
|
+
it("gives a filled chart's remaining height to its values instead of a data action (LC-215, LC-173)", () => {
|
|
110
|
+
renderWithProviders(
|
|
111
|
+
<YStack height={800}>
|
|
112
|
+
<BarChart
|
|
113
|
+
data={data}
|
|
114
|
+
height="fill"
|
|
115
|
+
title="Revenue"
|
|
116
|
+
valueFormatter={(value) => `USD ${value}`}
|
|
117
|
+
/>
|
|
118
|
+
</YStack>,
|
|
119
|
+
);
|
|
120
|
+
const inline = document.querySelector("[data-mpo-chart-data-inline]");
|
|
121
|
+
expect(inline).not.toBeNull();
|
|
122
|
+
expect(inline!.querySelectorAll("[data-mpo-chart-data-row]")).toHaveLength(13);
|
|
123
|
+
expect(screen.getByText("USD 13")).toBeInTheDocument();
|
|
124
|
+
expect(document.querySelector("[data-mpo-chart-data-trigger]")).toBeNull();
|
|
125
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button } from "@multiplatform.one/forms";
|
|
2
|
+
import { TableIcon } from "@phosphor-icons/react";
|
|
2
3
|
import { isWeb } from "@multiplatform.one/platform";
|
|
3
4
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
5
|
import { createContext, useContext, useId, useState, type ReactNode } from "react";
|
|
@@ -6,6 +7,8 @@ import { Paragraph, XStack, YStack } from "tamagui";
|
|
|
6
7
|
import { OverlayHeader } from "../actions/StandardAction";
|
|
7
8
|
import { Dialog } from "../Dialog";
|
|
8
9
|
import { ScrollView } from "../ScrollView";
|
|
10
|
+
import { Separator } from "../Separator";
|
|
11
|
+
import { Text } from "../Text";
|
|
9
12
|
import { sectionHeading } from "../componentColors";
|
|
10
13
|
import { useTranslation } from "../shared/i18n";
|
|
11
14
|
import { DialogContent, DialogOverlay } from "../surfaces";
|
|
@@ -44,13 +47,13 @@ export function ChartDataView({ data, title, valueFormatter, children }: ChartDa
|
|
|
44
47
|
const action = (
|
|
45
48
|
<Dialog.Trigger asChild>
|
|
46
49
|
<Button
|
|
47
|
-
|
|
50
|
+
circular
|
|
51
|
+
chromeless
|
|
52
|
+
icon={TableIcon}
|
|
48
53
|
id={triggerId}
|
|
49
54
|
{...(isWeb ? { "aria-label": actionLabel } : { accessibilityLabel: actionLabel })}
|
|
50
55
|
{...chartWebProps({ "data-mpo-chart-data-trigger": id })}
|
|
51
|
-
|
|
52
|
-
{t("View data")}
|
|
53
|
-
</Button>
|
|
56
|
+
/>
|
|
54
57
|
</Dialog.Trigger>
|
|
55
58
|
);
|
|
56
59
|
return (
|
|
@@ -128,15 +131,41 @@ export function ChartDataFrame({
|
|
|
128
131
|
title,
|
|
129
132
|
valueFormatter,
|
|
130
133
|
width,
|
|
134
|
+
fill,
|
|
131
135
|
children,
|
|
132
|
-
}: Omit<ChartDataViewProps, "children"> & {
|
|
136
|
+
}: Omit<ChartDataViewProps, "children"> & {
|
|
137
|
+
children: ReactNode;
|
|
138
|
+
width?: number;
|
|
139
|
+
/** The chart flexes to its region's height (LC-215). */
|
|
140
|
+
fill?: boolean;
|
|
141
|
+
}) {
|
|
133
142
|
const inherited = useContext(ChartDataContext);
|
|
134
143
|
const { knobProps } = useResolvedKnobs();
|
|
135
144
|
if (inherited) return children;
|
|
145
|
+
if (fill)
|
|
146
|
+
return (
|
|
147
|
+
<YStack
|
|
148
|
+
minWidth={0}
|
|
149
|
+
width={width ?? "100%"}
|
|
150
|
+
maxWidth="100%"
|
|
151
|
+
flex={1}
|
|
152
|
+
minHeight={0}
|
|
153
|
+
{...knobProps.gap}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
<ChartDataList data={data} title={title} valueFormatter={valueFormatter} />
|
|
157
|
+
</YStack>
|
|
158
|
+
);
|
|
136
159
|
return (
|
|
137
160
|
<ChartDataView data={data} title={title} valueFormatter={valueFormatter}>
|
|
138
161
|
{({ action }) => (
|
|
139
|
-
<YStack
|
|
162
|
+
<YStack
|
|
163
|
+
minWidth={0}
|
|
164
|
+
width={width ?? "100%"}
|
|
165
|
+
maxWidth="100%"
|
|
166
|
+
{...(fill ? { flex: 1, minHeight: 0 } : null)}
|
|
167
|
+
{...knobProps.gap}
|
|
168
|
+
>
|
|
140
169
|
{children}
|
|
141
170
|
<XStack justifyContent="flex-end">{action}</XStack>
|
|
142
171
|
</YStack>
|
|
@@ -145,6 +174,58 @@ export function ChartDataFrame({
|
|
|
145
174
|
);
|
|
146
175
|
}
|
|
147
176
|
|
|
177
|
+
/**
|
|
178
|
+
* LC-215: a chart that fills its region stops at its aspect and gives the
|
|
179
|
+
* rest of the height to its own values, one row per datum, so the page never
|
|
180
|
+
* ends in an empty band (LC-173) and the data needs no separate action.
|
|
181
|
+
*/
|
|
182
|
+
function ChartDataList({
|
|
183
|
+
data,
|
|
184
|
+
title,
|
|
185
|
+
valueFormatter,
|
|
186
|
+
}: Pick<ChartDataViewProps, "data" | "title" | "valueFormatter">) {
|
|
187
|
+
const { t } = useTranslation();
|
|
188
|
+
const { knobProps } = useResolvedKnobs();
|
|
189
|
+
const format = valueFormatter ?? formatChartValue;
|
|
190
|
+
const rows = data.map((datum, index) =>
|
|
191
|
+
typeof datum === "number" ? { label: String(index + 1), value: datum } : datum,
|
|
192
|
+
);
|
|
193
|
+
const label = title ? `${title}: ${t("data")}` : t("Chart data");
|
|
194
|
+
return (
|
|
195
|
+
<ScrollView
|
|
196
|
+
flex={1}
|
|
197
|
+
minHeight={0}
|
|
198
|
+
{...(isWeb ? { "aria-label": label } : { accessibilityLabel: label })}
|
|
199
|
+
{...chartWebProps({ "data-mpo-chart-data-inline": "true" })}
|
|
200
|
+
>
|
|
201
|
+
{rows.map((datum, index) => (
|
|
202
|
+
<YStack key={index}>
|
|
203
|
+
{index > 0 ? <Separator /> : null}
|
|
204
|
+
<XStack
|
|
205
|
+
minHeight={44}
|
|
206
|
+
alignItems="center"
|
|
207
|
+
{...knobProps.gap}
|
|
208
|
+
{...chartWebProps({ "data-mpo-chart-data-row": String(index) })}
|
|
209
|
+
{...(!isWeb
|
|
210
|
+
? {
|
|
211
|
+
accessible: true,
|
|
212
|
+
accessibilityLabel: describeDatum({ datum, formatValue: format }),
|
|
213
|
+
}
|
|
214
|
+
: {})}
|
|
215
|
+
>
|
|
216
|
+
<Text flex={1} minWidth={0} {...knobProps.body}>
|
|
217
|
+
{datum.label}
|
|
218
|
+
</Text>
|
|
219
|
+
<Text tabular {...knobProps.body} color={knobProps.textRole.meta.color}>
|
|
220
|
+
{format(datum.value)}
|
|
221
|
+
</Text>
|
|
222
|
+
</XStack>
|
|
223
|
+
</YStack>
|
|
224
|
+
))}
|
|
225
|
+
</ScrollView>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
148
229
|
export function useChartDataViewProps(): Record<string, unknown> {
|
|
149
230
|
const context = useContext(ChartDataContext);
|
|
150
231
|
if (!context) return {};
|
|
@@ -47,28 +47,37 @@ export function ChartSurface({
|
|
|
47
47
|
children,
|
|
48
48
|
...stackProps
|
|
49
49
|
}: ChartSurfaceProps) {
|
|
50
|
-
const
|
|
50
|
+
const fill = heightProp === "fill";
|
|
51
|
+
const fixedHeight = useChartHeight(heightProp, 200);
|
|
51
52
|
const dataViewProps = useChartDataViewProps();
|
|
52
53
|
const { motion, animationEnabled } = useChartTheme();
|
|
53
|
-
const [
|
|
54
|
-
const width = widthProp ??
|
|
54
|
+
const [measured, setMeasured] = useState({ width: 0, height: 0 });
|
|
55
|
+
const width = widthProp ?? measured.width;
|
|
56
|
+
const height = fill ? measured.height : fixedHeight;
|
|
57
|
+
// LC-215: a filled chart takes its region's height only up to its aspect,
|
|
58
|
+
// so a dozen bars never stand a phone screen tall or a desktop wide.
|
|
59
|
+
const fillCap = width > 0 ? Math.round(width * (width < 600 ? 0.9 : 0.5)) : undefined;
|
|
55
60
|
|
|
56
61
|
const handleLayout = useCallback((event: LayoutChangeEvent) => {
|
|
57
|
-
const
|
|
58
|
-
|
|
62
|
+
const { width: nextWidth, height: nextHeight } = event.nativeEvent.layout;
|
|
63
|
+
setMeasured((prev) =>
|
|
64
|
+
Math.abs(prev.width - nextWidth) >= 0.5 || Math.abs(prev.height - nextHeight) >= 0.5
|
|
65
|
+
? { width: nextWidth, height: nextHeight }
|
|
66
|
+
: prev,
|
|
67
|
+
);
|
|
59
68
|
}, []);
|
|
60
69
|
|
|
61
70
|
return (
|
|
62
71
|
<YStack
|
|
63
72
|
width="100%"
|
|
64
|
-
|
|
73
|
+
{...(fill ? { flex: 1, minHeight: 0, maxHeight: fillCap } : { height })}
|
|
65
74
|
overflow="hidden"
|
|
66
|
-
onLayout={widthProp === undefined ? handleLayout : undefined}
|
|
75
|
+
onLayout={widthProp === undefined || fill ? handleLayout : undefined}
|
|
67
76
|
{...transitionProps(motion)}
|
|
68
77
|
{...(animationEnabled ? { enterStyle: { opacity: 0, y: 6 } } : {})}
|
|
69
78
|
{...stackProps}
|
|
70
79
|
>
|
|
71
|
-
{width > 0 ? (
|
|
80
|
+
{width > 0 && height > 0 ? (
|
|
72
81
|
<Svg width={width} height={height} {...chartRootA11yProps(label, proof)} {...dataViewProps}>
|
|
73
82
|
{children({ width, height })}
|
|
74
83
|
</Svg>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { getActiveScaleTokens, usePresetContext, type SizeKey } from "@multiplatform.one/theme";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** `"fill"` flexes the chart to the height its region hands it (LC-215). */
|
|
4
|
+
export type ChartHeight = number | SizeKey | "fill";
|
|
4
5
|
|
|
5
6
|
export function useChartHeight(height: ChartHeight | undefined, fallback: number): number {
|
|
6
7
|
usePresetContext();
|
|
7
|
-
const value =
|
|
8
|
+
const value =
|
|
9
|
+
height === "fill"
|
|
10
|
+
? undefined
|
|
11
|
+
: typeof height === "string"
|
|
12
|
+
? getActiveScaleTokens().size[height]
|
|
13
|
+
: height;
|
|
8
14
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
9
15
|
}
|
|
@@ -76,7 +76,8 @@ const X_LABEL_SLOT_GAP = 8;
|
|
|
76
76
|
/**
|
|
77
77
|
* Pick an evenly-strided subset of category labels that fits the plot width
|
|
78
78
|
* without overlap (LC-10 at phone widths): every label gets a slot sized by
|
|
79
|
-
* the widest label (capped), and the stride
|
|
79
|
+
* the widest label (capped), and the stride counts back from the last label
|
|
80
|
+
* so the latest category always keeps its label.
|
|
80
81
|
*/
|
|
81
82
|
export function thinCategories(labels: string[], plotWidth: number, fontSize: number): string[] {
|
|
82
83
|
if (labels.length <= 1 || plotWidth <= 0) return labels;
|
|
@@ -85,5 +86,5 @@ export function thinCategories(labels: string[], plotWidth: number, fontSize: nu
|
|
|
85
86
|
const capacity = Math.max(1, Math.floor(plotWidth / slot));
|
|
86
87
|
if (labels.length <= capacity) return labels;
|
|
87
88
|
const stride = Math.ceil(labels.length / capacity);
|
|
88
|
-
return labels.filter((_, index) => index % stride === 0);
|
|
89
|
+
return labels.filter((_, index) => (labels.length - 1 - index) % stride === 0);
|
|
89
90
|
}
|
|
@@ -46,6 +46,19 @@ describe("CodeBlock copy (Input.Button + notify)", () => {
|
|
|
46
46
|
expect(style.display).not.toBe("none");
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it("a command is one code surface with its copy inside, top-trailing (LC-267)", () => {
|
|
50
|
+
const result = renderWithProviders(<CodeBlock language="sh">shc node join</CodeBlock>);
|
|
51
|
+
const surface = result.container.querySelector("[data-code-surface]") as HTMLElement | null;
|
|
52
|
+
if (!surface) throw new Error("code surface missing");
|
|
53
|
+
const copy = copyOf(result.container);
|
|
54
|
+
expect(copy && surface.contains(copy)).toBe(true);
|
|
55
|
+
const cell = surface.querySelector("[data-code-copy]") as HTMLElement;
|
|
56
|
+
expect(surface.lastElementChild).toBe(cell);
|
|
57
|
+
expect(getComputedStyle(cell).position).not.toBe("absolute");
|
|
58
|
+
const code = surface.querySelector("[data-register='code']") as HTMLElement;
|
|
59
|
+
expect(code.compareDocumentPosition(cell) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
|
|
49
62
|
it("empty code omits the copy cap", () => {
|
|
50
63
|
const result = renderWithProviders(<CodeBlock>{""}</CodeBlock>);
|
|
51
64
|
expect(copyOf(result.container)).toBeNull();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { LinearGradient } from "@tamagui/linear-gradient";
|
|
10
|
-
import {
|
|
10
|
+
import { CopyIcon } from "@phosphor-icons/react";
|
|
11
11
|
import {
|
|
12
12
|
MIN_PRESS_TARGET,
|
|
13
13
|
useTouchSurface,
|
|
@@ -30,6 +30,7 @@ import { Code } from "../Code";
|
|
|
30
30
|
import { HighlightedCode } from "../HighlightedCode";
|
|
31
31
|
import { useShikiTokens } from "../Highlighter";
|
|
32
32
|
import { Pre } from "../Pre";
|
|
33
|
+
import { componentColors } from "../../componentColors";
|
|
33
34
|
|
|
34
35
|
/** Visible lines before the "show more" clamp (content constraint, LC-18). */
|
|
35
36
|
const VISIBLE_LINES = 22;
|
|
@@ -131,7 +132,7 @@ export function CodeBlock({
|
|
|
131
132
|
const capSquare = sizeRecipeForToken(String(codeSize), { touch }).height;
|
|
132
133
|
|
|
133
134
|
return (
|
|
134
|
-
<YStack position="relative"
|
|
135
|
+
<YStack position="relative">
|
|
135
136
|
<ErrorBoundary>
|
|
136
137
|
<YStack
|
|
137
138
|
ref={clampRef}
|
|
@@ -160,9 +161,18 @@ export function CodeBlock({
|
|
|
160
161
|
<Spacer size="$4" />
|
|
161
162
|
</LinearGradient>
|
|
162
163
|
)}
|
|
163
|
-
<Pre
|
|
164
|
+
<Pre
|
|
165
|
+
{...knobProps.borderRadius}
|
|
166
|
+
padding={0}
|
|
167
|
+
marginBottom={0}
|
|
168
|
+
flexDirection="row"
|
|
169
|
+
backgroundColor={componentColors.code.background}
|
|
170
|
+
borderWidth={0}
|
|
171
|
+
data-code-surface=""
|
|
172
|
+
>
|
|
164
173
|
<ScrollView
|
|
165
|
-
|
|
174
|
+
flex={1}
|
|
175
|
+
minWidth={0}
|
|
166
176
|
contentContainerStyle={{ minWidth: "100%" }}
|
|
167
177
|
horizontal
|
|
168
178
|
showsHorizontalScrollIndicator={false}
|
|
@@ -185,33 +195,39 @@ export function CodeBlock({
|
|
|
185
195
|
</HighlightedCode>
|
|
186
196
|
</Code>
|
|
187
197
|
</ScrollView>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
type="button"
|
|
195
|
-
aria-label="Copy to clipboard"
|
|
196
|
-
data-press-min={MIN_PRESS_TARGET}
|
|
197
|
-
hitSlop={capSquare < MIN_PRESS_TARGET ? pressTargetHitSlop(capSquare) : undefined}
|
|
198
|
-
onPress={activateCopy}
|
|
199
|
-
onKeyDown={
|
|
200
|
-
((e: KeyboardEvent) => {
|
|
201
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
e.stopPropagation();
|
|
204
|
-
activateCopy();
|
|
205
|
-
}
|
|
206
|
-
}) as unknown as () => void
|
|
207
|
-
}
|
|
198
|
+
{showCopy && (
|
|
199
|
+
<XStack
|
|
200
|
+
flexShrink={0}
|
|
201
|
+
alignSelf={lines > 1 ? "flex-start" : "center"}
|
|
202
|
+
zIndex={zIndex.localTop}
|
|
203
|
+
data-code-copy=""
|
|
208
204
|
>
|
|
209
|
-
<Input.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
205
|
+
<Input.Button
|
|
206
|
+
glyphRing
|
|
207
|
+
size={codeSize}
|
|
208
|
+
color={componentColors.text.primary}
|
|
209
|
+
type="button"
|
|
210
|
+
aria-label="Copy to clipboard"
|
|
211
|
+
data-press-min={MIN_PRESS_TARGET}
|
|
212
|
+
hitSlop={capSquare < MIN_PRESS_TARGET ? pressTargetHitSlop(capSquare) : undefined}
|
|
213
|
+
onPress={activateCopy}
|
|
214
|
+
onKeyDown={
|
|
215
|
+
((e: KeyboardEvent) => {
|
|
216
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
activateCopy();
|
|
220
|
+
}
|
|
221
|
+
}) as unknown as () => void
|
|
222
|
+
}
|
|
223
|
+
>
|
|
224
|
+
<Input.Icon aria-hidden color={componentColors.text.primary}>
|
|
225
|
+
<CopyIcon weight="bold" />
|
|
226
|
+
</Input.Icon>
|
|
227
|
+
</Input.Button>
|
|
228
|
+
</XStack>
|
|
229
|
+
)}
|
|
230
|
+
</Pre>
|
|
215
231
|
</YStack>
|
|
216
232
|
</ErrorBoundary>
|
|
217
233
|
<NotifyRegion scope="field" region={region} size="$3" />
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useLayoutSizeClass, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { ScrollView, Text, View, XStack, YStack, useTheme, type YStackProps } from "tamagui";
|
|
4
|
+
import { WarningIcon, XCircleIcon } from "@phosphor-icons/react";
|
|
5
|
+
|
|
6
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
7
|
+
|
|
8
|
+
export interface LogEntry {
|
|
9
|
+
/** Stable key; defaults to the entry's index. */
|
|
10
|
+
id?: string;
|
|
11
|
+
/** When it happened: an ISO string or a Date. Shown as HH:MM:SS. */
|
|
12
|
+
time: string | Date;
|
|
13
|
+
level?: LogLevel;
|
|
14
|
+
/** Who wrote it ("worker-2"), a short muted column. */
|
|
15
|
+
source?: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LogViewProps extends Omit<YStackProps, "children"> {
|
|
20
|
+
entries: readonly LogEntry[];
|
|
21
|
+
/** Names the log for assistive tech ("Sync worker log"). */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Keep the newest entry in view as entries arrive. Default: true. */
|
|
24
|
+
follow?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const levelInk = { warn: "yellow11", error: "red11" } as const;
|
|
28
|
+
const levelGlyph = { warn: WarningIcon, error: XCircleIcon } as const;
|
|
29
|
+
|
|
30
|
+
function clockTime(time: string | Date): string {
|
|
31
|
+
const iso = typeof time === "string" ? time : time.toISOString();
|
|
32
|
+
return iso.match(/T(\d{2}:\d{2}:\d{2})/)?.[1] ?? iso;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A log or terminal stream (LC-215): one row per entry, the time and source
|
|
37
|
+
* in fixed muted columns and the message wrapping under itself, so the
|
|
38
|
+
* columns stay aligned at any width. At compact width the time, level and
|
|
39
|
+
* source sit on a line above the message, which then takes the full width.
|
|
40
|
+
* Only a warning or an error carries a mark: a glyph leading its message in
|
|
41
|
+
* the level's ink, never a word and never a dot between two values (LC-261).
|
|
42
|
+
* The log sits on the page ground and fills whatever
|
|
43
|
+
* region it is given; its first glyph sits on the page line.
|
|
44
|
+
*/
|
|
45
|
+
export function LogView({ entries, label, follow = true, ...rest }: LogViewProps) {
|
|
46
|
+
const { knobProps } = useResolvedKnobs({ component: "Screen" });
|
|
47
|
+
const compact = useLayoutSizeClass() === "compact";
|
|
48
|
+
const theme = useTheme();
|
|
49
|
+
const scroller = useRef<{ scrollToEnd?: (options?: { animated?: boolean }) => void } | null>(
|
|
50
|
+
null,
|
|
51
|
+
);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (follow) scroller.current?.scrollToEnd?.({ animated: false });
|
|
54
|
+
}, [entries, follow]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<YStack flex={1} minHeight={0} data-log-view="" role="log" aria-label={label} {...rest}>
|
|
58
|
+
<ScrollView ref={scroller as never} flex={1} keyboardShouldPersistTaps="handled">
|
|
59
|
+
<YStack
|
|
60
|
+
{...knobProps.panelPadding}
|
|
61
|
+
{...(compact ? null : { paddingHorizontal: 0 })}
|
|
62
|
+
gap={compact ? "$3" : "$1"}
|
|
63
|
+
>
|
|
64
|
+
{entries.map((entry, index) => {
|
|
65
|
+
const level = entry.level === "warn" || entry.level === "error" ? entry.level : null;
|
|
66
|
+
const ink = level ? (`$${levelInk[level]}` as const) : "$color12";
|
|
67
|
+
const Glyph = level ? levelGlyph[level] : null;
|
|
68
|
+
const meta = (
|
|
69
|
+
<>
|
|
70
|
+
<Text fontFamily="$mono" fontSize="$3" color="$color10" flexShrink={0}>
|
|
71
|
+
{clockTime(entry.time)}
|
|
72
|
+
</Text>
|
|
73
|
+
{entry.source ? (
|
|
74
|
+
<Text
|
|
75
|
+
fontFamily="$mono"
|
|
76
|
+
fontSize="$3"
|
|
77
|
+
color="$color10"
|
|
78
|
+
flexShrink={0}
|
|
79
|
+
numberOfLines={1}
|
|
80
|
+
>
|
|
81
|
+
{entry.source}
|
|
82
|
+
</Text>
|
|
83
|
+
) : null}
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
const message = (
|
|
87
|
+
<XStack
|
|
88
|
+
gap="$1.5"
|
|
89
|
+
alignItems="flex-start"
|
|
90
|
+
flex={compact ? undefined : 1}
|
|
91
|
+
minWidth={0}
|
|
92
|
+
>
|
|
93
|
+
{Glyph ? (
|
|
94
|
+
<View height="$1" justifyContent="center" flexShrink={0} aria-hidden>
|
|
95
|
+
<Glyph
|
|
96
|
+
size={14}
|
|
97
|
+
weight="bold"
|
|
98
|
+
color={
|
|
99
|
+
level ? (theme[levelInk[level]]?.get() as string | undefined) : undefined
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
</View>
|
|
103
|
+
) : null}
|
|
104
|
+
<Text fontFamily="$mono" fontSize="$3" color={ink} flex={1} minWidth={0}>
|
|
105
|
+
{entry.message}
|
|
106
|
+
</Text>
|
|
107
|
+
</XStack>
|
|
108
|
+
);
|
|
109
|
+
return compact ? (
|
|
110
|
+
<YStack key={entry.id ?? index} data-log-entry="">
|
|
111
|
+
<XStack gap="$2" alignItems="center">
|
|
112
|
+
{meta}
|
|
113
|
+
</XStack>
|
|
114
|
+
{message}
|
|
115
|
+
</YStack>
|
|
116
|
+
) : (
|
|
117
|
+
<XStack key={entry.id ?? index} gap="$3" alignItems="flex-start" data-log-entry="">
|
|
118
|
+
{meta}
|
|
119
|
+
{message}
|
|
120
|
+
</XStack>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</YStack>
|
|
124
|
+
</ScrollView>
|
|
125
|
+
</YStack>
|
|
126
|
+
);
|
|
127
|
+
}
|
package/src/code/index.ts
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
2
|
+
import { DownloadSimpleIcon, PauseIcon, type IconProps } from "@phosphor-icons/react";
|
|
3
|
+
import { Button } from "@multiplatform.one/forms";
|
|
4
|
+
import { BarChart } from "../charts/BarChart";
|
|
5
|
+
import { LogView, type LogEntry } from "../code/LogView";
|
|
6
|
+
import { DensePageLayout } from "./PageTemplates";
|
|
7
|
+
import { StatusDot } from "./StatusDot";
|
|
8
|
+
|
|
9
|
+
const PauseFillIcon = (props: IconProps) => <PauseIcon weight="fill" {...props} />;
|
|
10
|
+
|
|
11
|
+
const tables = ["customers", "orders", "invoices", "payments", "shipments", "returns"];
|
|
12
|
+
|
|
13
|
+
const logEntries: LogEntry[] = Array.from({ length: 64 }, (_, index) => {
|
|
14
|
+
const second = 7 + index * 13;
|
|
15
|
+
const time = `2026-10-29T14:${String(Math.floor(second / 60) % 60).padStart(2, "0")}:${String(second % 60).padStart(2, "0")}Z`;
|
|
16
|
+
const table = tables[index % tables.length];
|
|
17
|
+
if (index % 17 === 11)
|
|
18
|
+
return {
|
|
19
|
+
time,
|
|
20
|
+
level: "error",
|
|
21
|
+
source: `worker-${index % 4}`,
|
|
22
|
+
message: `push ${table} failed: 409 conflict on row ${1200 + index}, retrying in 4s`,
|
|
23
|
+
};
|
|
24
|
+
if (index % 9 === 4)
|
|
25
|
+
return {
|
|
26
|
+
time,
|
|
27
|
+
level: "warn",
|
|
28
|
+
source: `worker-${index % 4}`,
|
|
29
|
+
message: `${table} lagging 38s behind the primary`,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
time,
|
|
33
|
+
level: "info",
|
|
34
|
+
source: `worker-${index % 4}`,
|
|
35
|
+
message: `synced ${table}: ${(index * 37) % 400} rows in ${120 + ((index * 53) % 900)} ms`,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const weeks = [42, 57, 49, 63, 71, 66, 80, 74, 88, 92, 85, 97];
|
|
40
|
+
const deploys = weeks.map((value, index) => ({ label: `W${index + 31}`, value }));
|
|
41
|
+
|
|
42
|
+
const meta: Meta<typeof DensePageLayout> = {
|
|
43
|
+
title: "Components/DenseContent",
|
|
44
|
+
component: DensePageLayout,
|
|
45
|
+
parameters: {
|
|
46
|
+
status: { type: "beta" },
|
|
47
|
+
layout: "fullscreen",
|
|
48
|
+
docs: {
|
|
49
|
+
description: {
|
|
50
|
+
component:
|
|
51
|
+
"LC-215 DENSE-CONTENT-BLEEDS: a page whose content is a log, terminal or chart keeps its head (title, live status, icon actions) and gives the rest of the height to the surface. On a phone the surface bleeds to both screen edges; from `medium` up it stays in the page column at the measure its content reads at: the page column for a log, the readable `expanded` measure for a chart.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export default meta;
|
|
57
|
+
|
|
58
|
+
type Story = StoryObj<typeof DensePageLayout>;
|
|
59
|
+
|
|
60
|
+
export const Log: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<DensePageLayout
|
|
63
|
+
kind="log"
|
|
64
|
+
title="Sync worker"
|
|
65
|
+
status={<StatusDot tone="positive">Running</StatusDot>}
|
|
66
|
+
actions={
|
|
67
|
+
<>
|
|
68
|
+
<Button circular chromeless icon={PauseFillIcon} aria-label="Pause the stream" />
|
|
69
|
+
<Button circular chromeless icon={DownloadSimpleIcon} aria-label="Download the log" />
|
|
70
|
+
</>
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<LogView label="Sync worker log" entries={logEntries} />
|
|
74
|
+
</DensePageLayout>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Chart: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<DensePageLayout
|
|
81
|
+
kind="chart"
|
|
82
|
+
measure="readable"
|
|
83
|
+
gutter
|
|
84
|
+
title="Deploys"
|
|
85
|
+
subtitle="Last 12 weeks"
|
|
86
|
+
actions={
|
|
87
|
+
<Button circular chromeless icon={DownloadSimpleIcon} aria-label="Download the data" />
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<BarChart data={deploys} height="fill" title="Deploys per week" />
|
|
91
|
+
</DensePageLayout>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
@@ -102,5 +102,7 @@ describe("EmptyState state anatomy (LC-171)", () => {
|
|
|
102
102
|
expect(command.textContent).toContain("shc tenant create <name>");
|
|
103
103
|
expect(screen.getByLabelText("Copy to clipboard")).toBeTruthy();
|
|
104
104
|
expect(document.querySelectorAll("[data-empty-intent] p")).toHaveLength(0);
|
|
105
|
+
const surface = command.querySelector("[data-code-surface]");
|
|
106
|
+
expect(surface?.contains(screen.getByLabelText("Copy to clipboard"))).toBe(true);
|
|
105
107
|
});
|
|
106
108
|
});
|