ShopNex
A ready-to-use, self-hostable commerce platform: admin, hosted checkout, Storefront API, webhooks and an app marketplace. No developers required to run a shop.
Interactive seat maps for the web: a floorplan editor, an accessible reservation viewer and live seat updates.
CommerceTypeScript · HTMLGitHub @mataaiOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Open source seat reservation library for your applications
Taken from the repository README
Loading the full README.
Read the full README ↗An open-source, framework-agnostic JavaScript/TypeScript suite for interactive floorplan management and seat reservations:
@open-seats/editor): A feature-rich visual designer for authoring and shaping buildings, non-seating zones, tables/rows, and seats with live layout invariant enforcement, vertex manipulation, grid snapping, and undo/redo history.@open-seats/viewer): A high-performance canvas rendering engine for seat reservations with instant selection handling, custom status registry, async debounced tooltips, touch pinch-to-zoom/pan, and full screen-reader ARIA live announcements and 2D spatial keyboard navigation.@open-seats/angular): First-class standalone components (<seats-viewer> and <seats-editor>) for modern Angular applications.📖 Documentation: guides and a type-checked example for every feature live in docs/. Run them locally with npm run docs:dev.
The workspace is organized into modular packages using Nx:
libs/
core/ @open-seats/core Schema (Zod), 2D geometry, layout invariants, spatial index, typed event emitter
renderer/ @open-seats/renderer Konva scene builder, hit-testing, zoom/pan (hidden behind Renderer interface)
viewer/ @open-seats/viewer Runtime seat selection, status registry, async tooltip provider, ARIA keyboard flow
editor/ @open-seats/editor Tools (building, zone, row, seat), undo/redo command stack, vertex editing, preview mode
angular/ @open-seats/angular Angular standalone components: <seats-viewer> and <seats-editor>
apps/
demo-vanilla/ Interactive Vanilla TS demonstration (theme switcher, live event feed, reservations)
demo-angular/ Interactive Angular application (ng-demo) demonstrating <seats-viewer> and <seats-editor>
Install only the packages required by your application:
npm install @open-seats/viewer @open-seats/editor
npm install @open-seats/angular @open-seats/viewer @open-seats/editor
(Note: @open-seats/core and @open-seats/renderer are installed automatically as dependencies).
| Term | Meaning |
|---|---|
| Floorplan | Root document JSON containing dimensions, background image configuration, and buildings. |
| Building | A free-form polygon container for zones and rows. Moving a building translates its children. |
| Zone | A non-seating free-form polygon inside a building (stage, bar, dance floor, aisle, sound booth). |
| Row | A table or seating line with seats on one or both opposing sides (tableWidth: 0 for bare seat lines). |
| Seat | An individual seat belonging to a row with a stable ID, label, and coordinates. |
import { createViewer } from '@open-seats/viewer';
import auditoriumPlan from './samples/grand-auditorium.json';
const container = document.getElementById('seat-map')!;
const viewer = createViewer(container, {
floorplan: auditoriumPlan,
statuses: {
's-a-1a': 'reserved',
's-a-1b': 'locked',
},
maxSelectable: 4,
tooltipProvider: async (ctx) => ({
title: `Seat ${ctx.seat.label}`,
subtitle: `Row ${ctx.row.label} • ${ctx.building.label}`,
content: 'Standard Admission - $45.00',
}),
});
// Listen to selection changes
viewer.on('selectionChanged', ({ selectedSeatIds, selectedSeats }) => {
console.log('Selected seat IDs:', selectedSeatIds);
});
// Listen to individual seat selection
viewer.on('seatSelected', (ctx) => {
console.log('Seat selected:', ctx.seat.id);
});
import { createEditor } from '@open-seats/editor';
const container = document.getElementById('editor-map')!;
const editor = createEditor(container, {
gridSnapping: { enabled: true, size: 20 },
});
// Create a building polygon
const building = editor.createBuilding(
[
{ x: 0, y: 0 },
{ x: 800, y: 0 },
{ x: 800, y: 600 },
{ x: 0, y: 600 },
],
'Main Auditorium'
);
// Add a row of seats to the building
const row = editor.createRow(building.id, { x: 100, y: 150 }, 8, {
side: 'both',
seatSpacing: 42,
seatRadius: 14,
tableWidth: 28,
});
// Toggle preview mode (switches into interactive reservation viewer)
editor.togglePreview();
// Export valid floorplan JSON
const floorplanJson = editor.getFloorplan();
Import the standalone components in your Angular module or component:
import { Component } from '@angular/core';
import { SeatsViewerComponent, SeatsEditorComponent } from '@open-seats/angular';
import { SAMPLE_FLOORPLAN, type Floorplan } from '@open-seats/core';
@Component({
selector: 'app-booking',
standalone: true,
imports: [SeatsViewerComponent, SeatsEditorComponent],
template: ` <seats-viewer [floorplan]="floorplan" [statuses]="seatStatuses" [maxSelectable]="4" (selectionChanged)="onSelectionChanged($event)" (seatSelected)="onSeatSelected($event)" /> `,
styles: [
`
:host {
display: block;
width: 100vw;
height: 100vh;
}
`,
],
})
export class BookingComponent {
floorplan: Floorplan = SAMPLE_FLOORPLAN;
seatStatuses: Record<string, string> = {
's-a-1a': 'reserved',
's-a-1b': 'locked',
};
onSelectionChanged(event: { selectedSeatIds: string[] }) {
console.log('Current selection:', event.selectedSeatIds);
}
onSeatSelected(ctx: any) {
console.log('Selected:', ctx.seat.label);
}
}
select, pan, building (draw polygon), zone (draw non-seating area), row (place table/row), add-seat.both), customizable seat spacing, seat radius, table width, label numbering.Ctrl+Z) and Redo (Ctrl+Shift+Z / Ctrl+Y) tracking all mutations.maxSelectable.available, reserved, locked, vip) with custom colors and selectable flags.status and its runtime data, and is called again when either changes.setSeatData / seatData (prices, tickets, holder details), and receive shared data (such as live prices) from the server with live updates. Both live only in the viewer and are never written into the floorplan; getSeatData merges them, with the app's own fields taking precedence.A viewer shows one floorplan and receives status updates for the whole floorplan over Socket.IO. The app creates the socket (URL, auth, path), so the library has no Socket.IO dependency and never handles credentials:
import { io } from 'socket.io-client'; // v4
import { createViewer, socketIoStatusSource } from '@open-seats/viewer';
const socket = io('https://api.example.com', {
auth: { floorplanId: 'grand-hall', token },
});
const viewer = createViewer(container, {
floorplan,
statusSource: socketIoStatusSource(socket),
});
viewer.on('connectionStateChanged', ({ state, error }) => {
// 'connecting' | 'live' | 'offline', or null once the source is removed
});
viewer.on('selectionInvalidated', ({ seats }) => {
// These seats were just taken and removed from the selection.
});
syncTimeoutMs, 10 s by default), the state goes to offline with the error, and patches are ignored until the next reconnect syncs successfully: socketIoStatusSource(socket, { syncTimeoutMs: 5000 }).statusSource passed to createViewer is subscribed right after the constructor returns, so listeners you attach straight after creation receive its first events (such as connecting).connectionStateChanged fires when the state changes, when the error changes while the state stays the same (e.g. a new connect_error), and with state: null when setStatusSource(null) removes the source.selectable are deselected automatically and announced to screen readers. This also applies to setStatus / setStatuses.{ price: 120 }) with snapshots and patches; read it with getSeatData(seatId) or in the tooltip provider's data. Everyone viewing the floorplan receives it, so per-user data such as a ticket should be set locally with setSeatData, which live updates never overwrite.StatusSource (subscribe(sink) => unsubscribe), or call applyStatusSnapshot / applyStatusPatch directly.Reference servers for Node.js and Python, and the wire protocol, are in servers/.
The viewer strictly implements ARIA accessibility standards:
role="application", aria-label="Interactive Floorplan Seat Selection", and tabindex="0".aria-live="polite" region automatically announces seat focus, row details, status, and selection state to screen readers.ArrowUp, ArrowDown, ArrowLeft, or ArrowRight to jump focus to the spatially nearest seat in that direction.+ / -: Zoom in and zoom out.0: Reset zoom level.| Shortcut | Action |
|---|---|
Ctrl+Z / Cmd+Z | Undo |
Ctrl+Shift+Z / Ctrl+Y | Redo |
Delete / Backspace | Delete selected building, zone, row, or seat |
Escape | Clear current selection |
Strict layout invariants are enforced in the editor via canPlace() and validated via validateLayout():
seatSpacing >= 2 * seatRadius).Touching edges within configured tolerance (default 0.01) are permitted.
ViewerOptions| Option | Type | Description |
|---|---|---|
floorplan | Floorplan | The floorplan JSON object to render. |
statuses | Record<string, string> | Map of seat IDs to status names (e.g. 'reserved', 'booked'). |
statusDefinitions | SeatStatusDefinition[] | Custom status configurations (colors, selectability). |
maxSelectable | number | Maximum number of seats selectable simultaneously. |
selectedSeatIds | string[] | Initial pre-selected seat IDs. |
tooltipProvider | TooltipProvider | Async function returning tooltip title, subtitle, or content. |
tooltipRenderer | TooltipRenderer | Renders custom tooltip content into an unstyled host element: (ctx, host) => cleanup. Takes precedence over tooltipProvider. |
tooltipDebounceMs | number | Debounce delay in ms before showing tooltip (default: 150). |
enableKeyboardNavigation | boolean | Enable spatial 2D arrow navigation and accessibility (default: true). |
statusSource | StatusSource | Live status updates, e.g. socketIoStatusSource(socket). Subscribed right after creation. |
seatData | Record<string, SeatData> |
ViewerEventsready: Viewer initialized and ready.seatHovered / seatHoverEnded: Triggered when pointer moves over/off a seat.seatSelected / seatDeselected: Triggered when an individual seat is clicked or selected via keyboard.selectionChanged: Emitted whenever the selection changes, providing selectedSeatIds and selectedSeats.floorplanLoaded: Emitted when a new floorplan JSON is loaded.violationsUpdated: Emitted when layout violations are detected.statusesChanged: Emitted with { changes, replaced } whenever seat statuses change.seatDataChanged: Emitted with { changes, replaced, source } when seat data changes, from setSeatData (source: 'app') or a live update (source: 'live').selectionInvalidated: Emitted with { seatIds, seats, reason } when selected seats became unavailable and were deselected.connectionStateChanged: Emitted with { state, error? } when the live status source connects, reconnects or goes offline, when its error changes, and with state: null when the source is removed.EditorOptions| Option | Type | Description |
|---|---|---|
floorplan | Floorplan | Initial floorplan data to load. |
rules | { enforce: boolean; tolerance: number } | Layout invariant enforcement settings. |
gridSnapping | { enabled: boolean; size: number } | Grid snapping toggle and grid spacing in pixels. |
EditorTools'select': Select, move, and edit elements or vertices.'pan': Pan the canvas without selecting elements.'building': Click to add vertices and create building polygons.'zone': Click to draw non-seating zones inside buildings.'row': Click to place seating rows/tables.'add-seat': Append seats to existing rows.EditorEventschange: Emitted with the updated Floorplan after any modification.toolChange: Emitted when active tool changes.selectionChange: Emitted with an array of selected element IDs.previewToggle: Emitted when preview mode is entered or exited.violationsUpdated: Emitted with current layout violations.<seats-viewer>[floorplan]: Floorplan[statuses]: Record<string, string>[maxSelectable]: number[options]: Partial<ViewerOptions>[statusSource]: StatusSource | null. Takes precedence over options.statusSource. Once this input has been given a value (including null to disconnect), options.statusSource is never used, so a custom source is never swapped for another one. Changes to options.statusSource apply only while this input is unused.[seatData]: Record<string, SeatData>. Replaces the previous seat data on change; never saved in the floorplan.[tooltipComponent]: Type<unknown>. Component rendered as the seat tooltip (see below).(ready)(seatHovered)(seatHoverEnded)(seatSelected)(seatDeselected)(selectionChanged)(floorplanLoaded)(violationsUpdated)(statusesChanged)(seatDataChanged)(selectionInvalidated)(connectionStateChanged)statusesChanged, seatDeselected, selectionInvalidated, selectionChanged) are emitted together in one NgZone.run, so each update costs one change-detection pass.Put an <ng-template seatTooltip> inside <seats-viewer> to render any Angular template or component as the seat tooltip. The viewer handles the hover delay, positioning and showing/hiding, and re-renders the template when the hovered seat's status or data changes. The tooltip box is unstyled, so the template brings its own look.
<seats-viewer [floorplan]="plan" [statusSource]="source">
<ng-template seatTooltip let-seat="seat" let-status="status" let-data="data">
<my-seat-card [label]="seat.label" [status]="status" [price]="data?.price" />
</ng-template>
</seats-viewer>
Template variables: seat, row, building, status, data, and the whole TooltipContext as let-tooltip ($implicit). Alternatively pass a component class with [tooltipComponent]="MyTooltip"; it receives the context through a context = input.required<TooltipContext>() input. Both take precedence over options.tooltipProvider, and are read once, when the viewer is created. Outside Angular, the same hook is the tooltipRenderer(ctx, host) viewer option.
<seats-editor>[floorplan]: Floorplan[tool]: ToolType (default 'select')[options]: Partial<EditorOptions>(change): Emits updated Floorplan(toolChange): Emits active ToolType(selectionChange): Emits selected element IDs(previewToggle): Emits preview boolean flag(violationsUpdated): Emits layout violationsnpm start
# or
npx nx serve ng-demo
Access the application at http://localhost:4200.
npx nx serve demo-vanilla
Access the application at http://localhost:5173.
npm run docs:dev # VitePress dev server with hot reload
npm run docs:build # type-check the examples, then build to docs/.vitepress/dist
Every code example on the site is a real file under docs/snippets/ that npm run check type-checks and lints against the current library source, so examples can't silently go out of date.
npx nx run-many -t test
npx nx run-many -t build
npx nx run-many -t lint
Packages are published to npm from CI when a version tag is pushed. See RELEASING.md.
This project is licensed under the GNU General Public License v3.0 (GPLv3).
| Runtime data per seat (price, ticket, ...), passed to the tooltip provider. Never saved in the floorplan. |