@moloodev/layout-sdk 0.0.0-stage → 0.2.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 +148 -2
- package/package.json +50 -3
- package/src/basic.tsx +366 -0
- package/src/catalog/index.ts +3 -0
- package/src/catalog/resolve.ts +195 -0
- package/src/catalog/validate.ts +146 -0
- package/src/catalog-format/contract.ts +131 -0
- package/src/catalog-format/fixtures.ts +148 -0
- package/src/catalog-format/index.ts +10 -0
- package/src/catalog-format/schema.ts +146 -0
- package/src/catalog-format/value-schema.ts +110 -0
- package/src/compose.tsx +78 -0
- package/src/examples/components.tsx +156 -0
- package/src/examples/index.ts +1 -0
- package/src/examples/packages.ts +42 -0
- package/src/index.ts +18 -0
- package/src/layout-format/bind.ts +88 -0
- package/src/layout-format/contract.ts +146 -0
- package/src/layout-format/edit.ts +278 -0
- package/src/layout-format/index.ts +18 -0
- package/src/layout-format/schema.ts +97 -0
- package/src/layout.ts +74 -0
- package/src/react.tsx +34 -0
- package/src/render-fixture.ts +51 -0
- package/src/runtime.tsx +165 -0
- package/src/three.tsx +19 -0
package/README.md
CHANGED
|
@@ -1,3 +1,149 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Layout runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runtime foundation for the layout editor. This package does not mount an editor,
|
|
4
|
+
fetch assets, install packages, or own persistence. See the
|
|
5
|
+
[living spec](../../docs/superpowers/specs/2026-10-20-layout-catalog-runtime.md).
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { createStateStore } from "@json-render/core";
|
|
9
|
+
import { createExampleCatalogs } from "@moloodev/layout-sdk/catalog-format/fixtures";
|
|
10
|
+
import { resolveCatalog } from "@moloodev/layout-sdk/catalog";
|
|
11
|
+
import { Canvas } from "@react-three/fiber";
|
|
12
|
+
import { prepareSurface } from "@moloodev/layout-sdk";
|
|
13
|
+
import { examplePackages } from "@moloodev/layout-sdk/examples";
|
|
14
|
+
import { DomSurface } from "@moloodev/layout-sdk/react";
|
|
15
|
+
import { ThreeScene } from "@moloodev/layout-sdk/three";
|
|
16
|
+
|
|
17
|
+
// Supply real workspace asset IDs. These examples do not create assets.
|
|
18
|
+
const { puzzle, winter } = createExampleCatalogs({
|
|
19
|
+
puzzle: puzzleCatalogId,
|
|
20
|
+
tileImage: tileImageId,
|
|
21
|
+
winterImage: winterImageId,
|
|
22
|
+
buildingModel: buildingModelId,
|
|
23
|
+
});
|
|
24
|
+
const catalog = resolveCatalog([
|
|
25
|
+
{ assetId: puzzleCatalogId, content: puzzle },
|
|
26
|
+
{ assetId: winterCatalogId, content: winter },
|
|
27
|
+
]);
|
|
28
|
+
const spec = {
|
|
29
|
+
root: "tile",
|
|
30
|
+
elements: {
|
|
31
|
+
tile: {
|
|
32
|
+
type: "puzzle.Tile",
|
|
33
|
+
props: { label: "Snowflake tile", selected: { $state: "/selected" } },
|
|
34
|
+
on: { press: { action: "select" } },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// Resolve image/model URLs through the application's existing asset access.
|
|
39
|
+
const prepare = (target: string) =>
|
|
40
|
+
prepareSurface({
|
|
41
|
+
catalog,
|
|
42
|
+
target,
|
|
43
|
+
spec,
|
|
44
|
+
packages: examplePackages,
|
|
45
|
+
resolveResource,
|
|
46
|
+
});
|
|
47
|
+
const hud = prepare("react-dom");
|
|
48
|
+
const scene = prepare("react-three-fiber");
|
|
49
|
+
const store = createStateStore({ selected: false });
|
|
50
|
+
const handlers = { select: () => store.set("/selected", true) };
|
|
51
|
+
|
|
52
|
+
<div>
|
|
53
|
+
<DomSurface surface={hud} handlers={handlers} store={store} />
|
|
54
|
+
<Canvas>
|
|
55
|
+
<ThreeScene surface={scene} handlers={handlers} store={store} />
|
|
56
|
+
</Canvas>
|
|
57
|
+
</div>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `./react` for a DOM host and `./three` for a scene under an existing R3F
|
|
61
|
+
root. `./examples` registers the sample DOM and R3F implementations. Catalog assembly
|
|
62
|
+
and server-side reference inspection do not need React or Three.
|
|
63
|
+
|
|
64
|
+
Run `pnpm --filter @moloodev/layout-sdk/catalog-format test`,
|
|
65
|
+
`pnpm --filter @moloodev/layout-sdk/catalog test`, and
|
|
66
|
+
`pnpm --filter @moloodev/layout-sdk test` from the repository root.
|
|
67
|
+
UI verification belongs to the human; these are unit tests, not browser tests.
|
|
68
|
+
|
|
69
|
+
`prepareLayout` reads the saved layout's explicit catalog stack and prepares all
|
|
70
|
+
surfaces, reusing the prepared spec copies returned by `validateLayout` rather
|
|
71
|
+
than repeating per-surface validation. `composeSurfaces(prepared, boundary)` makes
|
|
72
|
+
per-element runtime registry entries and supplies mounted children through `RuntimeComponentProps.surfaces`.
|
|
73
|
+
The injected boundary callback owns renderer transitions. The host supplies the
|
|
74
|
+
Canvas for a DOM → R3F mount and renders `ThreeScene` for an R3F → R3F mount.
|
|
75
|
+
Saved component keys, props and native children are
|
|
76
|
+
unchanged. Unsupported transitions must fail explicitly.
|
|
77
|
+
|
|
78
|
+
An optional third argument to `composeSurfaces` is an `ElementObserver` component.
|
|
79
|
+
It receives `{ identity: { surface, element, type, target }, render(rootRef) }`.
|
|
80
|
+
Call `render(rootRef)` to observe the implementation's native root without adding
|
|
81
|
+
a DOM wrapper or Three group. Implementations opt in by attaching the optional
|
|
82
|
+
`RuntimeComponentProps.rootRef` to their primary DOM element or Object3D; all basic
|
|
83
|
+
and example implementations do this. Forward the ref's null notification on
|
|
84
|
+
unmount. The observer must tolerate no root (suspense/visibility) and multiple
|
|
85
|
+
instances of the same spec element (repeat). The original component type, native
|
|
86
|
+
children, named slots and mounted surface identities are preserved. This is a
|
|
87
|
+
runtime-only observation contract; selection, highlighting and interaction modes
|
|
88
|
+
belong to the editor's renderer adapters, not catalog JSON or saved props.
|
|
89
|
+
|
|
90
|
+
Observers also receive `nativeSlots`, the component contract's native slot names
|
|
91
|
+
(including `default`, even when empty). Surface mount slots are excluded.
|
|
92
|
+
An observer can call `render(rootRef, slotRefs)`; only declared native names reach
|
|
93
|
+
the implementation's optional `RuntimeComponentProps.slotRefs`. Existing observers
|
|
94
|
+
that only pass `rootRef`, and renderers without an observer, keep working.
|
|
95
|
+
|
|
96
|
+
Attach `slotRefs?.header` directly to the implementation's actual header container,
|
|
97
|
+
and `slotRefs?.default` to its native children container. No wrapper is introduced.
|
|
98
|
+
These are React 19 callback refs that may return a cleanup for each mounted root.
|
|
99
|
+
If forwarding through another callback, return that cleanup; do not discard it.
|
|
100
|
+
Each repeated instance has separate registrations. Multiple roots for one slot
|
|
101
|
+
remain observable so consumers can reject ambiguous geometry. Slots are transient
|
|
102
|
+
renderer observations, not element selection roots or persisted metadata.
|
|
103
|
+
This optional observer extension changes neither catalog/layout schemas nor the
|
|
104
|
+
implementation ABI or existing pinned package references.
|
|
105
|
+
|
|
106
|
+
`@moloodev/layout-sdk/basic` exports `basicCatalog` and `basicPackages`.
|
|
107
|
+
The catalog defines DOM Stack, Grid, Frame, Panel, Text, Button, and Viewport,
|
|
108
|
+
plus R3F Group, Transform, and Box. It has no resource dependencies.
|
|
109
|
+
Every DOM definition pins `@moloodev/layout-basic@1.3.0`.
|
|
110
|
+
Every R3F definition pins `@moloodev/layout-basic@1.1.0`.
|
|
111
|
+
`basicPackages` installs those two packages.
|
|
112
|
+
Grid uses row auto-placement, 1 to 12 equal-width columns, and a configurable gap.
|
|
113
|
+
Frame is a border-box column flex container with integer width (64–1600px) and
|
|
114
|
+
height (64–1200px), defaulting to 320×240px. It declares `editor.resize["react-dom"]`
|
|
115
|
+
property mappings. The runtime owns no resize gestures.
|
|
116
|
+
Panel supplies header, default, and footer native slots with column flex containers
|
|
117
|
+
and optional `slotRefs`, including empty regions with a 48px minimum height.
|
|
118
|
+
Its gap property applies both between slots and within them, ranging from 0 to
|
|
119
|
+
128px (default 16). Editor-only empty-slot hints are not part of Panel's output.
|
|
120
|
+
Import the catalog JSON as a workspace `.mcat` asset and reference its actual asset
|
|
121
|
+
ID. The runtime does not invent built-in asset IDs or install catalogs into workspaces.
|
|
122
|
+
|
|
123
|
+
Example Tile labels are supplied through `props.label` (default: "Tile"). Set a
|
|
124
|
+
meaningful label for each tile; the DOM implementation exposes it as its accessible
|
|
125
|
+
name and shows selection with an inset shadow, leaving the browser's focus outline
|
|
126
|
+
available for keyboard navigation. In forced colors, a double border marks selection
|
|
127
|
+
using system colors. Both states reserve the same border width inside the tile's
|
|
128
|
+
dimensions. React hoists and deduplicates the example's stylesheet; consumers do not
|
|
129
|
+
need a separate CSS import. Interactive Three examples consume
|
|
130
|
+
click propagation so occluded objects and parent handlers do not also activate.
|
|
131
|
+
|
|
132
|
+
Catalog documents are schema version 1. Optional `editor.transform[target]`
|
|
133
|
+
maps numeric props to local position, Euler XYZ rotation in radians, and
|
|
134
|
+
unitless scale. Each operation maps one or more x/y/z axes. Properties must
|
|
135
|
+
be distinct across operations for a target.
|
|
136
|
+
Omitted axes remain unchanged. Implementations that opt in must apply these
|
|
137
|
+
semantics to the observed root, including when a seasonal override supplies the
|
|
138
|
+
values. Metadata stays owned by the original definition. A document whose
|
|
139
|
+
version is not 1 is `unsupported_schema`. Exact implementation versions and ABI
|
|
140
|
+
stay unchanged. This contract does not add transform gestures or infer support
|
|
141
|
+
for existing Group/Box components.
|
|
142
|
+
|
|
143
|
+
`basic.Transform` opts into the transform contract with `positionX/Y/Z`,
|
|
144
|
+
`rotationX/Y/Z`, and `scaleX/Y/Z`. It renders an observed Three Group with a default
|
|
145
|
+
child slot, local Euler XYZ rotation, and defaults of zero position/rotation and
|
|
146
|
+
unit scale. Scale props have a minimum of 0.01; position and rotation accept any
|
|
147
|
+
finite number. json-render state bindings update the group without changing the
|
|
148
|
+
stored spec. Group and Box share `@moloodev/layout-basic@1.1.0` and do not declare
|
|
149
|
+
`editor.transform`. The runtime adds no gesture controls.
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moloodev/layout-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.ts",
|
|
7
|
+
"./format": "./src/layout-format/index.ts",
|
|
8
|
+
"./catalog": "./src/catalog/index.ts",
|
|
9
|
+
"./catalog-format": "./src/catalog-format/index.ts",
|
|
10
|
+
"./catalog-format/fixtures": "./src/catalog-format/fixtures.ts",
|
|
11
|
+
"./react": "./src/react.tsx",
|
|
12
|
+
"./three": "./src/three.tsx",
|
|
13
|
+
"./examples": "./src/examples/index.ts",
|
|
14
|
+
"./basic": "./src/basic.tsx"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@json-render/core": "^0.20.0",
|
|
18
|
+
"@json-render/react": "^0.20.0",
|
|
19
|
+
"@react-three/fiber": "9.8.2",
|
|
20
|
+
"three": "0.186.1",
|
|
21
|
+
"zod": "^4.5.4",
|
|
22
|
+
"@moloodev/platform": "0.2.0",
|
|
23
|
+
"@moloodev/editor-sdk": "0.2.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^19.0.0",
|
|
27
|
+
"react-dom": "^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@react-three/test-renderer": "9.1.1",
|
|
31
|
+
"@types/node": "^24.13.3",
|
|
32
|
+
"@types/react": "^19.2.18",
|
|
33
|
+
"@types/react-dom": "^19.2.7",
|
|
34
|
+
"@types/three": "0.186.0",
|
|
35
|
+
"react": "^19.2.8",
|
|
36
|
+
"react-dom": "^19.2.8",
|
|
37
|
+
"tsx": "^4.23.13",
|
|
38
|
+
"typescript": "^7.0.2",
|
|
39
|
+
"@moloodev/typescript-config": "0.2.0"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"src",
|
|
46
|
+
"!src/**/*.test.ts",
|
|
47
|
+
"!src/**/*.test.tsx"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "tsx --test 'src/**/*.test.ts' 'src/**/*.test.tsx'"
|
|
52
|
+
}
|
|
6
53
|
}
|
package/src/basic.tsx
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ComponentContract,
|
|
4
|
+
ComponentDefinition,
|
|
5
|
+
Content,
|
|
6
|
+
ObjectSchema,
|
|
7
|
+
} from "./catalog-format";
|
|
8
|
+
import type { ImplementationPackage, RuntimeComponentProps } from "./runtime";
|
|
9
|
+
|
|
10
|
+
const props = (properties: ObjectSchema["properties"]): ObjectSchema => ({
|
|
11
|
+
type: "object",
|
|
12
|
+
properties,
|
|
13
|
+
required: Object.keys(properties),
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
});
|
|
16
|
+
const contract = (
|
|
17
|
+
properties: ObjectSchema["properties"],
|
|
18
|
+
slots = false,
|
|
19
|
+
events: string[] = [],
|
|
20
|
+
): ComponentContract => ({
|
|
21
|
+
version: 1,
|
|
22
|
+
props: props(properties),
|
|
23
|
+
slots: slots ? { default: { accepts: [] } } : {},
|
|
24
|
+
events,
|
|
25
|
+
resources: {},
|
|
26
|
+
});
|
|
27
|
+
const contracts = {
|
|
28
|
+
Stack: contract(
|
|
29
|
+
{
|
|
30
|
+
direction: { type: "string", enum: ["row", "column"] },
|
|
31
|
+
gap: { type: "number", minimum: 0, maximum: 128 },
|
|
32
|
+
},
|
|
33
|
+
true,
|
|
34
|
+
),
|
|
35
|
+
Grid: contract(
|
|
36
|
+
{
|
|
37
|
+
columns: { type: "integer", minimum: 1, maximum: 12 },
|
|
38
|
+
gap: { type: "number", minimum: 0, maximum: 128 },
|
|
39
|
+
},
|
|
40
|
+
true,
|
|
41
|
+
),
|
|
42
|
+
Frame: contract(
|
|
43
|
+
{
|
|
44
|
+
width: { type: "integer", minimum: 64, maximum: 1600 },
|
|
45
|
+
height: { type: "integer", minimum: 64, maximum: 1200 },
|
|
46
|
+
},
|
|
47
|
+
true,
|
|
48
|
+
),
|
|
49
|
+
Panel: {
|
|
50
|
+
...contract({ gap: { type: "number", minimum: 0, maximum: 128 } }),
|
|
51
|
+
slots: {
|
|
52
|
+
header: { accepts: [] },
|
|
53
|
+
default: { accepts: [] },
|
|
54
|
+
footer: { accepts: [] },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
Text: contract({
|
|
58
|
+
text: { type: "string" },
|
|
59
|
+
size: { type: "number", minimum: 8, maximum: 96 },
|
|
60
|
+
}),
|
|
61
|
+
Button: contract({ text: { type: "string" } }, false, ["press"]),
|
|
62
|
+
Viewport: {
|
|
63
|
+
...contract({ height: { type: "number", minimum: 100, maximum: 1200 } }),
|
|
64
|
+
surfaceSlots: { scene: { target: "react-three-fiber" } },
|
|
65
|
+
},
|
|
66
|
+
Group: contract({}, true),
|
|
67
|
+
Transform: contract(
|
|
68
|
+
{
|
|
69
|
+
positionX: { type: "number" },
|
|
70
|
+
positionY: { type: "number" },
|
|
71
|
+
positionZ: { type: "number" },
|
|
72
|
+
rotationX: { type: "number" },
|
|
73
|
+
rotationY: { type: "number" },
|
|
74
|
+
rotationZ: { type: "number" },
|
|
75
|
+
scaleX: { type: "number", minimum: 0.01 },
|
|
76
|
+
scaleY: { type: "number", minimum: 0.01 },
|
|
77
|
+
scaleZ: { type: "number", minimum: 0.01 },
|
|
78
|
+
},
|
|
79
|
+
true,
|
|
80
|
+
),
|
|
81
|
+
Box: contract(
|
|
82
|
+
{
|
|
83
|
+
color: { type: "string" },
|
|
84
|
+
size: { type: "number", minimum: 0.1, maximum: 10 },
|
|
85
|
+
},
|
|
86
|
+
false,
|
|
87
|
+
["press"],
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
const packageVersion = {
|
|
91
|
+
"react-dom": "1.3.0",
|
|
92
|
+
"react-three-fiber": "1.1.0",
|
|
93
|
+
};
|
|
94
|
+
function definition(
|
|
95
|
+
entry: keyof typeof contracts,
|
|
96
|
+
target: keyof typeof packageVersion,
|
|
97
|
+
defaults: ComponentDefinition["defaults"],
|
|
98
|
+
layout: "leaf" | "flow" | "grid" | "spatial",
|
|
99
|
+
editing?: Pick<ComponentDefinition["editor"], "resize" | "transform">,
|
|
100
|
+
): ComponentDefinition {
|
|
101
|
+
return {
|
|
102
|
+
key: `basic.${entry}`,
|
|
103
|
+
contract: contracts[entry],
|
|
104
|
+
description: entry,
|
|
105
|
+
defaults,
|
|
106
|
+
resources: {},
|
|
107
|
+
implementations: {
|
|
108
|
+
[target]: {
|
|
109
|
+
package: "@moloodev/layout-basic",
|
|
110
|
+
version: packageVersion[target],
|
|
111
|
+
entry,
|
|
112
|
+
abi: 1,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
editor: {
|
|
116
|
+
category: target === "react-dom" ? "화면" : "3D 장면",
|
|
117
|
+
layout: { [target]: layout },
|
|
118
|
+
...editing,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Download/import as an ordinary workspace asset. No virtual catalog IDs or automatic installation. */
|
|
124
|
+
export const basicCatalog: Content = {
|
|
125
|
+
version: 1,
|
|
126
|
+
requires: [],
|
|
127
|
+
entries: [
|
|
128
|
+
definition("Stack", "react-dom", { direction: "column", gap: 16 }, "flow"),
|
|
129
|
+
definition("Grid", "react-dom", { columns: 3, gap: 16 }, "grid"),
|
|
130
|
+
definition(
|
|
131
|
+
"Frame",
|
|
132
|
+
"react-dom",
|
|
133
|
+
{ width: 320, height: 240 },
|
|
134
|
+
"flow",
|
|
135
|
+
{ resize: { "react-dom": { width: "width", height: "height" } } },
|
|
136
|
+
),
|
|
137
|
+
definition("Panel", "react-dom", { gap: 16 }, "flow"),
|
|
138
|
+
definition("Text", "react-dom", { text: "새 텍스트", size: 24 }, "leaf"),
|
|
139
|
+
definition("Button", "react-dom", { text: "버튼" }, "leaf"),
|
|
140
|
+
definition("Viewport", "react-dom", { height: 320 }, "leaf"),
|
|
141
|
+
definition("Group", "react-three-fiber", {}, "spatial"),
|
|
142
|
+
definition(
|
|
143
|
+
"Transform",
|
|
144
|
+
"react-three-fiber",
|
|
145
|
+
{
|
|
146
|
+
positionX: 0,
|
|
147
|
+
positionY: 0,
|
|
148
|
+
positionZ: 0,
|
|
149
|
+
rotationX: 0,
|
|
150
|
+
rotationY: 0,
|
|
151
|
+
rotationZ: 0,
|
|
152
|
+
scaleX: 1,
|
|
153
|
+
scaleY: 1,
|
|
154
|
+
scaleZ: 1,
|
|
155
|
+
},
|
|
156
|
+
"spatial",
|
|
157
|
+
{
|
|
158
|
+
transform: {
|
|
159
|
+
"react-three-fiber": {
|
|
160
|
+
position: { x: "positionX", y: "positionY", z: "positionZ" },
|
|
161
|
+
rotation: { x: "rotationX", y: "rotationY", z: "rotationZ" },
|
|
162
|
+
scale: { x: "scaleX", y: "scaleY", z: "scaleZ" },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
definition(
|
|
168
|
+
"Box",
|
|
169
|
+
"react-three-fiber",
|
|
170
|
+
{ color: "#76a9fa", size: 1 },
|
|
171
|
+
"leaf",
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function Stack({ element, children, rootRef }: RuntimeComponentProps) {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
ref={rootRef}
|
|
180
|
+
style={{
|
|
181
|
+
display: "flex",
|
|
182
|
+
flexDirection: element.props.direction as "row" | "column",
|
|
183
|
+
gap: Number(element.props.gap),
|
|
184
|
+
padding: 16,
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{children}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
function Text({ element, rootRef }: RuntimeComponentProps) {
|
|
192
|
+
return (
|
|
193
|
+
<span ref={rootRef} style={{ fontSize: Number(element.props.size) }}>
|
|
194
|
+
{String(element.props.text)}
|
|
195
|
+
</span>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function Grid({ element, children, rootRef }: RuntimeComponentProps) {
|
|
199
|
+
return (
|
|
200
|
+
<div
|
|
201
|
+
ref={rootRef}
|
|
202
|
+
style={{
|
|
203
|
+
display: "grid",
|
|
204
|
+
gridTemplateColumns: `repeat(${Number(element.props.columns)}, minmax(0, 1fr))`,
|
|
205
|
+
gridAutoFlow: "row",
|
|
206
|
+
gap: Number(element.props.gap),
|
|
207
|
+
padding: 16,
|
|
208
|
+
minHeight: 80,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function Frame({ element, children, rootRef }: RuntimeComponentProps) {
|
|
216
|
+
return (
|
|
217
|
+
<div
|
|
218
|
+
ref={rootRef}
|
|
219
|
+
style={{
|
|
220
|
+
display: "flex",
|
|
221
|
+
flexDirection: "column",
|
|
222
|
+
gap: 16,
|
|
223
|
+
padding: 16,
|
|
224
|
+
boxSizing: "border-box",
|
|
225
|
+
flexShrink: 0,
|
|
226
|
+
width: Number(element.props.width),
|
|
227
|
+
height: Number(element.props.height),
|
|
228
|
+
overflow: "auto",
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function Panel({
|
|
236
|
+
element,
|
|
237
|
+
children,
|
|
238
|
+
slots,
|
|
239
|
+
rootRef,
|
|
240
|
+
slotRefs,
|
|
241
|
+
}: RuntimeComponentProps) {
|
|
242
|
+
const gap = Number(element.props.gap);
|
|
243
|
+
const style: CSSProperties = {
|
|
244
|
+
display: "flex",
|
|
245
|
+
flexDirection: "column",
|
|
246
|
+
gap,
|
|
247
|
+
minHeight: 48,
|
|
248
|
+
minWidth: 80,
|
|
249
|
+
};
|
|
250
|
+
return (
|
|
251
|
+
<section
|
|
252
|
+
ref={rootRef}
|
|
253
|
+
style={{ display: "flex", flexDirection: "column", gap, padding: 16 }}
|
|
254
|
+
>
|
|
255
|
+
<header ref={slotRefs?.header} style={style}>
|
|
256
|
+
{slots?.header}
|
|
257
|
+
</header>
|
|
258
|
+
<div ref={slotRefs?.default} style={style}>
|
|
259
|
+
{children}
|
|
260
|
+
</div>
|
|
261
|
+
<footer ref={slotRefs?.footer} style={style}>
|
|
262
|
+
{slots?.footer}
|
|
263
|
+
</footer>
|
|
264
|
+
</section>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
function Button({ element, emit, rootRef }: RuntimeComponentProps) {
|
|
268
|
+
return (
|
|
269
|
+
<button
|
|
270
|
+
ref={rootRef}
|
|
271
|
+
type="button"
|
|
272
|
+
style={{
|
|
273
|
+
border: "1px solid currentColor",
|
|
274
|
+
padding: "8px 16px",
|
|
275
|
+
borderRadius: 6,
|
|
276
|
+
}}
|
|
277
|
+
onClick={() => emit("press")}
|
|
278
|
+
>
|
|
279
|
+
{String(element.props.text)}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function Viewport({ element, surfaces, rootRef }: RuntimeComponentProps) {
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
ref={rootRef}
|
|
287
|
+
style={{ height: Number(element.props.height), minWidth: 200 }}
|
|
288
|
+
>
|
|
289
|
+
{surfaces?.scene ?? <p>3D surface를 연결하세요.</p>}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
function Group({ children, rootRef }: RuntimeComponentProps) {
|
|
294
|
+
return <group ref={rootRef}>{children}</group>;
|
|
295
|
+
}
|
|
296
|
+
function Transform({ element, children, rootRef }: RuntimeComponentProps) {
|
|
297
|
+
const value = element.props;
|
|
298
|
+
return (
|
|
299
|
+
<group
|
|
300
|
+
ref={rootRef}
|
|
301
|
+
position={[
|
|
302
|
+
Number(value.positionX),
|
|
303
|
+
Number(value.positionY),
|
|
304
|
+
Number(value.positionZ),
|
|
305
|
+
]}
|
|
306
|
+
rotation={[
|
|
307
|
+
Number(value.rotationX),
|
|
308
|
+
Number(value.rotationY),
|
|
309
|
+
Number(value.rotationZ),
|
|
310
|
+
"XYZ",
|
|
311
|
+
]}
|
|
312
|
+
scale={[Number(value.scaleX), Number(value.scaleY), Number(value.scaleZ)]}
|
|
313
|
+
>
|
|
314
|
+
{children}
|
|
315
|
+
</group>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
function Box({ element, emit, rootRef }: RuntimeComponentProps) {
|
|
319
|
+
return (
|
|
320
|
+
<mesh
|
|
321
|
+
ref={rootRef}
|
|
322
|
+
onClick={(event) => {
|
|
323
|
+
event.stopPropagation();
|
|
324
|
+
emit("press");
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<boxGeometry
|
|
328
|
+
args={[
|
|
329
|
+
Number(element.props.size),
|
|
330
|
+
Number(element.props.size),
|
|
331
|
+
Number(element.props.size),
|
|
332
|
+
]}
|
|
333
|
+
/>
|
|
334
|
+
<meshBasicMaterial color={String(element.props.color)} />
|
|
335
|
+
</mesh>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export const basicPackages: ImplementationPackage[] = [
|
|
340
|
+
{
|
|
341
|
+
name: "@moloodev/layout-basic",
|
|
342
|
+
version: packageVersion["react-dom"],
|
|
343
|
+
abi: 1,
|
|
344
|
+
target: "react-dom",
|
|
345
|
+
entries: {
|
|
346
|
+
Stack: { contract: contracts.Stack, component: Stack },
|
|
347
|
+
Grid: { contract: contracts.Grid, component: Grid },
|
|
348
|
+
Frame: { contract: contracts.Frame, component: Frame },
|
|
349
|
+
Panel: { contract: contracts.Panel, component: Panel },
|
|
350
|
+
Text: { contract: contracts.Text, component: Text },
|
|
351
|
+
Button: { contract: contracts.Button, component: Button },
|
|
352
|
+
Viewport: { contract: contracts.Viewport, component: Viewport },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "@moloodev/layout-basic",
|
|
357
|
+
version: packageVersion["react-three-fiber"],
|
|
358
|
+
abi: 1,
|
|
359
|
+
target: "react-three-fiber",
|
|
360
|
+
entries: {
|
|
361
|
+
Group: { contract: contracts.Group, component: Group },
|
|
362
|
+
Transform: { contract: contracts.Transform, component: Transform },
|
|
363
|
+
Box: { contract: contracts.Box, component: Box },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
];
|