@jeremyckahn/farmhand 1.23.3 → 1.23.4
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 +4 -0
- package/dist/assets/index-4ynbl-GC.js +392 -0
- package/dist/assets/index-4ynbl-GC.js.map +1 -0
- package/dist/assets/index-CaYdR-lC.css +1 -0
- package/dist/assets/jersey-10-latin-400-normal-DrL73VYW.woff2 +0 -0
- package/dist/assets/jersey-10-latin-ext-400-normal-uIKqQSaG.woff2 +0 -0
- package/dist/assets/jersey-20-latin-400-normal-bNi78tgO.woff2 +0 -0
- package/dist/assets/jersey-20-latin-ext-400-normal-BO3lmWef.woff2 +0 -0
- package/dist/assets/jersey-25-latin-400-normal-BuYJnnt7.woff2 +0 -0
- package/dist/assets/jersey-25-latin-ext-400-normal-CP5Jl3WN.woff2 +0 -0
- package/dist/index.html +2 -2
- package/dist/service-worker.js +1 -1
- package/dist/service-worker.js.map +1 -1
- package/package.json +4 -8
- package/src/components/AccountingView/AccountingView.tsx +3 -1
- package/src/components/Achievement/Achievement.tsx +2 -2
- package/src/components/AppBar/AppBar.tsx +21 -7
- package/src/components/CowCard/Bloodline/Bloodline.tsx +0 -1
- package/src/components/CowCard/CowCard.tsx +7 -13
- package/src/components/CowCard/Subheader/Subheader.tsx +10 -18
- package/src/components/CowPen/Cow.tsx +5 -11
- package/src/components/CowPen/CowPen.test.tsx +4 -4
- package/src/components/CowPen/CowPen.tsx +1 -1
- package/src/components/CowPenContextMenu/CowPenContextMenu.tsx +2 -2
- package/src/components/Farmhand/Farmhand.tsx +7 -4
- package/src/components/Farmhand/useFarmhand.ts +12 -2
- package/src/components/Field/Field.tsx +2 -2
- package/src/components/Home/Home.tsx +2 -1
- package/src/components/Inventory/Inventory.tsx +2 -1
- package/src/components/Item/Item.tsx +11 -8
- package/src/components/LogView/LogView.tsx +6 -22
- package/src/components/Navigation/DayAndProgressContainer.tsx +0 -1
- package/src/components/Navigation/Navigation.tsx +12 -10
- package/src/components/NotificationSystem/NotificationSystem.test.tsx +41 -2
- package/src/components/NotificationSystem/NotificationSystem.tsx +40 -20
- package/src/components/PixelIcon/createPixelIcon.test.tsx +59 -0
- package/src/components/PixelIcon/createPixelIcon.tsx +97 -0
- package/src/components/PixelIcon/icons.ts +445 -0
- package/src/components/PixelIcon/index.ts +3 -0
- package/src/components/ProgressBar/ProgressBar.tsx +9 -3
- package/src/components/QuantityInput/QuantityInput.tsx +6 -2
- package/src/components/SearchBar/SearchBar.tsx +4 -4
- package/src/components/SettingsView/SettingsView.tsx +0 -2
- package/src/components/StatsView/StatsView.tsx +0 -3
- package/src/components/UpdateNotifier/UpdateNotifier.tsx +33 -5
- package/src/farmhand.d.ts +4 -0
- package/src/game-logic/reducers/showNotification.test.ts +20 -0
- package/src/game-logic/reducers/showNotification.ts +4 -2
- package/src/handlers/ui-events.tsx +8 -3
- package/src/index.tsx +0 -2
- package/src/mui-theme.ts +255 -35
- package/src/setupTests.ts +18 -12
- package/src/strings.ts +1 -1
- package/src/styles/fonts.ts +73 -0
- package/src/styles/pixel.ts +151 -0
- package/src/styles/sx.ts +17 -8
- package/src/styles/tokens.ts +11 -0
- package/src/utils/getCowImage.ts +0 -11
- package/dist/assets/francois-one-latin-400-normal-DD1rFI7H.woff2 +0 -0
- package/dist/assets/francois-one-latin-400-normal-DNXs7uOx.woff +0 -0
- package/dist/assets/francois-one-latin-ext-400-normal-CwRdWRZD.woff2 +0 -0
- package/dist/assets/francois-one-latin-ext-400-normal-JvkpT0Av.woff +0 -0
- package/dist/assets/francois-one-vietnamese-400-normal-BLIKeG5Y.woff2 +0 -0
- package/dist/assets/francois-one-vietnamese-400-normal-CuWeHlXs.woff +0 -0
- package/dist/assets/index-CNcrTYN-.css +0 -1
- package/dist/assets/index-Dihg7UqQ.js +0 -844
- package/dist/assets/index-Dihg7UqQ.js.map +0 -1
- package/dist/assets/public-sans-latin-400-normal-8Rpg0ruU.woff2 +0 -0
- package/dist/assets/public-sans-latin-400-normal-SBbinRkI.woff +0 -0
- package/dist/assets/public-sans-latin-ext-400-normal-B02wwOdZ.woff +0 -0
- package/dist/assets/public-sans-latin-ext-400-normal-mk90oQqJ.woff2 +0 -0
- package/dist/assets/public-sans-vietnamese-400-normal-BO1meqcx.woff +0 -0
- package/dist/assets/public-sans-vietnamese-400-normal-TxZB8OSn.woff2 +0 -0
- package/src/fontsource.d.ts +0 -3
|
@@ -541,8 +541,13 @@ export default {
|
|
|
541
541
|
this: Farmhand,
|
|
542
542
|
updateServiceWorker: (reloadPage?: boolean) => Promise<void>
|
|
543
543
|
) {
|
|
544
|
-
this.showNotification(
|
|
545
|
-
|
|
546
|
-
|
|
544
|
+
this.showNotification(
|
|
545
|
+
UPDATE_AVAILABLE,
|
|
546
|
+
'success',
|
|
547
|
+
() => {
|
|
548
|
+
updateServiceWorker(true)
|
|
549
|
+
},
|
|
550
|
+
'Reload'
|
|
551
|
+
)
|
|
547
552
|
},
|
|
548
553
|
}
|
package/src/index.tsx
CHANGED
|
@@ -9,8 +9,6 @@ import { HashRouter as Router, Route } from 'react-router-dom'
|
|
|
9
9
|
|
|
10
10
|
import Farmhand from './components/Farmhand/index.js'
|
|
11
11
|
import { features } from './config.js'
|
|
12
|
-
import '@fontsource/francois-one'
|
|
13
|
-
import '@fontsource/public-sans'
|
|
14
12
|
|
|
15
13
|
const FarmhandRoute = (props: any) => <Farmhand {...{ ...props, features }} />
|
|
16
14
|
|
package/src/mui-theme.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { darken, Theme } from '@mui/material/styles/index.js'
|
|
2
|
+
import { createElement } from 'react'
|
|
1
3
|
import createTheme from '@mui/material/styles/createTheme.js'
|
|
4
|
+
import type { Shadows } from '@mui/material/styles/shadows.js'
|
|
2
5
|
|
|
6
|
+
import {
|
|
7
|
+
ArrowDropDownIcon,
|
|
8
|
+
CheckBoxIcon,
|
|
9
|
+
CheckBoxOutlineBlankIcon,
|
|
10
|
+
ErrorIcon,
|
|
11
|
+
IndeterminateCheckBoxIcon,
|
|
12
|
+
InfoIcon,
|
|
13
|
+
SuccessIcon,
|
|
14
|
+
WarningIcon,
|
|
15
|
+
} from './components/PixelIcon/index.js'
|
|
3
16
|
import blueStripeBg from './img/ui/blue-stripe-bg.png'
|
|
4
17
|
import lightBlueStripeBg from './img/ui/light-blue-stripe-bg.png'
|
|
5
18
|
import {
|
|
@@ -7,7 +20,61 @@ import {
|
|
|
7
20
|
cardStyleSx,
|
|
8
21
|
spriteShadowSx,
|
|
9
22
|
} from './styles/sx.js'
|
|
10
|
-
import {
|
|
23
|
+
import { fontFaces } from './styles/fonts.js'
|
|
24
|
+
import {
|
|
25
|
+
pixelBevel,
|
|
26
|
+
pixelBevelPressed,
|
|
27
|
+
pixelFrameSx,
|
|
28
|
+
pixelPressedSx,
|
|
29
|
+
pixelShadowColor,
|
|
30
|
+
px,
|
|
31
|
+
} from './styles/pixel.js'
|
|
32
|
+
import { breakpoints, colors, fonts, layout } from './styles/tokens.js'
|
|
33
|
+
|
|
34
|
+
type PaletteColorName =
|
|
35
|
+
'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success'
|
|
36
|
+
|
|
37
|
+
const paletteColorNames = new Set<string>([
|
|
38
|
+
'primary',
|
|
39
|
+
'secondary',
|
|
40
|
+
'error',
|
|
41
|
+
'warning',
|
|
42
|
+
'info',
|
|
43
|
+
'success',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const isPaletteColorName = (color: unknown): color is PaletteColorName =>
|
|
47
|
+
typeof color === 'string' && paletteColorNames.has(color)
|
|
48
|
+
|
|
49
|
+
// Pixel art outlines read best when they're a deep shade of the fill color
|
|
50
|
+
// rather than a flat black.
|
|
51
|
+
const outlineFor = (theme: Theme, color: unknown) =>
|
|
52
|
+
isPaletteColorName(color)
|
|
53
|
+
? darken(theme.palette[color].main, 0.5)
|
|
54
|
+
: colors.neutralOutline
|
|
55
|
+
|
|
56
|
+
// Every elevation gets the same hard, unblurred pixel art drop shadow rather
|
|
57
|
+
// than MUI's soft Material Design shadows.
|
|
58
|
+
const pixelShadow = `${px(1)} ${px(1)} 0 0 ${pixelShadowColor}`
|
|
59
|
+
const shadows = [
|
|
60
|
+
'none',
|
|
61
|
+
...Array<string>(24).fill(pixelShadow),
|
|
62
|
+
] as unknown as Shadows
|
|
63
|
+
|
|
64
|
+
// Shared by Button and Fab: a raised 9-slice frame that gets pushed into its
|
|
65
|
+
// shadow when pressed.
|
|
66
|
+
const raisedControlSx = (outline: string) =>
|
|
67
|
+
({
|
|
68
|
+
...pixelFrameSx({ outline, shadow: true }),
|
|
69
|
+
boxShadow: pixelBevel(),
|
|
70
|
+
'&:hover': { boxShadow: pixelBevel() },
|
|
71
|
+
'&.Mui-focusVisible': { boxShadow: pixelBevel() },
|
|
72
|
+
'&:active': pixelPressedSx(outline),
|
|
73
|
+
'&.Mui-disabled': {
|
|
74
|
+
...pixelFrameSx({ outline: colors.disabledOutline, shadow: 'pressed' }),
|
|
75
|
+
boxShadow: 'none',
|
|
76
|
+
},
|
|
77
|
+
}) as const
|
|
11
78
|
|
|
12
79
|
// These global styles used to live in Farmhand.sass, scoped by a `.Farmhand`
|
|
13
80
|
// class applied to many DOM roots across the app (including MUI Dialogs,
|
|
@@ -15,7 +82,13 @@ import { breakpoints, colors, layout } from './styles/tokens.js'
|
|
|
15
82
|
// MuiCssBaseline is a direct replacement that also sidesteps the reason
|
|
16
83
|
// that class trick existed in the first place (styling portalled content).
|
|
17
84
|
const globalStyleOverrides = {
|
|
18
|
-
body: {
|
|
85
|
+
body: {
|
|
86
|
+
overscrollBehavior: 'contain',
|
|
87
|
+
// The pixel fonts only ship the weights they're designed for. Faux bold
|
|
88
|
+
// and italics smear their pixel grid, so don't let the browser
|
|
89
|
+
// synthesize them.
|
|
90
|
+
fontSynthesis: 'none',
|
|
91
|
+
},
|
|
19
92
|
'ul, ol': { listStyle: 'none', margin: 0, padding: 0 },
|
|
20
93
|
p: { margin: 0 },
|
|
21
94
|
'.markdown': {
|
|
@@ -35,7 +108,7 @@ const globalStyleOverrides = {
|
|
|
35
108
|
width: '1px',
|
|
36
109
|
},
|
|
37
110
|
'h4, h5, h6, p, p.MuiTypography-root': {
|
|
38
|
-
fontFamily:
|
|
111
|
+
fontFamily: fonts.body,
|
|
39
112
|
},
|
|
40
113
|
'h3, h4, h5, h6': { fontWeight: 'bold' },
|
|
41
114
|
h2: { fontSize: '1.4em' },
|
|
@@ -51,7 +124,7 @@ const globalStyleOverrides = {
|
|
|
51
124
|
th: { fontWeight: 'bold' },
|
|
52
125
|
'h1, h2, h3, h4, h5, h6, legend, td, th, .MuiTypography-h1, .MuiTypography-h2, .MuiTypography-h3, .MuiTypography-h4, .MuiTypography-h5, .MuiTypography-h6, .MuiButtonBase-root':
|
|
53
126
|
{
|
|
54
|
-
fontFamily:
|
|
127
|
+
fontFamily: fonts.display,
|
|
55
128
|
},
|
|
56
129
|
'.danger-text': { color: colors.error },
|
|
57
130
|
'.success-text': { color: colors.success },
|
|
@@ -77,42 +150,51 @@ export default createTheme({
|
|
|
77
150
|
palette: {
|
|
78
151
|
mode: 'light',
|
|
79
152
|
},
|
|
80
|
-
|
|
153
|
+
// Pixel art has no anti-aliased curves. Corners are notched by the 9-slice
|
|
154
|
+
// frames instead.
|
|
155
|
+
shape: { borderRadius: 0 },
|
|
156
|
+
shadows,
|
|
81
157
|
typography: {
|
|
82
|
-
fontFamily:
|
|
158
|
+
fontFamily: fonts.body,
|
|
83
159
|
// Buttons and Tabs derive their default styles from this rather than
|
|
84
160
|
// the `.MuiButtonBase-root` CssBaseline override below, which loses the
|
|
85
161
|
// cascade to their own emotion-generated styles.
|
|
86
162
|
button: {
|
|
87
|
-
fontFamily:
|
|
163
|
+
fontFamily: fonts.display,
|
|
88
164
|
textTransform: 'none',
|
|
89
165
|
},
|
|
90
166
|
// DialogTitle (used by every modal header, e.g. Farmer's Log, Price
|
|
91
167
|
// Events) renders variant="h6". Same cascade issue as `button` above.
|
|
92
168
|
h6: {
|
|
93
|
-
fontFamily:
|
|
169
|
+
fontFamily: fonts.display,
|
|
94
170
|
fontSize: '1.4em',
|
|
95
171
|
},
|
|
96
172
|
// CardHeader titles default to variant="h5" when no avatar is passed
|
|
97
173
|
// (e.g. the peer name headers in the Active Players modal). Same
|
|
98
174
|
// cascade issue as `button`/`h6` above.
|
|
99
175
|
h5: {
|
|
100
|
-
fontFamily:
|
|
176
|
+
fontFamily: fonts.display,
|
|
101
177
|
},
|
|
102
178
|
},
|
|
103
179
|
components: {
|
|
104
180
|
MuiCssBaseline: {
|
|
105
|
-
|
|
181
|
+
// Each @font-face rule needs its own style object: emotion collapses
|
|
182
|
+
// an array of them under a single '@font-face' key into one rule.
|
|
183
|
+
styleOverrides: () => [
|
|
184
|
+
...fontFaces.map(fontFace => ({ '@font-face': fontFace })),
|
|
185
|
+
globalStyleOverrides,
|
|
186
|
+
],
|
|
106
187
|
},
|
|
107
188
|
MuiTabs: {
|
|
108
189
|
styleOverrides: {
|
|
109
|
-
root:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
190
|
+
root: {
|
|
191
|
+
...pixelFrameSx({ outline: colors.cardOutline }),
|
|
192
|
+
backgroundColor: '#ffeec6',
|
|
193
|
+
boxShadow: pixelBevel(),
|
|
194
|
+
},
|
|
195
|
+
indicator: {
|
|
196
|
+
height: px(1),
|
|
197
|
+
},
|
|
116
198
|
},
|
|
117
199
|
},
|
|
118
200
|
MuiInput: {
|
|
@@ -128,7 +210,7 @@ export default createTheme({
|
|
|
128
210
|
...cardStyleSx(theme),
|
|
129
211
|
'&.is-selected': cardStyleSelectedSx,
|
|
130
212
|
'& .MuiCard-root': {
|
|
131
|
-
|
|
213
|
+
backgroundColor: colors.cardBackgroundNested,
|
|
132
214
|
},
|
|
133
215
|
}),
|
|
134
216
|
},
|
|
@@ -163,7 +245,57 @@ export default createTheme({
|
|
|
163
245
|
},
|
|
164
246
|
MuiButtonBase: {
|
|
165
247
|
styleOverrides: {
|
|
166
|
-
root: {
|
|
248
|
+
root: {
|
|
249
|
+
textTransform: 'none',
|
|
250
|
+
// Ripples and focus pulses are circular by default, which clashes
|
|
251
|
+
// with the square pixel art controls.
|
|
252
|
+
'& .MuiTouchRipple-root .MuiTouchRipple-child': { borderRadius: 0 },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
MuiButton: {
|
|
257
|
+
defaultProps: {
|
|
258
|
+
disableElevation: true,
|
|
259
|
+
},
|
|
260
|
+
styleOverrides: {
|
|
261
|
+
root: ({ ownerState, theme }) => {
|
|
262
|
+
const outline = outlineFor(theme, ownerState.color)
|
|
263
|
+
|
|
264
|
+
switch (ownerState.variant) {
|
|
265
|
+
case 'contained':
|
|
266
|
+
return raisedControlSx(outline)
|
|
267
|
+
|
|
268
|
+
case 'outlined':
|
|
269
|
+
return {
|
|
270
|
+
...pixelFrameSx({ outline, shadow: true }),
|
|
271
|
+
'&:hover': pixelFrameSx({ outline, shadow: true }),
|
|
272
|
+
'&:active': {
|
|
273
|
+
...pixelPressedSx(outline),
|
|
274
|
+
boxShadow: 'none',
|
|
275
|
+
},
|
|
276
|
+
'&.Mui-disabled': pixelFrameSx({
|
|
277
|
+
outline: colors.disabledOutline,
|
|
278
|
+
shadow: 'pressed',
|
|
279
|
+
}),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
default:
|
|
283
|
+
// Text buttons get an invisible frame of the same size so
|
|
284
|
+
// they don't shift the layout when toggled to another variant
|
|
285
|
+
// (e.g. selected items in ItemList and Toolbelt).
|
|
286
|
+
return pixelFrameSx({ outline: 'transparent', shadow: 'pressed' })
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
MuiFab: {
|
|
292
|
+
styleOverrides: {
|
|
293
|
+
root: ({ ownerState, theme }) => ({
|
|
294
|
+
...raisedControlSx(outlineFor(theme, ownerState.color)),
|
|
295
|
+
// Fabs are normally round. Keep them square (with notched
|
|
296
|
+
// corners) to match the rest of the pixel art UI.
|
|
297
|
+
borderRadius: 0,
|
|
298
|
+
}),
|
|
167
299
|
},
|
|
168
300
|
},
|
|
169
301
|
MuiPaper: {
|
|
@@ -171,9 +303,76 @@ export default createTheme({
|
|
|
171
303
|
rounded: { backgroundColor: '#fff7e7' },
|
|
172
304
|
},
|
|
173
305
|
},
|
|
306
|
+
MuiPopover: {
|
|
307
|
+
styleOverrides: {
|
|
308
|
+
paper: {
|
|
309
|
+
...pixelFrameSx({ outline: colors.cardOutline, shadow: true }),
|
|
310
|
+
boxShadow: pixelBevel(),
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
MuiAccordion: {
|
|
315
|
+
styleOverrides: {
|
|
316
|
+
root: ({ theme }) => ({
|
|
317
|
+
...cardStyleSx(theme),
|
|
318
|
+
'&:before': { display: 'none' },
|
|
319
|
+
'&.Mui-expanded': { margin: `${px(2)} 0` },
|
|
320
|
+
}),
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
MuiOutlinedInput: {
|
|
324
|
+
styleOverrides: {
|
|
325
|
+
notchedOutline: {
|
|
326
|
+
borderWidth: 2,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
MuiSwitch: {
|
|
331
|
+
styleOverrides: {
|
|
332
|
+
thumb: {
|
|
333
|
+
borderRadius: 0,
|
|
334
|
+
boxShadow: `${pixelBevel()}, ${px(1)} ${px(1)} 0 0 ${pixelShadowColor}`,
|
|
335
|
+
},
|
|
336
|
+
track: {
|
|
337
|
+
borderRadius: 0,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
MuiSlider: {
|
|
342
|
+
styleOverrides: {
|
|
343
|
+
thumb: {
|
|
344
|
+
borderRadius: 0,
|
|
345
|
+
boxShadow: pixelBevel(),
|
|
346
|
+
'&:hover, &.Mui-focusVisible, &.Mui-active': {
|
|
347
|
+
boxShadow: pixelBevelPressed,
|
|
348
|
+
},
|
|
349
|
+
'&:before': { boxShadow: 'none' },
|
|
350
|
+
},
|
|
351
|
+
rail: { borderRadius: 0 },
|
|
352
|
+
track: { borderRadius: 0 },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
MuiSelect: {
|
|
356
|
+
defaultProps: {
|
|
357
|
+
IconComponent: ArrowDropDownIcon,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
MuiCheckbox: {
|
|
361
|
+
defaultProps: {
|
|
362
|
+
icon: createElement(CheckBoxOutlineBlankIcon),
|
|
363
|
+
checkedIcon: createElement(CheckBoxIcon),
|
|
364
|
+
indeterminateIcon: createElement(IndeterminateCheckBoxIcon),
|
|
365
|
+
},
|
|
366
|
+
},
|
|
174
367
|
MuiTooltip: {
|
|
175
368
|
styleOverrides: {
|
|
176
369
|
tooltip: {
|
|
370
|
+
...pixelFrameSx({ outline: colors.neutralOutline }),
|
|
371
|
+
backgroundColor: 'rgba(60, 50, 40, 0.94)',
|
|
372
|
+
boxShadow: pixelBevel({
|
|
373
|
+
highlight: 'rgba(255, 255, 255, 0.15)',
|
|
374
|
+
lowlight: 'rgba(0, 0, 0, 0.25)',
|
|
375
|
+
}),
|
|
177
376
|
'& p': { textAlign: 'center' },
|
|
178
377
|
},
|
|
179
378
|
},
|
|
@@ -181,7 +380,11 @@ export default createTheme({
|
|
|
181
380
|
MuiDialog: {
|
|
182
381
|
styleOverrides: {
|
|
183
382
|
root: { display: 'block' },
|
|
184
|
-
paper: {
|
|
383
|
+
paper: {
|
|
384
|
+
...pixelFrameSx({ outline: colors.dialogOutline, shadow: true }),
|
|
385
|
+
backgroundImage: `url(${lightBlueStripeBg})`,
|
|
386
|
+
boxShadow: pixelBevel(),
|
|
387
|
+
},
|
|
185
388
|
},
|
|
186
389
|
},
|
|
187
390
|
MuiDivider: {
|
|
@@ -190,22 +393,39 @@ export default createTheme({
|
|
|
190
393
|
},
|
|
191
394
|
},
|
|
192
395
|
MuiAlert: {
|
|
396
|
+
defaultProps: {
|
|
397
|
+
iconMapping: {
|
|
398
|
+
success: createElement(SuccessIcon, { fontSize: 'inherit' }),
|
|
399
|
+
info: createElement(InfoIcon, { fontSize: 'inherit' }),
|
|
400
|
+
warning: createElement(WarningIcon, { fontSize: 'inherit' }),
|
|
401
|
+
error: createElement(ErrorIcon, { fontSize: 'inherit' }),
|
|
402
|
+
},
|
|
403
|
+
},
|
|
193
404
|
styleOverrides: {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
405
|
+
// 24px keeps each of the 12x12 pixel icon's art pixels at a whole
|
|
406
|
+
// number of CSS pixels (MUI's default is 22px).
|
|
407
|
+
icon: { fontSize: 24 },
|
|
408
|
+
root: ({ ownerState, theme }) => {
|
|
409
|
+
const outline = outlineFor(theme, ownerState.severity ?? 'success')
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
...pixelFrameSx({ outline, shadow: true }),
|
|
413
|
+
boxShadow: pixelBevel(),
|
|
414
|
+
marginBottom: '1em',
|
|
415
|
+
[`@media (min-width: 0px) and (orientation: landscape)`]: {
|
|
416
|
+
top: '3.5em',
|
|
417
|
+
},
|
|
418
|
+
[`@media (min-width: ${breakpoints.sm}px)`]: { top: '4.5em' },
|
|
419
|
+
'& p': {
|
|
420
|
+
margin: '1em 0',
|
|
421
|
+
'&:first-of-type': { marginTop: 0 },
|
|
422
|
+
'&:last-child': { marginBottom: 0 },
|
|
423
|
+
},
|
|
424
|
+
'& strong': { fontWeight: 'bold' },
|
|
425
|
+
'& li': { marginLeft: '1em' },
|
|
426
|
+
'& ul li': { listStyle: 'disc' },
|
|
427
|
+
'& ol li': { listStyle: 'decimal' },
|
|
428
|
+
}
|
|
209
429
|
},
|
|
210
430
|
},
|
|
211
431
|
},
|
package/src/setupTests.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import '@testing-library/jest-dom'
|
|
2
1
|
import 'jest-extended'
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
// Conditionally import DOM matchers and React cleanup only when running in a
|
|
4
|
+
// browser/JSDOM environment. Keeping them out of Node-based test runners avoids
|
|
5
|
+
// loading heavy React and DOM libraries into pure logic test workers.
|
|
6
|
+
if (typeof window !== 'undefined') {
|
|
7
|
+
await import('@testing-library/jest-dom')
|
|
8
|
+
const { cleanup } = await import('@testing-library/react')
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup()
|
|
12
|
+
// Farmhand mirrors navigation state (current view, tabbed screens' active
|
|
13
|
+
// tab) into the real window.location.hash (see src/utils/hashQueryParams.ts),
|
|
14
|
+
// which - unlike React/component state - isn't reset between tests on its
|
|
15
|
+
// own and would otherwise leak into whichever test runs next in the same
|
|
16
|
+
// file.
|
|
17
|
+
window.history.replaceState({}, '', window.location.pathname)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
4
20
|
|
|
5
21
|
const localForageStore = new Map<string, any>()
|
|
6
22
|
|
|
@@ -49,13 +65,3 @@ beforeEach(() => {
|
|
|
49
65
|
// on a specific month is not run in tests (unless getMonth is re-mocked).
|
|
50
66
|
vitest.spyOn(Date.prototype, 'getMonth').mockReturnValue(-1)
|
|
51
67
|
})
|
|
52
|
-
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
cleanup()
|
|
55
|
-
// Farmhand mirrors navigation state (current view, tabbed screens' active
|
|
56
|
-
// tab) into the real window.location.hash (see src/utils/hashQueryParams.ts),
|
|
57
|
-
// which - unlike React/component state - isn't reset between tests on its
|
|
58
|
-
// own and would otherwise leak into whichever test runs next in the same
|
|
59
|
-
// file.
|
|
60
|
-
window.history.replaceState({}, '', window.location.pathname)
|
|
61
|
-
})
|
package/src/strings.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const COW_SOLD_TOOLTIP_TEXT =
|
|
|
18
18
|
export const DATA_DELETED = 'Your local game data has been deleted.'
|
|
19
19
|
export const INVALID_DATA_PROVIDED = 'Invalid Farmhand data provided.'
|
|
20
20
|
export const UPDATE_AVAILABLE =
|
|
21
|
-
"A game update is available!
|
|
21
|
+
"A game update is available! Reload to see what's new."
|
|
22
22
|
export const SERVER_ERROR =
|
|
23
23
|
'There was an issue connecting to the server. Please try again in a moment.'
|
|
24
24
|
export const CONNECTING_TO_SERVER = 'Connecting...'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Pixel font faces: Jersey 10 for headings and UI labels, Jersey 20 for body
|
|
2
|
+
// text, and Jersey 25 (a heavier cut of the same design) as the body bold.
|
|
3
|
+
//
|
|
4
|
+
// The app refers to two families, "Farmhand Display" (headings, buttons and
|
|
5
|
+
// other UI labels) and "Farmhand Body" (running text), so the typefaces
|
|
6
|
+
// behind them can be swapped here without touching any components.
|
|
7
|
+
//
|
|
8
|
+
// These are declared here rather than by importing the @fontsource CSS so
|
|
9
|
+
// that each face can set `size-adjust`. Pixel fonts have much smaller glyphs
|
|
10
|
+
// per em than the fonts they replaced (Francois One and Public Sans), so
|
|
11
|
+
// without it every heading and paragraph in the app would need its
|
|
12
|
+
// font-size tweaked. The adjustments below match each face's cap height to
|
|
13
|
+
// that of the font it replaced.
|
|
14
|
+
|
|
15
|
+
import jersey10Latin from '@fontsource/jersey-10/files/jersey-10-latin-400-normal.woff2'
|
|
16
|
+
import jersey10LatinExt from '@fontsource/jersey-10/files/jersey-10-latin-ext-400-normal.woff2'
|
|
17
|
+
import jersey20Latin from '@fontsource/jersey-20/files/jersey-20-latin-400-normal.woff2'
|
|
18
|
+
import jersey20LatinExt from '@fontsource/jersey-20/files/jersey-20-latin-ext-400-normal.woff2'
|
|
19
|
+
import jersey25Latin from '@fontsource/jersey-25/files/jersey-25-latin-400-normal.woff2'
|
|
20
|
+
import jersey25LatinExt from '@fontsource/jersey-25/files/jersey-25-latin-ext-400-normal.woff2'
|
|
21
|
+
|
|
22
|
+
// Copied from the @fontsource CSS.
|
|
23
|
+
const unicodeRanges = {
|
|
24
|
+
latin:
|
|
25
|
+
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
|
26
|
+
latinExt:
|
|
27
|
+
'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fontFace = (
|
|
31
|
+
family: string,
|
|
32
|
+
weight: number,
|
|
33
|
+
sizeAdjust: string,
|
|
34
|
+
url: string,
|
|
35
|
+
unicodeRange: string
|
|
36
|
+
) => ({
|
|
37
|
+
// Quoted because unquoted family names can't contain a word that starts
|
|
38
|
+
// with a digit.
|
|
39
|
+
fontFamily: `"${family}"`,
|
|
40
|
+
fontStyle: 'normal',
|
|
41
|
+
fontDisplay: 'swap',
|
|
42
|
+
fontWeight: weight,
|
|
43
|
+
sizeAdjust,
|
|
44
|
+
src: `url("${url}") format('woff2')`,
|
|
45
|
+
unicodeRange,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const fontFaces = [
|
|
49
|
+
fontFace('Farmhand Display', 400, '140%', jersey10Latin, unicodeRanges.latin),
|
|
50
|
+
fontFace(
|
|
51
|
+
'Farmhand Display',
|
|
52
|
+
400,
|
|
53
|
+
'140%',
|
|
54
|
+
jersey10LatinExt,
|
|
55
|
+
unicodeRanges.latinExt
|
|
56
|
+
),
|
|
57
|
+
fontFace('Farmhand Body', 400, '124%', jersey20Latin, unicodeRanges.latin),
|
|
58
|
+
fontFace(
|
|
59
|
+
'Farmhand Body',
|
|
60
|
+
400,
|
|
61
|
+
'124%',
|
|
62
|
+
jersey20LatinExt,
|
|
63
|
+
unicodeRanges.latinExt
|
|
64
|
+
),
|
|
65
|
+
fontFace('Farmhand Body', 700, '120%', jersey25Latin, unicodeRanges.latin),
|
|
66
|
+
fontFace(
|
|
67
|
+
'Farmhand Body',
|
|
68
|
+
700,
|
|
69
|
+
'120%',
|
|
70
|
+
jersey25LatinExt,
|
|
71
|
+
unicodeRanges.latinExt
|
|
72
|
+
),
|
|
73
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Retro pixel art UI chrome built on CSS 9-slice scaling (`border-image`).
|
|
2
|
+
//
|
|
3
|
+
// Each frame is a tiny SVG "sprite" (a few pixels square) that the browser
|
|
4
|
+
// slices into corners, edges and a center. Corners are drawn at a fixed
|
|
5
|
+
// size and edges stretch, so a frame scales to any element size while every
|
|
6
|
+
// art pixel stays exactly PIXEL_SIZE CSS pixels. Because the sprites are
|
|
7
|
+
// generated as SVGs, their colors can be derived from the MUI palette at
|
|
8
|
+
// runtime instead of needing a hand-drawn PNG per color.
|
|
9
|
+
//
|
|
10
|
+
// Only the outline, the notched corners and the hard drop shadow live in
|
|
11
|
+
// the 9-slice image. The element's background is clipped to its padding box
|
|
12
|
+
// so it fills the frame's interior, and the bevel highlight is drawn with
|
|
13
|
+
// inset box-shadows. That keeps `backgroundColor` overrides (hover states,
|
|
14
|
+
// selected cards, palette colors, etc.) working without regenerating any
|
|
15
|
+
// images.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The size, in CSS pixels, of one "art pixel" in the UI chrome.
|
|
19
|
+
*/
|
|
20
|
+
export const PIXEL_SIZE = 3
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts a number of art pixels into a CSS length.
|
|
24
|
+
*/
|
|
25
|
+
export const px = (units: number) => `${units * PIXEL_SIZE}px`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a pixel map into a CSS `url()` for an SVG data URI. Each string in
|
|
29
|
+
* `rows` is one row of pixels, and each character is a key of `palette`. A
|
|
30
|
+
* `.` is a transparent pixel.
|
|
31
|
+
*/
|
|
32
|
+
export const pixelArtUrl = (
|
|
33
|
+
rows: string[],
|
|
34
|
+
palette: Record<string, string>
|
|
35
|
+
): string => {
|
|
36
|
+
const height = rows.length
|
|
37
|
+
const width = rows[0].length
|
|
38
|
+
let rects = ''
|
|
39
|
+
|
|
40
|
+
rows.forEach((row, y) => {
|
|
41
|
+
;[...row].forEach((char, x) => {
|
|
42
|
+
const fill = palette[char]
|
|
43
|
+
|
|
44
|
+
if (char === '.' || !fill) return
|
|
45
|
+
|
|
46
|
+
rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${fill}"/>`
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">${rects}</svg>`
|
|
51
|
+
|
|
52
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// O = outline, D = drop shadow
|
|
56
|
+
//
|
|
57
|
+
// A 1px notch in each corner gives the classic pixel art "rounded" box. The
|
|
58
|
+
// center pixel is left transparent; the element's own background shows
|
|
59
|
+
// through it.
|
|
60
|
+
const FRAME = ['.O.', 'O.O', '.O.']
|
|
61
|
+
const FRAME_SLICE = '1'
|
|
62
|
+
const FRAME_WIDTH = px(1)
|
|
63
|
+
|
|
64
|
+
// Same as FRAME, plus a hard drop shadow offset by one art pixel to the
|
|
65
|
+
// bottom right. The right and bottom slices are two pixels wide to hold the
|
|
66
|
+
// shadow.
|
|
67
|
+
const SHADOWED_FRAME = ['.O..', 'O.OD', '.ODD', '.DD.']
|
|
68
|
+
const SHADOWED_FRAME_SLICE = '1 2 2 1'
|
|
69
|
+
const SHADOWED_FRAME_WIDTH = `${px(1)} ${px(2)} ${px(2)} ${px(1)}`
|
|
70
|
+
|
|
71
|
+
export const pixelShadowColor = 'rgba(0, 0, 0, 0.3)'
|
|
72
|
+
|
|
73
|
+
export interface PixelFrameOptions {
|
|
74
|
+
/**
|
|
75
|
+
* The outline color.
|
|
76
|
+
*/
|
|
77
|
+
outline: string
|
|
78
|
+
/**
|
|
79
|
+
* Whether to render a hard drop shadow. When this is `'pressed'`, space
|
|
80
|
+
* for the shadow is preserved (so the element's size doesn't change) but
|
|
81
|
+
* the shadow itself is not drawn. Pair that with `pixelPressedSx`.
|
|
82
|
+
*/
|
|
83
|
+
shadow?: boolean | 'pressed'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns sx styles that render a scalable 9-slice pixel art frame around an
|
|
88
|
+
* element.
|
|
89
|
+
*/
|
|
90
|
+
export const pixelFrameSx = ({
|
|
91
|
+
outline,
|
|
92
|
+
shadow = false,
|
|
93
|
+
}: PixelFrameOptions) => {
|
|
94
|
+
const rows = shadow ? SHADOWED_FRAME : FRAME
|
|
95
|
+
const slice = shadow ? SHADOWED_FRAME_SLICE : FRAME_SLICE
|
|
96
|
+
const width = shadow ? SHADOWED_FRAME_WIDTH : FRAME_WIDTH
|
|
97
|
+
const image = pixelArtUrl(rows, {
|
|
98
|
+
O: outline,
|
|
99
|
+
D: shadow === true ? pixelShadowColor : 'transparent',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
borderStyle: 'solid',
|
|
104
|
+
borderColor: 'transparent',
|
|
105
|
+
borderWidth: width,
|
|
106
|
+
borderRadius: 0,
|
|
107
|
+
borderImageSource: image,
|
|
108
|
+
borderImageSlice: slice,
|
|
109
|
+
borderImageWidth: width,
|
|
110
|
+
borderImageRepeat: 'stretch',
|
|
111
|
+
// Keep the background inside the outline so the notched corners and the
|
|
112
|
+
// drop shadow stay transparent.
|
|
113
|
+
backgroundClip: 'padding-box',
|
|
114
|
+
} as const
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns a `box-shadow` value that draws a one-art-pixel bevel inside an
|
|
119
|
+
* element: a light edge on the top and left, and a dark edge on the bottom
|
|
120
|
+
* and right.
|
|
121
|
+
*/
|
|
122
|
+
export const pixelBevel = ({
|
|
123
|
+
highlight = 'rgba(255, 255, 255, 0.45)',
|
|
124
|
+
lowlight = 'rgba(0, 0, 0, 0.12)',
|
|
125
|
+
}: { highlight?: string; lowlight?: string } = {}) =>
|
|
126
|
+
`inset ${px(1)} ${px(1)} 0 ${highlight}, inset -${px(1)} -${px(1)} 0 ${lowlight}`
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Inverted bevel for pressed/active controls.
|
|
130
|
+
*/
|
|
131
|
+
export const pixelBevelPressed = pixelBevel({
|
|
132
|
+
highlight: 'rgba(0, 0, 0, 0.15)',
|
|
133
|
+
lowlight: 'rgba(255, 255, 255, 0.25)',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A crisp (unblurred) drop shadow for sprites and other arbitrarily shaped
|
|
138
|
+
* elements.
|
|
139
|
+
*/
|
|
140
|
+
export const pixelDropShadowFilter = `drop-shadow(${px(1)} ${px(1)} 0 rgba(0, 0, 0, 0.2))`
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Styles for the pressed state of a control framed with `shadow: true`. The
|
|
144
|
+
* control shifts into the space its shadow occupied, so it appears to be
|
|
145
|
+
* pushed down.
|
|
146
|
+
*/
|
|
147
|
+
export const pixelPressedSx = (outline: string) => ({
|
|
148
|
+
...pixelFrameSx({ outline, shadow: 'pressed' }),
|
|
149
|
+
transform: `translate(${px(1)}, ${px(1)})`,
|
|
150
|
+
boxShadow: pixelBevelPressed,
|
|
151
|
+
})
|