Skip to content

Commit f014e84

Browse files
committed
oh boy!
1 parent b87a195 commit f014e84

File tree

5 files changed

+166
-14
lines changed

5 files changed

+166
-14
lines changed

site/src/api/queries/users.ts

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const me = (): UseQueryOptions<User> & {
131131
queryKey: meKey,
132132
initialData: initialUserData,
133133
queryFn: API.getAuthenticatedUser,
134+
refetchOnWindowFocus: true,
134135
};
135136
};
136137

site/src/pages/IconsPage/IconsPage.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
23
import { IconsPage } from "./IconsPage";
34

45
const meta: Meta<typeof IconsPage> = {
56
title: "pages/IconsPage",
7+
parameters: { chromatic },
68
component: IconsPage,
79
args: {},
810
};

site/src/pages/IconsPage/IconsPage.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import {
2020
import { Stack } from "components/Stack/Stack";
2121
import icons from "theme/icons.json";
2222
import {
23-
defaultModeForBuiltinIcons,
24-
parseModeParameters,
23+
defaultParametersForBuiltinIcons,
24+
parseImageParameters,
2525
} from "theme/externalImages";
2626
import { pageTitle } from "utils/page";
2727

@@ -175,9 +175,9 @@ export const IconsPage: FC = () => {
175175
pointerEvents: "none",
176176
padding: 12,
177177
},
178-
parseModeParameters(
178+
parseImageParameters(
179179
theme.externalImages,
180-
defaultModeForBuiltinIcons.get(icon.url) ?? "",
180+
defaultParametersForBuiltinIcons.get(icon.url) ?? "",
181181
),
182182
]}
183183
/>

site/src/theme/externalImages.test.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
forDarkThemes,
3+
forLightThemes,
4+
getExternalImageStylesFromUrl,
5+
parseImageParameters,
6+
} from "./externalImages";
7+
8+
describe("externalImage parameters", () => {
9+
test("default parameters", () => {
10+
// Correctly selects default
11+
const widgetsStyles = getExternalImageStylesFromUrl(
12+
forDarkThemes,
13+
"/icon/widgets.svg",
14+
);
15+
expect(widgetsStyles).toBe(forDarkThemes.monochrome);
16+
17+
// Allows overrides
18+
const overrideStyles = getExternalImageStylesFromUrl(
19+
forDarkThemes,
20+
"/icon/widgets.svg?fullcolor",
21+
);
22+
expect(overrideStyles).toBe(forDarkThemes.fullcolor);
23+
24+
// Not actually a built-in
25+
const someoneElsesWidgetsStyles = getExternalImageStylesFromUrl(
26+
forDarkThemes,
27+
"https://example.com/icon/widgets.svg",
28+
);
29+
expect(someoneElsesWidgetsStyles).toBeUndefined();
30+
});
31+
32+
test("blackWithColor brightness", () => {
33+
const tryCase = (params: string) =>
34+
parseImageParameters(forDarkThemes, params);
35+
36+
const withDecimalValue = tryCase("?blackWithColor&brightness=1.5");
37+
expect(withDecimalValue?.filter).toBe(
38+
"invert(1) hue-rotate(180deg) brightness(1.5)",
39+
);
40+
41+
const withPercentageValue = tryCase("?blackWithColor&brightness=150%");
42+
expect(withPercentageValue?.filter).toBe(
43+
"invert(1) hue-rotate(180deg) brightness(150%)",
44+
);
45+
46+
// Sketchy `brightness` value will be ignored.
47+
const niceTry = tryCase(
48+
"?blackWithColor&brightness=</style><script>alert('leet hacking');</script>",
49+
);
50+
expect(niceTry?.filter).toBe("invert(1) hue-rotate(180deg)");
51+
52+
const withLightTheme = parseImageParameters(
53+
forLightThemes,
54+
"?blackWithColor&brightness=1.5",
55+
);
56+
expect(withLightTheme).toBeUndefined();
57+
});
58+
59+
test("whiteWithColor brightness", () => {
60+
const tryCase = (params: string) =>
61+
parseImageParameters(forLightThemes, params);
62+
63+
const withDecimalValue = tryCase("?whiteWithColor&brightness=1.5");
64+
expect(withDecimalValue?.filter).toBe(
65+
"invert(1) hue-rotate(180deg) brightness(1.5)",
66+
);
67+
68+
const withPercentageValue = tryCase("?whiteWithColor&brightness=150%");
69+
expect(withPercentageValue?.filter).toBe(
70+
"invert(1) hue-rotate(180deg) brightness(150%)",
71+
);
72+
73+
// Sketchy `brightness` value will be ignored.
74+
const niceTry = tryCase(
75+
"?whiteWithColor&brightness=</style><script>alert('leet hacking');</script>",
76+
);
77+
expect(niceTry?.filter).toBe("invert(1) hue-rotate(180deg)");
78+
79+
const withDarkTheme = parseImageParameters(
80+
forDarkThemes,
81+
"?whiteWithColor&brightness=1.5",
82+
);
83+
expect(withDarkTheme).toBeUndefined();
84+
});
85+
});

site/src/theme/externalImages.ts

+74-10
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,34 @@ import { type CSSObject } from "@emotion/react";
33
export type ExternalImageMode = keyof ExternalImageModeStyles;
44

55
export interface ExternalImageModeStyles {
6+
/**
7+
* monochrome icons will be flattened to a neutral, theme-appropriate color.
8+
* eg. white, light gray, dark gray, black
9+
*/
610
monochrome?: CSSObject;
11+
/**
12+
* @default
13+
* fullcolor icons should look their best of any background, with distinct colors
14+
* and good contrast. This is the default, and won't alter the image.
15+
*/
716
fullcolor?: CSSObject;
17+
/**
18+
* whiteWithColor is useful for icons that are primarily white, or contain white text,
19+
* which are hard to see or look incorrect on light backgrounds. This setting will apply
20+
* a color-respecting inversion filter to turn white into black when appropriate to
21+
* improve contrast.
22+
* You can also specify a `brightness` level if your icon still doesn't look quite right.
23+
* eg. /icon/aws.svg?blackWithColor&brightness=1.5
24+
*/
825
whiteWithColor?: CSSObject;
26+
/**
27+
* blackWithColor is useful for icons that are primarily black, or contain black text,
28+
* which are hard to see or look incorrect on dark backgrounds. This setting will apply
29+
* a color-respecting inversion filter to turn black into white when appropriate to
30+
* improve contrast.
31+
* You can also specify a `brightness` level if your icon still doesn't look quite right.
32+
* eg. /icon/aws.svg?blackWithColor&brightness=1.5
33+
*/
934
blackWithColor?: CSSObject;
1035
}
1136

@@ -31,16 +56,29 @@ export const forLightThemes: ExternalImageModeStyles = {
3156
blackWithColor: undefined,
3257
};
3358

34-
const parseExtraParams = (params: URLSearchParams, baseStyles?: CSSObject) => {
59+
// multiplier matches the beginning of the string (^), a number, optionally followed
60+
// followed by a decimal portion, optionally followed by a percent symbol, and the
61+
// end of the string ($).
62+
const multiplier = /^\d+(\.\d+)?%?$/;
63+
64+
/**
65+
* Used with `whiteWithColor` and `blackWithColor` to allow for finer tuning
66+
*/
67+
const parseInvertFilterParameters = (
68+
params: URLSearchParams,
69+
baseStyles?: CSSObject,
70+
) => {
71+
// Only apply additional styles if the current theme supports this mode
3572
if (!baseStyles) {
3673
return;
3774
}
3875

3976
let extraStyles: CSSObject | undefined;
4077

41-
if (params.has("brightness")) {
78+
const brightness = params.get("brightness");
79+
if (multiplier.test(brightness!)) {
4280
let filter = baseStyles.filter ?? "";
43-
filter += `brightness(${params.get("brightness")})`;
81+
filter += ` brightness(${brightness})`;
4482
extraStyles = { ...extraStyles, filter };
4583
}
4684

@@ -54,7 +92,7 @@ const parseExtraParams = (params: URLSearchParams, baseStyles?: CSSObject) => {
5492
};
5593
};
5694

57-
export function parseModeParameters(
95+
export function parseImageParameters(
5896
modes: ExternalImageModeStyles,
5997
searchString: string,
6098
): CSSObject | undefined {
@@ -64,18 +102,43 @@ export function parseModeParameters(
64102

65103
if (params.has("monochrome")) {
66104
styles = modes.monochrome;
105+
} else if (params.has("whiteWithColor")) {
106+
styles = parseInvertFilterParameters(params, modes.whiteWithColor);
107+
} else if (params.has("blackWithColor")) {
108+
styles = parseInvertFilterParameters(params, modes.blackWithColor);
67109
}
68-
if (params.has("whiteWithColor")) {
69-
styles = parseExtraParams(params, modes.whiteWithColor);
110+
111+
return styles;
112+
}
113+
114+
export function getExternalImageStylesFromUrl(
115+
modes: ExternalImageModeStyles,
116+
urlString: string,
117+
) {
118+
const url = new URL(urlString, location.origin);
119+
120+
if (url.search) {
121+
return parseImageParameters(modes, url.search);
70122
}
71-
if (params.has("blackWithColor")) {
72-
styles = parseExtraParams(params, modes.blackWithColor);
123+
124+
if (
125+
url.origin === location.origin &&
126+
defaultParametersForBuiltinIcons.has(url.pathname)
127+
) {
128+
return parseImageParameters(
129+
modes,
130+
defaultParametersForBuiltinIcons.get(url.pathname)!,
131+
);
73132
}
74133

75-
return parseExtraParams(params, styles);
134+
return undefined;
76135
}
77136

78-
export const defaultModeForBuiltinIcons = new Map<string, string>([
137+
/**
138+
* defaultModeForBuiltinIcons contains modes for all of our built-in icons that
139+
* don't look their best in all of our themes with the default fullcolor mode.
140+
*/
141+
export const defaultParametersForBuiltinIcons = new Map<string, string>([
79142
["/icon/apple-black.svg", "monochrome"],
80143
["/icon/aws.png", "whiteWithColor&brightness=1.5"],
81144
["/icon/aws.svg", "blackWithColor&brightness=1.5"],
@@ -87,6 +150,7 @@ export const defaultModeForBuiltinIcons = new Map<string, string>([
87150
["/icon/folder.svg", "monochrome"],
88151
["/icon/github.svg", "monochrome"],
89152
["/icon/image.svg", "monochrome"],
153+
["/icon/jupyter.svg", "blackWithColor"],
90154
["/icon/kasmvnc.svg", "whiteWithColor"],
91155
["/icon/memory.svg", "monochrome"],
92156
["/icon/rust.svg", "monochrome"],

0 commit comments

Comments
 (0)