@zthun/romulator-web 1.14.0 → 1.15.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/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Romulator: Organize your Games</title>
7
- <script type="module" crossorigin src="/assets/index-nHUlV-0_.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-D8ZGfkPQ.js"></script>
8
8
  </head>
9
9
  <body>
10
10
  <div id="zthunworks-romulator"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zthun/romulator-web",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Romulator frontend",
5
5
  "author": "Anthony Bonta",
6
6
  "license": "MIT",
@@ -21,14 +21,14 @@
21
21
  "@types/node": "^24.9.1",
22
22
  "@zthun/cirque": "^7.1.11",
23
23
  "@zthun/cirque-du-react": "^7.1.11",
24
- "@zthun/fashion-boutique": "^12.0.0",
25
- "@zthun/fashion-tailor": "^12.0.0",
26
- "@zthun/fashion-theme": "^12.0.0",
24
+ "@zthun/fashion-boutique": "^12.1.1",
25
+ "@zthun/fashion-tailor": "^12.1.1",
26
+ "@zthun/fashion-theme": "^12.1.1",
27
27
  "@zthun/helpful-fn": "^9.10.0",
28
28
  "@zthun/helpful-query": "^9.10.0",
29
29
  "@zthun/helpful-react": "^9.10.0",
30
30
  "@zthun/janitor-build-config": "^19.4.1",
31
- "@zthun/romulator-client": "^1.14.0",
31
+ "@zthun/romulator-client": "^1.15.0",
32
32
  "@zthun/webigail-http": "^5.0.0",
33
33
  "@zthun/webigail-rest": "^5.0.0",
34
34
  "@zthun/webigail-url": "^5.0.0",
@@ -40,8 +40,8 @@
40
40
  "typescript": "^5.9.3",
41
41
  "vite": "^7.1.12",
42
42
  "vite-plugin-node-polyfills": "^0.24.0",
43
- "vitest": "^4.0.3",
43
+ "vitest": "^4.0.4",
44
44
  "vitest-mock-extended": "^3.1.0"
45
45
  },
46
- "gitHead": "28aff2325d3e89c2ad5b4c7cb9c632f55f26fa15"
46
+ "gitHead": "c263d200e7f91916af1242c88c970ddcc4a1ba77"
47
47
  }
package/src/app/app.tsx CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  ZRouteMap,
8
8
  } from "@zthun/fashion-boutique";
9
9
  import { createDarkTheme } from "@zthun/fashion-theme";
10
+ import { ZRomulatorGamesPage } from "../games/games-page.js";
10
11
  import { ZRomulatorMenu } from "../menu/menu.js";
11
12
  import { ZRomulatorSettingPage } from "../settings/setting-page.js";
12
13
  import { ZRomulatorSettingsPage } from "../settings/settings-page.js";
@@ -32,6 +33,7 @@ export function ZRomulatorApp() {
32
33
  <ZRoute path="/settings/:id" element={<ZRomulatorSettingPage />} />
33
34
  <ZRoute path="/systems" element={<ZRomulatorSystemsPage />} />
34
35
  <ZRoute path="/systems/:id" element={<ZRomulatorSystemPage />} />
36
+ <ZRoute path="/games" element={<ZRomulatorGamesPage />} />
35
37
  <ZRoute path="" element={<ZNavigate to="/systems" />} />
36
38
  <ZRoute path="*" element={<ZNotFound />} />
37
39
  </ZRouteMap>
@@ -1,13 +1,15 @@
1
1
  import {
2
+ useCss,
2
3
  useFashionTheme,
3
4
  useNavigate,
4
- ZBox,
5
5
  ZGridView,
6
6
  ZImageSource,
7
7
  ZStack,
8
+ ZTile,
8
9
  type IZComponentValue,
9
10
  } from "@zthun/fashion-boutique";
10
11
  import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
12
+ import { css, cssJoinDefined } from "@zthun/helpful-fn";
11
13
  import { ZDataRequestBuilder, type IZDataRequest } from "@zthun/helpful-query";
12
14
  import { useAmbassadorState } from "@zthun/helpful-react";
13
15
  import type { IZRomulatorGame } from "@zthun/romulator-client";
@@ -27,35 +29,45 @@ export function ZRomulatorGamesList(props: IZRomulatorGamesList) {
27
29
  const { body } = useFashionTheme();
28
30
  const navigate = useNavigate();
29
31
 
32
+ const _className = useCss(css`
33
+ .ZRomulatorGameTile-root {
34
+ height: 10rem;
35
+ }
36
+
37
+ .ZRomulatorGameTile-marquee > img {
38
+ width: 100%;
39
+ max-height: 10rem;
40
+ }
41
+ `);
42
+
30
43
  const renderTile = (value: IZRomulatorGame) => {
31
44
  const { api } = new ZRomulatorEnvironmentBuilder().build();
32
45
  const id = `${value.id}-marquees`;
33
- const wheel = `${api}/media/${id}`;
46
+ const marquee = `${api}/media/${id}`;
47
+ const onActivate = () => navigate(`/games/${value.id}`);
34
48
 
35
49
  return (
36
- <ZBox
37
- className="ZRomulatorGameTile-root"
50
+ <ZTile
51
+ className={"ZRomulatorGameTile-root"}
38
52
  fashion={body}
39
- interactive
40
53
  key={value.id}
41
- cursor="pointer"
42
- padding={ZSizeFixed.Small}
43
- data-name={value.id}
44
- onClick={() => navigate(`/games/${value.id}`)}
54
+ name={value.id}
55
+ onActivate={onActivate}
45
56
  >
46
57
  <ZStack
47
58
  justify={{ content: "center" }}
48
59
  align={{ items: "center" }}
49
60
  height={ZSizeVaried.Full}
50
61
  >
51
- <ZImageSource src={wheel} width={ZSizeVaried.Full} />
62
+ <ZImageSource className="ZRomulatorGameTile-marquee" src={marquee} />
52
63
  </ZStack>
53
- </ZBox>
64
+ </ZTile>
54
65
  );
55
66
  };
56
67
 
57
68
  return (
58
69
  <ZGridView
70
+ className={cssJoinDefined(".ZRomulatorGameList-root", _className)}
59
71
  GridProps={{
60
72
  columns: {
61
73
  xl: "1fr 1fr 1fr 1fr 1fr 1fr",
@@ -0,0 +1,27 @@
1
+ import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
2
+ import {
3
+ ZGridViewComponentModel,
4
+ ZTileComponentModel,
5
+ } from "@zthun/fashion-boutique";
6
+
7
+ export class ZRomulatorGamesPageComponentModel extends ZCircusComponentModel {
8
+ public static readonly Selector = ".ZRomulatorGamesPage-root";
9
+
10
+ public grid(): Promise<ZGridViewComponentModel> {
11
+ return Promise.resolve(new ZGridViewComponentModel(this.driver));
12
+ }
13
+
14
+ public async game(id: string): Promise<ZTileComponentModel | null> {
15
+ const grid = await this.grid();
16
+ return ZCircusBy.optional(grid.driver, ZTileComponentModel, id);
17
+ }
18
+
19
+ public async games(): Promise<ZTileComponentModel[]> {
20
+ const grid = await this.grid();
21
+ return ZCircusBy.all(
22
+ grid.driver,
23
+ ZTileComponentModel,
24
+ ".ZRomulatorGameTile-root",
25
+ );
26
+ }
27
+ }
@@ -0,0 +1,94 @@
1
+ import type { IZCircusDriver, IZCircusSetup } from "@zthun/cirque";
2
+ import { ZCircusBy } from "@zthun/cirque";
3
+ import { ZCircusSetupRenderer } from "@zthun/cirque-du-react";
4
+ import { ZTestRouter } from "@zthun/fashion-boutique";
5
+ import { ZDataSourceStatic } from "@zthun/helpful-query";
6
+ import {
7
+ ZRomulatorGameBuilder,
8
+ ZRomulatorSystemId,
9
+ } from "@zthun/romulator-client";
10
+ import type { MemoryHistory } from "history";
11
+ import { createMemoryHistory } from "history";
12
+ import type { Mocked } from "vitest";
13
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
14
+ import { mock } from "vitest-mock-extended";
15
+ import { ZRomulatorGamesPageComponentModel } from "./games-page.cm.mjs";
16
+ import { ZRomulatorGamesPage } from "./games-page.js";
17
+ import type { IZRomulatorGamesService } from "./games-service.mjs";
18
+ import { ZRomulatorGamesServiceContext } from "./games-service.mjs";
19
+
20
+ describe("ZRomulatorGamesPage", () => {
21
+ const mario = new ZRomulatorGameBuilder()
22
+ .id("nes-mario")
23
+ .file("/path/to/games/nes/mario.zip")
24
+ .name("Super Mario Bros.")
25
+ .system(ZRomulatorSystemId.Nintendo)
26
+ .build();
27
+ const crash = new ZRomulatorGameBuilder()
28
+ .id("psx-crash")
29
+ .file("/path/to/games/psx/crash.zip")
30
+ .name("Crash Bandicoot")
31
+ .system(ZRomulatorSystemId.PSX)
32
+ .build();
33
+ const games = [mario, crash];
34
+
35
+ let _gamesService: Mocked<IZRomulatorGamesService>;
36
+ let _renderer: IZCircusSetup | undefined;
37
+ let _driver: IZCircusDriver | undefined;
38
+ let _history: MemoryHistory;
39
+
40
+ afterEach(async () => {
41
+ await _driver?.destroy?.call(_driver);
42
+ await _renderer?.destroy?.call(_renderer);
43
+ });
44
+
45
+ beforeEach(() => {
46
+ const source = new ZDataSourceStatic(games);
47
+
48
+ _gamesService = mock<IZRomulatorGamesService>();
49
+ _gamesService.retrieve.mockImplementation(source.retrieve.bind(source));
50
+ _gamesService.count.mockImplementation(source.count.bind(source));
51
+
52
+ _history = createMemoryHistory();
53
+ });
54
+
55
+ const createTestTarget = async () => {
56
+ const element = (
57
+ <ZTestRouter navigator={_history} location={_history.location}>
58
+ <ZRomulatorGamesServiceContext value={_gamesService}>
59
+ <ZRomulatorGamesPage />
60
+ </ZRomulatorGamesServiceContext>
61
+ </ZTestRouter>
62
+ );
63
+
64
+ _renderer = new ZCircusSetupRenderer(element);
65
+ _driver = await _renderer.setup();
66
+
67
+ return ZCircusBy.first(_driver, ZRomulatorGamesPageComponentModel);
68
+ };
69
+
70
+ it("should render all games", async () => {
71
+ // Arrange.
72
+ const target = await createTestTarget();
73
+ const expected = games.map((g) => g.id);
74
+
75
+ // Act.
76
+ const tiles = await target.games();
77
+ const ids = tiles.map((g) => g.driver.attribute("data-name"));
78
+ const actual = await Promise.all(ids);
79
+ // Assert.
80
+ expect(actual).toEqual(expect.arrayContaining(expected));
81
+ });
82
+
83
+ it("should navigate me to the game page when I click on one", async () => {
84
+ // Arrange.
85
+ const target = await createTestTarget();
86
+
87
+ // Act.
88
+ const system = await target.game(mario.id);
89
+ await system?.click();
90
+
91
+ // Assert.
92
+ expect(_history.location.pathname).toEqual(`/games/${mario.id}`);
93
+ });
94
+ });
@@ -0,0 +1,41 @@
1
+ import {
2
+ ZBreadcrumbsLocation,
3
+ ZCard,
4
+ ZIconFontAwesome,
5
+ ZStack,
6
+ } from "@zthun/fashion-boutique";
7
+ import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
8
+ import { ZDataRequestBuilder, ZSortBuilder } from "@zthun/helpful-query";
9
+ import { useState } from "react";
10
+ import { ZRomulatorGamesList } from "./games-list.js";
11
+
12
+ const DefaultGameSortOrder = new ZSortBuilder().ascending("name").build();
13
+
14
+ const DefaultGameRequest = new ZDataRequestBuilder()
15
+ .size(24)
16
+ .sort(DefaultGameSortOrder)
17
+ .build();
18
+
19
+ export function ZRomulatorGamesPage() {
20
+ const [request, setRequest] = useState(DefaultGameRequest);
21
+
22
+ return (
23
+ <ZStack
24
+ className="ZRomulatorGamesPage-root"
25
+ gap={ZSizeFixed.Medium}
26
+ width={ZSizeVaried.Full}
27
+ >
28
+ <ZBreadcrumbsLocation />
29
+ <ZCard
30
+ width={ZSizeVaried.Full}
31
+ TitleProps={{
32
+ avatar: <ZIconFontAwesome name="gamepad" width={ZSizeFixed.Medium} />,
33
+ heading: "Games",
34
+ subHeading: "Browse your library",
35
+ }}
36
+ >
37
+ <ZRomulatorGamesList value={request} onValueChange={setRequest} />
38
+ </ZCard>
39
+ </ZStack>
40
+ );
41
+ }
package/src/menu/menu.tsx CHANGED
@@ -81,7 +81,7 @@ export function ZRomulatorMenu() {
81
81
  <ZContentTitle
82
82
  avatar={<ZIconFontAwesome name="gamepad" />}
83
83
  heading={<ZH3 compact>Games</ZH3>}
84
- subHeading={<ZCaption>View all games</ZCaption>}
84
+ subHeading={<ZCaption>Browse your library</ZCaption>}
85
85
  />
86
86
  </ZListItem>
87
87
 
@@ -178,7 +178,7 @@ describe("SystemPage", () => {
178
178
  await target.load();
179
179
 
180
180
  // Act.
181
- const actual = target.media();
181
+ const actual = await target.media();
182
182
 
183
183
  // Assert.
184
184
  expect(actual).toBeTruthy();
@@ -190,7 +190,7 @@ describe("SystemPage", () => {
190
190
  await target.load();
191
191
 
192
192
  // Act.
193
- const actual = target.games();
193
+ const actual = await target.games();
194
194
 
195
195
  // Assert.
196
196
  expect(actual).toBeTruthy();
@@ -15,7 +15,7 @@ import {
15
15
  ZSuspenseProgress,
16
16
  } from "@zthun/fashion-boutique";
17
17
  import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
18
- import { firstDefined } from "@zthun/helpful-fn";
18
+ import { firstDefined, ZOrientation } from "@zthun/helpful-fn";
19
19
  import {
20
20
  ZDataRequestBuilder,
21
21
  ZFilterBinaryBuilder,
@@ -203,12 +203,19 @@ export function ZRomulatorSystemPage() {
203
203
  subHeading: startCase(media[mediaIndex]),
204
204
  }}
205
205
  >
206
- <ZCarousel
207
- count={media.length}
208
- renderAtIndex={(i) => renderSystemMedia(media[i], system)}
209
- value={mediaIndex}
210
- onValueChange={setMediaIndex}
211
- />
206
+ <ZStack
207
+ orientation={ZOrientation.Horizontal}
208
+ width={ZSizeVaried.Full}
209
+ align={{ items: "center" }}
210
+ justify={{ content: "center" }}
211
+ >
212
+ <ZCarousel
213
+ count={media.length}
214
+ renderAtIndex={(i) => renderSystemMedia(media[i], system)}
215
+ value={mediaIndex}
216
+ onValueChange={setMediaIndex}
217
+ />
218
+ </ZStack>
212
219
  </ZCard>
213
220
  );
214
221
  };
@@ -231,7 +238,7 @@ export function ZRomulatorSystemPage() {
231
238
  }
232
239
 
233
240
  return (
234
- <ZGrid columns={{ xl: "1fr auto", sm: "1fr" }} gap={ZSizeFixed.Medium}>
241
+ <ZGrid columns={{ xl: "1fr auto", md: "1fr" }} gap={ZSizeFixed.Medium}>
235
242
  {renderSystemGameListCard(system)}
236
243
 
237
244
  <ZStack gap={ZSizeFixed.Medium}>
@@ -1,7 +1,7 @@
1
1
  import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
2
2
  import {
3
- ZBoxComponentModel,
4
3
  ZGridViewComponentModel,
4
+ ZTileComponentModel,
5
5
  } from "@zthun/fashion-boutique";
6
6
 
7
7
  export class ZRomulatorSystemsPageComponentModel extends ZCircusComponentModel {
@@ -11,16 +11,16 @@ export class ZRomulatorSystemsPageComponentModel extends ZCircusComponentModel {
11
11
  return Promise.resolve(new ZGridViewComponentModel(this.driver));
12
12
  }
13
13
 
14
- public async system(id: string): Promise<ZBoxComponentModel | null> {
14
+ public async system(id: string): Promise<ZTileComponentModel | null> {
15
15
  const grid = await this.grid();
16
- return ZCircusBy.optional(grid.driver, ZBoxComponentModel, id);
16
+ return ZCircusBy.optional(grid.driver, ZTileComponentModel, id);
17
17
  }
18
18
 
19
- public async systems(): Promise<ZBoxComponentModel[]> {
19
+ public async systems(): Promise<ZTileComponentModel[]> {
20
20
  const grid = await this.grid();
21
21
  return ZCircusBy.all(
22
22
  grid.driver,
23
- ZBoxComponentModel,
23
+ ZTileComponentModel,
24
24
  ".ZRomulatorSystemsPage-tile",
25
25
  );
26
26
  }
@@ -2,13 +2,13 @@ import {
2
2
  useCss,
3
3
  useFashionTheme,
4
4
  useNavigate,
5
- ZBox,
6
5
  ZBreadcrumbsLocation,
7
6
  ZCard,
8
7
  ZGridView,
9
8
  ZIconFontAwesome,
10
9
  ZImageSource,
11
10
  ZStack,
11
+ ZTile,
12
12
  } from "@zthun/fashion-boutique";
13
13
  import { ZSizeFixed, ZSizeVaried } from "@zthun/fashion-tailor";
14
14
  import { css } from "@zthun/helpful-fn";
@@ -35,9 +35,13 @@ export function ZRomulatorSystemsPage() {
35
35
  const [request, setRequest] = useState(DefaultSystemRequest);
36
36
 
37
37
  const _className = useCss(css`
38
+ .ZRomulatorSystemsPage-tile {
39
+ height: 10rem;
40
+ }
41
+
38
42
  .ZRomulatorSystemsPage-wheel > img {
39
- max-width: 23rem;
40
- max-height: 11rem;
43
+ width: 100%;
44
+ max-height: 10rem;
41
45
  }
42
46
  `);
43
47
 
@@ -45,17 +49,15 @@ export function ZRomulatorSystemsPage() {
45
49
  const { api } = new ZRomulatorEnvironmentBuilder().build();
46
50
  const id = `${system.id}-wheel`;
47
51
  const wheel = `${api}/media/${id}`;
52
+ const onActivate = () => navigate(system.id);
48
53
 
49
54
  return (
50
- <ZBox
55
+ <ZTile
51
56
  className="ZRomulatorSystemsPage-tile"
52
57
  fashion={body}
53
- interactive
54
58
  key={system.id}
55
- cursor="pointer"
56
- padding={ZSizeFixed.Small}
57
- data-name={system.id}
58
- onClick={() => navigate(system.id)}
59
+ name={system.id}
60
+ onActivate={onActivate}
59
61
  >
60
62
  <ZStack
61
63
  justify={{ content: "center" }}
@@ -63,13 +65,9 @@ export function ZRomulatorSystemsPage() {
63
65
  height={ZSizeVaried.Full}
64
66
  width={ZSizeVaried.Full}
65
67
  >
66
- <ZImageSource
67
- className="ZRomulatorSystemsPage-wheel"
68
- src={wheel}
69
- width={ZSizeVaried.Full}
70
- />
68
+ <ZImageSource className="ZRomulatorSystemsPage-wheel" src={wheel} />
71
69
  </ZStack>
72
- </ZBox>
70
+ </ZTile>
73
71
  );
74
72
  };
75
73