impress/app/javascript/wardrobe-2020/AppProvider.js
Emi Matchu fcc17d3dcf Whoops, fix some style regressions for the React app!
Ah beans, I didn't notice when doing my Turbo fixes in
40804c1543, that I had accidentally
prevented Chakra from applying some of its usual global styles! This
caused some minor visual regressions in various parts of the app, e.g.,
the default border color for the search field in the wardrobe app
became way darker.

Here, instead of copy-pasting the styles and missing some parts, we
scope the global styles a bit more carefully: we first use
`extendTheme` with the same code as Impress 2020 to get a good
`globalStyles` function that includes Chakra's defaults, *then* delete
the key from the theme.

Then, in `ScopedCSSReset`, we use code similar to Chakra's own global
style application code: call the `globalStyles` function with the
current theme and color mode, use Chakra's `css` function to convert
values like `green.800` to CSS values, prepend our scoping rule onto
the selectors, and drop it into our Emotion CSS.

Tbh I was worried because when I first noticed this issue while on my
trip, I saw the black item search box border, and was like "ah dang,
did I destroy all the color in the app by breaking the part where
Chakra defines its CSS color variables??" And no, that's not actually
what happened, a lot of the app still had its colors!

So this was less pressing than I had thought, but still good to get
fixed!
2024-03-31 01:20:45 -07:00

420 lines
11 KiB
JavaScript

import React from "react";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import {
ChakraProvider,
Box,
css as resolveCSS,
extendTheme,
useColorMode,
useTheme,
} from "@chakra-ui/react";
import { mode } from "@chakra-ui/theme-tools";
import { ApolloProvider } from "@apollo/client";
import { BrowserRouter } from "react-router-dom";
import { css, Global } from "@emotion/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import produce from "immer";
import apolloClient from "./apolloClient";
const reactQueryClient = new QueryClient();
let theme = extendTheme({
styles: {
global: (props) => ({
body: {
background: mode("gray.50", "gray.800")(props),
color: mode("green.800", "green.50")(props),
transition: "all 0.25s",
},
}),
},
});
// Capture the global styles function from our theme, but remove it from the
// theme's `styles.global` key. We don't actually want to apply these styles
// globally, because they conflict with the app's other styles when embedded in
// e.g. the item page! We'll apply them more carefully in `ScopedCSSReset`.
//
// NOTE: We get this from `theme`, instead of just declaring the function
// separately, because `extendTheme` returns a wrapped version of our function
// that also returns some of Chakra's own default global styles.
const globalStyles = theme.styles.global;
theme.styles.global = {};
export default function AppProvider({ children }) {
React.useEffect(() => setupLogging(), []);
return (
<BrowserRouter>
<QueryClientProvider client={reactQueryClient}>
<ApolloProvider client={apolloClient}>
<ChakraProvider resetCSS={false} theme={theme}>
<ScopedCSSReset>{children}</ScopedCSSReset>
</ChakraProvider>
</ApolloProvider>
</QueryClientProvider>
</BrowserRouter>
);
}
function setupLogging() {
Sentry.init({
dsn: "https://c55875c3b0904264a1a99e5b741a221e@o506079.ingest.sentry.io/5595379",
autoSessionTracking: true,
integrations: [
new Integrations.BrowserTracing({
beforeNavigate: (context) => ({
...context,
// Assume any path segment starting with a digit is an ID, and replace
// it with `:id`. This will help group related routes in Sentry stats.
// NOTE: I'm a bit uncertain about the timing on this for tracking
// client-side navs... but we now only track first-time
// pageloads, and it definitely works correctly for them!
name: window.location.pathname.replaceAll(/\/[0-9][^/]*/g, "/:id"),
}),
// We have a _lot_ of location changes that don't actually signify useful
// navigations, like in the wardrobe page. It could be useful to trace
// them with better filtering someday, but frankly we don't use the perf
// features besides Web Vitals right now, and those only get tracked on
// first-time pageloads, anyway. So, don't track client-side navs!
startTransactionOnLocationChange: false,
}),
],
denyUrls: [
// Don't log errors that were probably triggered by extensions and not by
// our own app. (Apparently Sentry's setting to ignore browser extension
// errors doesn't do this anywhere near as consistently as I'd expect?)
//
// Adapted from https://gist.github.com/impressiver/5092952, as linked in
// https://docs.sentry.io/platforms/javascript/configuration/filtering/.
/^chrome-extension:\/\//,
/^moz-extension:\/\//,
],
// Since we're only tracking first-page loads and not navigations, 100%
// sampling isn't actually so much! Tune down if it becomes a problem, tho.
tracesSampleRate: 1.0,
});
}
/**
* ScopedCSSReset applies a copy of Chakra UI's CSS reset, but only to its
* children (or, well, any element with the chakra-css-reset class). It also
* applies to .chakra-portal elements mounted by <Portal> components.
*
* We also apply some base styles, like the default text color.
*
* NOTE: We use the `:where` CSS selector, instead of the .chakra-css-reset
* selector directly, to avoid specificity conflicts. e.g. the selector
* `.chakra-css-reset h1` is considered MORE specific than `.my-h1`, whereas
* the selector `:where(.chakra-css-reset) h1` is lower specificity.
*/
function ScopedCSSReset({ children }) {
// Get the current theme and color mode.
//
// NOTE: The theme object returned by `useTheme` has some extensions that are
// necessary for the code below, but aren't present in the theme config
// returned by `extendTheme`! That's why we use this here instead of `theme`.
const liveTheme = useTheme();
const colorMode = useColorMode();
// Resolve the theme's global styles into CSS objects for Emotion.
const globalStylesCSS = resolveCSS(
globalStyles({ theme: liveTheme, colorMode }),
)(liveTheme);
// Prepend our special scope selector to the global styles.
const scopedGlobalStylesCSS = {};
for (let [selector, rules] of Object.entries(globalStylesCSS)) {
// The `body` selector is where typography etc rules go, but `body` isn't
// actually *inside* our scoped element! Instead, ignore the `body` part,
// and just apply it to the scoping element itself.
if (selector.trim() === "body") {
selector = "";
}
const scopedSelector =
":where(.chakra-css-reset, .chakra-portal) " + selector;
scopedGlobalStylesCSS[scopedSelector] = rules;
}
return (
<>
<Box className="chakra-css-reset">{children}</Box>
<Global
styles={css`
/* Chakra's default global styles, placed here so we can override
* the actual _global_ styles in the theme to be empty. That way,
* it only affects Chakra stuff, not all elements! */
${scopedGlobalStylesCSS}
/* Chakra's CSS reset, copy-pasted and rescoped! */
:where(.chakra-css-reset, .chakra-portal) {
*,
*::before,
*::after {
border-width: 0;
border-style: solid;
box-sizing: border-box;
}
main {
display: block;
}
hr {
border-top-width: 1px;
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 1em;
}
a {
background-color: transparent;
color: inherit;
text-decoration: inherit;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bold;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none !important;
}
input[type="number"] {
-moz-appearance: textfield;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none !important;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
body,
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
button {
background: transparent;
padding: 0;
}
fieldset {
margin: 0;
padding: 0;
}
ol,
ul {
margin: 0;
padding: 0;
}
textarea {
resize: vertical;
}
button,
[role="button"] {
cursor: pointer;
}
button::-moz-focus-inner {
border: 0 !important;
}
table {
border-collapse: collapse;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
button,
input,
optgroup,
select,
textarea {
padding: 0;
line-height: inherit;
color: inherit;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
}
img,
video {
max-width: 100%;
height: auto;
}
[data-js-focus-visible] :focus:not([data-focus-visible-added]) {
outline: none;
box-shadow: none;
}
select::-ms-expand {
display: none;
}
}
`}
/>
</>
);
}