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.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Open source seat reservation library for your applications
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 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. |