@multiplatform.one/components 7.83.1 → 7.84.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/Alert/index.cjs +1 -1
- package/dist/cjs/Alert/index.native.js +1 -1
- package/dist/cjs/Alert/index.native.js.map +1 -1
- package/dist/cjs/Carousel/index.cjs +11 -9
- package/dist/cjs/Carousel/index.native.js +12 -10
- package/dist/cjs/Carousel/index.native.js.map +1 -1
- package/dist/cjs/Chip/index.cjs +2 -1
- package/dist/cjs/Chip/index.native.js +2 -1
- package/dist/cjs/Chip/index.native.js.map +1 -1
- package/dist/cjs/Meter/meter.cjs +1 -1
- package/dist/cjs/Meter/meter.native.js +1 -1
- package/dist/cjs/Meter/meter.native.js.map +1 -1
- package/dist/cjs/SheetModal/index.cjs +33 -5
- package/dist/cjs/appShell/TabBar.cjs +1 -1
- package/dist/cjs/appShell/TabBar.native.js +1 -1
- package/dist/cjs/appShell/TabBar.native.js.map +1 -1
- package/dist/cjs/appShell/TabHost.native.js +52 -5
- package/dist/cjs/appShell/TabHost.native.js.map +1 -1
- package/dist/cjs/layouts/PageTemplates.cjs +15 -12
- package/dist/cjs/layouts/PageTemplates.native.js +16 -13
- package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
- package/dist/cjs/layouts/bottomAccessory.cjs +25 -13
- package/dist/cjs/layouts/bottomAccessory.native.js +29 -13
- package/dist/cjs/layouts/bottomAccessory.native.js.map +1 -1
- package/dist/cjs/layouts/page.cjs +44 -6
- package/dist/cjs/layouts/page.native.js +48 -6
- package/dist/cjs/layouts/page.native.js.map +1 -1
- package/dist/cjs/layouts/tags.cjs +25 -12
- package/dist/cjs/layouts/tags.native.js +25 -16
- package/dist/cjs/layouts/tags.native.js.map +1 -1
- package/dist/esm/Alert/index.mjs +1 -1
- package/dist/esm/Alert/index.mjs.map +1 -1
- package/dist/esm/Alert/index.native.js +1 -1
- package/dist/esm/Alert/index.native.js.map +1 -1
- package/dist/esm/Carousel/index.mjs +12 -10
- package/dist/esm/Carousel/index.mjs.map +1 -1
- package/dist/esm/Carousel/index.native.js +13 -11
- package/dist/esm/Carousel/index.native.js.map +1 -1
- package/dist/esm/Chip/index.mjs +3 -2
- package/dist/esm/Chip/index.mjs.map +1 -1
- package/dist/esm/Chip/index.native.js +3 -2
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/Meter/meter.mjs +1 -1
- package/dist/esm/Meter/meter.mjs.map +1 -1
- package/dist/esm/Meter/meter.native.js +1 -1
- package/dist/esm/Meter/meter.native.js.map +1 -1
- package/dist/esm/SheetModal/index.mjs +34 -6
- package/dist/esm/SheetModal/index.mjs.map +1 -1
- package/dist/esm/appShell/TabBar.mjs +2 -2
- package/dist/esm/appShell/TabBar.mjs.map +1 -1
- package/dist/esm/appShell/TabBar.native.js +2 -2
- package/dist/esm/appShell/TabBar.native.js.map +1 -1
- package/dist/esm/appShell/TabHost.native.js +53 -9
- package/dist/esm/appShell/TabHost.native.js.map +1 -1
- package/dist/esm/layouts/PageTemplates.mjs +16 -13
- package/dist/esm/layouts/PageTemplates.mjs.map +1 -1
- package/dist/esm/layouts/PageTemplates.native.js +17 -14
- package/dist/esm/layouts/PageTemplates.native.js.map +1 -1
- package/dist/esm/layouts/bottomAccessory.mjs +26 -15
- package/dist/esm/layouts/bottomAccessory.mjs.map +1 -1
- package/dist/esm/layouts/bottomAccessory.native.js +30 -15
- package/dist/esm/layouts/bottomAccessory.native.js.map +1 -1
- package/dist/esm/layouts/page.mjs +44 -8
- package/dist/esm/layouts/page.mjs.map +1 -1
- package/dist/esm/layouts/page.native.js +48 -8
- package/dist/esm/layouts/page.native.js.map +1 -1
- package/dist/esm/layouts/tags.mjs +28 -15
- package/dist/esm/layouts/tags.mjs.map +1 -1
- package/dist/esm/layouts/tags.native.js +28 -19
- package/dist/esm/layouts/tags.native.js.map +1 -1
- package/dist/jsx/Alert/index.mjs +1 -1
- package/dist/jsx/Alert/index.mjs.map +1 -1
- package/dist/jsx/Alert/index.native.js +1 -1
- package/dist/jsx/Alert/index.native.js.map +1 -1
- package/dist/jsx/Carousel/index.mjs +12 -10
- package/dist/jsx/Carousel/index.mjs.map +1 -1
- package/dist/jsx/Carousel/index.native.js +12 -10
- package/dist/jsx/Carousel/index.native.js.map +1 -1
- package/dist/jsx/Chip/index.mjs +3 -2
- package/dist/jsx/Chip/index.mjs.map +1 -1
- package/dist/jsx/Chip/index.native.js +2 -1
- package/dist/jsx/Chip/index.native.js.map +1 -1
- package/dist/jsx/Meter/meter.mjs +1 -1
- package/dist/jsx/Meter/meter.mjs.map +1 -1
- package/dist/jsx/Meter/meter.native.js +1 -1
- package/dist/jsx/Meter/meter.native.js.map +1 -1
- package/dist/jsx/SheetModal/index.mjs +34 -6
- package/dist/jsx/SheetModal/index.mjs.map +1 -1
- package/dist/jsx/appShell/TabBar.mjs +2 -2
- package/dist/jsx/appShell/TabBar.mjs.map +1 -1
- package/dist/jsx/appShell/TabBar.native.js +1 -1
- package/dist/jsx/appShell/TabBar.native.js.map +1 -1
- package/dist/jsx/appShell/TabHost.native.js +52 -5
- package/dist/jsx/appShell/TabHost.native.js.map +1 -1
- package/dist/jsx/layouts/PageTemplates.mjs +16 -13
- package/dist/jsx/layouts/PageTemplates.mjs.map +1 -1
- package/dist/jsx/layouts/PageTemplates.native.js +16 -13
- package/dist/jsx/layouts/PageTemplates.native.js.map +1 -1
- package/dist/jsx/layouts/bottomAccessory.mjs +26 -15
- package/dist/jsx/layouts/bottomAccessory.mjs.map +1 -1
- package/dist/jsx/layouts/bottomAccessory.native.js +29 -13
- package/dist/jsx/layouts/bottomAccessory.native.js.map +1 -1
- package/dist/jsx/layouts/page.mjs +44 -8
- package/dist/jsx/layouts/page.mjs.map +1 -1
- package/dist/jsx/layouts/page.native.js +48 -6
- package/dist/jsx/layouts/page.native.js.map +1 -1
- package/dist/jsx/layouts/tags.mjs +28 -15
- package/dist/jsx/layouts/tags.mjs.map +1 -1
- package/dist/jsx/layouts/tags.native.js +25 -16
- package/dist/jsx/layouts/tags.native.js.map +1 -1
- package/package.json +9 -9
- package/src/Alert/Alert.spec.tsx +82 -1
- package/src/Alert/index.tsx +1 -1
- package/src/Carousel/Carousel.native.spec.tsx +25 -8
- package/src/Carousel/index.tsx +18 -11
- package/src/Chip/Chip.spec.tsx +33 -0
- package/src/Chip/index.tsx +10 -5
- package/src/Meter/Meter.spec.tsx +5 -4
- package/src/Meter/meter.ts +6 -4
- package/src/SheetModal/SheetModal.regularWidth.spec.tsx +87 -0
- package/src/SheetModal/index.tsx +52 -5
- package/src/Text.stories.tsx +13 -0
- package/src/appShell/TabBar.spec.tsx +10 -4
- package/src/appShell/TabBar.tsx +4 -8
- package/src/appShell/TabHost.native.spec.tsx +109 -1
- package/src/appShell/TabHost.native.tsx +84 -13
- package/src/hoverTouchPath.coarse.spec.tsx +166 -0
- package/src/icons/themedButtonInk.spec.tsx +117 -0
- package/src/layouts/PageTemplates.tsx +14 -11
- package/src/layouts/Screen.chromeClearance.spec.tsx +28 -0
- package/src/layouts/bottomAccessory.tsx +40 -15
- package/src/layouts/page.tsx +73 -19
- package/src/layouts/pageTitleOneLine.spec.tsx +42 -0
- package/src/layouts/pushedPageTitle.spec.tsx +116 -0
- package/src/layouts/tags-token-field.spec.tsx +21 -5
- package/src/layouts/tags.tsx +36 -20
- package/types/Carousel/index.d.ts.map +1 -1
- package/types/Chip/index.d.ts.map +1 -1
- package/types/Meter/meter.d.ts +6 -4
- package/types/Meter/meter.d.ts.map +1 -1
- package/types/SheetModal/index.d.ts.map +1 -1
- package/types/appShell/TabBar.d.ts.map +1 -1
- package/types/appShell/TabHost.native.d.ts +13 -1
- package/types/appShell/TabHost.native.d.ts.map +1 -1
- package/types/layouts/PageTemplates.d.ts.map +1 -1
- package/types/layouts/bottomAccessory.d.ts +6 -1
- package/types/layouts/bottomAccessory.d.ts.map +1 -1
- package/types/layouts/page.d.ts +16 -1
- package/types/layouts/page.d.ts.map +1 -1
- package/types/layouts/tags.d.ts.map +1 -1
package/src/Alert/Alert.spec.tsx
CHANGED
|
@@ -203,7 +203,7 @@ describe("Alert nested action press-floor paint (LC-54 / LC-71 / LC-73)", () =>
|
|
|
203
203
|
expect(floor).not.toBeNull();
|
|
204
204
|
expect(floor.className).not.toMatch(paintedStateAtom);
|
|
205
205
|
const pill = floor.firstElementChild as HTMLElement;
|
|
206
|
-
expect(pill.className).toMatch(/_bg-
|
|
206
|
+
expect(pill.className).toMatch(/_bg-_groupalertAction-hover_color5/);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it("action activates via Enter on the floor", () => {
|
|
@@ -320,6 +320,87 @@ describe("Alert action targets size to their labels on web (FZ-8000)", () => {
|
|
|
320
320
|
});
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
// FZ-7792: with a mouse the pointer is still over the pill while it is held,
|
|
324
|
+
// so the hover fill and the press fill both match. The press fill has to win
|
|
325
|
+
// the cascade, or the pill never shows its press colour on web.
|
|
326
|
+
describe("Alert action pill press fill beats its hover fill on web (FZ-7792)", () => {
|
|
327
|
+
afterEach(cleanup);
|
|
328
|
+
|
|
329
|
+
type Painted = { selector: string; important: boolean; specificity: number[]; order: number };
|
|
330
|
+
|
|
331
|
+
function specificity(selector: string): number[] {
|
|
332
|
+
const s = selector.replace(/::[\w-]+/g, "");
|
|
333
|
+
const ids = (s.match(/#[\w-]+/g) ?? []).length;
|
|
334
|
+
const classes = (s.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) ?? []).length;
|
|
335
|
+
const types = (s.match(/(^|[\s>+~])[a-zA-Z][\w-]*/g) ?? []).length;
|
|
336
|
+
return [ids, classes, types];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function backgroundRules(classes: string[]): Painted[] {
|
|
340
|
+
const found: Painted[] = [];
|
|
341
|
+
let order = 0;
|
|
342
|
+
const walk = (rules: CSSRuleList) => {
|
|
343
|
+
for (const rule of Array.from(rules)) {
|
|
344
|
+
order += 1;
|
|
345
|
+
if (rule instanceof CSSStyleRule) {
|
|
346
|
+
const value = rule.style.getPropertyValue("background-color");
|
|
347
|
+
if (!value) continue;
|
|
348
|
+
for (const selector of rule.selectorText.split(",").map((part) => part.trim())) {
|
|
349
|
+
const last = selector.split(/[\s>+~]+/).pop() ?? "";
|
|
350
|
+
if (!classes.some((c) => last.includes(`.${c}`))) continue;
|
|
351
|
+
found.push({
|
|
352
|
+
selector,
|
|
353
|
+
important: rule.style.getPropertyPriority("background-color") === "important",
|
|
354
|
+
specificity: specificity(selector),
|
|
355
|
+
order,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} else if ("cssRules" in rule) {
|
|
359
|
+
walk((rule as CSSGroupingRule).cssRules);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
for (const sheet of Array.from(document.styleSheets)) walk(sheet.cssRules);
|
|
364
|
+
return found;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const weight = (r: Painted) => [r.important ? 1 : 0, ...r.specificity, r.order];
|
|
368
|
+
const outranks = (a: Painted, b: Painted) => {
|
|
369
|
+
const wa = weight(a);
|
|
370
|
+
const wb = weight(b);
|
|
371
|
+
for (let i = 0; i < wa.length; i++) if (wa[i] !== wb[i]) return wa[i] > wb[i];
|
|
372
|
+
return false;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
it("the held pill paints $color6 over the $color5 hover fill", () => {
|
|
376
|
+
renderWithProviders(
|
|
377
|
+
<Alert
|
|
378
|
+
intent="warning"
|
|
379
|
+
title="Trial expiring"
|
|
380
|
+
actionLabel="Upgrade"
|
|
381
|
+
secondaryActionLabel="Remind me"
|
|
382
|
+
onAction={() => {}}
|
|
383
|
+
onSecondaryAction={() => {}}
|
|
384
|
+
>
|
|
385
|
+
Your trial ends in 3 days.
|
|
386
|
+
</Alert>,
|
|
387
|
+
);
|
|
388
|
+
for (const label of ["Upgrade", "Remind me"]) {
|
|
389
|
+
const pill = (document.querySelector(`[aria-label="${label}"]`) as HTMLElement)
|
|
390
|
+
.firstElementChild as HTMLElement;
|
|
391
|
+
const classes = String(pill.className).split(/\s+/).filter(Boolean);
|
|
392
|
+
const painted = backgroundRules(classes);
|
|
393
|
+
const hover = painted.find((r) => /hover/.test(r.selector));
|
|
394
|
+
const press = painted.find((r) => /:active|press/.test(r.selector));
|
|
395
|
+
expect(hover?.selector, `${label} hover fill`).toMatch(/color5/);
|
|
396
|
+
expect(press?.selector, `${label} press fill`).toMatch(/color6/);
|
|
397
|
+
expect(outranks(press as Painted, hover as Painted), JSON.stringify({ hover, press })).toBe(
|
|
398
|
+
true,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
323
404
|
describe("Alert spacing roles hold at every size token (LC-100)", () => {
|
|
324
405
|
afterEach(cleanup);
|
|
325
406
|
|
package/src/Alert/index.tsx
CHANGED
|
@@ -197,7 +197,7 @@ const AlertActionPill = styled(View, {
|
|
|
197
197
|
paddingVertical: "$1",
|
|
198
198
|
alignItems: "center",
|
|
199
199
|
justifyContent: "center",
|
|
200
|
-
|
|
200
|
+
"$group-alertAction-hover": { backgroundColor: "$color5" },
|
|
201
201
|
"$group-alertAction-press": { backgroundColor: "$color6" },
|
|
202
202
|
});
|
|
203
203
|
|
|
@@ -110,8 +110,7 @@ describe("Carousel native snapping (FZ-7677)", () => {
|
|
|
110
110
|
expect(host().onMomentumScrollEnd).toBeTypeOf("function");
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it
|
|
114
|
-
state.rtl = rtl;
|
|
113
|
+
it("settles offsets and dots only after momentum", () => {
|
|
115
114
|
const onSlideChange = vi.fn();
|
|
116
115
|
render({ gap: 16, onSlideChange });
|
|
117
116
|
measure();
|
|
@@ -138,24 +137,26 @@ describe("Carousel native snapping (FZ-7677)", () => {
|
|
|
138
137
|
"preserves loop navigation and ignores duplicate settle events, RTL=%s",
|
|
139
138
|
(rtl) => {
|
|
140
139
|
state.rtl = rtl;
|
|
140
|
+
const x = (index: number) => (rtl ? 2 - index : index) * 416;
|
|
141
141
|
const onSlideChange = vi.fn();
|
|
142
142
|
render({ gap: 16, loop: true, onSlideChange });
|
|
143
143
|
measure();
|
|
144
144
|
act(() => (state.dots.onChange as Function)(-1));
|
|
145
|
-
expect(scrollTo).toHaveBeenLastCalledWith({ x:
|
|
146
|
-
settle(
|
|
145
|
+
expect(scrollTo).toHaveBeenLastCalledWith({ x: x(2), animated: true });
|
|
146
|
+
settle(x(2));
|
|
147
147
|
expect(state.dots.activeIndex).toBe(2);
|
|
148
148
|
expect(onSlideChange).toHaveBeenCalledExactlyOnceWith(2);
|
|
149
149
|
act(() => (state.dots.onChange as Function)(3));
|
|
150
|
-
expect(scrollTo).toHaveBeenLastCalledWith({ x: 0, animated: true });
|
|
151
|
-
settle(0);
|
|
150
|
+
expect(scrollTo).toHaveBeenLastCalledWith({ x: x(0), animated: true });
|
|
151
|
+
settle(x(0));
|
|
152
152
|
expect(state.dots.activeIndex).toBe(0);
|
|
153
153
|
expect(onSlideChange).toHaveBeenCalledTimes(2);
|
|
154
154
|
},
|
|
155
155
|
);
|
|
156
|
-
|
|
156
|
+
|
|
157
|
+
it.each(["ios", "android"])("uses physical left-edge offsets in RTL on %s", (os) => {
|
|
157
158
|
state.rtl = true;
|
|
158
|
-
(runtime.reactNative.Platform as { OS: string }).OS =
|
|
159
|
+
(runtime.reactNative.Platform as { OS: string }).OS = os;
|
|
159
160
|
render({ gap: 16 });
|
|
160
161
|
measure();
|
|
161
162
|
expect(scrollTo).toHaveBeenLastCalledWith({ x: 832, animated: false });
|
|
@@ -195,4 +196,20 @@ describe("Carousel native snapping (FZ-7677)", () => {
|
|
|
195
196
|
settle(-1000);
|
|
196
197
|
expect(state.dots.activeIndex).toBe(0);
|
|
197
198
|
});
|
|
199
|
+
|
|
200
|
+
it.each([false, true])("puts next on the trailing edge, RTL=%s", (rtl) => {
|
|
201
|
+
state.rtl = rtl;
|
|
202
|
+
render({ gap: 16 });
|
|
203
|
+
measure();
|
|
204
|
+
const side = (label: string) => {
|
|
205
|
+
const props = runtime
|
|
206
|
+
.allHostProps("View")
|
|
207
|
+
.filter((p) => p["aria-label"] === label || p.accessibilityLabel === label)
|
|
208
|
+
.at(-1);
|
|
209
|
+
const style = Object.assign({}, props, ...[props?.style].flat(Infinity));
|
|
210
|
+
return "left" in style ? "left" : "right" in style ? "right" : undefined;
|
|
211
|
+
};
|
|
212
|
+
expect(side("Previous slide")).toBe("left");
|
|
213
|
+
expect(side("Next slide")).toBe("right");
|
|
214
|
+
});
|
|
198
215
|
});
|
package/src/Carousel/index.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
useResolvedKnobs,
|
|
17
17
|
} from "@multiplatform.one/theme";
|
|
18
18
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
19
|
-
import {
|
|
19
|
+
import { I18nManager } from "react-native";
|
|
20
20
|
import { useState, useRef, useCallback, useEffect, Children } from "react";
|
|
21
21
|
import { useTranslation } from "../shared/i18n";
|
|
22
22
|
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
|
|
@@ -194,7 +194,10 @@ export function Carousel({
|
|
|
194
194
|
const arrowHitSlop = pressTargetHitSlop(arrowSize);
|
|
195
195
|
const [activeIndex, setActiveIndex] = useState(initialSlide);
|
|
196
196
|
const isRTL = useDirection() === "rtl";
|
|
197
|
-
|
|
197
|
+
// A native RTL track lays slide 0 at the physical right, but contentOffset
|
|
198
|
+
// and scrollTo stay physical on iOS (Fabric) and Android alike, so slide i
|
|
199
|
+
// sits at offset (count - 1 - i) × pitch (FZ-7677, iOS 27 simulator).
|
|
200
|
+
const isNativeRTL = !isWeb && isRTL;
|
|
198
201
|
const activeIndexRef = useRef(activeIndex);
|
|
199
202
|
activeIndexRef.current = activeIndex;
|
|
200
203
|
const trackRef = useRef<TamaguiScrollView | null>(null);
|
|
@@ -219,10 +222,10 @@ export function Carousel({
|
|
|
219
222
|
(index: number) => {
|
|
220
223
|
if (!trackRef.current) return;
|
|
221
224
|
if (index < 0 || index >= slideCount) return;
|
|
222
|
-
const offset = (
|
|
225
|
+
const offset = (isNativeRTL ? slideCount - 1 - index : index) * (slideWidth + gapPx);
|
|
223
226
|
trackRef.current.scrollTo({ x: offset, animated: animateSnaps });
|
|
224
227
|
},
|
|
225
|
-
[slideCount, slideWidth, gapPx, animateSnaps,
|
|
228
|
+
[slideCount, slideWidth, gapPx, animateSnaps, isNativeRTL],
|
|
226
229
|
);
|
|
227
230
|
|
|
228
231
|
const goToSlide = useCallback(
|
|
@@ -250,7 +253,7 @@ export function Carousel({
|
|
|
250
253
|
(event: { nativeEvent: { contentOffset: { x: number } } }) => {
|
|
251
254
|
if (!slideWidth || slideCount === 0) return;
|
|
252
255
|
const offset = event.nativeEvent.contentOffset.x;
|
|
253
|
-
const scrollX =
|
|
256
|
+
const scrollX = isNativeRTL ? (slideCount - 1) * (slideWidth + gapPx) - offset : offset;
|
|
254
257
|
const roundedIndex = Math.round(scrollX / (slideWidth + gapPx));
|
|
255
258
|
const newIndex = isWeb ? roundedIndex : Math.max(0, Math.min(roundedIndex, slideCount - 1));
|
|
256
259
|
if (newIndex !== activeIndexRef.current && newIndex >= 0 && newIndex < slideCount) {
|
|
@@ -259,7 +262,7 @@ export function Carousel({
|
|
|
259
262
|
onSlideChange?.(newIndex);
|
|
260
263
|
}
|
|
261
264
|
},
|
|
262
|
-
[slideCount, onSlideChange, slideWidth, gapPx,
|
|
265
|
+
[slideCount, onSlideChange, slideWidth, gapPx, isNativeRTL],
|
|
263
266
|
);
|
|
264
267
|
|
|
265
268
|
const handleScrollEndDrag = useCallback(
|
|
@@ -302,10 +305,10 @@ export function Carousel({
|
|
|
302
305
|
if (!slideWidth || slideCount === 0 || !trackRef.current) return;
|
|
303
306
|
const index = Math.max(0, Math.min(activeIndexRef.current, slideCount - 1));
|
|
304
307
|
trackRef.current.scrollTo({
|
|
305
|
-
x: (
|
|
308
|
+
x: (isNativeRTL ? slideCount - 1 - index : index) * (slideWidth + gapPx),
|
|
306
309
|
animated: false,
|
|
307
310
|
});
|
|
308
|
-
}, [slideWidth, gapPx, slideCount,
|
|
311
|
+
}, [slideWidth, gapPx, slideCount, isNativeRTL]);
|
|
309
312
|
|
|
310
313
|
const canGoPrev = loop || activeIndex > 0;
|
|
311
314
|
const canGoNext = loop || activeIndex < slideCount - 1;
|
|
@@ -317,9 +320,13 @@ export function Carousel({
|
|
|
317
320
|
// and the glyph mirror together (glyph swap keyed on useDirection — the
|
|
318
321
|
// one mechanism for directional chrome): each edge button keeps pointing
|
|
319
322
|
// outward, toward the slide it navigates to. Semantics (labels, disabled)
|
|
320
|
-
// stay bound to the prev/next action, not the physical side.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
// stay bound to the prev/next action, not the physical side. Native RN
|
|
324
|
+
// already swaps left and right in RTL (doLeftAndRightSwapInRTL), so there
|
|
325
|
+
// the side prop stays logical or the arrows land back where LTR has them.
|
|
326
|
+
const swapsSides = isNativeRTL && I18nManager.getConstants?.().doLeftAndRightSwapInRTL !== false;
|
|
327
|
+
const mirrorSides = isRTL && !swapsSides;
|
|
328
|
+
const prevSide = mirrorSides ? ("right" as const) : ("left" as const);
|
|
329
|
+
const nextSide = mirrorSides ? ("left" as const) : ("right" as const);
|
|
323
330
|
const PrevGlyph = isRTL ? CaretRightIcon : CaretLeftIcon;
|
|
324
331
|
const NextGlyph = isRTL ? CaretLeftIcon : CaretRightIcon;
|
|
325
332
|
|
package/src/Chip/Chip.spec.tsx
CHANGED
|
@@ -284,6 +284,39 @@ describe("Chip on a preset's token scale (FZ-7158 WP-a2)", () => {
|
|
|
284
284
|
});
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
+
describe("Chip vertical inset on every preset's scale (FZ-8018 theme matrix)", () => {
|
|
288
|
+
const sizes = ["$2", "$3", "$4", "$5"] as const;
|
|
289
|
+
|
|
290
|
+
it("draws the default scale exactly as a quarter of padX", () => {
|
|
291
|
+
for (const size of sizes) {
|
|
292
|
+
const { paddingHorizontal, paddingVertical } = chipFrameGeometry(size);
|
|
293
|
+
expect(paddingVertical).toBe(Math.round(paddingHorizontal * 0.25));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("paints a space token on the installed scale wherever the default does", () => {
|
|
298
|
+
const themeConfig = createDefaultThemeConfig({ presets: { dense: densePreset } });
|
|
299
|
+
const dense = themeConfig.scales?.dense;
|
|
300
|
+
const defaults = themeConfig.scales?.[DEFAULT_SCALE_ID];
|
|
301
|
+
if (!dense || !defaults) throw new Error("scales missing");
|
|
302
|
+
const onDefault = Object.fromEntries(
|
|
303
|
+
sizes.map((size) => [size, chipFrameGeometry(size).paddingVertical]),
|
|
304
|
+
);
|
|
305
|
+
const defaultSpace = new Set(Object.values(defaults.tokens.space));
|
|
306
|
+
const denseSpace = new Set(Object.values(dense.tokens.space));
|
|
307
|
+
expect(defaultSpace.has(onDefault.$3)).toBe(true);
|
|
308
|
+
try {
|
|
309
|
+
applyThemeScale(dense);
|
|
310
|
+
for (const size of sizes) {
|
|
311
|
+
if (!defaultSpace.has(onDefault[size])) continue;
|
|
312
|
+
expect(denseSpace.has(chipFrameGeometry(size).paddingVertical)).toBe(true);
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
applyThemeScale(defaults);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
287
320
|
describe("Chip label line height", () => {
|
|
288
321
|
for (const [size, key] of [
|
|
289
322
|
[undefined, "3"],
|
package/src/Chip/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
pressTargetHitSlop,
|
|
9
9
|
radiusStopFromToken,
|
|
10
10
|
resolveRadiusClass,
|
|
11
|
+
scaleSpaceFractionPx,
|
|
11
12
|
sizeRecipeForToken,
|
|
12
13
|
sizeKeyBelow,
|
|
13
14
|
useGlyphColor,
|
|
@@ -192,9 +193,11 @@ export function chipTone(variant: ChipVariant | undefined): ChipTone {
|
|
|
192
193
|
* Chip geometry rides the GENERATED `chip` family (LC-75 / LC-80):
|
|
193
194
|
* padX / font / icon / inner gap come from the recipe table — no handwritten
|
|
194
195
|
* per-size px map. The pill has no recipe height: it hugs its label, and the
|
|
195
|
-
* vertical inset
|
|
196
|
-
*
|
|
197
|
-
*
|
|
196
|
+
* vertical inset is a quarter of the recipe padX (preserves the audited $3/$4
|
|
197
|
+
* pill height), painted as the installed scale's `$space` token where the
|
|
198
|
+
* default scale's quarter lands on one (`scaleSpaceFractionPx`). Module-eval
|
|
199
|
+
* objects so styled() can flatten (pilot pattern: InputParts spreads
|
|
200
|
+
* `recipeFamilies.control.boxVariants`).
|
|
198
201
|
* Kept in lockstep with the forms Chip.
|
|
199
202
|
*/
|
|
200
203
|
const CHIP_RECIPE_FAMILY = "chip" as const;
|
|
@@ -209,7 +212,7 @@ export function chipFrameGeometry(token: string) {
|
|
|
209
212
|
const recipe = sizeRecipeForToken(token, { family: CHIP_RECIPE_FAMILY });
|
|
210
213
|
return {
|
|
211
214
|
paddingHorizontal: recipe.paddingHorizontal,
|
|
212
|
-
paddingVertical:
|
|
215
|
+
paddingVertical: scaleSpaceFractionPx(recipe.paddingHorizontal, 0.25),
|
|
213
216
|
gap: recipe.gap,
|
|
214
217
|
};
|
|
215
218
|
}
|
|
@@ -419,7 +422,8 @@ export function Chip({
|
|
|
419
422
|
const frameGeometry = chipFrameGeometry(resolvedSize);
|
|
420
423
|
// End cap: the dismiss box stretches to the pill's padding-box height and
|
|
421
424
|
// mirrors it into its width, so the glyph's gap to the trailing edge equals
|
|
422
|
-
// its gap to top and bottom at every size token (forms Chip, FZ-8018).
|
|
425
|
+
// its gap to top and bottom at every size token (forms Chip, FZ-8018). The
|
|
426
|
+
// cap abuts the label: its own inset is the label-to-x gap.
|
|
423
427
|
const [dismissSide, setDismissSide] = useState<number | null>(null);
|
|
424
428
|
const dismissHitOutset = dismissSide ? Math.max(0, (MIN_PRESS_TARGET - dismissSide) / 2) : 0;
|
|
425
429
|
// DG-RAD-02 CIRCULAR-AT-FULL. At full, 1000 paints the same h/2 circle
|
|
@@ -621,6 +625,7 @@ export function Chip({
|
|
|
621
625
|
position="relative"
|
|
622
626
|
marginVertical={-frameGeometry.paddingVertical}
|
|
623
627
|
marginRight={-frameGeometry.paddingHorizontal}
|
|
628
|
+
marginLeft={typeof frameGeometry.gap === "number" ? -frameGeometry.gap : 0}
|
|
624
629
|
width={dismissSide ?? undefined}
|
|
625
630
|
hitSlop={pressTargetHitSlop(dismissSide ?? dismissVisual)}
|
|
626
631
|
onLayout={(e) => {
|
package/src/Meter/Meter.spec.tsx
CHANGED
|
@@ -78,13 +78,13 @@ describe("threshold intents map to theme intents, never raw brand", () => {
|
|
|
78
78
|
expect(resolveMeterZone({ value: 80, ...BATTERY })).toBe("ok");
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it("paints each zone with theme
|
|
81
|
+
it("paints each zone with theme tokens, never hex or the accent solid", () => {
|
|
82
82
|
const token = /^\$(color|orange|red)\d+$/;
|
|
83
83
|
for (const zone of Object.keys(METER_ZONE_PAINT) as MeterZone[]) {
|
|
84
84
|
const paint = METER_ZONE_PAINT[zone];
|
|
85
|
-
expect(paint.fill, `${zone} fill`).toMatch(token);
|
|
86
85
|
expect(paint.fill).not.toMatch(/^#/);
|
|
87
|
-
expect(paint.fill).not.
|
|
86
|
+
expect(paint.fill).not.toBe("$accentBackground");
|
|
87
|
+
if (zone !== "ok") expect(paint.fill, `${zone} fill`).toMatch(token);
|
|
88
88
|
if ("value" in paint) {
|
|
89
89
|
expect(paint.value, `${zone} value`).toMatch(token);
|
|
90
90
|
expect(paint.value).not.toMatch(/^#/);
|
|
@@ -93,7 +93,8 @@ describe("threshold intents map to theme intents, never raw brand", () => {
|
|
|
93
93
|
}
|
|
94
94
|
// MPO-154: ok declares no value ink — the resolver owns it.
|
|
95
95
|
expect("value" in METER_ZONE_PAINT.ok).toBe(false);
|
|
96
|
-
|
|
96
|
+
// LC-172 (FZ-8044): ok is a fill on a track, the theme's accent ink, as Progress paints it.
|
|
97
|
+
expect(METER_ZONE_PAINT.ok.fill).toBe("$accentColor");
|
|
97
98
|
expect(METER_ZONE_PAINT.warn.fill).toBe("$orange9");
|
|
98
99
|
expect(METER_ZONE_PAINT.warn.value).toBe("$orange11");
|
|
99
100
|
expect(METER_ZONE_PAINT.critical.fill).toBe("$red9");
|
package/src/Meter/meter.ts
CHANGED
|
@@ -23,9 +23,11 @@ export type MeterLayout = "adjacent" | "inline" | "bare";
|
|
|
23
23
|
export const METER_RADIUS_CLASS = "DEFAULT" as const;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Zone paint — theme
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Zone paint — theme tokens, never raw brand or the accent solid. ok carries
|
|
27
|
+
* no verdict (DG-FB-06), so it is the plain fill on a track Progress paints:
|
|
28
|
+
* the theme's accent ink, `formSelectedColors.trackFill` (LC-172, FZ-8044).
|
|
29
|
+
* warn/critical are the same step-9 solid / step-11 ink channel Progress uses
|
|
30
|
+
* (DG-FB-05).
|
|
29
31
|
*
|
|
30
32
|
* MPO-154: ok carries no `value`. Under no verdict the readout is ordinary
|
|
31
33
|
* secondary text, so its ink is `knobProps.textAccentColor` — a static
|
|
@@ -33,7 +35,7 @@ export const METER_RADIUS_CLASS = "DEFAULT" as const;
|
|
|
33
35
|
* and it pinned the readout dim at `textAccent: high`.
|
|
34
36
|
*/
|
|
35
37
|
export const METER_ZONE_PAINT = {
|
|
36
|
-
ok: { fill: "$
|
|
38
|
+
ok: { fill: "$accentColor" },
|
|
37
39
|
warn: { fill: "$orange9", value: "$orange11" },
|
|
38
40
|
critical: { fill: "$red9", value: "$red11" },
|
|
39
41
|
} as const;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FZ-8939 (LC-266): the web SheetModal at regular width is the native
|
|
3
|
+
* panel's twin (FZ-8116). On a desktop window the Tier 2 cells
|
|
4
|
+
* `components-sheetframe--default` and `components-formlayouts--sign-in-in-sheet`
|
|
5
|
+
* drew the sheet as a band across the whole window. It is a bottom panel
|
|
6
|
+
* SHEET_PANEL_WIDTH wide, centred, with the scrim showing on both sides, and a
|
|
7
|
+
* press beside it dismisses. At compact width it stays the full-width sheet.
|
|
8
|
+
*/
|
|
9
|
+
import { SHEET_PANEL_WIDTH } from "@multiplatform.one/forms";
|
|
10
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
11
|
+
import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { Text } from "tamagui";
|
|
14
|
+
import { SheetModal } from "./index";
|
|
15
|
+
|
|
16
|
+
// The DOM under test reports no window width of its own, so the window is
|
|
17
|
+
// set here, as the native regular-width spec does.
|
|
18
|
+
const screen = vi.hoisted(() => ({ width: 1280, height: 800 }));
|
|
19
|
+
vi.mock("tamagui", async (importOriginal) => ({
|
|
20
|
+
...(await importOriginal<typeof import("tamagui")>()),
|
|
21
|
+
useWindowDimensions: () => ({ ...screen, scale: 1, fontScale: 1 }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
function setWindowWidth(width: number) {
|
|
25
|
+
screen.width = width;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
cleanup();
|
|
30
|
+
screen.width = 1280;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function openAt(width: number) {
|
|
34
|
+
setWindowWidth(width);
|
|
35
|
+
const onOpenChange = vi.fn();
|
|
36
|
+
const view = renderWithProviders(
|
|
37
|
+
<SheetModal open onOpenChange={onOpenChange}>
|
|
38
|
+
<Text>Sign in</Text>
|
|
39
|
+
</SheetModal>,
|
|
40
|
+
);
|
|
41
|
+
// tamagui draws a cover under the frame with the frame's own props; the
|
|
42
|
+
// frame is the one that is not the cover.
|
|
43
|
+
let frame: HTMLElement | null = null;
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
frame = document.querySelector('[role="dialog"]:not([data-sheet-cover])');
|
|
46
|
+
expect(frame).not.toBeNull();
|
|
47
|
+
});
|
|
48
|
+
return { view, frame: frame as unknown as HTMLElement, onOpenChange };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const atoms = (node: Element) => String(node.className).split(/\s+/).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
describe("SheetModal web at regular width (FZ-8939, LC-266)", () => {
|
|
54
|
+
it("is a centred panel capped at the panel width on a desktop window", async () => {
|
|
55
|
+
const { frame } = await openAt(1280);
|
|
56
|
+
expect(frame.getAttribute("data-sheet-form")).toBe("panel");
|
|
57
|
+
const width = frame.style.width || atoms(frame).find((a) => a.startsWith("_w-")) || "";
|
|
58
|
+
expect(width).toContain(`${SHEET_PANEL_WIDTH}px`);
|
|
59
|
+
expect(atoms(frame).some((a) => a === "_als-center")).toBe(true);
|
|
60
|
+
// A panel keeps its side edges; the compact band drops them.
|
|
61
|
+
expect(atoms(frame)).not.toContain("_blw-0px");
|
|
62
|
+
expect(atoms(frame)).not.toContain("_brw-0px");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("dismisses on a press beside the panel, not on a press inside it", async () => {
|
|
66
|
+
const { frame, onOpenChange } = await openAt(1280);
|
|
67
|
+
await act(async () => {
|
|
68
|
+
fireEvent.click(frame);
|
|
69
|
+
});
|
|
70
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
71
|
+
await act(async () => {
|
|
72
|
+
fireEvent.click(frame.parentElement as HTMLElement);
|
|
73
|
+
});
|
|
74
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("stays the full-width bottom sheet at phone width", async () => {
|
|
78
|
+
const { frame, onOpenChange } = await openAt(390);
|
|
79
|
+
expect(frame.getAttribute("data-sheet-form")).toBe("bottom");
|
|
80
|
+
expect(atoms(frame)).toContain("_blw-0px");
|
|
81
|
+
expect(atoms(frame)).toContain("_brw-0px");
|
|
82
|
+
await act(async () => {
|
|
83
|
+
fireEvent.click(frame.parentElement as HTMLElement);
|
|
84
|
+
});
|
|
85
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/SheetModal/index.tsx
CHANGED
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
transitionProps,
|
|
16
16
|
useResolvedKnobs,
|
|
17
17
|
} from "@multiplatform.one/theme";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
OverlayStackProvider,
|
|
20
|
+
overlaySizeClass,
|
|
21
|
+
sheetPanelWidth,
|
|
22
|
+
useOverlayZIndex,
|
|
23
|
+
} from "@multiplatform.one/forms";
|
|
19
24
|
import { useEffect, useId, useRef, type ReactNode } from "react";
|
|
20
25
|
import { isWeb, Sheet, View, useWindowDimensions } from "tamagui";
|
|
21
26
|
import { OverlayTitleIdContext } from "../actions/StandardAction";
|
|
@@ -69,6 +74,47 @@ const IN_DIALOG_REGION = { inDialogRegion: true } as const;
|
|
|
69
74
|
const fillSlot = { flex: 1, minHeight: 0 } as const;
|
|
70
75
|
const nonShrinking = { flexShrink: 0 } as const;
|
|
71
76
|
|
|
77
|
+
/** A compact sheet spans the window: only its top edge meets the page. */
|
|
78
|
+
const bottomEdges = { borderLeftWidth: 0, borderRightWidth: 0, borderBottomWidth: 0 } as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* LC-266 (FZ-8939): at regular width the sheet is the native panel's twin, a
|
|
82
|
+
* bottom panel `sheetPanelWidth` wide and centred with the scrim on both
|
|
83
|
+
* sides. Its side edges stay, so it reads as a panel rather than a band.
|
|
84
|
+
*/
|
|
85
|
+
function regularPanel(windowWidth: number) {
|
|
86
|
+
return {
|
|
87
|
+
width: sheetPanelWidth(windowWidth),
|
|
88
|
+
alignSelf: "center",
|
|
89
|
+
borderBottomWidth: 0,
|
|
90
|
+
} as const;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The sheet's frame sits in a window-wide band that tamagui's pan responder
|
|
95
|
+
* owns, so a press on the band beside a regular-width panel never reaches the
|
|
96
|
+
* scrim. A press that lands on the band itself, not inside the panel or the
|
|
97
|
+
* grabber, is a press outside the sheet, and dismisses it (LC-266).
|
|
98
|
+
*/
|
|
99
|
+
function useBandDismiss(
|
|
100
|
+
enabled: boolean,
|
|
101
|
+
frameRef: { current: HTMLElement | null },
|
|
102
|
+
onDismiss: () => void,
|
|
103
|
+
) {
|
|
104
|
+
const onDismissRef = useRef(onDismiss);
|
|
105
|
+
onDismissRef.current = onDismiss;
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!isWeb || !enabled) return;
|
|
108
|
+
const band = frameRef.current?.parentElement;
|
|
109
|
+
if (!band) return;
|
|
110
|
+
const onClick = (event: MouseEvent) => {
|
|
111
|
+
if (event.target === band) onDismissRef.current();
|
|
112
|
+
};
|
|
113
|
+
band.addEventListener("click", onClick);
|
|
114
|
+
return () => band.removeEventListener("click", onClick);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
72
118
|
/** LC-71: the sheet frame is not a control. Auto-focus must not paint a ring. */
|
|
73
119
|
const frameRingReset = {
|
|
74
120
|
outlineWidth: 0,
|
|
@@ -112,7 +158,8 @@ export function SheetModal({
|
|
|
112
158
|
/>
|
|
113
159
|
) : null;
|
|
114
160
|
const snap = resolveWebSnap({ snapPoint, fill });
|
|
115
|
-
const { height: windowHeight } = useWindowDimensions();
|
|
161
|
+
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
162
|
+
const regular = overlaySizeClass(windowWidth) === "regular";
|
|
116
163
|
const { knobProps } = useResolvedKnobs();
|
|
117
164
|
const sheetZIndex = useOverlayZIndex("sheet");
|
|
118
165
|
const transition = knobProps.motion.slide.transition;
|
|
@@ -120,6 +167,7 @@ export function SheetModal({
|
|
|
120
167
|
|
|
121
168
|
useSheetFrameFocus(open, frameRef);
|
|
122
169
|
useSheetEscapeDismiss(open, () => onOpenChange(false));
|
|
170
|
+
useBandDismiss(open && regular, frameRef, () => onOpenChange(false));
|
|
123
171
|
|
|
124
172
|
return (
|
|
125
173
|
<Sheet
|
|
@@ -147,6 +195,7 @@ export function SheetModal({
|
|
|
147
195
|
role: "dialog",
|
|
148
196
|
"aria-modal": true,
|
|
149
197
|
"aria-labelledby": headerContent ? titleId : undefined,
|
|
198
|
+
"data-sheet-form": regular ? "panel" : "bottom",
|
|
150
199
|
} as Record<string, unknown>)}
|
|
151
200
|
{...knobProps.gap}
|
|
152
201
|
{...(snap.snapPointsMode === "fit"
|
|
@@ -154,9 +203,7 @@ export function SheetModal({
|
|
|
154
203
|
: null)}
|
|
155
204
|
borderBottomLeftRadius={0}
|
|
156
205
|
borderBottomRightRadius={0}
|
|
157
|
-
|
|
158
|
-
borderRightWidth={0}
|
|
159
|
-
borderBottomWidth={0}
|
|
206
|
+
{...(regular ? regularPanel(windowWidth) : bottomEdges)}
|
|
160
207
|
overflow="hidden"
|
|
161
208
|
{...frameRingReset}
|
|
162
209
|
>
|
package/src/Text.stories.tsx
CHANGED
|
@@ -71,6 +71,19 @@ export const FontWeightFlip: Story = {
|
|
|
71
71
|
),
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
export const InterFaces: Story = {
|
|
75
|
+
name: "Inter faces (400 to 800)",
|
|
76
|
+
render: () => (
|
|
77
|
+
<YStack gap="$2" maxWidth={520}>
|
|
78
|
+
{(["400", "500", "600", "700", "800"] as const).map((weight) => (
|
|
79
|
+
<SizableText key={weight} size="$6" fontWeight={weight}>
|
|
80
|
+
{weight} Hamburgefonstiv 0123
|
|
81
|
+
</SizableText>
|
|
82
|
+
))}
|
|
83
|
+
</YStack>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
74
87
|
const digitPresets = {
|
|
75
88
|
Default: defaultPreset,
|
|
76
89
|
Bold: boldPreset,
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
Preset,
|
|
11
11
|
createDefaultThemeConfig,
|
|
12
12
|
normalizeToHex,
|
|
13
|
-
|
|
13
|
+
useActiveTabInk,
|
|
14
14
|
useGlyphColor,
|
|
15
|
+
useIdentityInkOnSurface,
|
|
15
16
|
} from "@multiplatform.one/theme";
|
|
16
17
|
import { act, cleanup, fireEvent, renderHook, screen } from "@testing-library/react";
|
|
17
18
|
import fs from "node:fs";
|
|
@@ -67,9 +68,10 @@ function probeItems(seen: Record<string, GlyphProps>): TabBarItem[] {
|
|
|
67
68
|
];
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
const expected: { accent?: string; secondary?: string; primary?: string } = {};
|
|
71
|
+
const expected: { accent?: string; secondary?: string; primary?: string; identity?: string } = {};
|
|
71
72
|
function InkProbe() {
|
|
72
|
-
expected.accent =
|
|
73
|
+
expected.accent = useActiveTabInk();
|
|
74
|
+
expected.identity = useIdentityInkOnSurface();
|
|
73
75
|
expected.secondary = useGlyphColor(tabBarInactiveInk);
|
|
74
76
|
expected.primary = useGlyphColor("$color");
|
|
75
77
|
return null;
|
|
@@ -152,9 +154,13 @@ describe("TabBar (LC-150 TAB-BAR-ANATOMY)", () => {
|
|
|
152
154
|
});
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
it(`${scheme}, no hue: the active tab is the
|
|
157
|
+
it(`${scheme}, no hue: the active tab is the neutral accent's ink, bold mono, never grey`, () => {
|
|
156
158
|
const seen = renderBar(scheme, "/");
|
|
157
159
|
expect(expected.accent).toBeTruthy();
|
|
160
|
+
// The house accent is the neutral inverse: the active tab is the same
|
|
161
|
+
// ink the system bar draws, never the grey accent-ramp step (Round 54).
|
|
162
|
+
expect(expected.accent).toBe(expected.identity);
|
|
163
|
+
expect(expected.accent).not.toBe(expected.secondary);
|
|
158
164
|
expect(seen["/"]).toMatchObject({ color: expected.accent, weight: "fill" });
|
|
159
165
|
expect(seen["/moves"]).toMatchObject({ color: expected.secondary, weight: "regular" });
|
|
160
166
|
});
|