@1kbirds/chidori-mock-hub 0.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.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @1kbirds/chidori-mock-hub
2
+
3
+ Navigation hub for the Chidori mock packages. The hub embeds each existing React review app with its own seeded in-memory service instance and provides a home page for moving between virtual apps.
4
+
5
+ ## Included Apps
6
+
7
+ - Google Calendar: `@1kbirds/chidori-mock-google-calendar`
8
+ - Gmail: `@1kbirds/chidori-mock-gmail`
9
+ - Salesforce: `@1kbirds/chidori-mock-salesforce`
10
+ - Slack: `@1kbirds/chidori-mock-slack`
11
+ - Linear: `@1kbirds/chidori-mock-linear`
12
+
13
+ ## Review
14
+
15
+ Run verification:
16
+
17
+ ```sh
18
+ npm run build
19
+ npm test
20
+ ```
21
+
22
+ Start the hub:
23
+
24
+ ```sh
25
+ npm run dev:hub
26
+ ```
27
+
28
+ Then open `http://127.0.0.1:5178/`.
29
+
30
+ The home page links to each embedded app. Each app also shows its standalone dev command and URL.
@@ -0,0 +1,9 @@
1
+ import { type ComponentType } from "react";
2
+ import { type HubAppId, type HubAppMetadata } from "./metadata";
3
+ export type { HubAppId };
4
+ export interface HubAppDefinition extends HubAppMetadata {
5
+ App: ComponentType;
6
+ }
7
+ export declare const hubApps: HubAppDefinition[];
8
+ export declare function getHubApp(id: string | undefined): HubAppDefinition | undefined;
9
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAUA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEhF,YAAY,EAAE,QAAQ,EAAE,CAAC;AAEzB,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,GAAG,EAAE,aAAa,CAAC;CACpB;AAmCD,eAAO,MAAM,OAAO,EAAE,gBAAgB,EAGnC,CAAC;AAEJ,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAE9E"}
@@ -0,0 +1,46 @@
1
+ import { createGmailMock, demoSeed as gmailSeed } from "@1kbirds/chidori-mock-gmail";
2
+ import { GmailMockApp } from "@1kbirds/chidori-mock-gmail/ui";
3
+ import { createGoogleCalendarMock, demoSeed as googleCalendarSeed } from "@1kbirds/chidori-mock-google-calendar";
4
+ import { GoogleCalendarMockApp } from "@1kbirds/chidori-mock-google-calendar/ui";
5
+ import { createLinearMock, demoSeed as linearSeed } from "@1kbirds/chidori-mock-linear";
6
+ import { LinearMockApp } from "@1kbirds/chidori-mock-linear/ui";
7
+ import { createSalesforceMock, demoSeed as salesforceSeed } from "@1kbirds/chidori-mock-salesforce";
8
+ import { SalesforceMockApp } from "@1kbirds/chidori-mock-salesforce/ui";
9
+ import { createSlackMock, demoSeed as slackSeed } from "@1kbirds/chidori-mock-slack";
10
+ import { SlackMockApp } from "@1kbirds/chidori-mock-slack/ui";
11
+ import { createElement } from "react";
12
+ import { hubAppMetadata } from "./metadata";
13
+ const calendarMock = createGoogleCalendarMock({
14
+ now: "2026-06-01T15:00:00.000Z",
15
+ seed: googleCalendarSeed,
16
+ });
17
+ const gmailMock = createGmailMock({
18
+ now: "2026-06-01T16:30:00.000Z",
19
+ seed: gmailSeed,
20
+ });
21
+ const salesforceMock = createSalesforceMock({
22
+ now: "2026-06-01T15:00:00.000Z",
23
+ seed: salesforceSeed,
24
+ });
25
+ const slackMock = createSlackMock({
26
+ now: "2026-06-01T17:00:00.000Z",
27
+ seed: slackSeed,
28
+ });
29
+ const linearMock = createLinearMock({
30
+ now: "2026-06-01T17:30:00.000Z",
31
+ seed: linearSeed,
32
+ });
33
+ const appComponents = {
34
+ calendar: () => createElement(GoogleCalendarMockApp, { mock: calendarMock }),
35
+ gmail: () => createElement(GmailMockApp, { mock: gmailMock }),
36
+ salesforce: () => createElement(SalesforceMockApp, { mock: salesforceMock }),
37
+ slack: () => createElement(SlackMockApp, { mock: slackMock }),
38
+ linear: () => createElement(LinearMockApp, { mock: linearMock }),
39
+ };
40
+ export const hubApps = hubAppMetadata.map((app) => ({
41
+ ...app,
42
+ App: appComponents[app.id],
43
+ }));
44
+ export function getHubApp(id) {
45
+ return hubApps.find((app) => app.id === id);
46
+ }
@@ -0,0 +1,3 @@
1
+ export { getHubAppMetadata, hubAppMetadata } from "./metadata";
2
+ export type { HubAppId, HubAppMetadata } from "./metadata";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC/D,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { getHubAppMetadata, hubAppMetadata } from "./metadata";
@@ -0,0 +1,15 @@
1
+ export type HubAppId = "calendar" | "gmail" | "salesforce" | "slack" | "linear";
2
+ export interface HubAppMetadata {
3
+ id: HubAppId;
4
+ name: string;
5
+ packageName: string;
6
+ category: string;
7
+ description: string;
8
+ route: string;
9
+ devCommand: string;
10
+ standaloneUrl: string;
11
+ capabilities: string[];
12
+ }
13
+ export declare const hubAppMetadata: HubAppMetadata[];
14
+ export declare function getHubAppMetadata(id: string | undefined): HubAppMetadata | undefined;
15
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAAc,EAwD1C,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAEpF"}
@@ -0,0 +1,60 @@
1
+ export const hubAppMetadata = [
2
+ {
3
+ id: "calendar",
4
+ name: "Google Calendar",
5
+ packageName: "@1kbirds/chidori-mock-google-calendar",
6
+ category: "Scheduling",
7
+ description: "Calendar CRUD, event listing, free/busy, and a React calendar review app.",
8
+ route: "#/calendar",
9
+ devCommand: "npm run dev:google-calendar",
10
+ standaloneUrl: "http://127.0.0.1:5173/",
11
+ capabilities: ["Calendars", "Events", "Free/busy", "Week view"],
12
+ },
13
+ {
14
+ id: "gmail",
15
+ name: "Gmail",
16
+ packageName: "@1kbirds/chidori-mock-gmail",
17
+ category: "Email",
18
+ description: "Labels, messages, threads, drafts, send, search, attachments, and mailbox UI.",
19
+ route: "#/gmail",
20
+ devCommand: "npm run dev:gmail",
21
+ standaloneUrl: "http://127.0.0.1:5175/",
22
+ capabilities: ["Inbox", "Threads", "Draft/send", "Search"],
23
+ },
24
+ {
25
+ id: "salesforce",
26
+ name: "Salesforce",
27
+ packageName: "@1kbirds/chidori-mock-salesforce",
28
+ category: "CRM",
29
+ description: "Accounts, contacts, leads, opportunities, tasks, notes, SOQL, and sales console UI.",
30
+ route: "#/salesforce",
31
+ devCommand: "npm run dev:salesforce",
32
+ standaloneUrl: "http://127.0.0.1:5174/",
33
+ capabilities: ["CRM records", "Lead conversion", "SOQL", "Sales console"],
34
+ },
35
+ {
36
+ id: "slack",
37
+ name: "Slack",
38
+ packageName: "@1kbirds/chidori-mock-slack",
39
+ category: "Communication",
40
+ description: "Users, channels, messages, threads, reactions, files, search, and workspace UI.",
41
+ route: "#/slack",
42
+ devCommand: "npm run dev:slack",
43
+ standaloneUrl: "http://127.0.0.1:5176/",
44
+ capabilities: ["Channels", "Threads", "Reactions", "Files"],
45
+ },
46
+ {
47
+ id: "linear",
48
+ name: "Linear",
49
+ packageName: "@1kbirds/chidori-mock-linear",
50
+ category: "Planning",
51
+ description: "Teams, workflow states, issues, comments, projects, cycles, and board UI.",
52
+ route: "#/linear",
53
+ devCommand: "npm run dev:linear",
54
+ standaloneUrl: "http://127.0.0.1:5177/",
55
+ capabilities: ["Issues", "Workflow states", "Comments", "Board"],
56
+ },
57
+ ];
58
+ export function getHubAppMetadata(id) {
59
+ return hubAppMetadata.find((app) => app.id === id);
60
+ }
@@ -0,0 +1,3 @@
1
+ import "./styles.css";
2
+ export declare function HubApp(): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=HubApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HubApp.d.ts","sourceRoot":"","sources":["../../src/ui/HubApp.tsx"],"names":[],"mappings":"AAEA,OAAO,cAAc,CAAC;AAEtB,wBAAgB,MAAM,4CA6FrB"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { getHubApp, hubApps } from "../catalog";
4
+ import "./styles.css";
5
+ export function HubApp() {
6
+ const [hash, setHash] = useState(() => window.location.hash || "#/");
7
+ useEffect(() => {
8
+ const onHashChange = () => setHash(window.location.hash || "#/");
9
+ window.addEventListener("hashchange", onHashChange);
10
+ return () => window.removeEventListener("hashchange", onHashChange);
11
+ }, []);
12
+ const activeId = hash.replace(/^#\/?/, "");
13
+ const activeApp = getHubApp(activeId);
14
+ const grouped = useMemo(() => hubApps.reduce((groups, app) => {
15
+ groups[app.category] = groups[app.category] ?? [];
16
+ groups[app.category].push(app);
17
+ return groups;
18
+ }, {}), []);
19
+ return (_jsxs("div", { className: "hub-shell", children: [_jsxs("aside", { className: "hub-sidebar", children: [_jsx("a", { className: "hub-logo", href: "#/", children: "Chidori Mock" }), _jsxs("nav", { children: [_jsx("a", { className: !activeApp ? "active" : "", href: "#/", children: "Home" }), hubApps.map((app) => (_jsx("a", { className: activeApp?.id === app.id ? "active" : "", href: app.route, children: app.name }, app.id)))] })] }), _jsx("main", { className: "hub-main", children: activeApp ? (_jsxs("section", { className: "hub-app-view", children: [_jsxs("header", { className: "hub-app-header", children: [_jsxs("div", { children: [_jsx("p", { children: activeApp.packageName }), _jsx("h1", { children: activeApp.name })] }), _jsxs("div", { className: "hub-header-actions", children: [_jsx("code", { children: activeApp.devCommand }), _jsx("a", { href: activeApp.standaloneUrl, children: "Standalone" }), _jsx("a", { href: "#/", children: "Home" })] })] }), _jsx("div", { className: "hub-embedded-app", children: _jsx(activeApp.App, {}) })] })) : (_jsxs("section", { className: "hub-home", children: [_jsxs("header", { className: "hub-home-header", children: [_jsxs("div", { children: [_jsx("p", { children: "Virtual apps" }), _jsx("h1", { children: "Chidori Mock Hub" })] }), _jsxs("span", { children: [hubApps.length, " packages"] })] }), Object.entries(grouped).map(([category, apps]) => (_jsxs("section", { className: "hub-category", children: [_jsx("h2", { children: category }), _jsx("div", { className: "hub-card-grid", children: apps.map((app) => (_jsxs("a", { className: "hub-card", href: app.route, children: [_jsxs("div", { children: [_jsx("span", { children: app.packageName }), _jsx("h3", { children: app.name }), _jsx("p", { children: app.description })] }), _jsx("ul", { children: app.capabilities.map((capability) => (_jsx("li", { children: capability }, capability))) })] }, app.id))) })] }, category)))] })) })] }));
20
+ }
@@ -0,0 +1,2 @@
1
+ import "./styles.css";
2
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/ui/dev.tsx"],"names":[],"mappings":"AAGA,OAAO,cAAc,CAAC"}
package/dist/ui/dev.js ADDED
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { HubApp } from "./HubApp";
5
+ import "./styles.css";
6
+ createRoot(document.getElementById("root")).render(_jsx(React.StrictMode, { children: _jsx(HubApp, {}) }));
@@ -0,0 +1,2 @@
1
+ export { HubApp } from "./HubApp";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1 @@
1
+ export { HubApp } from "./HubApp";
@@ -0,0 +1,214 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
8
+ background: #f6f7f9;
9
+ color: #1f2937;
10
+ }
11
+
12
+ a {
13
+ color: inherit;
14
+ text-decoration: none;
15
+ }
16
+
17
+ .hub-shell {
18
+ min-height: 100vh;
19
+ display: grid;
20
+ grid-template-columns: 248px 1fr;
21
+ }
22
+
23
+ .hub-sidebar {
24
+ background: #101827;
25
+ color: #eef2ff;
26
+ padding: 18px 12px;
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 22px;
30
+ }
31
+
32
+ .hub-logo {
33
+ font-size: 22px;
34
+ font-weight: 750;
35
+ padding: 4px 8px;
36
+ }
37
+
38
+ .hub-sidebar nav {
39
+ display: grid;
40
+ gap: 4px;
41
+ }
42
+
43
+ .hub-sidebar nav a {
44
+ border-radius: 7px;
45
+ color: #cbd5e1;
46
+ padding: 9px 10px;
47
+ }
48
+
49
+ .hub-sidebar nav a.active,
50
+ .hub-sidebar nav a:hover {
51
+ background: #273449;
52
+ color: #fff;
53
+ }
54
+
55
+ .hub-main {
56
+ min-width: 0;
57
+ }
58
+
59
+ .hub-home {
60
+ padding: 24px;
61
+ display: grid;
62
+ gap: 26px;
63
+ }
64
+
65
+ .hub-home-header,
66
+ .hub-app-header {
67
+ background: #fff;
68
+ border: 1px solid #dfe3ea;
69
+ border-radius: 8px;
70
+ padding: 18px 20px;
71
+ display: flex;
72
+ justify-content: space-between;
73
+ gap: 16px;
74
+ align-items: center;
75
+ }
76
+
77
+ .hub-home-header p,
78
+ .hub-app-header p {
79
+ margin: 0;
80
+ color: #667085;
81
+ font-size: 13px;
82
+ }
83
+
84
+ .hub-home-header h1,
85
+ .hub-app-header h1 {
86
+ margin: 3px 0 0;
87
+ font-size: 28px;
88
+ }
89
+
90
+ .hub-home-header span {
91
+ border: 1px solid #dfe3ea;
92
+ border-radius: 999px;
93
+ padding: 7px 11px;
94
+ color: #475467;
95
+ background: #f8fafc;
96
+ }
97
+
98
+ .hub-category {
99
+ display: grid;
100
+ gap: 10px;
101
+ }
102
+
103
+ .hub-category h2 {
104
+ margin: 0;
105
+ font-size: 15px;
106
+ color: #475467;
107
+ }
108
+
109
+ .hub-card-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
112
+ gap: 14px;
113
+ }
114
+
115
+ .hub-card {
116
+ min-height: 214px;
117
+ background: #fff;
118
+ border: 1px solid #dfe3ea;
119
+ border-radius: 8px;
120
+ padding: 16px;
121
+ display: flex;
122
+ flex-direction: column;
123
+ justify-content: space-between;
124
+ gap: 16px;
125
+ }
126
+
127
+ .hub-card:hover {
128
+ border-color: #5b6ee1;
129
+ box-shadow: 0 8px 22px rgb(16 24 40 / 8%);
130
+ }
131
+
132
+ .hub-card span {
133
+ color: #667085;
134
+ font-size: 12px;
135
+ }
136
+
137
+ .hub-card h3 {
138
+ margin: 5px 0 7px;
139
+ font-size: 20px;
140
+ }
141
+
142
+ .hub-card p {
143
+ margin: 0;
144
+ color: #475467;
145
+ line-height: 1.45;
146
+ }
147
+
148
+ .hub-card ul {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 7px;
152
+ list-style: none;
153
+ padding: 0;
154
+ margin: 0;
155
+ }
156
+
157
+ .hub-card li {
158
+ background: #eef2ff;
159
+ color: #3447b6;
160
+ border-radius: 999px;
161
+ padding: 5px 8px;
162
+ font-size: 12px;
163
+ }
164
+
165
+ .hub-app-view {
166
+ min-height: 100vh;
167
+ display: grid;
168
+ grid-template-rows: auto 1fr;
169
+ }
170
+
171
+ .hub-app-header {
172
+ border-radius: 0;
173
+ border-left: 0;
174
+ border-right: 0;
175
+ border-top: 0;
176
+ }
177
+
178
+ .hub-header-actions {
179
+ display: flex;
180
+ flex-wrap: wrap;
181
+ justify-content: flex-end;
182
+ align-items: center;
183
+ gap: 8px;
184
+ }
185
+
186
+ .hub-header-actions code,
187
+ .hub-header-actions a {
188
+ border: 1px solid #dfe3ea;
189
+ border-radius: 6px;
190
+ background: #f8fafc;
191
+ color: #344054;
192
+ padding: 7px 10px;
193
+ }
194
+
195
+ .hub-embedded-app {
196
+ min-width: 0;
197
+ min-height: 0;
198
+ }
199
+
200
+ @media (max-width: 820px) {
201
+ .hub-shell {
202
+ grid-template-columns: 1fr;
203
+ }
204
+
205
+ .hub-home-header,
206
+ .hub-app-header {
207
+ align-items: stretch;
208
+ flex-direction: column;
209
+ }
210
+
211
+ .hub-header-actions {
212
+ justify-content: flex-start;
213
+ }
214
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@1kbirds/chidori-mock-hub",
3
+ "version": "0.1.0",
4
+ "description": "Mock app hub aggregating Chidori virtual app integrations.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./ui": {
15
+ "types": "./dist/ui/index.d.ts",
16
+ "import": "./dist/ui/index.js"
17
+ },
18
+ "./ui/styles.css": "./dist/ui/styles.css"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json && cp src/ui/styles.css dist/ui/styles.css",
27
+ "test": "vitest run",
28
+ "dev": "vite --host 127.0.0.1 --port 5178"
29
+ },
30
+ "dependencies": {
31
+ "@1kbirds/chidori-mock-gmail": "0.1.0",
32
+ "@1kbirds/chidori-mock-google-calendar": "0.1.0",
33
+ "@1kbirds/chidori-mock-linear": "0.1.0",
34
+ "@1kbirds/chidori-mock-salesforce": "0.1.0",
35
+ "@1kbirds/chidori-mock-slack": "0.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "react": ">=18",
39
+ "react-dom": ">=18"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react": "^19.1.0",
43
+ "@types/react-dom": "^19.1.0",
44
+ "@vitejs/plugin-react": "^5.0.0",
45
+ "react": "^19.1.0",
46
+ "react-dom": "^19.1.0",
47
+ "typescript": "^5.8.0",
48
+ "vite": "^7.0.0",
49
+ "vitest": "^3.2.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/ThousandBirdsInc/chidori-mock.git",
57
+ "directory": "packages/hub"
58
+ },
59
+ "homepage": "https://github.com/ThousandBirdsInc/chidori-mock/tree/main/packages/hub",
60
+ "bugs": {
61
+ "url": "https://github.com/ThousandBirdsInc/chidori-mock/issues"
62
+ }
63
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getHubAppMetadata, hubAppMetadata } from "../index";
3
+
4
+ describe("hub catalog", () => {
5
+ it("lists every implemented mock app", () => {
6
+ expect(hubAppMetadata.map((app) => app.id)).toEqual(["calendar", "gmail", "salesforce", "slack", "linear"]);
7
+ });
8
+
9
+ it("includes package references and navigation metadata", () => {
10
+ for (const app of hubAppMetadata) {
11
+ expect(app.packageName).toMatch(/^@1kbirds\/chidori-mock-/);
12
+ expect(app.route).toBe(`#/${app.id}`);
13
+ expect(app.devCommand).toMatch(/^npm run dev:/);
14
+ expect(app.standaloneUrl).toMatch(/^http:\/\/127\.0\.0\.1:/);
15
+ expect(app.capabilities.length).toBeGreaterThan(0);
16
+ }
17
+ });
18
+
19
+ it("resolves apps by id", () => {
20
+ expect(getHubAppMetadata("gmail")?.name).toBe("Gmail");
21
+ expect(getHubAppMetadata("missing")).toBeUndefined();
22
+ });
23
+ });
package/src/catalog.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { createGmailMock, demoSeed as gmailSeed } from "@1kbirds/chidori-mock-gmail";
2
+ import { GmailMockApp } from "@1kbirds/chidori-mock-gmail/ui";
3
+ import { createGoogleCalendarMock, demoSeed as googleCalendarSeed } from "@1kbirds/chidori-mock-google-calendar";
4
+ import { GoogleCalendarMockApp } from "@1kbirds/chidori-mock-google-calendar/ui";
5
+ import { createLinearMock, demoSeed as linearSeed } from "@1kbirds/chidori-mock-linear";
6
+ import { LinearMockApp } from "@1kbirds/chidori-mock-linear/ui";
7
+ import { createSalesforceMock, demoSeed as salesforceSeed } from "@1kbirds/chidori-mock-salesforce";
8
+ import { SalesforceMockApp } from "@1kbirds/chidori-mock-salesforce/ui";
9
+ import { createSlackMock, demoSeed as slackSeed } from "@1kbirds/chidori-mock-slack";
10
+ import { SlackMockApp } from "@1kbirds/chidori-mock-slack/ui";
11
+ import { createElement, type ComponentType } from "react";
12
+ import { hubAppMetadata, type HubAppId, type HubAppMetadata } from "./metadata";
13
+
14
+ export type { HubAppId };
15
+
16
+ export interface HubAppDefinition extends HubAppMetadata {
17
+ App: ComponentType;
18
+ }
19
+
20
+ const calendarMock = createGoogleCalendarMock({
21
+ now: "2026-06-01T15:00:00.000Z",
22
+ seed: googleCalendarSeed,
23
+ });
24
+
25
+ const gmailMock = createGmailMock({
26
+ now: "2026-06-01T16:30:00.000Z",
27
+ seed: gmailSeed,
28
+ });
29
+
30
+ const salesforceMock = createSalesforceMock({
31
+ now: "2026-06-01T15:00:00.000Z",
32
+ seed: salesforceSeed,
33
+ });
34
+
35
+ const slackMock = createSlackMock({
36
+ now: "2026-06-01T17:00:00.000Z",
37
+ seed: slackSeed,
38
+ });
39
+
40
+ const linearMock = createLinearMock({
41
+ now: "2026-06-01T17:30:00.000Z",
42
+ seed: linearSeed,
43
+ });
44
+
45
+ const appComponents: Record<HubAppId, ComponentType> = {
46
+ calendar: () => createElement(GoogleCalendarMockApp, { mock: calendarMock }),
47
+ gmail: () => createElement(GmailMockApp, { mock: gmailMock }),
48
+ salesforce: () => createElement(SalesforceMockApp, { mock: salesforceMock }),
49
+ slack: () => createElement(SlackMockApp, { mock: slackMock }),
50
+ linear: () => createElement(LinearMockApp, { mock: linearMock }),
51
+ };
52
+
53
+ export const hubApps: HubAppDefinition[] = hubAppMetadata.map((app) => ({
54
+ ...app,
55
+ App: appComponents[app.id],
56
+ }));
57
+
58
+ export function getHubApp(id: string | undefined): HubAppDefinition | undefined {
59
+ return hubApps.find((app) => app.id === id);
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { getHubAppMetadata, hubAppMetadata } from "./metadata";
2
+ export type { HubAppId, HubAppMetadata } from "./metadata";
@@ -0,0 +1,75 @@
1
+ export type HubAppId = "calendar" | "gmail" | "salesforce" | "slack" | "linear";
2
+
3
+ export interface HubAppMetadata {
4
+ id: HubAppId;
5
+ name: string;
6
+ packageName: string;
7
+ category: string;
8
+ description: string;
9
+ route: string;
10
+ devCommand: string;
11
+ standaloneUrl: string;
12
+ capabilities: string[];
13
+ }
14
+
15
+ export const hubAppMetadata: HubAppMetadata[] = [
16
+ {
17
+ id: "calendar",
18
+ name: "Google Calendar",
19
+ packageName: "@1kbirds/chidori-mock-google-calendar",
20
+ category: "Scheduling",
21
+ description: "Calendar CRUD, event listing, free/busy, and a React calendar review app.",
22
+ route: "#/calendar",
23
+ devCommand: "npm run dev:google-calendar",
24
+ standaloneUrl: "http://127.0.0.1:5173/",
25
+ capabilities: ["Calendars", "Events", "Free/busy", "Week view"],
26
+ },
27
+ {
28
+ id: "gmail",
29
+ name: "Gmail",
30
+ packageName: "@1kbirds/chidori-mock-gmail",
31
+ category: "Email",
32
+ description: "Labels, messages, threads, drafts, send, search, attachments, and mailbox UI.",
33
+ route: "#/gmail",
34
+ devCommand: "npm run dev:gmail",
35
+ standaloneUrl: "http://127.0.0.1:5175/",
36
+ capabilities: ["Inbox", "Threads", "Draft/send", "Search"],
37
+ },
38
+ {
39
+ id: "salesforce",
40
+ name: "Salesforce",
41
+ packageName: "@1kbirds/chidori-mock-salesforce",
42
+ category: "CRM",
43
+ description: "Accounts, contacts, leads, opportunities, tasks, notes, SOQL, and sales console UI.",
44
+ route: "#/salesforce",
45
+ devCommand: "npm run dev:salesforce",
46
+ standaloneUrl: "http://127.0.0.1:5174/",
47
+ capabilities: ["CRM records", "Lead conversion", "SOQL", "Sales console"],
48
+ },
49
+ {
50
+ id: "slack",
51
+ name: "Slack",
52
+ packageName: "@1kbirds/chidori-mock-slack",
53
+ category: "Communication",
54
+ description: "Users, channels, messages, threads, reactions, files, search, and workspace UI.",
55
+ route: "#/slack",
56
+ devCommand: "npm run dev:slack",
57
+ standaloneUrl: "http://127.0.0.1:5176/",
58
+ capabilities: ["Channels", "Threads", "Reactions", "Files"],
59
+ },
60
+ {
61
+ id: "linear",
62
+ name: "Linear",
63
+ packageName: "@1kbirds/chidori-mock-linear",
64
+ category: "Planning",
65
+ description: "Teams, workflow states, issues, comments, projects, cycles, and board UI.",
66
+ route: "#/linear",
67
+ devCommand: "npm run dev:linear",
68
+ standaloneUrl: "http://127.0.0.1:5177/",
69
+ capabilities: ["Issues", "Workflow states", "Comments", "Board"],
70
+ },
71
+ ];
72
+
73
+ export function getHubAppMetadata(id: string | undefined): HubAppMetadata | undefined {
74
+ return hubAppMetadata.find((app) => app.id === id);
75
+ }
@@ -0,0 +1,98 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { getHubApp, hubApps } from "../catalog";
3
+ import "./styles.css";
4
+
5
+ export function HubApp() {
6
+ const [hash, setHash] = useState(() => window.location.hash || "#/");
7
+
8
+ useEffect(() => {
9
+ const onHashChange = () => setHash(window.location.hash || "#/");
10
+ window.addEventListener("hashchange", onHashChange);
11
+ return () => window.removeEventListener("hashchange", onHashChange);
12
+ }, []);
13
+
14
+ const activeId = hash.replace(/^#\/?/, "");
15
+ const activeApp = getHubApp(activeId);
16
+ const grouped = useMemo(
17
+ () =>
18
+ hubApps.reduce<Record<string, typeof hubApps>>((groups, app) => {
19
+ groups[app.category] = groups[app.category] ?? [];
20
+ groups[app.category].push(app);
21
+ return groups;
22
+ }, {}),
23
+ [],
24
+ );
25
+
26
+ return (
27
+ <div className="hub-shell">
28
+ <aside className="hub-sidebar">
29
+ <a className="hub-logo" href="#/">
30
+ Chidori Mock
31
+ </a>
32
+ <nav>
33
+ <a className={!activeApp ? "active" : ""} href="#/">
34
+ Home
35
+ </a>
36
+ {hubApps.map((app) => (
37
+ <a className={activeApp?.id === app.id ? "active" : ""} href={app.route} key={app.id}>
38
+ {app.name}
39
+ </a>
40
+ ))}
41
+ </nav>
42
+ </aside>
43
+
44
+ <main className="hub-main">
45
+ {activeApp ? (
46
+ <section className="hub-app-view">
47
+ <header className="hub-app-header">
48
+ <div>
49
+ <p>{activeApp.packageName}</p>
50
+ <h1>{activeApp.name}</h1>
51
+ </div>
52
+ <div className="hub-header-actions">
53
+ <code>{activeApp.devCommand}</code>
54
+ <a href={activeApp.standaloneUrl}>Standalone</a>
55
+ <a href="#/">Home</a>
56
+ </div>
57
+ </header>
58
+ <div className="hub-embedded-app">
59
+ <activeApp.App />
60
+ </div>
61
+ </section>
62
+ ) : (
63
+ <section className="hub-home">
64
+ <header className="hub-home-header">
65
+ <div>
66
+ <p>Virtual apps</p>
67
+ <h1>Chidori Mock Hub</h1>
68
+ </div>
69
+ <span>{hubApps.length} packages</span>
70
+ </header>
71
+
72
+ {Object.entries(grouped).map(([category, apps]) => (
73
+ <section className="hub-category" key={category}>
74
+ <h2>{category}</h2>
75
+ <div className="hub-card-grid">
76
+ {apps.map((app) => (
77
+ <a className="hub-card" href={app.route} key={app.id}>
78
+ <div>
79
+ <span>{app.packageName}</span>
80
+ <h3>{app.name}</h3>
81
+ <p>{app.description}</p>
82
+ </div>
83
+ <ul>
84
+ {app.capabilities.map((capability) => (
85
+ <li key={capability}>{capability}</li>
86
+ ))}
87
+ </ul>
88
+ </a>
89
+ ))}
90
+ </div>
91
+ </section>
92
+ ))}
93
+ </section>
94
+ )}
95
+ </main>
96
+ </div>
97
+ );
98
+ }
package/src/ui/dev.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { HubApp } from "./HubApp";
4
+ import "./styles.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <HubApp />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1 @@
1
+ export { HubApp } from "./HubApp";
@@ -0,0 +1,214 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
8
+ background: #f6f7f9;
9
+ color: #1f2937;
10
+ }
11
+
12
+ a {
13
+ color: inherit;
14
+ text-decoration: none;
15
+ }
16
+
17
+ .hub-shell {
18
+ min-height: 100vh;
19
+ display: grid;
20
+ grid-template-columns: 248px 1fr;
21
+ }
22
+
23
+ .hub-sidebar {
24
+ background: #101827;
25
+ color: #eef2ff;
26
+ padding: 18px 12px;
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 22px;
30
+ }
31
+
32
+ .hub-logo {
33
+ font-size: 22px;
34
+ font-weight: 750;
35
+ padding: 4px 8px;
36
+ }
37
+
38
+ .hub-sidebar nav {
39
+ display: grid;
40
+ gap: 4px;
41
+ }
42
+
43
+ .hub-sidebar nav a {
44
+ border-radius: 7px;
45
+ color: #cbd5e1;
46
+ padding: 9px 10px;
47
+ }
48
+
49
+ .hub-sidebar nav a.active,
50
+ .hub-sidebar nav a:hover {
51
+ background: #273449;
52
+ color: #fff;
53
+ }
54
+
55
+ .hub-main {
56
+ min-width: 0;
57
+ }
58
+
59
+ .hub-home {
60
+ padding: 24px;
61
+ display: grid;
62
+ gap: 26px;
63
+ }
64
+
65
+ .hub-home-header,
66
+ .hub-app-header {
67
+ background: #fff;
68
+ border: 1px solid #dfe3ea;
69
+ border-radius: 8px;
70
+ padding: 18px 20px;
71
+ display: flex;
72
+ justify-content: space-between;
73
+ gap: 16px;
74
+ align-items: center;
75
+ }
76
+
77
+ .hub-home-header p,
78
+ .hub-app-header p {
79
+ margin: 0;
80
+ color: #667085;
81
+ font-size: 13px;
82
+ }
83
+
84
+ .hub-home-header h1,
85
+ .hub-app-header h1 {
86
+ margin: 3px 0 0;
87
+ font-size: 28px;
88
+ }
89
+
90
+ .hub-home-header span {
91
+ border: 1px solid #dfe3ea;
92
+ border-radius: 999px;
93
+ padding: 7px 11px;
94
+ color: #475467;
95
+ background: #f8fafc;
96
+ }
97
+
98
+ .hub-category {
99
+ display: grid;
100
+ gap: 10px;
101
+ }
102
+
103
+ .hub-category h2 {
104
+ margin: 0;
105
+ font-size: 15px;
106
+ color: #475467;
107
+ }
108
+
109
+ .hub-card-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
112
+ gap: 14px;
113
+ }
114
+
115
+ .hub-card {
116
+ min-height: 214px;
117
+ background: #fff;
118
+ border: 1px solid #dfe3ea;
119
+ border-radius: 8px;
120
+ padding: 16px;
121
+ display: flex;
122
+ flex-direction: column;
123
+ justify-content: space-between;
124
+ gap: 16px;
125
+ }
126
+
127
+ .hub-card:hover {
128
+ border-color: #5b6ee1;
129
+ box-shadow: 0 8px 22px rgb(16 24 40 / 8%);
130
+ }
131
+
132
+ .hub-card span {
133
+ color: #667085;
134
+ font-size: 12px;
135
+ }
136
+
137
+ .hub-card h3 {
138
+ margin: 5px 0 7px;
139
+ font-size: 20px;
140
+ }
141
+
142
+ .hub-card p {
143
+ margin: 0;
144
+ color: #475467;
145
+ line-height: 1.45;
146
+ }
147
+
148
+ .hub-card ul {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 7px;
152
+ list-style: none;
153
+ padding: 0;
154
+ margin: 0;
155
+ }
156
+
157
+ .hub-card li {
158
+ background: #eef2ff;
159
+ color: #3447b6;
160
+ border-radius: 999px;
161
+ padding: 5px 8px;
162
+ font-size: 12px;
163
+ }
164
+
165
+ .hub-app-view {
166
+ min-height: 100vh;
167
+ display: grid;
168
+ grid-template-rows: auto 1fr;
169
+ }
170
+
171
+ .hub-app-header {
172
+ border-radius: 0;
173
+ border-left: 0;
174
+ border-right: 0;
175
+ border-top: 0;
176
+ }
177
+
178
+ .hub-header-actions {
179
+ display: flex;
180
+ flex-wrap: wrap;
181
+ justify-content: flex-end;
182
+ align-items: center;
183
+ gap: 8px;
184
+ }
185
+
186
+ .hub-header-actions code,
187
+ .hub-header-actions a {
188
+ border: 1px solid #dfe3ea;
189
+ border-radius: 6px;
190
+ background: #f8fafc;
191
+ color: #344054;
192
+ padding: 7px 10px;
193
+ }
194
+
195
+ .hub-embedded-app {
196
+ min-width: 0;
197
+ min-height: 0;
198
+ }
199
+
200
+ @media (max-width: 820px) {
201
+ .hub-shell {
202
+ grid-template-columns: 1fr;
203
+ }
204
+
205
+ .hub-home-header,
206
+ .hub-app-header {
207
+ align-items: stretch;
208
+ flex-direction: column;
209
+ }
210
+
211
+ .hub-header-actions {
212
+ justify-content: flex-start;
213
+ }
214
+ }