@multiplatform.one/theme 7.4.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +5 -5
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +6 -19
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +5 -3
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +233 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +213 -3
- package/src/theme/cornerSmoothing.ts +132 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +21 -0
- package/src/theme/defaults/fonts.spec.ts +93 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +128 -0
- package/src/theme/defaults/fonts.ts +155 -27
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +110 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryStacks.ts +24 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +139 -11
- package/src/theme/resolveKnobs.ts +190 -25
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +2 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +19 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +135 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryStacks.d.ts +11 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +3 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
import { defaultConfig } from "@tamagui/config/v5";
|
|
1
2
|
import { createInterFont } from "@tamagui/font-inter";
|
|
3
|
+
import type { GenericFont } from "tamagui";
|
|
2
4
|
|
|
3
5
|
// Default purpose fonts for components that reference $heading / $body
|
|
4
6
|
// directly, both backed by Inter.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
//
|
|
8
|
+
// MPO-19 X8 + X9. These tables used to be @tamagui/config/v4's — headings on a
|
|
9
|
+
// 1.4x scale (H1 $10 = 64px) at whatever weight the knob happened to default
|
|
10
|
+
// to, body at 14px with `lineHeight = size + 10`. Against the v5 era this
|
|
11
|
+
// package now pins (see `TAMAGUI_ERA` in createDefaultThemeConfig.ts) that is a
|
|
12
|
+
// different typographic voice, not a variation on one: H1 was 64px/400 where
|
|
13
|
+
// stock v5 is 40px/800, large and light against compact and heavy. And
|
|
14
|
+
// `size + 10` is not a ratio at all — 1.91 at 11px, 1.22 at 46px — which is
|
|
15
|
+
// why v5 replaced it with an explicit taper.
|
|
16
|
+
//
|
|
17
|
+
// So the tables are no longer transcribed. They are READ from
|
|
18
|
+
// `defaultConfig.fonts`, which means the era pin is load-bearing rather than
|
|
19
|
+
// decorative: re-point it and the type scale follows, and there is no local
|
|
20
|
+
// copy that can drift from upstream between pins. That also retires the
|
|
21
|
+
// hand-mirrored `interBaseSizes` table this file used to carry purely so
|
|
22
|
+
// tracking could be computed per key — the size table it needed IS the one
|
|
23
|
+
// upstream ships.
|
|
10
24
|
//
|
|
11
25
|
// Tracking-by-size (Axiom 15 OPTICS + LC-11): letter-spacing is a function
|
|
12
26
|
// of the rendered size, not a constant — slightly loose at caption sizes,
|
|
@@ -16,6 +30,11 @@ import { createInterFont } from "@tamagui/font-inter";
|
|
|
16
30
|
// These values are Inter-specific: when the headingFont/bodyFont knob picks
|
|
17
31
|
// another family, FontKnobStyles' per-category metrics (LC-11) neutralize
|
|
18
32
|
// this table for fixed-pitch and connected-script categories.
|
|
33
|
+
//
|
|
34
|
+
// Category fonts (serif/mono/rounded/…) MUST be registered on this same
|
|
35
|
+
// size+10 ramp, including the `true` key. An unregistered `$serif` token
|
|
36
|
+
// collapses the label TEXT NODE to -apple-system 14px/normal — the frame
|
|
37
|
+
// stays Inter, so measuring the frame hides the defect (MPO-44).
|
|
19
38
|
|
|
20
39
|
const TRACKING_A = -0.0223;
|
|
21
40
|
const TRACKING_B = 0.185;
|
|
@@ -31,11 +50,20 @@ export function interTrackingPx(sizePx: number): number {
|
|
|
31
50
|
return Math.round(interTrackingEm(sizePx) * sizePx * 100) / 100;
|
|
32
51
|
}
|
|
33
52
|
|
|
53
|
+
/** Body leading stays on the ramp: size + 10 (14 → 24). Never CSS `normal`. */
|
|
54
|
+
export const BODY_LINE_HEIGHT_OFFSET = 10;
|
|
55
|
+
|
|
56
|
+
export function bodyLineHeightPx(sizePx: number): number {
|
|
57
|
+
return sizePx + BODY_LINE_HEIGHT_OFFSET;
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
// Mirrors @tamagui/font-inter's default size table (pinned 2.0.0-rc.41) so
|
|
35
61
|
// tracking can be computed per size key. The fonts.spec.ts invariant test
|
|
36
62
|
// (letterSpacing[k] === interTrackingPx(size[k]) on the BUILT font) catches
|
|
37
|
-
// any upstream drift.
|
|
38
|
-
|
|
63
|
+
// any upstream drift. `true` is the default size token — omit it and
|
|
64
|
+
// fontFamily="$serif" falls back to the system face with line-height:normal.
|
|
65
|
+
// Category fonts (not heading/body) stay on this transcribed ramp.
|
|
66
|
+
export const bodyFontSizes: Record<string, number> = {
|
|
39
67
|
1: 11,
|
|
40
68
|
2: 12,
|
|
41
69
|
3: 13,
|
|
@@ -55,35 +83,135 @@ const interBaseSizes: Record<string, number> = {
|
|
|
55
83
|
16: 134,
|
|
56
84
|
};
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
/**
|
|
87
|
+
* A tamagui font table with Variables unwrapped and `$` prefixes stripped.
|
|
88
|
+
* Upstream hands these back either raw or wrapped depending on where in the
|
|
89
|
+
* config pipeline they were read, and `createInterFont` wants them raw.
|
|
90
|
+
*/
|
|
91
|
+
function plain<T>(raw: unknown): Record<string, T> {
|
|
92
|
+
const out: Record<string, T> = {};
|
|
93
|
+
for (const [key, value] of Object.entries((raw ?? {}) as Record<string, T | { val?: T }>)) {
|
|
94
|
+
const unwrapped =
|
|
95
|
+
value && typeof value === "object" && "val" in value
|
|
96
|
+
? (value as { val?: T }).val
|
|
97
|
+
: (value as T);
|
|
98
|
+
if (unwrapped !== undefined) out[key.replace(/^\$/, "")] = unwrapped;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
62
101
|
}
|
|
63
102
|
|
|
64
|
-
|
|
103
|
+
/** A tamagui font table's numbers, with Variables and `$` prefixes unwrapped. */
|
|
104
|
+
const numbers = (raw: unknown) => plain<number>(raw);
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
/** Inter dynamic tracking for every key of a size table. */
|
|
107
|
+
function trackingFor(sizes: Record<string, number>): Record<string, number> {
|
|
108
|
+
return Object.fromEntries(Object.entries(sizes).map(([key, px]) => [key, interTrackingPx(px)]));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const v5Body = defaultConfig.fonts.body;
|
|
112
|
+
const v5Heading = defaultConfig.fonts.heading;
|
|
113
|
+
const bodySizes = numbers(v5Body.size);
|
|
114
|
+
const headingSizes = numbers(v5Heading.size);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Inter faces the workspace ships. v5 heading weights are sparse — 600 up to
|
|
118
|
+
* $5, 700 at $6–$8, 800 from $9 — and A4's reading of that is the point:
|
|
119
|
+
* everything between inherits, so bold is rare and therefore means something.
|
|
120
|
+
* Each declared weight needs a face or the browser synthesises one.
|
|
121
|
+
*/
|
|
122
|
+
const interFaces = {
|
|
123
|
+
400: { normal: "Inter" },
|
|
124
|
+
600: { normal: "InterSemiBold" },
|
|
125
|
+
700: { normal: "InterBold" },
|
|
126
|
+
800: { normal: "InterExtraBold" },
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Concrete family stacks for each `headingFont`/`bodyFont` category.
|
|
131
|
+
* Distinct first families so a text-node measurement cannot collapse three
|
|
132
|
+
* stops onto one system fallback. Matches `packages/config/fonts.ts`
|
|
133
|
+
* (serif/mono are explicit system aliases; the rest are shipped webfonts).
|
|
134
|
+
*/
|
|
135
|
+
export const fontFamilyStacks = {
|
|
136
|
+
"sans-serif":
|
|
137
|
+
'Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
138
|
+
serif: "Georgia, 'Times New Roman', Times, serif",
|
|
139
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
140
|
+
slab: "'Roboto Slab', 'Rockwell', 'Courier New', serif",
|
|
141
|
+
rounded: "'Nunito', 'Varela Round', system-ui, sans-serif",
|
|
142
|
+
condensed: "'Roboto Condensed', 'Arial Narrow', sans-serif",
|
|
143
|
+
cursive: "'Dancing Script', 'Pacifico', cursive",
|
|
144
|
+
handwriting: "'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive",
|
|
145
|
+
pixel: "'Silkscreen', 'Press Start 2P', monospace",
|
|
146
|
+
blackletter: "'UnifrakturCook', 'Fraktur', serif",
|
|
147
|
+
geometric: "'Futura', 'Poppins', 'Century Gothic', sans-serif",
|
|
148
|
+
} as const;
|
|
149
|
+
|
|
150
|
+
export type FontFamilyCategory = keyof typeof fontFamilyStacks;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Category font on the body ramp: same size table as Inter (including
|
|
154
|
+
* `true: 14`) and lineHeight size+10. Inter tracking is NOT copied — that
|
|
155
|
+
* curve is Inter-specific (LC-11); FontKnobStyles applies per-category
|
|
156
|
+
* floors when the knob restyles `$heading`/`$body`.
|
|
157
|
+
*/
|
|
158
|
+
export function createCategoryFont(family: string): GenericFont {
|
|
159
|
+
return createInterFont(
|
|
160
|
+
{
|
|
161
|
+
family,
|
|
162
|
+
letterSpacing: {},
|
|
70
163
|
},
|
|
71
|
-
|
|
72
|
-
|
|
164
|
+
{
|
|
165
|
+
sizeLineHeight: bodyLineHeightPx,
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const defaultHeadingFont = createInterFont(
|
|
73
171
|
{
|
|
74
|
-
|
|
75
|
-
|
|
172
|
+
face: interFaces,
|
|
173
|
+
size: headingSizes,
|
|
174
|
+
lineHeight: numbers(v5Heading.lineHeight),
|
|
175
|
+
weight: plain<string>(v5Heading.weight),
|
|
176
|
+
letterSpacing: trackingFor(headingSizes),
|
|
76
177
|
},
|
|
178
|
+
{ sizeSize: (size) => size },
|
|
77
179
|
);
|
|
78
180
|
|
|
79
181
|
export const defaultBodyFont = createInterFont(
|
|
80
182
|
{
|
|
81
|
-
face:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
sizeLineHeight: (size) => size + 10,
|
|
183
|
+
face: interFaces,
|
|
184
|
+
size: bodySizes,
|
|
185
|
+
lineHeight: numbers(v5Body.lineHeight),
|
|
186
|
+
weight: plain<string>(v5Body.weight),
|
|
187
|
+
letterSpacing: trackingFor(bodySizes),
|
|
88
188
|
},
|
|
189
|
+
{ sizeSize: (size) => size },
|
|
89
190
|
);
|
|
191
|
+
|
|
192
|
+
export const defaultSerifFont = createCategoryFont(fontFamilyStacks.serif);
|
|
193
|
+
export const defaultMonoFont = createCategoryFont(fontFamilyStacks.mono);
|
|
194
|
+
export const defaultSlabFont = createCategoryFont(fontFamilyStacks.slab);
|
|
195
|
+
export const defaultRoundedFont = createCategoryFont(fontFamilyStacks.rounded);
|
|
196
|
+
export const defaultCondensedFont = createCategoryFont(fontFamilyStacks.condensed);
|
|
197
|
+
export const defaultCursiveFont = createCategoryFont(fontFamilyStacks.cursive);
|
|
198
|
+
export const defaultHandwritingFont = createCategoryFont(fontFamilyStacks.handwriting);
|
|
199
|
+
export const defaultPixelFont = createCategoryFont(fontFamilyStacks.pixel);
|
|
200
|
+
export const defaultBlackletterFont = createCategoryFont(fontFamilyStacks.blackletter);
|
|
201
|
+
export const defaultGeometricFont = createCategoryFont(fontFamilyStacks.geometric);
|
|
202
|
+
|
|
203
|
+
/** All purpose + category fonts registered by `createDefaultThemeConfig`. */
|
|
204
|
+
export const defaultFonts = {
|
|
205
|
+
heading: defaultHeadingFont,
|
|
206
|
+
body: defaultBodyFont,
|
|
207
|
+
serif: defaultSerifFont,
|
|
208
|
+
mono: defaultMonoFont,
|
|
209
|
+
slab: defaultSlabFont,
|
|
210
|
+
rounded: defaultRoundedFont,
|
|
211
|
+
condensed: defaultCondensedFont,
|
|
212
|
+
cursive: defaultCursiveFont,
|
|
213
|
+
handwriting: defaultHandwritingFont,
|
|
214
|
+
pixel: defaultPixelFont,
|
|
215
|
+
blackletter: defaultBlackletterFont,
|
|
216
|
+
geometric: defaultGeometricFont,
|
|
217
|
+
} as const;
|
|
@@ -1170,15 +1170,6 @@ export function ThemeDevtoolsPanel({
|
|
|
1170
1170
|
}
|
|
1171
1171
|
}
|
|
1172
1172
|
}
|
|
1173
|
-
const fill = partial.fillStyle ?? knobs.fillStyle;
|
|
1174
|
-
const borderWidth = partial.borderWidth ?? knobs.borderWidth;
|
|
1175
|
-
if (fill === "outlined" && borderWidth === "none") {
|
|
1176
|
-
if (!pinnedKnobs.has("borderWidth")) {
|
|
1177
|
-
partial.borderWidth = pick(["small", "medium", "large"] as const);
|
|
1178
|
-
} else {
|
|
1179
|
-
partial.fillStyle = "filled";
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
1173
|
setOverrides(newOverrides);
|
|
1183
1174
|
setKnobs(partial);
|
|
1184
1175
|
if (!pinnedKnobs.has("color") && themeColors && onThemeColorChange) {
|
|
@@ -1200,8 +1191,6 @@ export function ThemeDevtoolsPanel({
|
|
|
1200
1191
|
expandedKnobs,
|
|
1201
1192
|
activeProfile,
|
|
1202
1193
|
animationOptions,
|
|
1203
|
-
knobs.fillStyle,
|
|
1204
|
-
knobs.borderWidth,
|
|
1205
1194
|
]);
|
|
1206
1195
|
|
|
1207
1196
|
// -------------------------------------------------------------------------
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { __resetDevWarnSeen, warnOutlineNone, warnPositiveTabIndex } from "../devWarn";
|
|
3
|
+
import { compositeFocusRingCss, keyboardFocusRingProps } from "../keyboardFocusRing";
|
|
3
4
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
4
5
|
import { createThemesBuilder } from "./createThemes";
|
|
5
6
|
import { defaultAccentTheme } from "./defaults/accent";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
FOCUS_RING_MIN_CONTRAST,
|
|
12
13
|
FOCUS_RING_MIN_WIDTH,
|
|
13
14
|
FOCUS_VISIBLE_RING,
|
|
15
|
+
FOCUS_VISIBLE_RING_INSET,
|
|
14
16
|
STATE_LAYER_HOVER,
|
|
15
17
|
STATE_LAYER_PRESS,
|
|
16
18
|
withDefaultStateLayer,
|
|
@@ -19,10 +21,13 @@ import { defaultKnobs } from "./knobs";
|
|
|
19
21
|
import { resolveKnobs } from "./resolveKnobs";
|
|
20
22
|
|
|
21
23
|
describe("focusState tokens (DG-A11Y-01 / DG-STATE-01 / W12)", () => {
|
|
22
|
-
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract", () => {
|
|
24
|
+
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset 0 (F1)", () => {
|
|
23
25
|
expect(FOCUS_VISIBLE_RING.outlineWidth).toBeGreaterThanOrEqual(FOCUS_RING_MIN_WIDTH);
|
|
24
26
|
expect(FOCUS_VISIBLE_RING.outlineStyle).toBe("solid");
|
|
25
27
|
expect(FOCUS_VISIBLE_RING.outlineColor).toBe("$outlineColor");
|
|
28
|
+
// Board verify pass F1: offset 0 — not −2 (band inside the fill, where it
|
|
29
|
+
// measured 1.17–1.31:1 on solid intents) and not +2 (a gapped halo).
|
|
30
|
+
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
|
|
26
31
|
expect(FOCUS_RING_MIN_CONTRAST).toBeGreaterThanOrEqual(3);
|
|
27
32
|
});
|
|
28
33
|
|
|
@@ -107,6 +112,110 @@ describe("$outlineColor token contrast (DF-03 / DG-A11Y-01)", () => {
|
|
|
107
112
|
expect(Math.min(c1, c2)).toBeGreaterThanOrEqual(FOCUS_RING_MIN_CONTRAST);
|
|
108
113
|
});
|
|
109
114
|
}
|
|
115
|
+
|
|
116
|
+
// MPO-41 — assert the RATIO on the surface the band actually draws on, not
|
|
117
|
+
// just the declaration. At offset 0 the band paints OUTSIDE the border box,
|
|
118
|
+
// on the page ground behind the control; intent Buttons pin the parent-scope
|
|
119
|
+
// `$outlineColor` literal on the ring (LC-51), so the pair that must clear
|
|
120
|
+
// the floor is base `outlineColor` × base ground — for accent and error
|
|
121
|
+
// controls alike, in both schemes.
|
|
122
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
123
|
+
for (const intent of ["accent", "error"] as const) {
|
|
124
|
+
it(`${scheme} ${intent}: offset-0 ring clears ≥3:1 on the page ground it draws on`, () => {
|
|
125
|
+
const base = themes[scheme];
|
|
126
|
+
expect(themes[`${scheme}_${intent}`]).toBeTruthy();
|
|
127
|
+
const ring = normalizeToHex(base.outlineColor);
|
|
128
|
+
const ground = normalizeToHex(base.color1);
|
|
129
|
+
expect(ring).toBeTruthy();
|
|
130
|
+
expect(ground).toBeTruthy();
|
|
131
|
+
expect(
|
|
132
|
+
contrastRatio(relativeLuminance(ring!), relativeLuminance(ground!)),
|
|
133
|
+
).toBeGreaterThanOrEqual(FOCUS_RING_MIN_CONTRAST);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it("the retired flat inset failed the floor over the solid intent fills (why offset is 0)", () => {
|
|
139
|
+
// The measured failures that forced the flip (MPO-41 / MPO-19 X7): a −2
|
|
140
|
+
// band paints INSIDE the control, so it had to contrast against the solid
|
|
141
|
+
// intent fill — light accent 1.27:1, light error 1.17:1, dark error
|
|
142
|
+
// 1.31:1. Locking these <3:1 keeps the offset-0 geometry load-bearing: if
|
|
143
|
+
// the fills ever lighten past the floor this test goes stale, not wrong.
|
|
144
|
+
for (const [scheme, intent] of [
|
|
145
|
+
["light", "accent"],
|
|
146
|
+
["light", "error"],
|
|
147
|
+
["dark", "error"],
|
|
148
|
+
] as const) {
|
|
149
|
+
const ring = normalizeToHex(themes[scheme].outlineColor);
|
|
150
|
+
const fill = normalizeToHex(themes[`${scheme}_${intent}`].color9);
|
|
151
|
+
expect(ring).toBeTruthy();
|
|
152
|
+
expect(fill).toBeTruthy();
|
|
153
|
+
expect(contrastRatio(relativeLuminance(ring!), relativeLuminance(fill!))).toBeLessThan(
|
|
154
|
+
FOCUS_RING_MIN_CONTRAST,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("the ring still paints inside a clipped / fused group (LC-71 §4 inset carve-out)", () => {
|
|
161
|
+
it("FOCUS_VISIBLE_RING_INSET is the same ring pulled fully inside the border box", () => {
|
|
162
|
+
expect(FOCUS_VISIBLE_RING_INSET).toMatchObject({
|
|
163
|
+
outlineWidth: FOCUS_RING_MIN_WIDTH,
|
|
164
|
+
outlineStyle: "solid",
|
|
165
|
+
outlineColor: "$outlineColor",
|
|
166
|
+
outlineOffset: -2,
|
|
167
|
+
});
|
|
168
|
+
// The band spans [outlineOffset, outlineOffset + outlineWidth] from the
|
|
169
|
+
// border edge. Outer edge ≤ 0 means a clipping ancestor (fused group
|
|
170
|
+
// clipped at the group radius, menu overlay, table frame — LC-72
|
|
171
|
+
// CONTAINER-CLIP) cannot sever any part of it.
|
|
172
|
+
expect(
|
|
173
|
+
FOCUS_VISIBLE_RING_INSET.outlineOffset + FOCUS_VISIBLE_RING_INSET.outlineWidth,
|
|
174
|
+
).toBeLessThanOrEqual(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("keyboardFocusRingProps — the manual ring for roving-focus composites — pins the inset", () => {
|
|
178
|
+
expect(keyboardFocusRingProps).toEqual(FOCUS_VISIBLE_RING_INSET);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to 0", () => {
|
|
182
|
+
expect(ensureFocusVisibleRing({ outlineOffset: -2 }).outlineOffset).toBe(-2);
|
|
183
|
+
expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(0);
|
|
184
|
+
expect(ensureFocusVisibleRing().outlineOffset).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("LC-71: keyboard-only; measure the ring / text node, not the frame", () => {
|
|
189
|
+
it("standalone ring paints a 2px band on the ring-carrying node's box at offset 0", () => {
|
|
190
|
+
// Measure the node that paints the outline (the ring carrier / text node),
|
|
191
|
+
// never an ancestor Frame. Band = [offset, offset + width] from THAT box.
|
|
192
|
+
const { outlineOffset, outlineWidth } = FOCUS_VISIBLE_RING;
|
|
193
|
+
expect(outlineWidth).toBe(2);
|
|
194
|
+
expect(outlineOffset).toBe(0);
|
|
195
|
+
expect(outlineOffset + outlineWidth).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("chip dismiss rings the glyph node, not the 44px hit-box frame", () => {
|
|
199
|
+
const frameRule = compositeFocusRingCss.match(
|
|
200
|
+
/\.mp-chip-dismiss:focus-visible\s*\{([^}]+)\}/,
|
|
201
|
+
);
|
|
202
|
+
const ringRule = compositeFocusRingCss.match(
|
|
203
|
+
/\.mp-chip-dismiss:focus-visible \.mp-chip-dismiss-ring\s*\{([^}]+)\}/,
|
|
204
|
+
);
|
|
205
|
+
expect(frameRule?.[1]).toMatch(/outline:\s*none/);
|
|
206
|
+
expect(ringRule?.[1]).toMatch(/outline:\s*2px\s+solid/);
|
|
207
|
+
expect(ringRule?.[1]).toMatch(/outline-offset:\s*0/);
|
|
208
|
+
expect(ringRule?.[1]).not.toMatch(/outline-offset:\s*2px/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("non-text composites ring on :focus-visible only; text-entry keeps the any-focus carve-out", () => {
|
|
212
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-composite-ring:focus-visible/);
|
|
213
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-composite-ring-deep:has\(:focus-visible\)/);
|
|
214
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-chip-dismiss:focus-visible/);
|
|
215
|
+
// Text-entry carve-out (LC-71 §1a): focusing an input means typing.
|
|
216
|
+
expect(compositeFocusRingCss).toMatch(/:has\(input:focus\)/);
|
|
217
|
+
expect(compositeFocusRingCss).toMatch(/:has\(textarea:focus\)/);
|
|
218
|
+
});
|
|
110
219
|
});
|
|
111
220
|
|
|
112
221
|
describe("resolveKnobs control recipes (W12)", () => {
|
package/src/theme/focusState.ts
CHANGED
|
@@ -47,18 +47,43 @@ export function focusRingColorMeetsContrast(ringColor: string, surfaceColor: str
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* ONE ring geometry (DG-A11Y-01): 2px solid `$outlineColor
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
50
|
+
* ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
|
|
51
|
+
* offset **0** — the band hugs the border box and paints OUTSIDE it, on the
|
|
52
|
+
* surface behind the control (reference §10; board F1 verify pass: offset 0,
|
|
53
|
+
* not −2 and not +2). Offset 0 is what makes the ≥3:1 floor computable: the
|
|
54
|
+
* ring contrasts against the page ground, not the control's own fill. The
|
|
55
|
+
* old flat inset (−2) painted the band INSIDE solid intent fills and sank to
|
|
56
|
+
* 1.17–1.31:1 (light accent 1.27, light error 1.17, dark error 1.31) where
|
|
57
|
+
* the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
|
|
58
|
+
*
|
|
59
|
+
* The inset did not die — it moved to `FOCUS_VISIBLE_RING_INSET`, an
|
|
60
|
+
* explicit per-call-site carve-out for clipping ancestors. MPO-19 X7 names
|
|
61
|
+
* the same offset `FOCUS_RING_CLIPPED_OFFSET` so call sites do not type `-2`.
|
|
56
62
|
*/
|
|
63
|
+
export const FOCUS_RING_CLIPPED_OFFSET = -2;
|
|
57
64
|
export const FOCUS_VISIBLE_RING = {
|
|
58
65
|
outlineWidth: FOCUS_RING_MIN_WIDTH,
|
|
59
66
|
outlineStyle: "solid" as const,
|
|
60
67
|
outlineColor: "$outlineColor",
|
|
61
|
-
outlineOffset:
|
|
68
|
+
outlineOffset: 0,
|
|
69
|
+
} satisfies ControlStateProps;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The LC-71 §4 clipping carve-out: the SAME ring, pulled fully inside the
|
|
73
|
+
* border box (offset −2 + width 2 ⇒ the band spans [−2, 0] from the edge, so
|
|
74
|
+
* no clipping ancestor can sever it). ONLY for items inside `overflow`
|
|
75
|
+
* clipping — fused/segmented groups clipped at the group radius, edge-to-edge
|
|
76
|
+
* menu rows under a clipped overlay (LC-72 CONTAINER-CLIP), table cells.
|
|
77
|
+
* Standalone controls take `FOCUS_VISIBLE_RING`; call sites opt into the
|
|
78
|
+
* inset explicitly (this constant, or
|
|
79
|
+
* `ensureFocusVisibleRing({ outlineOffset: -2 })`). Verification measures the
|
|
80
|
+
* painted composition, never the declared offset alone — a ring box inset
|
|
81
|
+
* from the boundary reports a compliant offset while painting inside it
|
|
82
|
+
* (the F1 pseudoInset trap, mpo-fields-measured.md §7).
|
|
83
|
+
*/
|
|
84
|
+
export const FOCUS_VISIBLE_RING_INSET = {
|
|
85
|
+
...FOCUS_VISIBLE_RING,
|
|
86
|
+
outlineOffset: FOCUS_RING_CLIPPED_OFFSET,
|
|
62
87
|
} satisfies ControlStateProps;
|
|
63
88
|
|
|
64
89
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FontCategory } from "./knobs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concrete stacks for each headingFont/bodyFont category. Used as the
|
|
5
|
+
* FontKnobStyles `--f-family` fallback when Tamagui has no `$serif` (etc.)
|
|
6
|
+
* font registered, and as the default category fonts on
|
|
7
|
+
* createDefaultThemeConfig so consumers do not have to pass a font map.
|
|
8
|
+
*
|
|
9
|
+
* Matches packages/config/fonts.ts (system serif/mono; webfonts for the rest).
|
|
10
|
+
*/
|
|
11
|
+
export const fontCategoryStacks: Record<FontCategory, string> = {
|
|
12
|
+
"sans-serif":
|
|
13
|
+
'Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
14
|
+
serif: "Georgia, 'Times New Roman', Times, serif",
|
|
15
|
+
mono: "monospace",
|
|
16
|
+
slab: "'Roboto Slab', 'Rockwell', 'Courier New', serif",
|
|
17
|
+
rounded: "'Nunito', 'Varela Round', system-ui, sans-serif",
|
|
18
|
+
condensed: "'Roboto Condensed', 'Arial Narrow', sans-serif",
|
|
19
|
+
cursive: "'Dancing Script', 'Pacifico', cursive",
|
|
20
|
+
handwriting: "'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive",
|
|
21
|
+
pixel: "'Silkscreen', 'Press Start 2P', monospace",
|
|
22
|
+
blackletter: "'UnifrakturCook', 'Fraktur', serif",
|
|
23
|
+
geometric: "'Futura', 'Poppins', 'Century Gothic', sans-serif",
|
|
24
|
+
};
|
|
@@ -55,9 +55,22 @@ describe("buildFontKnobCss", () => {
|
|
|
55
55
|
expect(css).toContain(":root .font_body { --f-family: monospace !important");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("
|
|
58
|
+
it("emits a fallback --f-family when serif is not registered in Tamagui (live SHC)", () => {
|
|
59
|
+
// Live console 2026-08-29: #mp-font-knob-styles was present but had no
|
|
60
|
+
// --f-family because SHC never registers a `serif` font. The knob still
|
|
61
|
+
// has to write a family, or headingFont/bodyFont are a no-op on painted
|
|
62
|
+
// $heading/$body text. Georgia is a system face — no webfont required.
|
|
63
|
+
const css = buildFontKnobCss("serif", "sans-serif", resolveFamily, noTables);
|
|
64
|
+
expect(css).toContain(
|
|
65
|
+
":root .font_heading { --f-family: Georgia, 'Times New Roman', Times, serif !important; }",
|
|
66
|
+
);
|
|
67
|
+
expect(css).toContain(":where(:root .font_heading) { line-height:");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("emits a fallback --f-family for other unregistered categories and keeps the category leading", () => {
|
|
59
71
|
const css = buildFontKnobCss("cursive", "cursive", resolveFamily, noTables);
|
|
60
|
-
expect(css).
|
|
72
|
+
expect(css).toContain(":root .font_heading { --f-family:");
|
|
73
|
+
expect(css).toContain("cursive !important");
|
|
61
74
|
// cursive heading floor 1.3 beats the 1.25 minimum; body max(1.5, 1.5)
|
|
62
75
|
expect(css).toContain(":where(:root .font_heading) { line-height: 1.3; }");
|
|
63
76
|
expect(css).toContain(":where(:root .font_body) { line-height: 1.5; }");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveGlyphPaint } from "./glyphPaint";
|
|
3
|
+
|
|
4
|
+
describe("resolveGlyphPaint (LC-84)", () => {
|
|
5
|
+
it("never returns undefined", () => {
|
|
6
|
+
expect(resolveGlyphPaint({})).toBe("currentColor");
|
|
7
|
+
expect(resolveGlyphPaint({}, undefined)).toBe("currentColor");
|
|
8
|
+
expect(resolveGlyphPaint({}, null)).toBe("currentColor");
|
|
9
|
+
expect(resolveGlyphPaint({}, "")).toBe("currentColor");
|
|
10
|
+
expect(resolveGlyphPaint({ color: {} })).toBe("currentColor");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("reads .val, never a missing get(web) path", () => {
|
|
14
|
+
expect(resolveGlyphPaint({ color: { val: "#fafafa" } })).toBe("#fafafa");
|
|
15
|
+
expect(resolveGlyphPaint({ color10: { val: "#abcabc" } }, "$color10")).toBe("#abcabc");
|
|
16
|
+
expect(resolveGlyphPaint({ placeholderColor: { val: "#888" } }, "placeholderColor")).toBe(
|
|
17
|
+
"#888",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("passes through already-resolved colors and falls back to ink", () => {
|
|
22
|
+
expect(resolveGlyphPaint({ color: { val: "#111" } }, "#ff00aa")).toBe("#ff00aa");
|
|
23
|
+
expect(resolveGlyphPaint({ color12: { val: "#eee" } })).toBe("#eee");
|
|
24
|
+
expect(resolveGlyphPaint({ color11: { val: "#ddd" } })).toBe("#ddd");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useTheme } from "tamagui";
|
|
2
|
+
|
|
3
|
+
type ThemeToken = { val?: unknown };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LC-84 — resolve a concrete glyph paint from a Tamagui theme token.
|
|
7
|
+
* Phosphor (and any SVG that defaults to `#000` / `currentColor` without a
|
|
8
|
+
* parent color) MUST receive this, never `theme.x.get("web")` which is
|
|
9
|
+
* undefined off-web and leaves the glyph black in dark mode.
|
|
10
|
+
*
|
|
11
|
+
* Never returns undefined.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveGlyphPaint(
|
|
14
|
+
theme: Record<string, ThemeToken | undefined>,
|
|
15
|
+
color?: string | null,
|
|
16
|
+
): string {
|
|
17
|
+
const seen = new Set<string>();
|
|
18
|
+
const push = (value: unknown) => {
|
|
19
|
+
if (typeof value === "string" && value.length > 0 && value !== "undefined") {
|
|
20
|
+
seen.add(value);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (typeof color === "string" && color.length > 0) {
|
|
25
|
+
const key = color.startsWith("$") ? color.slice(1) : color;
|
|
26
|
+
push(theme[key]?.val);
|
|
27
|
+
if (color.startsWith("#") || color.startsWith("rgb") || color.startsWith("hsl")) {
|
|
28
|
+
push(color);
|
|
29
|
+
} else if (!theme[key]) {
|
|
30
|
+
push(color);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
push(theme.color?.val);
|
|
35
|
+
push(theme.color12?.val);
|
|
36
|
+
push(theme.color11?.val);
|
|
37
|
+
|
|
38
|
+
for (const value of seen) return value;
|
|
39
|
+
return "currentColor";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Live theme ink for Phosphor / SVG carets (LC-84). Never undefined. */
|
|
43
|
+
export function useGlyphColor(color?: string): string {
|
|
44
|
+
const theme = useTheme() as unknown as Record<string, ThemeToken | undefined>;
|
|
45
|
+
return resolveGlyphPaint(theme, color);
|
|
46
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./createThemes";
|
|
|
8
8
|
export * from "./defaults/index";
|
|
9
9
|
export * from "./devtools/ThemeDevtoolsPanel";
|
|
10
10
|
export * from "./focusState";
|
|
11
|
+
export * from "./glyphPaint";
|
|
12
|
+
export * from "./fontCategoryStacks";
|
|
11
13
|
export * from "./FontKnobStyles";
|
|
12
14
|
export * from "./groupGeometry";
|
|
13
15
|
export * from "./hairline";
|
|
@@ -20,6 +22,7 @@ export type { Preset as PresetDefinition, IntentOverride } from "./preset.types"
|
|
|
20
22
|
export * from "./PresetContext";
|
|
21
23
|
export * from "./presets";
|
|
22
24
|
export * from "./radiusClass";
|
|
25
|
+
export * from "./radiusClassTable.generated";
|
|
23
26
|
export * from "./readableColor";
|
|
24
27
|
export * from "./recipes";
|
|
25
28
|
export * from "./resolveKnobs";
|
package/src/theme/knobs.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// ── Structure ──────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
+
/** Structural fill. Exactly two values (R9 G9): `filled` | `outlined`. Ghost is a Chip/Tag variant, not this knob. */
|
|
3
4
|
export const FillStyle = { Filled: "filled", Outlined: "outlined" } as const;
|
|
4
5
|
export type FillStyle = (typeof FillStyle)[keyof typeof FillStyle];
|
|
6
|
+
/** R9 G9 — adding a third FillStyle member fails this lock. */
|
|
7
|
+
type _FillStyleExactlyTwo = FillStyle extends "filled" | "outlined"
|
|
8
|
+
? "filled" | "outlined" extends FillStyle
|
|
9
|
+
? true
|
|
10
|
+
: never
|
|
11
|
+
: never;
|
|
12
|
+
const _fillStyleExactlyTwo: _FillStyleExactlyTwo = true;
|
|
13
|
+
void _fillStyleExactlyTwo;
|
|
5
14
|
|
|
6
15
|
export const BorderRadius = {
|
|
7
16
|
None: "none",
|
|
@@ -15,8 +24,10 @@ export type BorderRadius = (typeof BorderRadius)[keyof typeof BorderRadius];
|
|
|
15
24
|
/**
|
|
16
25
|
* Corner curvature continuity (Axiom 15 OPTICS). `smooth` renders
|
|
17
26
|
* continuous-curvature (superellipse/squircle) corners where the platform
|
|
18
|
-
* can (web:
|
|
19
|
-
*
|
|
27
|
+
* can (web: `corner-shape: squircle`, computed `superellipse(2)` on
|
|
28
|
+
* Chromium 152; engines without the property drop the declaration — that
|
|
29
|
+
* fallback is measured via `cssSupportsCornerShape`, not assumed); `round`
|
|
30
|
+
* is today's circular-arc border-radius everywhere.
|
|
20
31
|
* Two values, not a numeric dial: every shipped smoothing system converges
|
|
21
32
|
* on one blessed curve (Apple ≈ Figma 0.6 ≈ superellipse K=2), and a free
|
|
22
33
|
* number invites per-screen drift (Axiom 5). `none` is not needed —
|
|
@@ -226,6 +237,11 @@ export type InteractionState = (typeof interactionStates)[number];
|
|
|
226
237
|
|
|
227
238
|
export interface Knobs {
|
|
228
239
|
fillStyle: FillStyle;
|
|
240
|
+
/**
|
|
241
|
+
* Per-role radius (MPO-21 R9): leftover radius maps onto DEFAULT /
|
|
242
|
+
* CIRCULAR-AT-FULL / BINARY / CONTAINER-CAP. No improvised radius prop
|
|
243
|
+
* and no fifth class on this interface.
|
|
244
|
+
*/
|
|
229
245
|
borderRadius: BorderRadius;
|
|
230
246
|
/**
|
|
231
247
|
* Corner curvature continuity (Axiom 15 OPTICS). Optional so existing
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MIN_PRESS_TARGET,
|
|
4
4
|
NARROW_BREAKPOINT,
|
|
5
5
|
OVERLAY_ANCHOR_GAP,
|
|
6
|
+
OVERLAY_ATTACH_GAP,
|
|
6
7
|
OVERLAY_BREAKPOINT,
|
|
7
8
|
READING_WIDTH_CH,
|
|
8
9
|
READING_WIDTH_MAX_CH,
|
|
@@ -69,8 +70,9 @@ describe("readingWidthStyle (DG-LAY-05)", () => {
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
describe("overlay anchoring", () => {
|
|
72
|
-
it("pins the
|
|
73
|
+
it("pins the free-overlay gap at 4px and the attach gap at 0", () => {
|
|
73
74
|
expect(OVERLAY_ANCHOR_GAP).toBe(4);
|
|
75
|
+
expect(OVERLAY_ATTACH_GAP).toBe(0);
|
|
74
76
|
});
|
|
75
77
|
});
|
|
76
78
|
|