Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 15 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,17 @@ jobs:
# Tag + GitHub Release whenever ANY publish happened. Tag scheme
# branches on what actually shipped so a one-package bump can't
# collide with a stale tag from a prior multi-package run:
# cli shipped → v$CLI_VERSION (CLI is the user-facing handle
# for `npx openhop@beta`, so its
# version anchors the combined
# release name)
# server only → server/v$SERVER_VERSION
# web only → web/v$WEB_VERSION
# cli shipped → v$CLI_VERSION (CLI is the user-facing
# handle for `npx
# openhop@beta`, so its
# version anchors the
# combined release)
# server (± web) → server/v$SERVER_VERSION (server is the lowest
# layer and a more
# meaningful anchor
# than web when both
# ship together)
# web only → web/v$WEB_VERSION
- name: Create tag + GitHub Release
# Gate on actual step outcomes, not the pre-check intent — a failed
# `npm publish` (network, auth, registry rejection) shouldn't still
Expand All @@ -138,10 +143,11 @@ jobs:
run: |
if [ "$CLI_PUBLISHED" = "true" ]; then
tag="v$CLI_VERSION"
elif [ "$WEB_PUBLISHED" = "true" ]; then
tag="web/v$WEB_VERSION"
else
elif [ "$SERVER_PUBLISHED" = "true" ]; then
# Covers server-only AND server+web combined runs.
tag="server/v$SERVER_VERSION"
else
tag="web/v$WEB_VERSION"
fi

# If THIS tag already exists (re-run of a workflow whose tag
Expand Down
32 changes: 28 additions & 4 deletions packages/web/src/AppFragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { FlowCanvas } from './components/FlowCanvas'
import { DataInspectionPanel, BookmarkTab, type DockSide } from './components/DataInspectionPanel'
import { FlowEditorModal } from './components/FlowEditorModal'
import { buildStarterYaml } from './lib/starter-yaml'
import { buildShareUrl, decodeFragment } from './lib/share-url'
import { buildShareUrl, decodeFragment, encodeFragment } from './lib/share-url'
import { EXAMPLE_FLOWS } from './lib/example-flows'
import type { FlowNode, FlowStep, FlowData, Flow } from './types'

interface FlowNavItem {
Expand Down Expand Up @@ -328,13 +329,36 @@ export default function AppFragment() {
)}
{!decodedFlow ? (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center max-w-md px-6">
<div className="text-center max-w-lg px-6">
<p className="font-pixel text-text/60 mb-2" style={{ fontSize: 14 }}>
{decodeError ? 'No flow loaded' : 'No flow shared'}
</p>
<p className="font-terminal text-text/40 text-sm mb-4">
Open a flow by visiting a share URL, or click "+ New flow" above to create one.
<p className="font-terminal text-text/40 text-sm mb-6">
Open a flow by visiting a share URL, click "+ New flow" above, or pick a
pre-built example:
</p>
<ul className="flex flex-col gap-2 text-left">
{EXAMPLE_FLOWS.map((ex) => (
<li key={ex.id}>
<button
onClick={() => {
// Setting hash to the encoded YAML triggers the
// existing decode → render path. The ?example=<id>
// is purely cosmetic so the URL bar reads
// recognizably; the example loads from the hash.
window.location.hash = encodeFragment(ex.yaml)
}}
className="w-full text-left px-3 py-2 border border-border hover:border-accent hover:text-accent transition-colors font-terminal text-sm"
style={{ background: '#0d2612', color: '#7fffaa' }}
>
<div className="font-pixel mb-1" style={{ fontSize: 11 }}>
{ex.title}
</div>
<div className="text-text/50 text-xs">{ex.description}</div>
</button>
</li>
))}
</ul>
</div>
</div>
) : displayFlow ? (
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/components/DataPixel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useViewport } from '@xyflow/react'
import type { FlowStep, FlowData } from '../types'
import { DataTooltip } from './DataTooltip'

const CARROT_SPRITE = '/sprites/carrot_pixels.svg'
// Use Vite's BASE_URL so the sprite resolves under the project base on
// the Pages deploy (`/openhop/sprites/...`) and at the root in dev (`/`).
const CARROT_SPRITE = `${import.meta.env.BASE_URL}sprites/carrot_pixels.svg`
const DEFAULT_PIXEL_COLOR = '#ff8a4a' // VARIANT_ACCENT[0] — sprite's original orange.

interface DataPixelProps {
Expand Down
29 changes: 18 additions & 11 deletions packages/web/src/components/nodes/node-sprites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@ export interface BuildingProps {
active?: boolean
}

// Vite's BASE_URL is `/` in dev and `/openhop/` on the GitHub Pages
// deploy. Without it, root-absolute sprite paths 404 on Pages because
// the page lives under /openhop/ but `/sprites/foo.svg` resolves to the
// site root, not the project base. BASE_URL always ends with `/`, so
// `${BASE}sprites/foo.svg` is the correct concatenation.
const BASE = import.meta.env.BASE_URL

// Sprite per node type. Types not in this map fall back to the service sprite
// at render time (see FlowNode.tsx).
export const NODE_TYPE_SPRITE: Record<string, string> = {
actor: '/sprites/user_node.svg',
endpoint: '/sprites/endpoint_node.svg',
auth: '/sprites/auth_node.svg',
database: '/sprites/database_node.svg',
external: '/sprites/external_node.svg',
cache: '/sprites/cache_node.svg',
queue: '/sprites/queue_node.svg',
service: '/sprites/service_node.svg',
docker: '/sprites/docker_node.svg',
k8s: '/sprites/k8s_node.svg',
scheduler: '/sprites/scheduler_node.svg',
actor: `${BASE}sprites/user_node.svg`,
endpoint: `${BASE}sprites/endpoint_node.svg`,
auth: `${BASE}sprites/auth_node.svg`,
database: `${BASE}sprites/database_node.svg`,
external: `${BASE}sprites/external_node.svg`,
cache: `${BASE}sprites/cache_node.svg`,
queue: `${BASE}sprites/queue_node.svg`,
service: `${BASE}sprites/service_node.svg`,
docker: `${BASE}sprites/docker_node.svg`,
k8s: `${BASE}sprites/k8s_node.svg`,
scheduler: `${BASE}sprites/scheduler_node.svg`,
}

export const SPRITE_SIZE = 108
Expand Down
7 changes: 7 additions & 0 deletions packages/web/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/** Global ambient declarations for OpenHop's web bundle. */

// Vite's `?raw` query — load a file as its raw text contents at build
// time. Used by example-flows.ts to embed YAML examples in the bundle.
declare module '*.yaml?raw' {
const content: string
export default content
}

declare global {
interface Window {
/** Test/debug hook: scales animation speed (default 1, e.g. 4 for 4× faster). */
Expand Down
40 changes: 40 additions & 0 deletions packages/web/src/lib/example-flows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Curated example flows shown on the empty state of the Pages deploy.
*
* Each entry is the same YAML that lives in `examples/*.yaml` at the
* repo root, imported as a raw string via Vite's `?raw` query so the
* source-of-truth stays in one place. Vite inlines these into the
* bundle — together they're ~6 KB which is comfortable to ship.
*/

import authFlow from '../../../../examples/auth-flow.yaml?raw'
import orderFlow from '../../../../examples/order-flow.yaml?raw'
import simpleCrud from '../../../../examples/simple-crud.yaml?raw'

export interface ExampleFlow {
id: string
title: string
description: string
yaml: string
}

export const EXAMPLE_FLOWS: ExampleFlow[] = [
{
id: 'simple-crud',
title: 'Simple CRUD',
description: 'Basic REST API CRUD — the smallest useful flow.',
yaml: simpleCrud,
},
{
id: 'auth-flow',
title: 'OAuth2 Login',
description: 'Browser → app → Google OAuth → DB + cache.',
yaml: authFlow,
},
{
id: 'order-flow',
title: 'Order Processing',
description: 'Multi-service order pipeline with payment, audit, and retry.',
yaml: orderFlow,
},
]
Loading