@zthun/romulator-web 1.1.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/dist/assets/index-CtJiKvl7.js +52645 -0
  4. package/dist/favicon.ico +0 -0
  5. package/dist/index.html +12 -0
  6. package/dist/png/romulator-256x256.png +0 -0
  7. package/dist/systems/uk/megadrive-256x256.png +0 -0
  8. package/dist/systems/us/megadrive-256x256.png +0 -0
  9. package/dist/systems/us/nes-256x256.png +0 -0
  10. package/dist/systems/us/snes-256x256.png +0 -0
  11. package/index.html +12 -0
  12. package/package.json +48 -0
  13. package/public/favicon.ico +0 -0
  14. package/public/png/romulator-256x256.png +0 -0
  15. package/public/systems/uk/megadrive-256x256.png +0 -0
  16. package/public/systems/us/megadrive-256x256.png +0 -0
  17. package/public/systems/us/nes-256x256.png +0 -0
  18. package/public/systems/us/snes-256x256.png +0 -0
  19. package/src/app/app-avatar.tsx +8 -0
  20. package/src/app/app-title.tsx +10 -0
  21. package/src/app/app.tsx +41 -0
  22. package/src/environment/environment-service.mts +19 -0
  23. package/src/environment/environment.mts +29 -0
  24. package/src/index.tsx +14 -0
  25. package/src/menu/menu.cm.mts +60 -0
  26. package/src/menu/menu.spec.tsx +115 -0
  27. package/src/menu/menu.tsx +130 -0
  28. package/src/settings/setting-page.cm.mts +22 -0
  29. package/src/settings/setting-page.spec.tsx +154 -0
  30. package/src/settings/setting-page.tsx +83 -0
  31. package/src/settings/settings-page.cm.mts +18 -0
  32. package/src/settings/settings-page.spec.tsx +99 -0
  33. package/src/settings/settings-page.tsx +82 -0
  34. package/src/settings/settings-service.mts +38 -0
  35. package/src/systems/system-avatar-card.cm.mts +17 -0
  36. package/src/systems/system-avatar-card.tsx +41 -0
  37. package/src/systems/system-page.cm.mts +33 -0
  38. package/src/systems/system-page.spec.tsx +127 -0
  39. package/src/systems/system-page.tsx +48 -0
  40. package/src/systems/systems-page.cm.mts +39 -0
  41. package/src/systems/systems-page.spec.tsx +81 -0
  42. package/src/systems/systems-page.tsx +48 -0
  43. package/src/systems/systems-service.mts +65 -0
  44. package/src/systems/systems-service.spec.ts +144 -0
  45. package/tsconfig.json +10 -0
  46. package/vite.config.mts +9 -0
  47. package/vitest.config.mts +9 -0
Binary file
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Romulator: Organize your Games</title>
7
+ <script type="module" crossorigin src="/assets/index-CtJiKvl7.js"></script>
8
+ </head>
9
+ <body>
10
+ <div id="zthunworks-romulator"></div>
11
+ </body>
12
+ </html>
Binary file
Binary file
Binary file
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Romulator: Organize your Games</title>
7
+ </head>
8
+ <body>
9
+ <div id="zthunworks-romulator"></div>
10
+ <script type="module" src="/src/index.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@zthun/romulator-web",
3
+ "version": "1.1.0",
4
+ "description": "Romulator frontend",
5
+ "author": "Anthony Bonta",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/zthun/romulator",
11
+ "directory": "packages/romulator-web"
12
+ },
13
+ "scripts": {
14
+ "build": "vite build",
15
+ "start": "vite"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^24.0.3",
22
+ "@vitejs/plugin-react-swc": "^3.10.2",
23
+ "@zthun/cirque": "^7.1.4",
24
+ "@zthun/cirque-du-react": "^7.1.4",
25
+ "@zthun/fashion-boutique": "^10.1.6",
26
+ "@zthun/fashion-tailor": "^10.1.6",
27
+ "@zthun/fashion-theme": "^10.1.6",
28
+ "@zthun/helpful-fn": "^9.3.0",
29
+ "@zthun/helpful-query": "^9.3.0",
30
+ "@zthun/helpful-react": "^9.3.0",
31
+ "@zthun/janitor-build-config": "^19.2.4",
32
+ "@zthun/romulator-client": "^1.1.0",
33
+ "@zthun/webigail-http": "^4.0.3",
34
+ "@zthun/webigail-rest": "^4.0.3",
35
+ "@zthun/webigail-url": "^4.0.3",
36
+ "history": "^5.3.0",
37
+ "lodash-es": "^4.17.21",
38
+ "react": "^19.1.0",
39
+ "react-dom": "^19.1.0",
40
+ "tsconfig-paths": "^4.2.0",
41
+ "typescript": "^5.8.3",
42
+ "vite": "^6.3.5",
43
+ "vite-tsconfig-paths": "^5.1.4",
44
+ "vitest": "^3.2.4",
45
+ "vitest-mock-extended": "^3.1.0"
46
+ },
47
+ "gitHead": "52bd432dd2df0980d450b20de58d7f58c6d0da94"
48
+ }
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ import { ZImageSource } from "@zthun/fashion-boutique";
2
+ import { ZSizeFixed } from "@zthun/fashion-tailor";
3
+
4
+ export function ZRomulatorAvatar() {
5
+ return (
6
+ <ZImageSource src="/png/romulator-256x256.png" width={ZSizeFixed.Medium} />
7
+ );
8
+ }
@@ -0,0 +1,10 @@
1
+ import { ZCaption, ZH1 } from "@zthun/fashion-boutique";
2
+
3
+ export function ZRomulatorTitle() {
4
+ return (
5
+ <div className="ZRomulatorTitle-root">
6
+ <ZH1 compact>Romulator</ZH1>
7
+ <ZCaption compact>Organize your Games</ZCaption>
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ ZBannerMain,
3
+ ZFashionThemeContext,
4
+ ZNavigate,
5
+ ZNotFound,
6
+ ZRoute,
7
+ ZRouteMap,
8
+ } from "@zthun/fashion-boutique";
9
+ import { createDarkTheme } from "@zthun/fashion-theme";
10
+ import { ZRomulatorMenu } from "../menu/menu.js";
11
+ import { ZRomulatorSettingPage } from "../settings/setting-page.js";
12
+ import { ZRomulatorSettingsPage } from "../settings/settings-page.js";
13
+ import { ZRomulatorSystemPage } from "../systems/system-page.js";
14
+ import { ZRomulatorSystemsPage } from "../systems/systems-page.js";
15
+ import { ZRomulatorAvatar } from "./app-avatar.js";
16
+ import { ZRomulatorTitle } from "./app-title.js";
17
+
18
+ const FashionTheme = createDarkTheme();
19
+
20
+ export function ZRomulatorApp() {
21
+ return (
22
+ <ZFashionThemeContext.Provider value={FashionTheme}>
23
+ <ZBannerMain
24
+ TitleProps={{
25
+ avatar: <ZRomulatorAvatar />,
26
+ prefix: <ZRomulatorTitle />,
27
+ suffix: <ZRomulatorMenu />,
28
+ }}
29
+ >
30
+ <ZRouteMap>
31
+ <ZRoute path="/settings" element={<ZRomulatorSettingsPage />} />
32
+ <ZRoute path="/settings/:id" element={<ZRomulatorSettingPage />} />
33
+ <ZRoute path="/systems" element={<ZRomulatorSystemsPage />} />
34
+ <ZRoute path="/systems/:id" element={<ZRomulatorSystemPage />} />
35
+ <ZRoute path="" element={<ZNavigate to="/systems" />} />
36
+ <ZRoute path="*" element={<ZNotFound />} />
37
+ </ZRouteMap>
38
+ </ZBannerMain>
39
+ </ZFashionThemeContext.Provider>
40
+ );
41
+ }
@@ -0,0 +1,19 @@
1
+ /* istanbul ignore file -- @preserve */
2
+ import type { IZRomulatorEnvironment } from "./environment.mjs";
3
+ import { ZRomulatorEnvironmentBuilder } from "./environment.mjs";
4
+
5
+ export interface IZRomulatorEnvironmentService {
6
+ read(): Promise<IZRomulatorEnvironment>;
7
+ }
8
+
9
+ export class ZRomulatorEnvironmentService
10
+ implements IZRomulatorEnvironmentService
11
+ {
12
+ public read(): Promise<IZRomulatorEnvironment> {
13
+ return Promise.resolve(new ZRomulatorEnvironmentBuilder().build());
14
+ }
15
+ }
16
+
17
+ export function createDefaultEnvironmentService(): IZRomulatorEnvironmentService {
18
+ return new ZRomulatorEnvironmentService();
19
+ }
@@ -0,0 +1,29 @@
1
+ import { ZUrlBuilder } from "@zthun/webigail-url";
2
+
3
+ export interface IZRomulatorEnvironment {
4
+ api: string;
5
+ }
6
+
7
+ export class ZRomulatorEnvironmentBuilder {
8
+ private _env: IZRomulatorEnvironment;
9
+
10
+ public constructor() {
11
+ this._env = {
12
+ api: new ZUrlBuilder()
13
+ .protocol("http")
14
+ .hostname("localhost")
15
+ .port(3000)
16
+ .append("api")
17
+ .build(),
18
+ };
19
+ }
20
+
21
+ public api(url: string) {
22
+ this._env.api = url;
23
+ return this;
24
+ }
25
+
26
+ public build() {
27
+ return structuredClone(this._env);
28
+ }
29
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import { ZRouter } from "@zthun/fashion-boutique";
2
+ import React from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { ZRomulatorApp } from "./app/app.js";
5
+
6
+ const container = createRoot(document.getElementById("zthunworks-romulator")!);
7
+
8
+ container.render(
9
+ <React.StrictMode>
10
+ <ZRouter>
11
+ <ZRomulatorApp />
12
+ </ZRouter>
13
+ </React.StrictMode>,
14
+ );
@@ -0,0 +1,60 @@
1
+ import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
2
+ import {
3
+ ZButtonComponentModel,
4
+ ZDialogComponentModel,
5
+ ZListItemComponentModel,
6
+ } from "@zthun/fashion-boutique";
7
+
8
+ export class ZRomulatorMenuComponentModel extends ZCircusComponentModel {
9
+ public static readonly Selector = ".ZRomulatorMenu-root";
10
+
11
+ private async listItem(
12
+ name: string,
13
+ ): Promise<ZListItemComponentModel | null> {
14
+ const drawer = await this.drawer();
15
+ const opened = await drawer.opened();
16
+
17
+ if (!opened) {
18
+ return null;
19
+ }
20
+
21
+ return ZCircusBy.first(drawer.driver, ZListItemComponentModel, name);
22
+ }
23
+
24
+ public systems = this.listItem.bind(this, "systems");
25
+ public games = this.listItem.bind(this, "games");
26
+ public audits = this.listItem.bind(this, "audits");
27
+ public steam = this.listItem.bind(this, "steam");
28
+ public settings = this.listItem.bind(this, "settings");
29
+
30
+ public drawer(): Promise<ZDialogComponentModel> {
31
+ return ZCircusBy.first(this.driver, ZDialogComponentModel, "navigation");
32
+ }
33
+
34
+ public button(): Promise<ZButtonComponentModel> {
35
+ return ZCircusBy.first(this.driver, ZButtonComponentModel, "toggler");
36
+ }
37
+
38
+ public async open(): Promise<void> {
39
+ const drawer = await this.drawer();
40
+
41
+ if (await drawer.opened()) {
42
+ return;
43
+ }
44
+
45
+ const button = await this.button();
46
+ await button.click();
47
+ await drawer.waitForOpen();
48
+ }
49
+
50
+ public async close(): Promise<void> {
51
+ const drawer = await this.drawer();
52
+
53
+ if (!(await drawer.opened())) {
54
+ return;
55
+ }
56
+
57
+ await drawer.close();
58
+ await drawer.waitForClose();
59
+ }
60
+ }
@@ -0,0 +1,115 @@
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 { createMemoryHistory, type MemoryHistory } from "history";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { ZRomulatorMenuComponentModel } from "./menu.cm.mjs";
8
+ import { ZRomulatorMenu } from "./menu.js";
9
+
10
+ describe("ZRomulatorMenu", () => {
11
+ let _renderer: IZCircusSetup<IZCircusDriver>;
12
+ let _driver: IZCircusDriver;
13
+ let _history: MemoryHistory;
14
+
15
+ const createTestTarget = async () => {
16
+ const element = (
17
+ <ZTestRouter location={_history.location} navigator={_history}>
18
+ <ZRomulatorMenu />
19
+ </ZTestRouter>
20
+ );
21
+
22
+ _renderer = new ZCircusSetupRenderer(element);
23
+ _driver = await _renderer.setup();
24
+
25
+ return ZCircusBy.first(_driver, ZRomulatorMenuComponentModel);
26
+ };
27
+
28
+ beforeEach(() => {
29
+ _history = createMemoryHistory();
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await _driver?.destroy?.call(_driver);
34
+ await _renderer?.destroy?.call(_renderer);
35
+ });
36
+
37
+ describe("Open", () => {
38
+ it("should open the menu when the menu is clicked", async () => {
39
+ // Arrange.
40
+ const target = await createTestTarget();
41
+
42
+ // Act.
43
+ await target.open();
44
+ await target.open();
45
+ const drawer = await target.drawer();
46
+ const actual = await drawer?.opened();
47
+
48
+ // Assert.
49
+ expect(actual).toBeTruthy();
50
+ });
51
+
52
+ it("should close the menu", async () => {
53
+ // Arrange.
54
+ const target = await createTestTarget();
55
+ await target.open();
56
+
57
+ // Act.
58
+ await target.close();
59
+ await target.close();
60
+ const drawer = await target.drawer();
61
+ const actual = await drawer.opened();
62
+
63
+ // Assert.
64
+ expect(actual).toBeFalsy();
65
+ });
66
+
67
+ it("should not have menu items if the drawer is closed", async () => {
68
+ // Arrange.
69
+ const target = await createTestTarget();
70
+
71
+ // Act.
72
+ const actual = await target.systems();
73
+
74
+ // Assert.
75
+ expect(actual).toBeNull();
76
+ });
77
+ });
78
+
79
+ describe("Navigation", () => {
80
+ type NavigationName = "systems" | "settings" | "steam" | "audits" | "games";
81
+
82
+ const shouldNavigateTo = async (expected: string, name: NavigationName) => {
83
+ // Arrange.
84
+ const target = await createTestTarget();
85
+ await target.open();
86
+
87
+ // Act.
88
+ const item = await target[name]();
89
+ await item?.click();
90
+
91
+ // Assert.
92
+ expect(_history.location.pathname).toEqual(expected);
93
+ };
94
+
95
+ it("should navigation to the systems page", async () => {
96
+ await shouldNavigateTo("/systems", "systems");
97
+ });
98
+
99
+ it("should navigation to the games page", async () => {
100
+ await shouldNavigateTo("/games", "games");
101
+ });
102
+
103
+ it("should navigate to the settings page", async () => {
104
+ await shouldNavigateTo("/settings", "settings");
105
+ });
106
+
107
+ it("should navigate to the audits page", async () => {
108
+ await shouldNavigateTo("/audits", "audits");
109
+ });
110
+
111
+ it("should navigate to the steam page", async () => {
112
+ await shouldNavigateTo("/steam", "steam");
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,130 @@
1
+ import {
2
+ useFashionTheme,
3
+ useNavigate,
4
+ ZButton,
5
+ ZCaption,
6
+ ZContentTitle,
7
+ ZDrawer,
8
+ ZH2,
9
+ ZH3,
10
+ ZIconFontAwesome,
11
+ ZList,
12
+ ZListItem,
13
+ } from "@zthun/fashion-boutique";
14
+ import { ZSizeFixed } from "@zthun/fashion-tailor";
15
+ import { ZHorizontalAnchor } from "@zthun/helpful-fn";
16
+ import { useMemo, useState } from "react";
17
+
18
+ export function ZRomulatorMenu() {
19
+ const [expanded, setExpanded] = useState(false);
20
+ const open = useMemo(() => setExpanded.bind(null, true), []);
21
+ const close = useMemo(() => setExpanded.bind(null, false), []);
22
+ const { secondary } = useFashionTheme();
23
+ const navigate = useNavigate();
24
+
25
+ const navigateAndClose = (path: string) => {
26
+ navigate(path);
27
+ close();
28
+ };
29
+
30
+ return (
31
+ <div className="ZRomulatorMenu-root">
32
+ <ZButton
33
+ name="toggler"
34
+ onClick={open}
35
+ label={<ZIconFontAwesome name="bars" width={ZSizeFixed.ExtraSmall} />}
36
+ />
37
+ <ZDrawer
38
+ name="navigation"
39
+ open={expanded}
40
+ onClose={close}
41
+ anchor={ZHorizontalAnchor.Right}
42
+ fashion={secondary}
43
+ renderHeader={() => (
44
+ <ZContentTitle
45
+ avatar={<ZIconFontAwesome name="bars" />}
46
+ heading={<ZH2 compact>Menu</ZH2>}
47
+ suffix={
48
+ <ZButton
49
+ label={
50
+ <ZIconFontAwesome
51
+ name="xmark"
52
+ width={ZSizeFixed.ExtraSmall}
53
+ />
54
+ }
55
+ onClick={close}
56
+ />
57
+ }
58
+ />
59
+ )}
60
+ >
61
+ <ZList compact>
62
+ <ZListItem
63
+ name="systems"
64
+ cursor="pointer"
65
+ interactive
66
+ onClick={navigateAndClose.bind(null, "/systems")}
67
+ >
68
+ <ZContentTitle
69
+ avatar={<ZIconFontAwesome name="puzzle-piece" />}
70
+ heading={<ZH3 compact>Systems</ZH3>}
71
+ subHeading={<ZCaption>Your games organized by systems</ZCaption>}
72
+ />
73
+ </ZListItem>
74
+
75
+ <ZListItem
76
+ name="games"
77
+ interactive
78
+ cursor="pointer"
79
+ onClick={navigateAndClose.bind(null, "/games")}
80
+ >
81
+ <ZContentTitle
82
+ avatar={<ZIconFontAwesome name="gamepad" />}
83
+ heading={<ZH3 compact>Games</ZH3>}
84
+ subHeading={<ZCaption>View all games</ZCaption>}
85
+ />
86
+ </ZListItem>
87
+
88
+ <ZListItem
89
+ name="audits"
90
+ interactive
91
+ cursor="pointer"
92
+ onClick={navigateAndClose.bind(null, "/audits")}
93
+ >
94
+ <ZContentTitle
95
+ avatar={<ZIconFontAwesome name="magnifying-glass" />}
96
+ heading={<ZH3 compact>Audits</ZH3>}
97
+ subHeading={<ZCaption>Audit your games</ZCaption>}
98
+ />
99
+ </ZListItem>
100
+
101
+ <ZListItem
102
+ name="steam"
103
+ interactive
104
+ cursor="pointer"
105
+ onClick={navigateAndClose.bind(null, "/steam")}
106
+ >
107
+ <ZContentTitle
108
+ avatar={<ZIconFontAwesome name="steam" family="brands" />}
109
+ heading={<ZH3 compact>Steam</ZH3>}
110
+ subHeading={<ZCaption>Integrate your games with Steam</ZCaption>}
111
+ />
112
+ </ZListItem>
113
+
114
+ <ZListItem
115
+ name="settings"
116
+ interactive
117
+ cursor="pointer"
118
+ onClick={navigateAndClose.bind(null, "/settings")}
119
+ >
120
+ <ZContentTitle
121
+ avatar={<ZIconFontAwesome name="gear" />}
122
+ heading={<ZH3 compact>Settings</ZH3>}
123
+ subHeading={<ZCaption>Modify configs and options</ZCaption>}
124
+ />
125
+ </ZListItem>
126
+ </ZList>
127
+ </ZDrawer>
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,22 @@
1
+ import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
2
+ import {
3
+ ZCardComponentModel,
4
+ ZFormComponentModel,
5
+ ZSuspenseComponentModel,
6
+ } from "@zthun/fashion-boutique";
7
+
8
+ export class ZRomulatorSettingPageComponentModel extends ZCircusComponentModel {
9
+ public static readonly Selector = ".ZRomulatorSettingPage-root";
10
+
11
+ public card(): Promise<ZCardComponentModel> {
12
+ return ZCircusBy.first(this.driver, ZCardComponentModel);
13
+ }
14
+
15
+ public suspense(): Promise<ZSuspenseComponentModel> {
16
+ return ZCircusBy.first(this.driver, ZSuspenseComponentModel);
17
+ }
18
+
19
+ public form(): Promise<ZFormComponentModel> {
20
+ return ZCircusBy.first(this.driver, ZFormComponentModel);
21
+ }
22
+ }