Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
59e564e
simple vite/ts slider to serve as test for iframe based external live…
disconcision Aug 28, 2025
a772285
basic embedding working
disconcision Aug 28, 2025
06dc5f7
partial message bridge for exolivelits
disconcision Aug 28, 2025
682cb57
insane schedule_action wiring job
disconcision Aug 28, 2025
079a269
exoslider style
disconcision Aug 28, 2025
4e509cb
debug print cleanup
disconcision Aug 28, 2025
640179d
unthread unthread unthreadgit statusgit status
disconcision Aug 29, 2025
98ee0ac
fix exoslider flashing/focus loss
disconcision Aug 29, 2025
e2f8669
exoproj interface cleanup. make a font_metrics ref rather than thread…
disconcision Aug 29, 2025
324ee92
cleanup
disconcision Aug 29, 2025
0d84352
cleanup
disconcision Aug 29, 2025
6449e53
functorize exoprojectors
disconcision Aug 29, 2025
d884c1e
hazel-protocol cleanup
disconcision Aug 29, 2025
c85d222
json conversion and testing
disconcision Aug 29, 2025
9897e85
labelled tuples conversion
disconcision Aug 29, 2025
3a2cfd2
broader json <-> hazel value conversion, including tuples and adts
disconcision Aug 29, 2025
6aec382
value builder stub project
disconcision Aug 29, 2025
5aca856
value builder updates
disconcision Aug 29, 2025
0a91cc7
valuebuilder style tweaks
disconcision Aug 30, 2025
b315458
builder tweaks
disconcision Aug 30, 2025
e645874
ci attempt for exoapps
disconcision Aug 30, 2025
d970d8b
fix builder linter errors
disconcision Aug 30, 2025
9b9f13a
implement resizing and size constraints for exolivelits
disconcision Aug 30, 2025
3fce8b6
cleanup
disconcision Aug 30, 2025
8c2aee7
exolivelit docs. path fix for web build
disconcision Aug 30, 2025
edb5982
path fix fix
disconcision Aug 30, 2025
d2fe2e3
path fix fix fix
disconcision Aug 30, 2025
e8d014a
fix^4
disconcision Aug 30, 2025
41e725f
cleanup
disconcision Aug 30, 2025
df04574
docs
disconcision Aug 30, 2025
7ec48d9
fixed value builder path bug. fixed bug with empty tuples causing bui…
disconcision Aug 30, 2025
5085463
fix labelled tuple focus/reordering bug
disconcision Aug 30, 2025
1420eb3
fix value builder sizing issues
disconcision Aug 30, 2025
b64db66
cleanup
disconcision Aug 31, 2025
d5485ad
nool integration
disconcision Aug 31, 2025
a270c46
cleanup
disconcision Sep 1, 2025
1feb205
doc slide for exoapps
disconcision Sep 2, 2025
8f0cec1
disable reparse slides test for now
disconcision Sep 2, 2025
5c7fd0d
add Petrinaut as livelit
CiaranMn Sep 19, 2025
b84ef42
catcolab stub
disconcision Sep 25, 2025
d8403c0
hook up dev url
disconcision Sep 25, 2025
38fcfea
Add Petrinaut livelit (#1964)
disconcision Oct 2, 2025
1fe7269
dev merge
disconcision Oct 2, 2025
b798659
slide for petrinaut
disconcision Oct 2, 2025
1ed140f
tweak
disconcision Oct 2, 2025
291537e
..
disconcision Oct 3, 2025
21020f9
Merge branch 'dev' into exolivelits
disconcision Oct 17, 2025
a1eae64
Merge branch 'dev' into exolivelits
disconcision Oct 27, 2025
837bb56
slightly broken merge
disconcision Nov 4, 2025
372a0c7
graph projector and dev merge
disconcision Nov 4, 2025
bba6eec
Merge branch 'exolivelits' of github.com:hazelgrove/hazel into exoliv…
disconcision Nov 4, 2025
b633a81
graph proj now tolerant of holes
disconcision Nov 4, 2025
977133f
observable plot vibelit
disconcision Nov 5, 2025
c5327ad
observable plot vibelit
disconcision Nov 5, 2025
ba04a86
drag plot to resize
disconcision Nov 5, 2025
4f5a3b0
refactor graphdata to observable plot
disconcision Nov 5, 2025
6e94d09
rename observable plot vibelit, separate from graphproj
disconcision Nov 6, 2025
efe9ec6
sanitize labels that coincide with hazel keyword in JSON serialization
disconcision Nov 8, 2025
58b8389
merge in refractor probes
disconcision Nov 10, 2025
2cd1b94
fix cut crash issue
disconcision Nov 10, 2025
6434db3
breadth strategy for abbreviating tuples
disconcision Nov 10, 2025
dd2b811
probe: better colors in single mode. better abbreviation behavior for…
disconcision Nov 10, 2025
9fa241a
probe style
disconcision Nov 10, 2025
8787804
probe style cleanup. abbreviation behavior
disconcision Nov 10, 2025
b57401f
probe many colors
disconcision Nov 13, 2025
4668729
probe many colors 2
disconcision Nov 13, 2025
f396037
hack parameter to exp_to_seg to insert linebreaks in tuples/lists
disconcision Nov 15, 2025
409ff42
accidentally a
disconcision Nov 15, 2025
385c767
temp scrolling fix; scrolling on selection disabled
disconcision Dec 1, 2025
2474cae
inline value rendering probe proj hack for demo
disconcision Dec 3, 2025
bdee6b4
Merge branch 'probemoar' into exolivelits
disconcision Dec 15, 2025
620738d
probemoar merge
disconcision Jan 12, 2026
50920aa
dev merge
disconcision Jan 17, 2026
881ccce
Fix tests: lazy-init DOM event listeners in ObservablePlotProj
disconcision Jan 17, 2026
6b63c35
fix doc slides
disconcision Jan 17, 2026
5e6442d
Auto-generate context menu entries for all projectors
disconcision Jan 17, 2026
53aa970
Merge dev: rename Inline.t variants for clarity
disconcision Jan 29, 2026
509c4d4
migrate new slides
disconcision Jan 29, 2026
b1faba3
Merge branch 'dev' into exolivelits
disconcision Feb 2, 2026
0b0faa7
Merge branch 'dev' into exolivelits
disconcision Feb 3, 2026
8857267
Fix type error after merge: use Inline.t instead of bool
disconcision Feb 3, 2026
c882974
Merge branch 'dev' of github.com:hazelgrove/hazel into exolivelits
disconcision Feb 4, 2026
62fb98c
rm plans
disconcision Feb 4, 2026
b26f4ef
merge fix
disconcision Feb 5, 2026
a6d03fa
merge fix
disconcision Feb 5, 2026
9ab4af7
Merge branch 'dev' into exolivelits
disconcision Feb 9, 2026
a41e1fa
Merge branch 'dev' into exolivelits
disconcision Feb 13, 2026
bf29646
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Feb 27, 2026
22b018a
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Feb 27, 2026
028321f
fix: adapt ModuleExp inline check to Inline.t enum type
disconcision Feb 27, 2026
311f9fd
Merge remote-tracking branch 'origin/dev' into HEAD
disconcision Feb 28, 2026
84ce4cc
Merge branch 'dev' into exolivelits
disconcision Mar 10, 2026
ee117a5
Merge branch 'dev' into exolivelits
disconcision Mar 14, 2026
55cdb29
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Mar 19, 2026
a4dfc21
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Mar 31, 2026
540dc8a
Merge remote-tracking branch 'origin/dev' into HEAD
disconcision Mar 31, 2026
16b051b
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Apr 1, 2026
2d939dd
chore: update package-lock.json after npm install
disconcision Apr 1, 2026
840513c
Merge branch 'dev' into exolivelits
disconcision Apr 8, 2026
e2c52a1
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Apr 11, 2026
ef93655
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Apr 27, 2026
48d234e
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision May 13, 2026
a523565
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision May 20, 2026
e168122
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision May 27, 2026
19824c8
style: auto-format after merge
disconcision May 27, 2026
20e1a9c
Merge remote-tracking branch 'origin/dev' into exolivelits
disconcision Jul 31, 2026
c0ce666
fix: adapt dev-side code to branch API after merge
disconcision Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/deploy_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ jobs:
eval $(opam env)
export OPAMYES=1
opam clean --all-switches --download-cache --logs --repo-cache --unused-repositories
- name: Build in-repo external apps (simple-slider)
run: |
npm ci
npm run build -- --base=./
working-directory: ./source/external-apps/simple-slider
- name: Build in-repo external apps (value-builder)
run: |
npm ci
npm run build -- --base=./
working-directory: ./source/external-apps/value-builder
- name: Setup zarith native BigInt runtime
run: opam exec -- make setup-zarith
working-directory: ./source
Expand All @@ -68,6 +78,12 @@ jobs:
export DUNE_CACHE=enabled
opam exec -- dune build @src/fmt --auto-promote src --profile release
working-directory: ./source
- name: Stage external apps into Hazel www
run: |
mkdir -p "./source/_build/default/src/web/www/external/exoslider"
mkdir -p "./source/_build/default/src/web/www/external/exovaluebuilder"
cp -r "./source/external-apps/simple-slider/dist/"* "./source/_build/default/src/web/www/external/exoslider/"
cp -r "./source/external-apps/value-builder/dist/"* "./source/_build/default/src/web/www/external/exovaluebuilder/"
- name: Checkout the website build artifacts repo
uses: actions/checkout@v4
with:
Expand Down
168 changes: 168 additions & 0 deletions HAZEL_EXOLIVELIT_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Hazel Exo Livelit: External App Integration Guide

Use this guide to make your app embeddable in Hazel as an ExoLivelit. Your app runs in an iframe and communicates with Hazel using `postMessage`. This is a minimal proof of concept implementation; we will be implementing more message types to expose more Hazel functionality, as well as support for incremental syntax updates when it becomes necessary.

This guide has been claude hardened; a claude was able to oneshot adapt an unrelated ts app to an exolivelit and make the relevant hazelside changes by being pointed at this file in an alongside clone of the hazel repo, given additionally a description of how the relevant part of that app's data model should be represented in the JSON format hazel can consume. You can check out the value builder exolivelit to familiarize yourself with the data schema supported.

## Requirements

- **Iframe-compatible**: Your app must work inside an iframe
- **PostMessage communication**: Follow Hazel's protocol for bidirectional messaging
- **URL parameters**: Read `id` and `parentOrigin` from query string
- **Auto-resize**: Measure and report your content dimensions
- **Value serialization**: Accept initial state and send updated state as stringified JSON

## URL Contract

Hazel loads your app with: `https://your-app/?id=<uuid>&parentOrigin=<hazel-origin>`

- **`id`**: Unique identifier for this projector instance
- **`parentOrigin`**: Hazel's origin for `postMessage` target (security)

## Message Protocol

### App → Hazel (ToHazelMessage)

To start, it suffices to send `ready` and `setSyntax`, and to handle `init`. More of the internal livelits API will be exposed here in the future.

```typescript
type ToHazelMessage =
| { type: "ready"; id: string } // Sent on mount
| { type: "setSyntax"; id: string; codec: string; value: string } // Send edits
| { type: "resize"; id: string; width: number; height: number }; // Report size
```

### Hazel → App (FromHazelMessage)

```typescript
type FromHazelMessage =
| { type: "init"; id: string; value: string } // Initial value
| {
type: "constraints";
id: string;
maxWidth: number;
maxHeight: number;
minWidth?: number;
minHeight?: number;
}; // Size limits
```

## Communication Lifecycle

1. **App mounts** → sends `{type: 'ready', id}`
2. **Hazel responds** → `{type: 'init', value: "..."}` + `{type: 'constraints', ...}`
3. **App applies constraints** → sets `maxWidth` CSS, starts auto-resize
4. **User edits** → app sends `{type: 'setSyntax', codec: 'json', value: JSON.stringify(newValue)}`
5. **Content grows** → app sends `{type: 'resize', width, height}` (debounced)

## Integration Starter Library

Copy these **3 files** into your e.g. `src/hooks/` directory:

### 1. Core Hook (`hazel-integration-base.ts`)

### 2. Resize Strategies (`resize-strategies.ts`)

### 3. App-Specific Hook (`useHazelIntegration.ts`)

```
src/
├── components/
│ └── MyEditor.tsx # Core app logic (Hazel-agnostic)
├── hooks/
│ ├── hazel-integration-base.ts # Shared base hook
│ ├── resize-strategies.ts # Resize implementations
│ └── useHazelIntegration.ts # App-specific wrapper
└── App.tsx # Entry point, uses Hazel integration
```

## App Integration Example

```typescript
// App.tsx
import { useState } from "react";
import { useHazelIntegration } from "./useHazelIntegration";

export default function App() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id") || "local-demo";
const [value, setValue] = useState<any>(0);

const { setSyntax } = useHazelIntegration({
id,
codec: "json" /* The only codec supported for now */,
onInit: (valueStr) => {
setValue(JSON.parse(valueStr));
},
onConstraints: (c) => {
document.body.style.maxWidth = `${c.maxWidth}px`;
},
});

const handleChange = (newValue: any) => {
setSyntax(JSON.stringify(newValue));
};

return (
<div style={{}}>
<h3>My Hazel-Embedded App</h3>
{/* Your editor UI here */}
<button onClick={() => handleChange(value + 1)}>
Increment: {value}
</button>
</div>
);
}
```

## CSS Guidelines

**DO:**

- Use flexible, responsive layouts (`flex`, `grid`)
- Allow content-driven height (`min-height` instead of fixed `height`)
- Apply `maxWidth` from constraints for responsive behavior

**DON'T:**

- Set fixed `height: 100vh` on root containers
- Use viewport units that ignore iframe constraints
- Create horizontal scrolling (respect `maxWidth`)

## Hazel Integration (Hazel-side)

We don't currently support dynamic registration for new kinds of exolivelit, although there is no particular blocker to doing so. Right now to add a new one, you'll need to clone the hazel repo and change two definitions in the `Exo.re` file.

### 1. Create a static identifier for your exolivelit

Add YourApp to the `Exo.kind` type. This will determine the Hazel UI name of your exolivelit.

```ocaml
type kind =
| ...
| YourApp;
```

### 2. Specify static and default properties for your exolivelit

Add a corresponding case to the `Exo.module_of_kind` function. This requires a `prod` and option `dev` URL for your app. The `shape` property determines how the text flow resumes to the right of your livelit; pick `Block` if in doubt. The rest of the properties (`guard` and `size`) are work-in-progress and will likely be set dynamically via content negoiation in the future; copying the values below should suffice for a prototype.

```ocaml
| YourApp => {
kind,
prod: "https://yourdomain.com", (* Your public URL for the app *)
dev: "http://localhost:port", (* Your internal dev path, if applicable *)
shape: Block, (* Block: After livelit, text flow continues from bottom line. Tab: Continues from top *)
guard: _ => true, (* Determines what Hazel syntax your app can be applied to; okay to leave as this for now *)
size: {
width: 680, (* init width in px; *)
height: 490, (* init height in px *)
},
}
```

## Example Apps

- **`external-apps/simple-slider/`**: Integer slider
- **`external-apps/value-builder/`**: Compositional Hazel value editor
- **`https://github.com/disconcision/nool/pull/4`**: A PR updating a math toy to support livelit embedding
24 changes: 24 additions & 0 deletions external-apps/simple-slider/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
99 changes: 99 additions & 0 deletions external-apps/simple-slider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Simple Slider - External Hazel GUI

A standalone React TypeScript slider component designed to integrate with the Hazel editor as an external projector.

## Features

- **Integer Slider**: Interactive range input for integer values
- **Hazel Integration**: Implements postMessage protocol for seamless communication with Hazel
- **Multiple Variants**: Different slider configurations (basic, small range, large range)
- **Responsive Design**: Clean, modern UI that works well in iframes
- **Type Safety**: Full TypeScript support

## Quick Start

```bash
# Install dependencies
npm install

# Start development server
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview
```

## Development

The app will run at `http://localhost:5173` by default. You can view it standalone in your browser or embed it in an iframe for testing the Hazel integration.

### Hazel Integration Protocol

The slider implements a postMessage-based protocol for communication with Hazel:

#### Messages sent to Hazel (parent):

- `ready` - Component is loaded and ready
- `setSyntax` - User changed the slider value (includes codec and new value)
- `resize` - Component wants to change its size (future feature)
- `requestFocus` - Component wants focus (future feature)

#### Messages received from Hazel (parent):

- `init` - Initial value when projector is created
- `update` - Value changed externally (e.g., user edited the underlying syntax)

#### Codec

The slider uses the `int` codec, which converts between:

- **Hazel side**: `Atom(Int(Bigint.t))` (integer literal in the AST)
- **JSON**: String representation of the integer (e.g., `"42"`)

## Integration with Hazel

When integrated with Hazel, this component will:

1. **Replace integer literals** in the editor with an interactive slider
2. **Receive initial value** from the underlying Hazel syntax
3. **Send updates** back to Hazel when the user moves the slider
4. **Stay in sync** when the underlying syntax changes externally

## Project Structure

```
src/
├── components/
│ ├── IntegerSlider.tsx # Main slider component
│ └── IntegerSlider.css # Slider styles
├── hooks/
│ └── useHazelIntegration.ts # Hazel communication hook
├── types/
│ └── hazel-protocol.ts # TypeScript types for the protocol
├── App.tsx # Main app with demo
├── App.css # App-level styles
└── main.tsx # Entry point
```

## Building for Production

```bash
npm run build
```

The built files will be in the `dist/` directory. For Hazel integration, these files can be:

- Served from a static web server
- Copied into Hazel's `src/web/www/external/simple-slider/` directory
- Served via Hazel's Vite development server

## Next Steps

1. **Test standalone** - Verify the slider works in your browser
2. **Hazel iframe wrapper** - Create a projector that embeds this in an iframe
3. **Message bridge** - Add global postMessage handler in Hazel
4. **Codec implementation** - Add int literal ↔ JSON conversion in Hazel
5. **Integration testing** - Test the full round-trip communication
23 changes: 23 additions & 0 deletions external-apps/simple-slider/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
13 changes: 13 additions & 0 deletions external-apps/simple-slider/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading