Skip to content

Commit fe180e6

Browse files
refactor(lint): drive ui_primitives to zero on the type-assertion rule
Every `require-safety-comment-for-type-assertion` finding in web/src/components/ui_primitives is gone, by naming what the assertions were papering over rather than commenting them. Production: - `snapSpacing` now returns `SpacingStep`, the type its own step tuple already implies, so FormGrid stops re-asserting the result. - `Surface` binds `RADIUS_MAP[rounded]` once; TypeScript cannot narrow a repeated index expression, which is what the `keyof Theme["rounded"]` assertion was standing in for. - `getThemeColor` resolves a palette path through entry lookups checked by `isObjectLike`/`isString` instead of three `Record` casts, and now says in one place what a `colorKey` prop means. - `sxEntries` in tokens.ts normalizes the three `SxProps` forms, so Popover and ContextMenu compose the caller's `sx` under their defaults as an array. ContextMenu previously spread it through `as object`, which silently dropped the callback and array forms. - `Stack` reads a component's display name through a declared shape; `InfoTooltip` names the CSS-custom-property style it builds; the generic-erasing `memo` cast on `Autocomplete` keeps its assertion and gains the SAFETY comment stating the invariant. Tests: `firstElement`, `queryElement`, `queryInput` and `asElement` in test-utils/doubles.ts replace 120 DOM assertions with checked narrowings, so a query that misses fails where it queried. `HoverActionGroup`'s pass-through case names its `data-*` prop instead of `as any`, and the media-locator suite mints its branded URLs with `asResolvedMediaUrl`. The enforced overrides are regenerated: that also picks up 19 pairs in packages/godot, packages/godot-templates and packages/blender-nodes that were already clean but never ratcheted.
1 parent e363485 commit fe180e6

34 files changed

Lines changed: 317 additions & 176 deletions

.oxlintrc.anti-slop-enforced.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"packages/execution/src/**",
3535
"packages/fal-codegen/src/**",
3636
"packages/fal-nodes/src/**",
37+
"packages/godot/src/**",
38+
"packages/godot-templates/src/**",
3739
"packages/gpu/src/**",
3840
"packages/huggingface/src/**",
3941
"packages/huggingface-nodes/src/**",
@@ -74,6 +76,7 @@
7476
"packages/auth/src/**",
7577
"packages/automation-nodes/src/**",
7678
"packages/base-nodes/src/**",
79+
"packages/blender-nodes/src/**",
7780
"packages/browser/src/**",
7881
"packages/chat/src/**",
7982
"packages/code-nodes/src/**",
@@ -85,6 +88,8 @@
8588
"packages/dsl/src/**",
8689
"packages/elevenlabs-nodes/src/**",
8790
"packages/fal-codegen/src/**",
91+
"packages/godot/src/**",
92+
"packages/godot-templates/src/**",
8893
"packages/gpu/src/**",
8994
"packages/huggingface/src/**",
9095
"packages/image-editor/src/**",
@@ -123,6 +128,7 @@
123128
"packages/document-nodes/src/**",
124129
"packages/dsl/src/**",
125130
"packages/execution/src/**",
131+
"packages/godot-templates/src/**",
126132
"packages/huggingface-nodes/src/**",
127133
"packages/image-editor/src/**",
128134
"packages/kie-codegen/src/**",
@@ -150,6 +156,7 @@
150156
"packages/auth/src/**",
151157
"packages/automation-nodes/src/**",
152158
"packages/base-nodes/src/**",
159+
"packages/blender-nodes/src/**",
153160
"packages/browser/src/**",
154161
"packages/chat/src/**",
155162
"packages/cli/src/**",
@@ -165,6 +172,8 @@
165172
"packages/execution/src/**",
166173
"packages/fal-codegen/src/**",
167174
"packages/fal-nodes/src/**",
175+
"packages/godot/src/**",
176+
"packages/godot-templates/src/**",
168177
"packages/gpu/src/**",
169178
"packages/huggingface/src/**",
170179
"packages/huggingface-nodes/src/**",
@@ -220,6 +229,8 @@
220229
"packages/config/src/**",
221230
"packages/data-nodes/src/**",
222231
"packages/document-nodes/src/**",
232+
"packages/godot/src/**",
233+
"packages/godot-templates/src/**",
223234
"packages/image-editor/src/**",
224235
"packages/image-nodes/src/**",
225236
"packages/kie-codegen/src/**",
@@ -257,6 +268,8 @@
257268
"packages/data-nodes/src/**",
258269
"packages/document-nodes/src/**",
259270
"packages/elevenlabs-nodes/src/**",
271+
"packages/godot/src/**",
272+
"packages/godot-templates/src/**",
260273
"packages/kie-codegen/src/**",
261274
"packages/minimax-nodes/src/**",
262275
"packages/model-pricing/src/**",
@@ -280,6 +293,7 @@
280293
"packages/auth/src/**",
281294
"packages/automation-nodes/src/**",
282295
"packages/base-nodes/src/**",
296+
"packages/blender-nodes/src/**",
283297
"packages/chat/src/**",
284298
"packages/code-nodes/src/**",
285299
"packages/compute/src/**",
@@ -291,6 +305,8 @@
291305
"packages/elevenlabs-nodes/src/**",
292306
"packages/fal-codegen/src/**",
293307
"packages/fal-nodes/src/**",
308+
"packages/godot/src/**",
309+
"packages/godot-templates/src/**",
294310
"packages/huggingface/src/**",
295311
"packages/huggingface-nodes/src/**",
296312
"packages/image-editor/src/**",
@@ -334,6 +350,8 @@
334350
"packages/config/src/**",
335351
"packages/data-nodes/src/**",
336352
"packages/document-nodes/src/**",
353+
"packages/godot/src/**",
354+
"packages/godot-templates/src/**",
337355
"packages/kie-codegen/src/**",
338356
"packages/model-pricing/src/**",
339357
"packages/nodes-utils/src/**",
@@ -359,6 +377,7 @@
359377
"packages/config/src/**",
360378
"packages/data-nodes/src/**",
361379
"packages/document-nodes/src/**",
380+
"packages/godot-templates/src/**",
362381
"packages/image-editor/src/**",
363382
"packages/kie-codegen/src/**",
364383
"packages/model-pricing/src/**",

web/src/components/ui_primitives/Autocomplete.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,8 @@ function AutocompleteInternal<
150150
);
151151
}
152152

153-
export const Autocomplete = memo(AutocompleteInternal) as typeof AutocompleteInternal;
153+
export const Autocomplete =
154+
// SAFETY: `memo` returns a non-generic `MemoExoticComponent`, dropping the
155+
// three type parameters callers pass. The runtime value is unchanged; only
156+
// the generic call signature is restored.
157+
memo(AutocompleteInternal) as typeof AutocompleteInternal;

web/src/components/ui_primitives/CircularActionButton.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -183,25 +183,26 @@ export interface CircularActionButtonProps {
183183
tabIndex?: number;
184184
}
185185

186+
/**
187+
* Resolves a `"group.variant"` palette path, or a top-level palette color's
188+
* `main`. MUI's `Palette` carries no index signature, so the dynamic key a
189+
* `color` prop names is matched against the palette's entries. An unresolvable
190+
* key comes back unchanged, which is how a literal CSS color passes through.
191+
*/
186192
const getThemeColor = (theme: Theme, colorKey: string): string => {
187-
const palette = theme.vars.palette as Record<string, unknown>;
188-
189-
const parts = colorKey.split(".");
190-
if (parts.length === 2 && parts[0] in palette) {
191-
const [category, variant] = parts;
192-
const categoryPalette = palette[category];
193-
if (categoryPalette && isObjectLike(categoryPalette) && variant in (categoryPalette as Record<string, unknown>)) {
194-
const value = (categoryPalette as Record<string, string>)[variant];
195-
return value || colorKey;
196-
}
197-
}
198-
if (colorKey in palette) {
199-
const colorEntry = palette[colorKey];
200-
if (colorEntry && isObjectLike(colorEntry) && "main" in colorEntry) {
201-
return (colorEntry as { main: string }).main || colorKey;
202-
}
193+
const [group, variant] = colorKey.split(".");
194+
const groupName = variant === undefined ? colorKey : group;
195+
const entry = Object.entries(theme.vars.palette).find(
196+
([name]) => name === groupName
197+
)?.[1];
198+
if (!isObjectLike(entry)) {
199+
return colorKey;
203200
}
204-
return colorKey;
201+
const variantName = variant ?? "main";
202+
const color = Object.entries(entry).find(
203+
([name]) => name === variantName
204+
)?.[1];
205+
return (isString(color) && color) || colorKey;
205206
};
206207

207208
export const CircularActionButton = memo(

web/src/components/ui_primitives/ContextMenu.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { useTheme } from "@mui/material/styles";
1515
import { SxProps, Theme } from "@mui/material";
1616
import { isNumber } from "../../utils/typePredicates";
17+
import { sxEntries } from "./tokens";
1718

1819
export interface ContextMenuProps extends Omit<MuiMenuProps, "anchorPosition"> {
1920
/** Position for right-click context menus */
@@ -87,19 +88,21 @@ const ContextMenuInternal: React.FC<ContextMenuProps> = ({
8788
...slotProps,
8889
paper: {
8990
...slotProps?.paper,
90-
sx: {
91-
borderRadius: borderRadiusValue,
92-
maxHeight,
93-
minWidth,
94-
...(compact && {
95-
"& .MuiMenuItem-root": {
96-
minHeight: "auto",
97-
py: 0.5,
98-
fontSize: theme.fontSizeSmall,
99-
},
100-
}),
101-
...(paperSx as object),
102-
} as SxProps<Theme>,
91+
sx: [
92+
{
93+
borderRadius: borderRadiusValue,
94+
maxHeight,
95+
minWidth,
96+
...(compact && {
97+
"& .MuiMenuItem-root": {
98+
minHeight: "auto",
99+
py: 0.5,
100+
fontSize: theme.fontSizeSmall,
101+
},
102+
}),
103+
},
104+
...sxEntries(paperSx),
105+
],
103106
},
104107
}}
105108
{...props}

web/src/components/ui_primitives/FormGrid.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SPACING_STEPS,
1616
resolveSpacing,
1717
snapSpacing,
18+
type SpacingStep,
1819
type SpacingValue
1920
} from "./spacing";
2021

@@ -31,9 +32,8 @@ export interface FormGridProps {
3132
}
3233

3334
/** One canonical step below the given spacing (xl → lg); floors at the smallest step. */
34-
const stepDownSpacing = (units: number): number => {
35-
const snapped = snapSpacing(units) as (typeof SPACING_STEPS)[number];
36-
const index = SPACING_STEPS.indexOf(snapped);
35+
const stepDownSpacing = (units: number): SpacingStep => {
36+
const index = SPACING_STEPS.indexOf(snapSpacing(units));
3737
return SPACING_STEPS[Math.max(index - 1, 0)];
3838
};
3939

web/src/components/ui_primitives/InfoTooltip.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ type TooltipOrigin = {
100100
horizontal: "left" | "center" | "right";
101101
};
102102

103+
/** An inline style that also sets CSS custom properties the stylesheet reads. */
104+
type StyleWithCssVars = React.CSSProperties & Record<`--${string}`, string>;
105+
103106
export const InfoTooltip: React.FC<InfoTooltipProps> = memo(({
104107
content,
105108
title,
@@ -188,7 +191,8 @@ export const InfoTooltip: React.FC<InfoTooltipProps> = memo(({
188191
};
189192

190193
const origins = getPopoverOrigins(placement);
191-
194+
const popoverStyle: StyleWithCssVars = { "--max-width": `${maxWidth}px` };
195+
192196
return (
193197
<div className={`info-tooltip nodrag ${className || ""}`} css={styles(theme)}>
194198
{button}
@@ -205,7 +209,7 @@ export const InfoTooltip: React.FC<InfoTooltipProps> = memo(({
205209
horizontal: origins.transform.horizontal
206210
}}
207211
css={popoverStyles(theme)}
208-
style={{ "--max-width": `${maxWidth}px` } as React.CSSProperties}
212+
style={popoverStyle}
209213
>
210214
<Box className="popover-content">
211215
{title && (

web/src/components/ui_primitives/Popover.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import MuiPopover, {
1212
import { useTheme } from "@mui/material/styles";
1313
import { SxProps, Theme } from "@mui/material";
1414
import { isNumber } from "../../utils/typePredicates";
15+
import { sxEntries } from "./tokens";
1516

1617
export type PopoverPlacement =
1718
| "bottom-left"
@@ -124,8 +125,6 @@ const PopoverInternal: React.FC<PopoverProps> = ({
124125
paperSlot && typeof paperSlot === "object" && "sx" in paperSlot
125126
? paperSlot.sx
126127
: undefined;
127-
const asArray = (s: SxProps<Theme> | undefined) =>
128-
s === undefined ? [] : Array.isArray(s) ? s : [s];
129128

130129
return (
131130
<MuiPopover
@@ -144,9 +143,9 @@ const PopoverInternal: React.FC<PopoverProps> = ({
144143
maxHeight,
145144
overflow: maxHeight ? "auto" : undefined
146145
},
147-
...asArray(callerPaperSx),
148-
...asArray(paperSx)
149-
] as SxProps<Theme>
146+
...sxEntries(callerPaperSx),
147+
...sxEntries(paperSx)
148+
]
150149
}
151150
}}
152151
{...props}

web/src/components/ui_primitives/Stack.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ export interface StackProps extends Omit<BoxProps, 'display' | 'flexDirection'>
2121
fullWidth?: boolean;
2222
}
2323

24+
/** The name React would show for an element's type: a tag, or a component name. */
25+
const elementTypeName = (type: React.ReactElement["type"]): string => {
26+
if (isString(type)) {
27+
return type;
28+
}
29+
// `displayName` is a React convention, not part of the constructor type.
30+
const named: { name: string; displayName?: string } = type;
31+
return named.displayName || named.name || "component";
32+
};
33+
2434
/**
2535
* Stack - A vertical stack container with consistent spacing
2636
*
@@ -51,9 +61,7 @@ const getChildKey = (child: React.ReactNode, index: number): string | number =>
5161
if (child.key) {
5262
return child.key;
5363
}
54-
const componentType = child.type as string | (React.FC & { displayName?: string });
55-
const type = isString(componentType) ? componentType : componentType?.displayName || componentType?.name || "component";
56-
return `${type}-${index}`;
64+
return `${elementTypeName(child.type)}-${index}`;
5765
}
5866
return index;
5967
};

web/src/components/ui_primitives/Surface.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export const Surface = memo(
7474
ref
7575
) => {
7676
const theme = useTheme();
77+
const radiusKey = RADIUS_MAP[rounded];
7778

7879
// Read backgrounds from theme.vars (CSS variables) so the active color
7980
// scheme is honored. theme.palette.* is baked from the default scheme at
@@ -90,10 +91,7 @@ export const Surface = memo(
9091
ref={ref}
9192
elevation={elevation}
9293
sx={{
93-
borderRadius:
94-
RADIUS_MAP[rounded] === "none"
95-
? 0
96-
: theme.rounded[RADIUS_MAP[rounded] as keyof Theme["rounded"]],
94+
borderRadius: radiusKey === "none" ? 0 : theme.rounded[radiusKey],
9795
padding:
9896
isNumber(padding)
9997
? theme.spacing(padding)

web/src/components/ui_primitives/ThemeToggleButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const ThemeToggleButtonInternal: React.FC<ThemeToggleButtonProps> = ({
7171
<Switch
7272
checked={isDark}
7373
onChange={handleToggle}
74-
slotProps={{ input: { "aria-label": tooltipText, role: "switch" } as React.InputHTMLAttributes<HTMLInputElement> }}
74+
slotProps={{ input: { "aria-label": tooltipText, role: "switch" } }}
7575
icon={<LightModeIcon fontSize="small" />}
7676
checkedIcon={<DarkModeIcon fontSize="small" />}
7777
size={buttonSize === "large" ? "medium" : "small"}

0 commit comments

Comments
 (0)