@dxos/plugin-game 0.0.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/LICENSE +105 -0
- package/PLUGIN.mdl +334 -0
- package/dist/lib/neutral/GameArticle-AUJ4YDKW.mjs +40 -0
- package/dist/lib/neutral/GameArticle-AUJ4YDKW.mjs.map +7 -0
- package/dist/lib/neutral/GameCard-QBKHIL4Q.mjs +32 -0
- package/dist/lib/neutral/GameCard-QBKHIL4Q.mjs.map +7 -0
- package/dist/lib/neutral/GamePlugin.mjs +38 -0
- package/dist/lib/neutral/GamePlugin.mjs.map +7 -0
- package/dist/lib/neutral/GamePlugin.node.mjs +21 -0
- package/dist/lib/neutral/GamePlugin.node.mjs.map +7 -0
- package/dist/lib/neutral/GamePlugin.workerd.mjs +18 -0
- package/dist/lib/neutral/GamePlugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +11 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-DSUJWWNN.mjs +98 -0
- package/dist/lib/neutral/chunk-DSUJWWNN.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-R4V5PVU7.mjs +50 -0
- package/dist/lib/neutral/chunk-R4V5PVU7.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +96 -0
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +11 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-UAD2JUWF.mjs +51 -0
- package/dist/lib/neutral/create-object-UAD2JUWF.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +24 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/meta.mjs +8 -0
- package/dist/lib/neutral/meta.mjs.map +7 -0
- package/dist/lib/neutral/plugin.mjs +12 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-GO3SKEMA.mjs +35 -0
- package/dist/lib/neutral/react-surface-GO3SKEMA.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +34 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +20 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/GamePlugin.d.ts +4 -0
- package/dist/types/src/GamePlugin.d.ts.map +1 -0
- package/dist/types/src/GamePlugin.node.d.ts +10 -0
- package/dist/types/src/GamePlugin.node.d.ts.map +1 -0
- package/dist/types/src/GamePlugin.workerd.d.ts +4 -0
- package/dist/types/src/GamePlugin.workerd.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +5 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +4 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
- package/dist/types/src/components/CreateGamePanel.d.ts +19 -0
- package/dist/types/src/components/CreateGamePanel.d.ts.map +1 -0
- package/dist/types/src/components/CreateGamePanel.stories.d.ts +49 -0
- package/dist/types/src/components/CreateGamePanel.stories.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/containers/GameArticle/GameArticle.d.ts +6 -0
- package/dist/types/src/containers/GameArticle/GameArticle.d.ts.map +1 -0
- package/dist/types/src/containers/GameArticle/index.d.ts +2 -0
- package/dist/types/src/containers/GameArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/GameCard/GameCard.d.ts +6 -0
- package/dist/types/src/containers/GameCard/GameCard.d.ts.map +1 -0
- package/dist/types/src/containers/GameCard/index.d.ts +2 -0
- package/dist/types/src/containers/GameCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +4 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +3 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +36 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/Game.d.ts +67 -0
- package/dist/types/src/types/Game.d.ts.map +1 -0
- package/dist/types/src/types/GameCapabilities.d.ts +10 -0
- package/dist/types/src/types/GameCapabilities.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +46 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/load-game.d.ts +21 -0
- package/dist/types/src/util/load-game.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +40 -0
- package/package.json +124 -0
- package/src/GamePlugin.node.tsx +24 -0
- package/src/GamePlugin.tsx +27 -0
- package/src/GamePlugin.workerd.tsx +13 -0
- package/src/capabilities/create-object.ts +67 -0
- package/src/capabilities/index.ts +8 -0
- package/src/capabilities/react-surface.tsx +36 -0
- package/src/components/CreateGamePanel.stories.tsx +92 -0
- package/src/components/CreateGamePanel.tsx +127 -0
- package/src/components/index.ts +5 -0
- package/src/containers/GameArticle/GameArticle.tsx +42 -0
- package/src/containers/GameArticle/index.ts +5 -0
- package/src/containers/GameCard/GameCard.tsx +35 -0
- package/src/containers/GameCard/index.ts +5 -0
- package/src/containers/index.ts +8 -0
- package/src/index.ts +6 -0
- package/src/meta.ts +9 -0
- package/src/plugin.ts +9 -0
- package/src/translations.ts +34 -0
- package/src/types/Game.ts +106 -0
- package/src/types/GameCapabilities.ts +19 -0
- package/src/types/index.ts +9 -0
- package/src/types/types.ts +53 -0
- package/src/util/index.ts +5 -0
- package/src/util/load-game.ts +41 -0
- package/src/vite-env.d.ts +10 -0
package/dx.config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Config2 } from '@dxos/app-framework/config';
|
|
6
|
+
import { trim } from '@dxos/util';
|
|
7
|
+
|
|
8
|
+
export default Config2.make({
|
|
9
|
+
plugin: {
|
|
10
|
+
key: 'org.dxos.plugin.game',
|
|
11
|
+
name: 'Game',
|
|
12
|
+
author: 'DXOS',
|
|
13
|
+
description: trim`
|
|
14
|
+
Generic game host plugin that provides shared infrastructure for local-first,
|
|
15
|
+
multi-player turn-based games inside DXOS Composer. The plugin owns the base
|
|
16
|
+
\`Game\` ECHO type — which stores players and a reference to variant-specific
|
|
17
|
+
state — and drives the two-stage game-creation flow (variant picker + optional
|
|
18
|
+
input form).
|
|
19
|
+
|
|
20
|
+
Game variants (chess, tic-tac-toe, and others) are contributed by separate
|
|
21
|
+
plugins via the \`GameCapabilities.VariantProvider\` capability. Each variant supplies
|
|
22
|
+
its own state schema, player roles, create factory, and surface components
|
|
23
|
+
(Article and Card). Plugin-game resolves the correct variant at runtime and
|
|
24
|
+
delegates rendering, keeping the host layer fully decoupled from game logic.
|
|
25
|
+
|
|
26
|
+
Variant state objects are stored as hidden ECHO objects alongside the top-level
|
|
27
|
+
\`Game\`, ensuring they replicate to all peers but do not appear as independent
|
|
28
|
+
items in the user's space graph. The entire creation flow is atomic: if the
|
|
29
|
+
Game write fails, the variant state is rolled back automatically.
|
|
30
|
+
`,
|
|
31
|
+
source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-game',
|
|
32
|
+
icon: { key: 'ph--sword--regular', hue: 'indigo' },
|
|
33
|
+
spec: 'PLUGIN.mdl',
|
|
34
|
+
screenshots: [
|
|
35
|
+
{
|
|
36
|
+
dark: 'https://customer-5rxcjpyab08avpmn.cloudflarestream.com/2797d56edc9658d018ad75fe48a47f27/iframe?poster=https%3A%2F%2Fcustomer-5rxcjpyab08avpmn.cloudflarestream.com%2F2797d56edc9658d018ad75fe48a47f27%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/plugin-game",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Game plugin for DXOS Composer (unifies game variants like chess, tic-tac-toe).",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
|
+
"author": "DXOS.org",
|
|
13
|
+
"sideEffects": true,
|
|
14
|
+
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#capabilities": {
|
|
17
|
+
"source": "./src/capabilities/index.ts",
|
|
18
|
+
"types": "./dist/types/src/capabilities/index.d.ts",
|
|
19
|
+
"default": "./dist/lib/neutral/capabilities/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"#components": {
|
|
22
|
+
"source": "./src/components/index.ts",
|
|
23
|
+
"types": "./dist/types/src/components/index.d.ts",
|
|
24
|
+
"default": "./dist/lib/neutral/components/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"#containers": {
|
|
27
|
+
"source": "./src/containers/index.ts",
|
|
28
|
+
"types": "./dist/types/src/containers/index.d.ts",
|
|
29
|
+
"default": "./dist/lib/neutral/containers/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"#meta": {
|
|
32
|
+
"source": "./src/meta.ts",
|
|
33
|
+
"types": "./dist/types/src/meta.d.ts",
|
|
34
|
+
"default": "./dist/lib/neutral/meta.mjs"
|
|
35
|
+
},
|
|
36
|
+
"#plugin": {
|
|
37
|
+
"source": {
|
|
38
|
+
"workerd": "./src/GamePlugin.workerd.tsx",
|
|
39
|
+
"node": "./src/GamePlugin.node.tsx",
|
|
40
|
+
"default": "./src/GamePlugin.tsx"
|
|
41
|
+
},
|
|
42
|
+
"types": "./dist/types/src/GamePlugin.d.ts",
|
|
43
|
+
"workerd": "./dist/lib/neutral/GamePlugin.workerd.mjs",
|
|
44
|
+
"node": "./dist/lib/neutral/GamePlugin.node.mjs",
|
|
45
|
+
"default": "./dist/lib/neutral/GamePlugin.mjs"
|
|
46
|
+
},
|
|
47
|
+
"#translations": {
|
|
48
|
+
"source": "./src/translations.ts",
|
|
49
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
50
|
+
"default": "./dist/lib/neutral/translations.mjs"
|
|
51
|
+
},
|
|
52
|
+
"#types": {
|
|
53
|
+
"source": "./src/types/index.ts",
|
|
54
|
+
"types": "./dist/types/src/types/index.d.ts",
|
|
55
|
+
"default": "./dist/lib/neutral/types/index.mjs"
|
|
56
|
+
},
|
|
57
|
+
"#util": "./src/util/index.ts"
|
|
58
|
+
},
|
|
59
|
+
"exports": {
|
|
60
|
+
".": {
|
|
61
|
+
"source": "./src/index.ts",
|
|
62
|
+
"types": "./dist/types/src/index.d.ts",
|
|
63
|
+
"default": "./dist/lib/neutral/index.mjs"
|
|
64
|
+
},
|
|
65
|
+
"./assets/PLUGIN.mdl": "./PLUGIN.mdl",
|
|
66
|
+
"./plugin": {
|
|
67
|
+
"source": "./src/plugin.ts",
|
|
68
|
+
"types": "./dist/types/src/plugin.d.ts",
|
|
69
|
+
"default": "./dist/lib/neutral/plugin.mjs"
|
|
70
|
+
},
|
|
71
|
+
"./translations": {
|
|
72
|
+
"source": "./src/translations.ts",
|
|
73
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
74
|
+
"default": "./dist/lib/neutral/translations.mjs"
|
|
75
|
+
},
|
|
76
|
+
"./types": {
|
|
77
|
+
"source": "./src/types/index.ts",
|
|
78
|
+
"types": "./dist/types/src/types/index.d.ts",
|
|
79
|
+
"default": "./dist/lib/neutral/types/index.mjs"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"types": "dist/types/src/index.d.ts",
|
|
83
|
+
"files": [
|
|
84
|
+
"dist",
|
|
85
|
+
"dx.config.ts",
|
|
86
|
+
"src",
|
|
87
|
+
"PLUGIN.mdl"
|
|
88
|
+
],
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@dxos/app-framework": "workspace:*",
|
|
91
|
+
"@dxos/app-toolkit": "workspace:*",
|
|
92
|
+
"@dxos/compute": "workspace:*",
|
|
93
|
+
"@dxos/echo": "workspace:*",
|
|
94
|
+
"@dxos/echo-react": "workspace:*",
|
|
95
|
+
"@dxos/invariant": "workspace:*",
|
|
96
|
+
"@dxos/keys": "workspace:*",
|
|
97
|
+
"@dxos/log": "workspace:*",
|
|
98
|
+
"@dxos/plugin-space": "workspace:*",
|
|
99
|
+
"@dxos/react-ui": "workspace:*",
|
|
100
|
+
"@dxos/react-ui-form": "workspace:*",
|
|
101
|
+
"@dxos/react-ui-search": "workspace:*",
|
|
102
|
+
"@dxos/util": "workspace:*",
|
|
103
|
+
"effect": "catalog:"
|
|
104
|
+
},
|
|
105
|
+
"devDependencies": {
|
|
106
|
+
"@dxos/plugin-testing": "workspace:*",
|
|
107
|
+
"@dxos/plugin-theme": "workspace:*",
|
|
108
|
+
"@dxos/ui-theme": "workspace:*",
|
|
109
|
+
"@types/react": "catalog:",
|
|
110
|
+
"@types/react-dom": "catalog:",
|
|
111
|
+
"react": "catalog:",
|
|
112
|
+
"react-dom": "catalog:",
|
|
113
|
+
"vite": "catalog:"
|
|
114
|
+
},
|
|
115
|
+
"peerDependencies": {
|
|
116
|
+
"@dxos/react-ui": "workspace:*",
|
|
117
|
+
"@dxos/ui-theme": "workspace:*",
|
|
118
|
+
"react": "catalog:",
|
|
119
|
+
"react-dom": "catalog:"
|
|
120
|
+
},
|
|
121
|
+
"publishConfig": {
|
|
122
|
+
"access": "public"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Plugin } from '@dxos/app-framework';
|
|
6
|
+
import { AppPlugin } from '@dxos/app-toolkit';
|
|
7
|
+
|
|
8
|
+
import { meta } from '#meta';
|
|
9
|
+
import { translations } from '#translations';
|
|
10
|
+
import { Game } from '#types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Headless variant of GamePlugin (no React surfaces / CreateObject panel).
|
|
14
|
+
* Used in node contexts (CLI, agents, tests) where rendering is unavailable —
|
|
15
|
+
* the full variant lazy-loads `CreateGamePanel` from `react-ui-search` which
|
|
16
|
+
* downstream bundlers don't resolve cleanly under nested pnpm symlinks.
|
|
17
|
+
*/
|
|
18
|
+
export const GamePlugin = Plugin.define(meta).pipe(
|
|
19
|
+
AppPlugin.addSchemaModule({ schema: [Game] }),
|
|
20
|
+
AppPlugin.addTranslationsModule({ translations }),
|
|
21
|
+
Plugin.make,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export default GamePlugin;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Plugin } from '@dxos/app-framework';
|
|
6
|
+
import { AppPlugin } from '@dxos/app-toolkit';
|
|
7
|
+
|
|
8
|
+
import { CreateObject, ReactSurface } from '#capabilities';
|
|
9
|
+
import { meta } from '#meta';
|
|
10
|
+
import { translations } from '#translations';
|
|
11
|
+
import { Game } from '#types';
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line import/no-relative-packages
|
|
14
|
+
import pluginSpec from '../PLUGIN.mdl?raw';
|
|
15
|
+
|
|
16
|
+
export const GamePlugin = Plugin.define(meta).pipe(
|
|
17
|
+
AppPlugin.addCreateObjectModule({ activate: CreateObject }),
|
|
18
|
+
AppPlugin.addSchemaModule({ schema: [Game] }),
|
|
19
|
+
AppPlugin.addSurfaceModule({ activate: ReactSurface }),
|
|
20
|
+
AppPlugin.addTranslationsModule({ translations }),
|
|
21
|
+
AppPlugin.addPluginAssetModule({
|
|
22
|
+
asset: { pluginId: meta.profile.key, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },
|
|
23
|
+
}),
|
|
24
|
+
Plugin.make,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export default GamePlugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Plugin } from '@dxos/app-framework';
|
|
6
|
+
import { AppPlugin } from '@dxos/app-toolkit';
|
|
7
|
+
|
|
8
|
+
import { meta } from '#meta';
|
|
9
|
+
import { Game } from '#types';
|
|
10
|
+
|
|
11
|
+
export const GamePlugin = Plugin.define(meta).pipe(AppPlugin.addSchemaModule({ schema: [Game] }), Plugin.make);
|
|
12
|
+
|
|
13
|
+
export default GamePlugin;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Capability } from '@dxos/app-framework';
|
|
8
|
+
import { Operation } from '@dxos/compute';
|
|
9
|
+
import { Database, Type } from '@dxos/echo';
|
|
10
|
+
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { SpaceOperation } from '@dxos/plugin-space';
|
|
12
|
+
import { SpaceCapabilities } from '@dxos/plugin-space';
|
|
13
|
+
|
|
14
|
+
import { CreateGamePanel } from '#components';
|
|
15
|
+
|
|
16
|
+
import { Game, GameCapabilities, make as makeGame } from '../types';
|
|
17
|
+
|
|
18
|
+
type CreateOptions = Parameters<SpaceCapabilities.CreateObjectEntry['createObject']>[1];
|
|
19
|
+
|
|
20
|
+
export default Capability.makeModule(
|
|
21
|
+
Effect.fnUntraced(function* () {
|
|
22
|
+
return Capability.contributes(SpaceCapabilities.CreateObjectEntry, {
|
|
23
|
+
id: Type.getTypename(Game),
|
|
24
|
+
customPanel: CreateGamePanel,
|
|
25
|
+
createObject: (
|
|
26
|
+
{ variantId, input }: { variantId: string; input?: Record<string, any> },
|
|
27
|
+
options: CreateOptions,
|
|
28
|
+
) =>
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
const variants = yield* Capability.getAll(GameCapabilities.VariantProvider);
|
|
31
|
+
const variant = variants.find((v) => v.id === variantId);
|
|
32
|
+
invariant(variant, `Unknown game variant: ${variantId}`);
|
|
33
|
+
|
|
34
|
+
// Build variant state object via the variant's factory.
|
|
35
|
+
const stateObject = yield* variant
|
|
36
|
+
.createVariant(input ?? {})
|
|
37
|
+
.pipe(Effect.provideService(Database.Service, Database.makeService(options.db)));
|
|
38
|
+
|
|
39
|
+
// Add variant state to the database. Stays hidden — it's referenced by Game and
|
|
40
|
+
// shouldn't appear as a top-level item in the user's space.
|
|
41
|
+
yield* Operation.invoke(SpaceOperation.AddObject, {
|
|
42
|
+
object: stateObject,
|
|
43
|
+
target: options.target,
|
|
44
|
+
targetNodeId: options.targetNodeId,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const game = makeGame({
|
|
48
|
+
name: typeof input?.name === 'string' ? input.name : undefined,
|
|
49
|
+
variant: stateObject,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Add the user-facing Game wrapper. Not hidden — this is the object the user sees
|
|
53
|
+
// and navigates to. If this second write fails, roll back the variant state so we
|
|
54
|
+
// don't leak an orphaned object into the space.
|
|
55
|
+
return yield* Operation.invoke(SpaceOperation.AddObject, {
|
|
56
|
+
object: game,
|
|
57
|
+
target: options.target,
|
|
58
|
+
targetNodeId: options.targetNodeId,
|
|
59
|
+
}).pipe(
|
|
60
|
+
Effect.tapError(() =>
|
|
61
|
+
Operation.invoke(SpaceOperation.RemoveObjects, { objects: [stateObject] }).pipe(Effect.ignore),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Capability } from '@dxos/app-framework';
|
|
6
|
+
|
|
7
|
+
export const CreateObject = Capability.lazy('CreateObject', () => import('./create-object'));
|
|
8
|
+
export const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Capabilities, Capability } from '@dxos/app-framework';
|
|
9
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
10
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
11
|
+
|
|
12
|
+
import { GameArticle, GameCard } from '#containers';
|
|
13
|
+
|
|
14
|
+
import { Game } from '../types';
|
|
15
|
+
|
|
16
|
+
export default Capability.makeModule(() =>
|
|
17
|
+
Effect.succeed(
|
|
18
|
+
Capability.contributes(Capabilities.ReactSurface, [
|
|
19
|
+
Surface.create({
|
|
20
|
+
id: 'game',
|
|
21
|
+
filter: AppSurface.oneOf(
|
|
22
|
+
AppSurface.object(AppSurface.Article, Game),
|
|
23
|
+
AppSurface.object(AppSurface.Section, Game),
|
|
24
|
+
),
|
|
25
|
+
component: ({ data, role }) => (
|
|
26
|
+
<GameArticle role={role} subject={data.subject} attendableId={data.attendableId} />
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
Surface.create({
|
|
30
|
+
id: 'gameCard',
|
|
31
|
+
filter: AppSurface.object(AppSurface.CardContent, Game),
|
|
32
|
+
component: ({ data, role }) => <GameCard role={role} subject={data.subject} />,
|
|
33
|
+
}),
|
|
34
|
+
]),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { DXN, Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { Dialog } from '@dxos/react-ui';
|
|
12
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
13
|
+
|
|
14
|
+
import { translations } from '#translations';
|
|
15
|
+
import { type GameVariant } from '#types';
|
|
16
|
+
|
|
17
|
+
import { CreateGamePanel } from './CreateGamePanel';
|
|
18
|
+
|
|
19
|
+
// Two dummy variants for the story — no real ECHO state required, just the shape
|
|
20
|
+
// the picker reads (label, icon, inputSchema). createVariant is a no-op since we
|
|
21
|
+
// don't actually submit through a database here.
|
|
22
|
+
const Card = Schema.Struct({
|
|
23
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
24
|
+
}).pipe(Type.makeObject(DXN.make('org.dxos.story.cards', '0.1.0')));
|
|
25
|
+
|
|
26
|
+
const Dice = Schema.Struct({
|
|
27
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
28
|
+
faces: Schema.optional(Schema.Number.annotations({ title: 'Number of faces' })),
|
|
29
|
+
}).pipe(Type.makeObject(DXN.make('org.dxos.story.dice', '0.1.0')));
|
|
30
|
+
|
|
31
|
+
const dummyVariants: GameVariant[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'org.dxos.story.cards',
|
|
34
|
+
label: 'Card game',
|
|
35
|
+
icon: 'ph--cards--regular',
|
|
36
|
+
variantType: Card,
|
|
37
|
+
inputSchema: Type.getSchema(Card),
|
|
38
|
+
roles: ['dealer', 'player'] as const,
|
|
39
|
+
createVariant: () => Effect.sync(() => ({}) as unknown as Obj.Any),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'org.dxos.story.dice',
|
|
43
|
+
label: 'Dice game',
|
|
44
|
+
icon: 'ph--dice-six--regular',
|
|
45
|
+
variantType: Dice,
|
|
46
|
+
inputSchema: Type.getSchema(Dice),
|
|
47
|
+
roles: ['rolling'] as const,
|
|
48
|
+
createVariant: () => Effect.sync(() => ({}) as unknown as Obj.Any),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const DefaultStory = () => {
|
|
53
|
+
const [submitted, setSubmitted] = useState<Record<string, any> | undefined>(undefined);
|
|
54
|
+
return (
|
|
55
|
+
<Dialog.Root open>
|
|
56
|
+
<Dialog.Overlay>
|
|
57
|
+
<Dialog.Content>
|
|
58
|
+
<Dialog.Header>
|
|
59
|
+
<Dialog.Title>Create game</Dialog.Title>
|
|
60
|
+
</Dialog.Header>
|
|
61
|
+
<Dialog.Body>
|
|
62
|
+
<CreateGamePanel
|
|
63
|
+
target={{} as any}
|
|
64
|
+
variants={dummyVariants}
|
|
65
|
+
onCreateObject={(data) => {
|
|
66
|
+
setSubmitted(data);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
{submitted && (
|
|
70
|
+
<pre className='mt-form-gap p-2 text-xs bg-group-surface rounded-xs overflow-auto'>
|
|
71
|
+
{JSON.stringify(submitted, null, 2)}
|
|
72
|
+
</pre>
|
|
73
|
+
)}
|
|
74
|
+
</Dialog.Body>
|
|
75
|
+
</Dialog.Content>
|
|
76
|
+
</Dialog.Overlay>
|
|
77
|
+
</Dialog.Root>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const meta = {
|
|
82
|
+
title: 'plugins/plugin-game/components/CreateGamePanel',
|
|
83
|
+
component: DefaultStory,
|
|
84
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
85
|
+
parameters: { layout: 'fullscreen', translations },
|
|
86
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
87
|
+
|
|
88
|
+
export default meta;
|
|
89
|
+
|
|
90
|
+
type Story = StoryObj<typeof meta>;
|
|
91
|
+
|
|
92
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useCapabilities } from '@dxos/app-framework/ui';
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
|
+
import { type SpaceCapabilities } from '@dxos/plugin-space';
|
|
10
|
+
import { Column, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
import { Form, omitId } from '@dxos/react-ui-form';
|
|
12
|
+
import { SearchList, useSearchListResults } from '@dxos/react-ui-search';
|
|
13
|
+
|
|
14
|
+
import { meta } from '#meta';
|
|
15
|
+
import { GameCapabilities, type GameVariant } from '#types';
|
|
16
|
+
|
|
17
|
+
export type CreateGamePanelProps = SpaceCapabilities.CreateObjectCustomPanelProps & {
|
|
18
|
+
/** Optional override (primarily for stories/tests). Defaults to GameCapabilities.VariantProvider. */
|
|
19
|
+
variants?: GameVariant[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Two-stage create panel for games:
|
|
24
|
+
* 1. Variant picker (SearchList over contributed `GameVariant[]`).
|
|
25
|
+
* 2. Variant-specific input form (rendered from variant.inputSchema).
|
|
26
|
+
*
|
|
27
|
+
* On submit, calls `onCreateObject({ variantId, name, input })` where `input` is the
|
|
28
|
+
* variant-specific form values. Plugin-game's CreateObjectEntry.createObject resolves
|
|
29
|
+
* the variantId, builds the variant state object via variant.createVariant, then wraps
|
|
30
|
+
* it in a Game.
|
|
31
|
+
*/
|
|
32
|
+
export const CreateGamePanel = ({ target, onCreateObject, variants: variantsProp }: CreateGamePanelProps) => {
|
|
33
|
+
const capabilityVariants = useCapabilities(GameCapabilities.VariantProvider);
|
|
34
|
+
const variants = variantsProp ?? capabilityVariants;
|
|
35
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
36
|
+
const selected = useMemo(() => variants.find((v) => v.id === selectedId), [variants, selectedId]);
|
|
37
|
+
|
|
38
|
+
const handleSelect = useCallback(
|
|
39
|
+
(id: string) => {
|
|
40
|
+
const variant = variants.find((v) => v.id === id);
|
|
41
|
+
if (!variant) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!variant.inputSchema) {
|
|
45
|
+
void onCreateObject({ variantId: id });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setSelectedId(id);
|
|
49
|
+
},
|
|
50
|
+
[variants, onCreateObject],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const handleSubmit = useCallback(
|
|
54
|
+
(input: Record<string, any>) => {
|
|
55
|
+
if (!selected) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
void onCreateObject({ variantId: selected.id, input });
|
|
59
|
+
},
|
|
60
|
+
[selected, onCreateObject],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!selected) {
|
|
64
|
+
return <VariantPicker variants={variants} onSelect={handleSelect} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const schema = selected.inputSchema ? omitId(selected.inputSchema) : undefined;
|
|
68
|
+
if (!schema) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Form.Root
|
|
74
|
+
autoFocus
|
|
75
|
+
schema={schema}
|
|
76
|
+
defaultValues={{}}
|
|
77
|
+
db={Obj.isObject(target) ? Obj.getDatabase(target) : target}
|
|
78
|
+
onSave={handleSubmit}
|
|
79
|
+
testId='create-game-form'
|
|
80
|
+
>
|
|
81
|
+
{/* Rendered inside the create dialog's Dialog.Body (which owns the gutter Column); use
|
|
82
|
+
Column.Center to align with the dialog title rather than nesting another Column.Root. */}
|
|
83
|
+
<Column.Center>
|
|
84
|
+
<Form.Content>
|
|
85
|
+
<Form.FieldSet />
|
|
86
|
+
<Form.Submit />
|
|
87
|
+
</Form.Content>
|
|
88
|
+
</Column.Center>
|
|
89
|
+
</Form.Root>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type VariantPickerProps = {
|
|
94
|
+
variants: GameVariant[];
|
|
95
|
+
onSelect: (id: string) => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const VariantPicker = ({ variants, onSelect }: VariantPickerProps) => {
|
|
99
|
+
const { t } = useTranslation(meta.profile.key);
|
|
100
|
+
const sorted = useMemo(() => [...variants].sort((a, b) => a.label.localeCompare(b.label)), [variants]);
|
|
101
|
+
const { results, handleSearch } = useSearchListResults({
|
|
102
|
+
items: sorted,
|
|
103
|
+
extract: (variant) => variant.label,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
108
|
+
<SearchList.Input
|
|
109
|
+
classNames='mb-form-gap'
|
|
110
|
+
autoFocus
|
|
111
|
+
data-testid='create-game-panel.variant-input'
|
|
112
|
+
placeholder={t('create-panel.variant.placeholder')}
|
|
113
|
+
/>
|
|
114
|
+
<SearchList.Viewport>
|
|
115
|
+
{results.map((variant) => (
|
|
116
|
+
<SearchList.Item
|
|
117
|
+
key={variant.id}
|
|
118
|
+
value={variant.id}
|
|
119
|
+
label={variant.label}
|
|
120
|
+
icon={variant.icon ?? 'ph--sword--regular'}
|
|
121
|
+
onSelect={() => onSelect(variant.id)}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</SearchList.Viewport>
|
|
125
|
+
</SearchList.Root>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { useCapabilities } from '@dxos/app-framework/ui';
|
|
8
|
+
import { type AppSurface } from '@dxos/app-toolkit/ui';
|
|
9
|
+
import { Obj, type Ref } from '@dxos/echo';
|
|
10
|
+
import { useObject } from '@dxos/echo-react';
|
|
11
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
12
|
+
|
|
13
|
+
import { meta } from '#meta';
|
|
14
|
+
import { type Game, GameCapabilities } from '#types';
|
|
15
|
+
|
|
16
|
+
export type GameArticleProps = AppSurface.ObjectArticleProps<Game>;
|
|
17
|
+
|
|
18
|
+
export const GameArticle = ({ role, attendableId, subject: game }: GameArticleProps) => {
|
|
19
|
+
const { t } = useTranslation(meta.profile.key);
|
|
20
|
+
const variants = useCapabilities(GameCapabilities.VariantProvider);
|
|
21
|
+
const ref = game.variant as Ref.Ref<Obj.Unknown>;
|
|
22
|
+
// Subscribe via useObject so the article re-renders when the variant ref loads or changes.
|
|
23
|
+
// The snapshot is discarded; we pass the live ref target to variant components so they can
|
|
24
|
+
// subscribe to their own properties via useObject(state, 'prop').
|
|
25
|
+
useObject(ref);
|
|
26
|
+
const variant = ref?.target as Obj.Unknown | undefined;
|
|
27
|
+
|
|
28
|
+
if (!variant) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const variantTypename = Obj.getTypename(variant);
|
|
33
|
+
const match = variants.find((v) => v.id === variantTypename);
|
|
34
|
+
if (!match?.article) {
|
|
35
|
+
return (
|
|
36
|
+
<div className='p-4 text-sm'>{t('unsupported-variant.label', { defaultValue: 'Unsupported game variant' })}</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const Component = match.article;
|
|
41
|
+
return <Component game={game} variant={variant} role={role} attendableId={attendableId} />;
|
|
42
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { useCapabilities } from '@dxos/app-framework/ui';
|
|
8
|
+
import { type AppSurface } from '@dxos/app-toolkit/ui';
|
|
9
|
+
import { Obj, type Ref } from '@dxos/echo';
|
|
10
|
+
import { useObject } from '@dxos/echo-react';
|
|
11
|
+
|
|
12
|
+
import { type Game, GameCapabilities } from '#types';
|
|
13
|
+
|
|
14
|
+
export type GameCardProps = AppSurface.ObjectCardProps<Game>;
|
|
15
|
+
|
|
16
|
+
export const GameCard = ({ role, subject: game }: GameCardProps) => {
|
|
17
|
+
const variants = useCapabilities(GameCapabilities.VariantProvider);
|
|
18
|
+
const ref = game.variant as Ref.Ref<Obj.Unknown>;
|
|
19
|
+
// Subscribe for re-renders; pass the live ref target down to variant components.
|
|
20
|
+
useObject(ref);
|
|
21
|
+
const variant = ref?.target as Obj.Unknown | undefined;
|
|
22
|
+
|
|
23
|
+
if (!variant) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const variantTypename = Obj.getTypename(variant);
|
|
28
|
+
const match = variants.find((v) => v.id === variantTypename);
|
|
29
|
+
if (!match?.card) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const Component = match.card;
|
|
34
|
+
return <Component game={game} variant={variant} role={role} />;
|
|
35
|
+
};
|