Skip to content

Commit 1387de4

Browse files
authored
add hazel livelit wrapper (#74)
1 parent 3211ed1 commit 1387de4

File tree

15 files changed

+3627
-0
lines changed

15 files changed

+3627
-0
lines changed

pocs/petrinaut-hazel/.eslintrc.cjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
root: true,
3+
env: { browser: true, es2020: true },
4+
extends: [
5+
"eslint:recommended",
6+
"plugin:@typescript-eslint/recommended",
7+
"plugin:react-hooks/recommended",
8+
],
9+
ignorePatterns: ["dist", ".eslintrc.cjs"],
10+
parser: "@typescript-eslint/parser",
11+
plugins: ["react-refresh"],
12+
rules: {
13+
"react-refresh/only-export-components": [
14+
"warn",
15+
{ allowConstantExport: true },
16+
],
17+
},
18+
};

pocs/petrinaut-hazel/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Petrinaut x Hazel
2+
3+
Experiment to create Petrinaut as a Hazel Livelit.
4+
5+
Following the instructions set out [here](https://github.com/hazelgrove/hazel/blob/exolivelits/HAZEL_EXOLIVELIT_GUIDE.md).

pocs/petrinaut-hazel/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/png" href="/hash.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Petrinaut Hazel Livelet</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
16+
</html>

pocs/petrinaut-hazel/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "petrinaut-hazel",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@emotion/react": "11.14.0",
14+
"@emotion/styled": "11.14.1",
15+
"@fortawesome/free-solid-svg-icons": "6.7.2",
16+
"@hashintel/block-design-system": "0.0.5",
17+
"@hashintel/design-system": "0.0.9-canary.2",
18+
"@hashintel/petrinaut": "0.0.5",
19+
"@mui/material": "5.18.0",
20+
"@mui/system": "5.18.0",
21+
"elkjs": "0.10.0",
22+
"immer": "10.1.3",
23+
"reactflow": "11.11.4",
24+
"react": "18.3.1",
25+
"react-dom": "18.3.1",
26+
"react-use": "17.6.0",
27+
"uuid": "11.1.0"
28+
},
29+
"devDependencies": {
30+
"@types/react": "18.3.12",
31+
"@types/react-dom": "18.3.1",
32+
"@vitejs/plugin-react": "4.3.3",
33+
"eslint": "8.45.0",
34+
"eslint-plugin-react-hooks": "4.6.0",
35+
"eslint-plugin-react-refresh": "0.4.3",
36+
"typescript": "5.7.3",
37+
"typescript-eslint": "7.3.1",
38+
"vite": "5"
39+
},
40+
"packageManager": "yarn@1.22.22+sha256.c17d3797fb9a9115bf375e31bfd30058cac6bc9c3b8807a3d8cb2094794b51ca"
41+
}
Lines changed: 3 additions & 0 deletions
Loading

pocs/petrinaut-hazel/src/index.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
html,
2+
body,
3+
#root {
4+
height: 100%;
5+
}

pocs/petrinaut-hazel/src/main.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React, { Suspense } from "react";
2+
import ReactDOM from "react-dom/client";
3+
import { CssBaseline, ThemeProvider } from "@mui/material";
4+
import { CacheProvider } from "@emotion/react";
5+
import { createEmotionCache, theme } from "@hashintel/design-system/theme";
6+
7+
import "./index.css";
8+
import { App } from "./main/app";
9+
10+
const emotionCache = createEmotionCache();
11+
12+
// biome-ignore lint/style/noNonNullAssertion: we know it exists
13+
ReactDOM.createRoot(document.getElementById("root")!).render(
14+
<React.StrictMode>
15+
<Suspense fallback={<div>Suspense fallback...</div>}>
16+
<CacheProvider value={emotionCache}>
17+
<ThemeProvider theme={theme}>
18+
<CssBaseline />
19+
<App />
20+
</ThemeProvider>
21+
</CacheProvider>
22+
</Suspense>
23+
</React.StrictMode>,
24+
);
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState } from "react";
2+
import {
3+
Petrinaut,
4+
defaultTokenTypes,
5+
type PetriNetDefinitionObject,
6+
} from "@hashintel/petrinaut";
7+
import { useHazelIntegration } from "./app/use-hazel-integration";
8+
import { produce } from "immer";
9+
10+
const createDefaultNetDefinition = (): PetriNetDefinitionObject => {
11+
return {
12+
nodes: [],
13+
arcs: [],
14+
tokenTypes: structuredClone(defaultTokenTypes),
15+
};
16+
};
17+
18+
/**
19+
* An incomplete type guard to check if a value is a valid Petri net definition.
20+
* Does not check the content of arrays.
21+
*/
22+
const isValidNetDefinition = (
23+
definition: unknown,
24+
): definition is PetriNetDefinitionObject => {
25+
if (typeof definition !== "object" || definition === null) {
26+
return false;
27+
}
28+
29+
if (!("nodes" in definition) || !Array.isArray(definition.nodes)) {
30+
return false;
31+
}
32+
33+
if (!("arcs" in definition) || !Array.isArray(definition.arcs)) {
34+
return false;
35+
}
36+
37+
if (!("tokenTypes" in definition) || !Array.isArray(definition.tokenTypes)) {
38+
return false;
39+
}
40+
41+
return true;
42+
};
43+
44+
/**
45+
* Wraps Petrinaut with the event handlers necessary for a Hazel Livelit.
46+
*/
47+
export const App = () => {
48+
const urlParams = new URLSearchParams(window.location.search);
49+
const id = urlParams.get("id") || "local-demo";
50+
51+
const [netDefinition, setNetDefinition] =
52+
useState<PetriNetDefinitionObject | null>(null);
53+
54+
const { setSyntax } = useHazelIntegration({
55+
id,
56+
codec: "json",
57+
onInit: (value) => {
58+
console.log("Received value", value);
59+
60+
try {
61+
const parsedValue = JSON.parse(value);
62+
63+
if (isValidNetDefinition(parsedValue)) {
64+
setNetDefinition(parsedValue);
65+
} else {
66+
console.error("Invalid net definition", parsedValue);
67+
const defaultNetDefinition = createDefaultNetDefinition();
68+
setNetDefinition(defaultNetDefinition);
69+
setSyntax(JSON.stringify(defaultNetDefinition));
70+
}
71+
} catch (error) {
72+
console.error("Error parsing net definition as JSON", error);
73+
}
74+
},
75+
});
76+
77+
if (!netDefinition) {
78+
return null;
79+
}
80+
81+
return (
82+
<Petrinaut
83+
key={id}
84+
hideNetManagementControls
85+
petriNetId={id}
86+
petriNetDefinition={netDefinition}
87+
existingNets={[]}
88+
mutatePetriNetDefinition={(definitionMutationFn) => {
89+
setNetDefinition((existingDefinition) => {
90+
const newDefinition = produce(
91+
existingDefinition,
92+
definitionMutationFn,
93+
);
94+
95+
setSyntax(JSON.stringify(newDefinition));
96+
97+
return newDefinition;
98+
});
99+
}}
100+
parentNet={null}
101+
createNewNet={() => {
102+
throw new Error(
103+
"Petrinaut should not be attemping to create new nets when wrapped by Patchwork",
104+
);
105+
}}
106+
loadPetriNet={() => {
107+
throw new Error(
108+
"Petrinaut should not be attemping to load other nets when wrapped by Patchwork",
109+
);
110+
}}
111+
setTitle={() => {
112+
throw new Error(
113+
"Petrinaut should not be attemping to set the net title when wrapped by Patchwork",
114+
);
115+
}}
116+
title={""}
117+
/>
118+
);
119+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
3+
export type MessageToHazel =
4+
| { type: "ready"; id: string }
5+
| { type: "setSyntax"; id: string; codec: string; value: string }
6+
| { type: "resize"; id: string; width: number; height: number };
7+
8+
export type MessageFromHazel =
9+
| { type: "init"; id: string; value: string }
10+
| {
11+
type: "constraints";
12+
id: string;
13+
maxWidth: number;
14+
maxHeight: number;
15+
minWidth?: number;
16+
minHeight?: number;
17+
};
18+
19+
export function isFromHazelMessage(data: unknown): data is MessageFromHazel {
20+
return (
21+
data !== null &&
22+
typeof data === "object" &&
23+
"type" in data &&
24+
"id" in data &&
25+
["init", "constraints"].includes(
26+
(data as Record<string, unknown>).type as string,
27+
) &&
28+
typeof (data as Record<string, unknown>).id === "string"
29+
);
30+
}
31+
32+
type HazelIntegrationConfig = {
33+
id: string;
34+
codec: string;
35+
onInit: (value: string) => void;
36+
};
37+
38+
const sendToHazel = (message: MessageToHazel, targetOrigin: string) => {
39+
if (window.parent && window.parent !== window) {
40+
console.log("Sending message to Hazel", message);
41+
window.parent.postMessage(message, targetOrigin);
42+
}
43+
};
44+
45+
/**
46+
* Core Hazel integration for SolidJS - handles protocol, messaging, and setup
47+
*/
48+
export const useHazelIntegration = (config: HazelIntegrationConfig) => {
49+
const { id, codec, onInit } = config;
50+
const [hasInit, setHasInit] = useState(false);
51+
52+
const targetOrigin =
53+
new URLSearchParams(window.location.search).get("parentOrigin") || "*";
54+
55+
const setSyntax = useCallback(
56+
(value: string) => {
57+
sendToHazel({ type: "setSyntax", id, codec, value }, targetOrigin);
58+
},
59+
[id, codec, targetOrigin],
60+
);
61+
62+
useEffect(() => {
63+
const handleMessage = (event: MessageEvent) => {
64+
const data = event.data;
65+
66+
if (!isFromHazelMessage(data) || data.id !== id) {
67+
return;
68+
}
69+
70+
console.log("Received message from Hazel", data);
71+
72+
switch (data.type) {
73+
case "init":
74+
if (onInit) {
75+
onInit(data.value);
76+
}
77+
break;
78+
}
79+
};
80+
81+
window.addEventListener("message", handleMessage);
82+
83+
// Send ready message when component mounts
84+
if (!hasInit) {
85+
sendToHazel({ type: "ready", id }, targetOrigin);
86+
setHasInit(true);
87+
}
88+
89+
return () => {
90+
window.removeEventListener("message", handleMessage);
91+
};
92+
});
93+
94+
return {
95+
setSyntax,
96+
};
97+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

0 commit comments

Comments
 (0)