@multiplatform.one/components 7.5.0 → 7.6.1
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/CopyField/index.cjs +4 -1
- package/dist/cjs/CopyField/index.native.js +4 -1
- package/dist/cjs/CopyField/index.native.js.map +1 -1
- package/dist/cjs/ProgressSteps/index.cjs +5 -2
- package/dist/cjs/ProgressSteps/index.native.js +5 -2
- package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
- package/dist/cjs/Separator/index.native.js.map +1 -1
- package/dist/cjs/Wizard/index.cjs +1 -0
- package/dist/cjs/Wizard/index.native.js +3 -12
- package/dist/cjs/Wizard/index.native.js.map +1 -1
- package/dist/cjs/code/CodeBlock/index.cjs +38 -22
- package/dist/cjs/code/CodeBlock/index.native.js +38 -22
- package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/surfaces.cjs +30 -7
- package/dist/cjs/surfaces.native.js +30 -7
- package/dist/cjs/surfaces.native.js.map +1 -1
- package/dist/cjs/views/index.cjs +3 -0
- package/dist/cjs/views/index.native.js +3 -0
- package/dist/cjs/views/index.native.js.map +1 -1
- package/dist/esm/CopyField/index.mjs +4 -1
- package/dist/esm/CopyField/index.mjs.map +1 -1
- package/dist/esm/CopyField/index.native.js +4 -1
- package/dist/esm/CopyField/index.native.js.map +1 -1
- package/dist/esm/ProgressSteps/index.mjs +5 -2
- package/dist/esm/ProgressSteps/index.mjs.map +1 -1
- package/dist/esm/ProgressSteps/index.native.js +5 -2
- package/dist/esm/ProgressSteps/index.native.js.map +1 -1
- package/dist/esm/Separator/index.mjs.map +1 -1
- package/dist/esm/Separator/index.native.js.map +1 -1
- package/dist/esm/Wizard/index.mjs +1 -0
- package/dist/esm/Wizard/index.mjs.map +1 -1
- package/dist/esm/Wizard/index.native.js +3 -12
- package/dist/esm/Wizard/index.native.js.map +1 -1
- package/dist/esm/code/CodeBlock/index.mjs +42 -26
- package/dist/esm/code/CodeBlock/index.mjs.map +1 -1
- package/dist/esm/code/CodeBlock/index.native.js +42 -26
- package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +1 -0
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/surfaces.mjs +31 -8
- package/dist/esm/surfaces.mjs.map +1 -1
- package/dist/esm/surfaces.native.js +31 -8
- package/dist/esm/surfaces.native.js.map +1 -1
- package/dist/esm/views/index.mjs +2 -2
- package/dist/esm/views/index.mjs.map +1 -1
- package/dist/esm/views/index.native.js +2 -2
- package/dist/esm/views/index.native.js.map +1 -1
- package/dist/jsx/CopyField/index.mjs +4 -1
- package/dist/jsx/CopyField/index.mjs.map +1 -1
- package/dist/jsx/CopyField/index.native.js +4 -1
- package/dist/jsx/CopyField/index.native.js.map +1 -1
- package/dist/jsx/ProgressSteps/index.mjs +5 -2
- package/dist/jsx/ProgressSteps/index.mjs.map +1 -1
- package/dist/jsx/ProgressSteps/index.native.js +5 -2
- package/dist/jsx/ProgressSteps/index.native.js.map +1 -1
- package/dist/jsx/Separator/index.mjs.map +1 -1
- package/dist/jsx/Separator/index.native.js.map +1 -1
- package/dist/jsx/Wizard/index.mjs +1 -0
- package/dist/jsx/Wizard/index.mjs.map +1 -1
- package/dist/jsx/Wizard/index.native.js +3 -12
- package/dist/jsx/Wizard/index.native.js.map +1 -1
- package/dist/jsx/code/CodeBlock/index.mjs +42 -26
- package/dist/jsx/code/CodeBlock/index.mjs.map +1 -1
- package/dist/jsx/code/CodeBlock/index.native.js +38 -22
- package/dist/jsx/code/CodeBlock/index.native.js.map +1 -1
- package/dist/jsx/index.js +1 -0
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +1 -0
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +1 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/surfaces.mjs +31 -8
- package/dist/jsx/surfaces.mjs.map +1 -1
- package/dist/jsx/surfaces.native.js +30 -7
- package/dist/jsx/surfaces.native.js.map +1 -1
- package/dist/jsx/views/index.mjs +2 -2
- package/dist/jsx/views/index.mjs.map +1 -1
- package/dist/jsx/views/index.native.js +3 -0
- package/dist/jsx/views/index.native.js.map +1 -1
- package/package.json +9 -9
- package/src/Carousel/Carousel.stories.tsx +0 -1
- package/src/CopyField/index.tsx +10 -15
- package/src/Heading.native.spec.tsx +156 -0
- package/src/Heading.spec.tsx +14 -79
- package/src/Meter/Meter.spec.tsx +9 -1
- package/src/ProgressSteps/index.tsx +18 -3
- package/src/Separator/Separator.spec.tsx +68 -32
- package/src/Separator/index.tsx +27 -9
- package/src/Text.native.spec.tsx +144 -0
- package/src/Text.spec.tsx +12 -70
- package/src/Tooltip/tooltipGapContract.spec.ts +54 -0
- package/src/Wizard/Wizard.spec.tsx +23 -1
- package/src/Wizard/index.tsx +1 -8
- package/src/code/CodeBlock/CodeBlock.spec.tsx +6 -6
- package/src/code/CodeBlock/index.tsx +44 -27
- package/src/index.ts +12 -6
- package/src/overlayElevation.spec.tsx +154 -0
- package/src/surfaces.spec.tsx +98 -1
- package/src/surfaces.tsx +28 -6
- package/src/tamagui.spec.ts +11 -2
- package/src/tamagui.ts +5 -5
- package/src/views/index.ts +15 -0
- package/types/CopyField/index.d.ts +8 -14
- package/types/CopyField/index.d.ts.map +1 -1
- package/types/ProgressSteps/index.d.ts +8 -1
- package/types/ProgressSteps/index.d.ts.map +1 -1
- package/types/Separator/index.d.ts +27 -9
- package/types/Separator/index.d.ts.map +1 -1
- package/types/Tabs/index.d.ts +1 -1
- package/types/Wizard/index.d.ts.map +1 -1
- package/types/code/CodeBlock/index.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/surfaces.d.ts +6 -6
- package/types/surfaces.d.ts.map +1 -1
- package/types/views/index.d.ts +1 -1
- package/types/views/index.d.ts.map +1 -1
package/src/Text.spec.tsx
CHANGED
|
@@ -12,19 +12,18 @@
|
|
|
12
12
|
* family. Between knob flips and against explicit-prop renders of the
|
|
13
13
|
* same value. Hashed class names are compared by identity only.
|
|
14
14
|
*
|
|
15
|
-
* WEB arm
|
|
16
|
-
*
|
|
17
|
-
* module graph
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* — the only channel native has.
|
|
15
|
+
* This file is the WEB arm (the default module graph, isWeb true). The NATIVE
|
|
16
|
+
* arm is ./Text.native.spec.tsx, a separate FILE so that a file-scoped
|
|
17
|
+
* `vi.mock` gives it one coherent module graph. FontKnobStyles' CSS-variable
|
|
18
|
+
* rescue is absent in BOTH arms (renderWithProviders mounts no theme
|
|
19
|
+
* provider), so a flip that reaches the node proves the PROPS channel — the
|
|
20
|
+
* only channel native has.
|
|
22
21
|
*/
|
|
23
22
|
import { cleanup, screen } from "@testing-library/react";
|
|
24
23
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
25
24
|
import { Preset } from "@multiplatform.one/theme";
|
|
26
25
|
import { XStack } from "tamagui";
|
|
27
|
-
import { afterEach, describe, expect, it
|
|
26
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
28
27
|
import { Text } from "./Text";
|
|
29
28
|
|
|
30
29
|
afterEach(cleanup);
|
|
@@ -182,65 +181,8 @@ describe("house Text — fontWeight and bodyFont reach the text node", () => {
|
|
|
182
181
|
});
|
|
183
182
|
});
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
isWeb: false,
|
|
191
|
-
isNative: true,
|
|
192
|
-
}));
|
|
193
|
-
try {
|
|
194
|
-
// Everything must come from the SAME fresh graph: a Preset from the
|
|
195
|
-
// old graph would provide a context the fresh Text never reads.
|
|
196
|
-
const [{ Text: NativeText }, { Preset: NativePreset }, testUtils, testingLibrary] =
|
|
197
|
-
await Promise.all([
|
|
198
|
-
import("./Text"),
|
|
199
|
-
import("@multiplatform.one/theme"),
|
|
200
|
-
import("@multiplatform.one/test-utils"),
|
|
201
|
-
import("@testing-library/react"),
|
|
202
|
-
]);
|
|
203
|
-
|
|
204
|
-
const renderNative = (node: React.ReactElement, prefixes: string[]): string[] => {
|
|
205
|
-
testUtils.renderWithProviders(node);
|
|
206
|
-
const el = testingLibrary.screen.getByText(LABEL);
|
|
207
|
-
const classes = classesByPrefix(el, prefixes);
|
|
208
|
-
testingLibrary.cleanup();
|
|
209
|
-
return classes;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const high = renderNative(
|
|
213
|
-
<NativePreset overrides={{ textAccent: "high" }}>
|
|
214
|
-
<NativeText>{LABEL}</NativeText>
|
|
215
|
-
</NativePreset>,
|
|
216
|
-
["_col-"],
|
|
217
|
-
);
|
|
218
|
-
const low = renderNative(
|
|
219
|
-
<NativePreset overrides={{ textAccent: "low" }}>
|
|
220
|
-
<NativeText>{LABEL}</NativeText>
|
|
221
|
-
</NativePreset>,
|
|
222
|
-
["_col-"],
|
|
223
|
-
);
|
|
224
|
-
expect(high).not.toHaveLength(0);
|
|
225
|
-
expect(high).not.toEqual(low);
|
|
226
|
-
|
|
227
|
-
const regular = renderNative(
|
|
228
|
-
<NativePreset overrides={{ fontWeight: "regular" }}>
|
|
229
|
-
<NativeText>{LABEL}</NativeText>
|
|
230
|
-
</NativePreset>,
|
|
231
|
-
["_fow-"],
|
|
232
|
-
);
|
|
233
|
-
const bold = renderNative(
|
|
234
|
-
<NativePreset overrides={{ fontWeight: "bold" }}>
|
|
235
|
-
<NativeText>{LABEL}</NativeText>
|
|
236
|
-
</NativePreset>,
|
|
237
|
-
["_fow-"],
|
|
238
|
-
);
|
|
239
|
-
expect(regular).not.toHaveLength(0);
|
|
240
|
-
expect(regular).not.toEqual(bold);
|
|
241
|
-
} finally {
|
|
242
|
-
vi.doUnmock("@multiplatform.one/platform");
|
|
243
|
-
vi.resetModules();
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
});
|
|
184
|
+
// The NATIVE arm lives in ./Text.native.spec.tsx. It used to sit here as a
|
|
185
|
+
// `vi.resetModules()` + dynamic-import block, but that returned a SPLIT module
|
|
186
|
+
// graph — a fresh `renderWithProviders` over a stale `useResolvedKnobs` — and
|
|
187
|
+
// threw "Missing theme." instead of measuring anything. A file-scoped
|
|
188
|
+
// `vi.mock` in its own spec file is one coherent graph.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-38 AC-7, the source half: Tooltip owns no gap constant of its own.
|
|
3
|
+
*
|
|
4
|
+
* The runtime value is asserted where it lives — `layoutTokens.spec.ts` pins
|
|
5
|
+
* `OVERLAY_ANCHOR_GAP` at 4. Asserting that number again from a Tooltip spec
|
|
6
|
+
* proves nothing about Tooltip: it passes just as happily if this component
|
|
7
|
+
* goes back to a hardcoded `offset={4}`, which is exactly the drift the AC
|
|
8
|
+
* was written to close.
|
|
9
|
+
*
|
|
10
|
+
* So this is the source half, in the shape of `panelWidthContract.spec.ts`.
|
|
11
|
+
* A Tooltip is a FREE overlay — no trigger to attach to — so it is one of the
|
|
12
|
+
* few overlays `OVERLAY_ANCHOR_GAP` still governs after the cover ruling
|
|
13
|
+
* (`OVERLAY_ATTACH_GAP = 0`). It must READ that token rather than restate its
|
|
14
|
+
* value under a second name (LC-16 ONE-IMPLEMENTATION).
|
|
15
|
+
*
|
|
16
|
+
* Both twins are checked. The web/native split is where a duplicate constant
|
|
17
|
+
* survives longest: fixing one file and leaving the other still renders the
|
|
18
|
+
* right gap on the platform anyone happened to look at.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { dirname, resolve } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
const tooltipDir = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
/** The web twin, the native twin, and the module they share. */
|
|
28
|
+
const tooltipSources = ["index.tsx", "index.native.tsx", "tooltipShared.tsx"];
|
|
29
|
+
|
|
30
|
+
function read(relative: string): string {
|
|
31
|
+
return readFileSync(resolve(tooltipDir, relative), "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("Tooltip gap contract (MPO-38 AC-7)", () => {
|
|
35
|
+
it.each(tooltipSources)("%s declares no gap constant of its own", (relative) => {
|
|
36
|
+
// Named for the constant this AC deleted, but the assertion is the rule,
|
|
37
|
+
// not the name: any local OFFSET/GAP constant is the same drift again.
|
|
38
|
+
expect(read(relative)).not.toMatch(/\b(?:const|let|var)\s+\w*(?:OFFSET|GAP)\w*\s*=/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it.each(["index.tsx", "index.native.tsx"])(
|
|
42
|
+
"%s positions the bubble with the shared FREE-overlay token",
|
|
43
|
+
(relative) => {
|
|
44
|
+
const source = read(relative);
|
|
45
|
+
expect(source).toMatch(
|
|
46
|
+
/import\s*\{[^}]*\bOVERLAY_ANCHOR_GAP\b[^}]*\}\s*from\s*"@multiplatform\.one\/theme"/s,
|
|
47
|
+
);
|
|
48
|
+
expect(source).toContain("offset={OVERLAY_ANCHOR_GAP}");
|
|
49
|
+
// A literal offset is the regression: it renders the same 4px today and
|
|
50
|
+
// silently stops tracking the token the moment the house rule moves.
|
|
51
|
+
expect(source).not.toMatch(/offset=\{\s*\d/);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Form, Input, Switch, useFormContext } from "@multiplatform.one/forms";
|
|
2
2
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
3
|
+
import { __resetDevWarnSeen } from "@multiplatform.one/theme";
|
|
3
4
|
import { act, fireEvent } from "@testing-library/react";
|
|
4
5
|
import { useEffect } from "react";
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
7
|
import { Wizard, useWizard, type WizardFormLike, type WizardStep } from "./index";
|
|
7
8
|
|
|
8
9
|
const STEPS: WizardStep[] = [
|
|
@@ -214,6 +215,27 @@ describe("Wizard LC-63 / DG-VAL", () => {
|
|
|
214
215
|
});
|
|
215
216
|
});
|
|
216
217
|
|
|
218
|
+
describe("Wizard LC-57 guardrail-clean at rest", () => {
|
|
219
|
+
it("a ready wizard at rest emits zero [theme] guardrail warnings", async () => {
|
|
220
|
+
__resetDevWarnSeen();
|
|
221
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
222
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
223
|
+
try {
|
|
224
|
+
renderWizard();
|
|
225
|
+
await act(async () => {
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
227
|
+
});
|
|
228
|
+
const themeWarns = [...warn.mock.calls, ...error.mock.calls]
|
|
229
|
+
.map((args) => String(args[0]))
|
|
230
|
+
.filter((msg) => msg.startsWith("[theme]"));
|
|
231
|
+
expect(themeWarns).toEqual([]);
|
|
232
|
+
} finally {
|
|
233
|
+
warn.mockRestore();
|
|
234
|
+
error.mockRestore();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
217
239
|
describe("Wizard states", () => {
|
|
218
240
|
it("draws loading, empty, and error shells", () => {
|
|
219
241
|
const loading = renderWithProviders(
|
package/src/Wizard/index.tsx
CHANGED
|
@@ -521,17 +521,10 @@ function WizardRoot({
|
|
|
521
521
|
{...props}
|
|
522
522
|
>
|
|
523
523
|
<YStack data-wizard-rail="" width="100%">
|
|
524
|
-
{/*
|
|
525
|
-
* MPO-81: no `labels` prop here. ProgressStepsProps does not declare
|
|
526
|
-
* one and ProgressSteps reads no such prop, so passing it was inert
|
|
527
|
-
* at runtime and failed the build with TS2322. `hideLabels` still
|
|
528
|
-
* drives the per-step caption below, which is where it has always
|
|
529
|
-
* had a real effect. Re-add the pass in the same change that gives
|
|
530
|
-
* ProgressSteps a labels prop it actually honours.
|
|
531
|
-
*/}
|
|
532
524
|
<ProgressSteps
|
|
533
525
|
steps={railSteps}
|
|
534
526
|
current={current}
|
|
527
|
+
labels={hideLabels ? "hidden" : "visible"}
|
|
535
528
|
compact={compact}
|
|
536
529
|
linear
|
|
537
530
|
onStepChange={(index) => {
|
|
@@ -85,19 +85,19 @@ function greet() {
|
|
|
85
85
|
describe("CodeBlock", () => {
|
|
86
86
|
it("keeps the copy action reachable at the 44px press floor", () => {
|
|
87
87
|
renderWithProviders(<CodeBlock>const x = 1;</CodeBlock>);
|
|
88
|
-
const button = screen.getByLabelText("Copy
|
|
88
|
+
const button = screen.getByLabelText("Copy to clipboard");
|
|
89
89
|
expect(button).toBeTruthy();
|
|
90
90
|
expect(button.getAttribute("data-press-min")).toBe(String(MIN_PRESS_TARGET));
|
|
91
|
+
// Default knob: the glyph-ring end-cap square itself meets the floor
|
|
92
|
+
// (44). Smaller size steps keep the floor by spilling hitSlop instead
|
|
93
|
+
// (CodeBlock.copy.spec covers the 36/52 cap geometry).
|
|
94
|
+
expect(Number(button.getAttribute("data-end-cap"))).toBeGreaterThanOrEqual(MIN_PRESS_TARGET);
|
|
91
95
|
const style = getComputedStyle(button);
|
|
92
96
|
expect(style.display).not.toBe("none");
|
|
93
|
-
const minWidth = Number.parseFloat(style.minWidth);
|
|
94
|
-
const minHeight = Number.parseFloat(style.minHeight);
|
|
95
|
-
if (!Number.isNaN(minWidth)) expect(minWidth).toBeGreaterThanOrEqual(MIN_PRESS_TARGET);
|
|
96
|
-
if (!Number.isNaN(minHeight)) expect(minHeight).toBeGreaterThanOrEqual(MIN_PRESS_TARGET);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("hides the copy action only when disableCopy is set", () => {
|
|
100
100
|
renderWithProviders(<CodeBlock disableCopy>const x = 1;</CodeBlock>);
|
|
101
|
-
expect(screen.queryByLabelText("Copy
|
|
101
|
+
expect(screen.queryByLabelText("Copy to clipboard")).toBeNull();
|
|
102
102
|
});
|
|
103
103
|
});
|
|
@@ -7,21 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { LinearGradient } from "@tamagui/linear-gradient";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { ClipboardIcon } from "@phosphor-icons/react";
|
|
11
|
+
import {
|
|
12
|
+
MIN_PRESS_TARGET,
|
|
13
|
+
isTouchSurface,
|
|
14
|
+
pressTargetHitSlop,
|
|
15
|
+
sizeRecipeForToken,
|
|
16
|
+
useResolvedKnobs,
|
|
17
|
+
} from "@multiplatform.one/theme";
|
|
12
18
|
import { useEffect, useRef, useState } from "react";
|
|
13
19
|
import { useTranslation } from "../../shared/i18n";
|
|
14
20
|
import type { TamaguiElement } from "tamagui";
|
|
15
21
|
import { ScrollView } from "tamagui";
|
|
16
|
-
import { Button, zIndex } from "@multiplatform.one/forms";
|
|
17
|
-
import { Spacer,
|
|
22
|
+
import { Button, Input, zIndex } from "@multiplatform.one/forms";
|
|
23
|
+
import { Spacer, XStack, YStack } from "tamagui";
|
|
18
24
|
import { settleMs } from "../../AnimateHeight";
|
|
19
25
|
import { ErrorBoundary } from "../../ErrorBoundary";
|
|
26
|
+
import { NotifyRegion } from "../../feedback/NotifyHost";
|
|
27
|
+
import { copyAndNotify } from "../../feedback/copy";
|
|
20
28
|
import { Code } from "../Code";
|
|
21
29
|
import { HighlightedCode } from "../HighlightedCode";
|
|
22
30
|
import { useShikiTokens } from "../Highlighter";
|
|
23
31
|
import { Pre } from "../Pre";
|
|
24
|
-
import { useClipboard } from "../../hooks/useClipboard";
|
|
25
32
|
|
|
26
33
|
/** Visible lines before the "show more" clamp (content constraint, LC-18). */
|
|
27
34
|
const VISIBLE_LINES = 22;
|
|
@@ -111,7 +118,14 @@ export function CodeBlock({
|
|
|
111
118
|
}, settleMs(transition));
|
|
112
119
|
};
|
|
113
120
|
const clamped = isCutoff || expandTarget != null;
|
|
114
|
-
|
|
121
|
+
// Copy is the same Input.Button glyph-ring cap the CopyField/Password
|
|
122
|
+
// slot uses (LC-16); confirm is notify() and the glyph does not flip
|
|
123
|
+
// (DG-FB-01 — the D-copyfield verdict replaces TooltipSimple + icon-swap).
|
|
124
|
+
const showCopy = Boolean(codeText) && !disableCopy;
|
|
125
|
+
const activateCopy = () => {
|
|
126
|
+
void copyAndNotify(codeText);
|
|
127
|
+
};
|
|
128
|
+
const capSquare = sizeRecipeForToken(String(codeSize), { touch: isTouchSurface() }).height;
|
|
115
129
|
|
|
116
130
|
return (
|
|
117
131
|
<YStack position="relative" marginBottom="$4">
|
|
@@ -156,6 +170,7 @@ export function CodeBlock({
|
|
|
156
170
|
borderWidth={0}
|
|
157
171
|
{...knobProps.panelPadding}
|
|
158
172
|
size={codeSize}
|
|
173
|
+
data-register="code"
|
|
159
174
|
>
|
|
160
175
|
<HighlightedCode
|
|
161
176
|
tokens={tokens}
|
|
@@ -168,33 +183,35 @@ export function CodeBlock({
|
|
|
168
183
|
</Code>
|
|
169
184
|
</ScrollView>
|
|
170
185
|
</Pre>
|
|
171
|
-
{
|
|
172
|
-
<XStack position="absolute" top=
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
{showCopy && (
|
|
187
|
+
<XStack position="absolute" top={0} insetInlineEnd={0} zIndex={zIndex.localTop}>
|
|
188
|
+
<Input.Button
|
|
189
|
+
glyphRing
|
|
190
|
+
size={codeSize}
|
|
191
|
+
type="button"
|
|
192
|
+
aria-label="Copy to clipboard"
|
|
193
|
+
data-press-min={MIN_PRESS_TARGET}
|
|
194
|
+
hitSlop={capSquare < MIN_PRESS_TARGET ? pressTargetHitSlop(capSquare) : undefined}
|
|
195
|
+
onPress={activateCopy}
|
|
196
|
+
onKeyDown={
|
|
197
|
+
((e: KeyboardEvent) => {
|
|
198
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
activateCopy();
|
|
202
|
+
}
|
|
203
|
+
}) as unknown as () => void
|
|
180
204
|
}
|
|
181
205
|
>
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
minHeight={MIN_PRESS_TARGET}
|
|
187
|
-
data-press-min={MIN_PRESS_TARGET}
|
|
188
|
-
icon={
|
|
189
|
-
hasCopied ? CheckCircleIcon : hasCopyFailed ? WarningCircleIcon : ClipboardIcon
|
|
190
|
-
}
|
|
191
|
-
onPress={onCopy}
|
|
192
|
-
/>
|
|
193
|
-
</TooltipSimple>
|
|
206
|
+
<Input.Icon aria-hidden>
|
|
207
|
+
<ClipboardIcon />
|
|
208
|
+
</Input.Icon>
|
|
209
|
+
</Input.Button>
|
|
194
210
|
</XStack>
|
|
195
211
|
)}
|
|
196
212
|
</YStack>
|
|
197
213
|
</ErrorBoundary>
|
|
214
|
+
<NotifyRegion scope="field" compact />
|
|
198
215
|
</YStack>
|
|
199
216
|
);
|
|
200
217
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type {
|
|
|
15
15
|
export * from "./Badge";
|
|
16
16
|
export * from "./Carousel";
|
|
17
17
|
export * from "./Chip";
|
|
18
|
+
export * from "./CopyField";
|
|
18
19
|
export * from "./DotIndicator";
|
|
19
20
|
export * from "./DropdownMenu";
|
|
20
21
|
export * from "./ContextMenu";
|
|
@@ -154,12 +155,17 @@ export type { AnchorProps, ParagraphProps, SizableTextProps, TextProps } from ".
|
|
|
154
155
|
export { H1, H2, H3, H4, H5, H6, Heading } from "./Heading";
|
|
155
156
|
export type { HeadingProps } from "./Heading";
|
|
156
157
|
|
|
157
|
-
// Shadow tamagui Separator with the house Separator (MPO-47)
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// The
|
|
162
|
-
//
|
|
158
|
+
// Shadow tamagui Separator with the house Separator (MPO-47). Two reasons,
|
|
159
|
+
// both measured: LC-45 / LC-70 — a bare name a house barrel exports is a
|
|
160
|
+
// catalog part, and this one resolved to a raw primitive; and DG-OVF-07 —
|
|
161
|
+
// the raw primitive is a `flex: 1` item, so the rule can absorb free space.
|
|
162
|
+
// The house rule is a painted 1px line in $borderColor (measured law §7.3:
|
|
163
|
+
// 1px never 0.5px; quiet = opacity 0.5, never a lighter colour) that cannot
|
|
164
|
+
// flex-grow. The curated ./tamagui surface no longer exports the bare name;
|
|
165
|
+
// raw stays reachable as TamaguiSeparator.
|
|
166
|
+
//
|
|
167
|
+
// Correction 2026-09-01: the ticket's "96 of 96 paint nothing" was a probe
|
|
168
|
+
// artefact, not a defect — see ./Separator/index.tsx for the measurement.
|
|
163
169
|
export { Separator } from "./Separator";
|
|
164
170
|
export type { SeparatorProps } from "./Separator";
|
|
165
171
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { Preset } from "@multiplatform.one/theme";
|
|
3
|
+
import { cleanup } from "@testing-library/react";
|
|
4
|
+
import { AlertDialog, Dialog, Theme } from "tamagui";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { Button } from "./Button";
|
|
7
|
+
import { AlertDialogContent, DialogContent } from "./surfaces";
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
// ── What this file measures, and why not the class list ──────────────────
|
|
12
|
+
//
|
|
13
|
+
// MPO-49 AC: "DialogContent paints the reference §8 overlay shadow" and "a
|
|
14
|
+
// spec asserts the dialog shadow differs from the Button shadow".
|
|
15
|
+
//
|
|
16
|
+
// The obvious probe — read the frame's `_bxsh-*` atom — passes on a frame
|
|
17
|
+
// that paints something else entirely. `resolveKnobs.elevationPaint` emits
|
|
18
|
+
// its web paint as `style: { boxShadow }`, an INLINE style, and Tamagui only
|
|
19
|
+
// stamps `!important` on PSEUDO atoms (`getCSSStylesAtomic`: `const
|
|
20
|
+
// important = !!pseudo`). A base atom therefore loses the cascade to any
|
|
21
|
+
// inline style on the same node. Measured on `6eb0995`, before this lane:
|
|
22
|
+
//
|
|
23
|
+
// class _bxsh-0px12px24px… (the §8 value — dead)
|
|
24
|
+
// style 0 3px 6px rgba(0,0,0,0.15), 0 2px 4px … (the `$2` value — painted)
|
|
25
|
+
//
|
|
26
|
+
// So these assert the INLINE style, which is what a browser paints, and read
|
|
27
|
+
// the class only to prove the two now agree. Reading the class alone is the
|
|
28
|
+
// same hole MPO-40 / MPO-44 / MPO-48 all fell through, one layer further in:
|
|
29
|
+
// the fragment reaches the frame and still never reaches the pixel.
|
|
30
|
+
|
|
31
|
+
/** Reference §8, tamagui-rendered-reference.md — the one dialog shadow. */
|
|
32
|
+
const referenceOverlayShadow = "0px 12px 24px rgba(0,0,0,0.12)";
|
|
33
|
+
/** Same §8 geometry and alpha, hue inverted — LC-85 forbids a dark umbra. */
|
|
34
|
+
const referenceOverlayShadowDark = "0px 12px 24px rgba(255,255,255,0.12)";
|
|
35
|
+
/** `webShadowLight.$1` — the in-page control stop every Button carries. */
|
|
36
|
+
const controlStopPaint = "0 1px 2px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)";
|
|
37
|
+
/** `webShadowLight.$2` — `elevatedSurface`'s own overlay paint. */
|
|
38
|
+
const elevatedSurfacePaint = "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.10)";
|
|
39
|
+
|
|
40
|
+
function frame(id: string): HTMLElement {
|
|
41
|
+
const el = document.querySelector(`[data-testid="${id}"]`);
|
|
42
|
+
if (!el) throw new Error(`no frame rendered for ${id}`);
|
|
43
|
+
return el as HTMLElement;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The value a browser paints: the inline style wins over every base atom. */
|
|
47
|
+
function paintedShadow(id: string): string {
|
|
48
|
+
return frame(id).style.boxShadow;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shadowAtoms(id: string): string[] {
|
|
52
|
+
return String(frame(id).className || "")
|
|
53
|
+
.split(/\s+/)
|
|
54
|
+
.filter((c) => c.startsWith("_bxsh-"))
|
|
55
|
+
.sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bgAtoms(id: string): string[] {
|
|
59
|
+
return String(frame(id).className || "")
|
|
60
|
+
.split(/\s+/)
|
|
61
|
+
.filter((c) => c.startsWith("_bg-"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderDialogsBesideAButton() {
|
|
65
|
+
renderWithProviders(
|
|
66
|
+
<Preset>
|
|
67
|
+
<Button testID="control">Save</Button>
|
|
68
|
+
<Dialog modal open>
|
|
69
|
+
<Dialog.Portal>
|
|
70
|
+
<DialogContent testID="dialog">dialog</DialogContent>
|
|
71
|
+
</Dialog.Portal>
|
|
72
|
+
</Dialog>
|
|
73
|
+
<AlertDialog open>
|
|
74
|
+
<AlertDialog.Portal>
|
|
75
|
+
<AlertDialogContent testID="alertdialog">confirm</AlertDialogContent>
|
|
76
|
+
</AlertDialog.Portal>
|
|
77
|
+
</AlertDialog>
|
|
78
|
+
</Preset>,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("overlay elevation is a different distance off the surface (MPO-49)", () => {
|
|
83
|
+
it("DialogContent PAINTS the reference §8 shadow, not the $2 fragment underneath it", () => {
|
|
84
|
+
renderDialogsBesideAButton();
|
|
85
|
+
expect(paintedShadow("dialog")).toBe(referenceOverlayShadow);
|
|
86
|
+
expect(paintedShadow("dialog")).not.toBe(elevatedSurfacePaint);
|
|
87
|
+
expect(paintedShadow("dialog")).not.toBe(controlStopPaint);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("the atom and the inline style agree — no dead §8 class over a live $2 paint", () => {
|
|
91
|
+
renderDialogsBesideAButton();
|
|
92
|
+
// The atom half alone does NOT discriminate: it reads `_bxsh-0px12px24px…`
|
|
93
|
+
// with the defect present too, which is why a class-only probe passes
|
|
94
|
+
// against a frame painting `$2`. Pair it with the painted value.
|
|
95
|
+
expect(shadowAtoms("dialog")).toHaveLength(1);
|
|
96
|
+
expect(shadowAtoms("dialog")[0]).toMatch(/^_bxsh-0px12px24px/);
|
|
97
|
+
expect(paintedShadow("dialog")).toContain("12px 24px");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("the dialog shadow differs from the Button shadow, and the Button keeps the $1 stop", () => {
|
|
101
|
+
renderDialogsBesideAButton();
|
|
102
|
+
// "differs" alone does not discriminate: with the defect present the
|
|
103
|
+
// dialog paints `$2` and the Button `$1`, which also differ. The §8 value
|
|
104
|
+
// itself is pinned by the first case in this file; here the control side
|
|
105
|
+
// is pinned to `$1` so a Button regression cannot fake this one either.
|
|
106
|
+
expect(paintedShadow("control")).toBe(controlStopPaint);
|
|
107
|
+
expect(paintedShadow("dialog")).not.toBe(paintedShadow("control"));
|
|
108
|
+
expect(shadowAtoms("dialog")).not.toEqual(shadowAtoms("control"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("AlertDialogContent wears the same §8 shadow — one dialog row, one distance", () => {
|
|
112
|
+
// Tamagui's AlertDialog.Content IS Dialog.Content, and reference §8
|
|
113
|
+
// carries a single dialog row. confirm-dialog-{destructive,gated,warning}
|
|
114
|
+
// render through this frame.
|
|
115
|
+
renderDialogsBesideAButton();
|
|
116
|
+
expect(paintedShadow("alertdialog")).toBe(referenceOverlayShadow);
|
|
117
|
+
expect(paintedShadow("alertdialog")).toBe(paintedShadow("dialog"));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("dark inverts the hue and keeps the §8 geometry; the ground steps up the ramp (LC-85)", () => {
|
|
121
|
+
renderWithProviders(
|
|
122
|
+
<Preset>
|
|
123
|
+
<Dialog modal open>
|
|
124
|
+
<Dialog.Portal>
|
|
125
|
+
<DialogContent testID="dialog">dialog</DialogContent>
|
|
126
|
+
</Dialog.Portal>
|
|
127
|
+
</Dialog>
|
|
128
|
+
</Preset>,
|
|
129
|
+
);
|
|
130
|
+
expect(paintedShadow("dialog")).toBe(referenceOverlayShadow);
|
|
131
|
+
expect(bgAtoms("dialog")).toEqual(["_bg-background"]);
|
|
132
|
+
cleanup();
|
|
133
|
+
|
|
134
|
+
renderWithProviders(
|
|
135
|
+
<Theme name="dark">
|
|
136
|
+
<Preset>
|
|
137
|
+
<Dialog modal open>
|
|
138
|
+
<Dialog.Portal>
|
|
139
|
+
<DialogContent testID="dialog">dialog</DialogContent>
|
|
140
|
+
</Dialog.Portal>
|
|
141
|
+
</Dialog>
|
|
142
|
+
</Preset>
|
|
143
|
+
</Theme>,
|
|
144
|
+
);
|
|
145
|
+
// LC-85 DARK-ELEVATION-TINT: light-coloured shadow, never a black umbra.
|
|
146
|
+
expect(paintedShadow("dialog")).toBe(referenceOverlayShadowDark);
|
|
147
|
+
expect(paintedShadow("dialog")).not.toBe(referenceOverlayShadow);
|
|
148
|
+
expect(paintedShadow("dialog")).not.toMatch(/rgba\(0,\s*0,\s*0/);
|
|
149
|
+
// resolveKnobs owns the dark ground: `overlayFill` steps the filled rest
|
|
150
|
+
// fill up the ramp under dark + elevation (LC-85). Asserted so a later
|
|
151
|
+
// change to that rule shows up here rather than silently on a board.
|
|
152
|
+
expect(bgAtoms("dialog")).toEqual(["_bg-color2"]);
|
|
153
|
+
});
|
|
154
|
+
});
|
package/src/surfaces.spec.tsx
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { Preset } from "@multiplatform.one/theme";
|
|
3
|
+
import { cleanup } from "@testing-library/react";
|
|
1
4
|
import { readFileSync } from "node:fs";
|
|
2
5
|
import { dirname, join } from "node:path";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { Card as TamaguiCard, Dialog, Popover, Sheet } from "tamagui";
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { Button } from "./Button";
|
|
6
10
|
import * as pkg from "./index";
|
|
7
11
|
import {
|
|
8
12
|
Card,
|
|
@@ -21,6 +25,8 @@ import {
|
|
|
21
25
|
SheetFrame,
|
|
22
26
|
} from "./surfaces";
|
|
23
27
|
|
|
28
|
+
afterEach(cleanup);
|
|
29
|
+
|
|
24
30
|
describe("surfaces package exports", () => {
|
|
25
31
|
it("package Card is the themed surface, not the tamagui primitive", () => {
|
|
26
32
|
expect(pkg.Card).toBe(Card);
|
|
@@ -86,3 +92,94 @@ describe("DialogContent overlay elevation (MPO-49)", () => {
|
|
|
86
92
|
expect(dialog).toContain("panelPadding");
|
|
87
93
|
});
|
|
88
94
|
});
|
|
95
|
+
|
|
96
|
+
// ── Render proofs ────────────────────────────────────────────────────────
|
|
97
|
+
//
|
|
98
|
+
// The source-text assertions above prove the CALL SITE. These prove the
|
|
99
|
+
// FRAME, which is the distinction MPO-40 / MPO-44 / MPO-48 all fell through:
|
|
100
|
+
// a fragment that exists in source and dies on the way to the frame reads
|
|
101
|
+
// identical to one that never existed.
|
|
102
|
+
//
|
|
103
|
+
// happy-dom cannot cascade Tamagui's class CSS, so — as in Button.spec.tsx —
|
|
104
|
+
// these read the atomic classes the knob emits. The atom name is hashed from
|
|
105
|
+
// the VALUE, so the class IS the value, and no theme config is needed: what
|
|
106
|
+
// the fragment addresses is visible in the class name itself.
|
|
107
|
+
//
|
|
108
|
+
// Measured both ways on 2026-09-01 by reverting each defect in the working
|
|
109
|
+
// tree and re-running (see the MR description):
|
|
110
|
+
//
|
|
111
|
+
// surface addressing defect `$color1` -> `_bg-color1`
|
|
112
|
+
// fixed `$background` -> `_bg-background`
|
|
113
|
+
// dialog elevation defect Button/control `$1` -> `_bxsh-0px1px2px…`
|
|
114
|
+
// fixed reference §8 -> `_bxsh-0px12px24px…`
|
|
115
|
+
//
|
|
116
|
+
// A "differs from the Button" assertion does NOT discriminate: the Button
|
|
117
|
+
// paints `0 6px 12px` from its own source in both states, so that comparison
|
|
118
|
+
// passes with the defect present. Assert against the $1 CONTROL stop.
|
|
119
|
+
|
|
120
|
+
function atomsWithPrefix(el: Element | null, prefix: string): string[] {
|
|
121
|
+
if (!el) return [];
|
|
122
|
+
return String((el as HTMLElement).className || "")
|
|
123
|
+
.split(/\s+/)
|
|
124
|
+
.filter((c) => c.startsWith(prefix))
|
|
125
|
+
.sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The `$1` control paint — `webShadowLight.$1`, "0 1px 2px …" (resolveKnobs.ts). */
|
|
129
|
+
const controlStopShadowAtom = /^_bxsh-0px1px2px/;
|
|
130
|
+
/** Rendered reference §8 DialogContent — `rgba(0,0,0,0.12) 0 12px 24px`. */
|
|
131
|
+
const overlayStopShadowAtom = /^_bxsh-0px12px24px/;
|
|
132
|
+
|
|
133
|
+
describe("surfaces address $background at the FRAME (MPO-19 X5/X6)", () => {
|
|
134
|
+
it("Card, Panel and DialogContent all paint _bg-background, never a raw ramp step", () => {
|
|
135
|
+
renderWithProviders(
|
|
136
|
+
<Preset>
|
|
137
|
+
<Card testID="mpo-card">card</Card>
|
|
138
|
+
<Panel testID="mpo-panel">panel</Panel>
|
|
139
|
+
<Dialog modal open>
|
|
140
|
+
<Dialog.Portal>
|
|
141
|
+
<DialogContent testID="mpo-dialog">dialog</DialogContent>
|
|
142
|
+
</Dialog.Portal>
|
|
143
|
+
</Dialog>
|
|
144
|
+
</Preset>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
for (const id of ["mpo-card", "mpo-panel", "mpo-dialog"]) {
|
|
148
|
+
const frame = document.querySelector(`[data-testid="${id}"]`);
|
|
149
|
+
expect(frame, `${id} frame`).toBeTruthy();
|
|
150
|
+
const bg = atomsWithPrefix(frame, "_bg-");
|
|
151
|
+
expect(bg, `${id} background atom`).toEqual(["_bg-background"]);
|
|
152
|
+
expect(bg.join(" "), `${id} must not address a raw ramp step`).not.toMatch(/_bg-color[12]\b/);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("DialogContent paints the OVERLAY elevation stop, not the control one (MPO-49)", () => {
|
|
158
|
+
it("renders the reference §8 shadow and not the Button $1 stop", () => {
|
|
159
|
+
renderWithProviders(
|
|
160
|
+
<Preset>
|
|
161
|
+
<Button testID="mpo-button-control">Save</Button>
|
|
162
|
+
<Dialog modal open>
|
|
163
|
+
<Dialog.Portal>
|
|
164
|
+
<DialogContent testID="mpo-dialog-overlay">body</DialogContent>
|
|
165
|
+
</Dialog.Portal>
|
|
166
|
+
</Dialog>
|
|
167
|
+
</Preset>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const dialog = document.querySelector('[data-testid="mpo-dialog-overlay"]');
|
|
171
|
+
const button = document.querySelector('[data-testid="mpo-button-control"]');
|
|
172
|
+
expect(dialog, "DialogContent frame").toBeTruthy();
|
|
173
|
+
expect(button, "Button frame").toBeTruthy();
|
|
174
|
+
|
|
175
|
+
const dialogShadow = atomsWithPrefix(dialog, "_bxsh-");
|
|
176
|
+
expect(dialogShadow, "DialogContent paints exactly one shadow atom").toHaveLength(1);
|
|
177
|
+
expect(dialogShadow[0], "dialog wears the §8 overlay shadow").toMatch(overlayStopShadowAtom);
|
|
178
|
+
expect(dialogShadow[0], "dialog must not wear the $1 control stop").not.toMatch(
|
|
179
|
+
controlStopShadowAtom,
|
|
180
|
+
);
|
|
181
|
+
expect(dialogShadow, "dialog and control are different distances off the surface").not.toEqual(
|
|
182
|
+
atomsWithPrefix(button, "_bxsh-"),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|