Extension for the Virtual Table Top platform Owlbear Rodeo to be used in Shadowdark RPG adventures.
Enable the game mechanic for torches that burns for one hour of real time.
- Real time counter (default to one hour, can be changed)
- Timer can be paused and resumed at any time
- Timer can be changed at any time (e.g. account for time passing in game that should affect the torch timer)
- Visibility modes: only GM or everyone can see the timer
- Display modes: number or hourglass
- By default only GM can start, stop, resume, reset or change the torch timer but there is an option to allow players to manage the torch
- Look and feel inspired by Shadowdark art, format and tables from the core book
- React (currently 19.x) with TypeScript
- Vite for build tooling and development server
- @owlbear-rodeo/sdk for Owlbear Rodeo integration
- Material-UI (@mui/material) - Consistent with official examples
- @emotion/react & @emotion/styled for styling
- @fontsource/ fonts for typography
- Zustand - Lightweight, simple state management
- React hooks for local component state
- TypeScript for type safety
- ESLint + Prettier for code quality
- Vite with CORS configuration for development and Owlbear embedding
darktorch/
├── public/
│ ├── manifest.json # Owlbear Rodeo extension manifest
│ ├── icon.png # Extension icon (PNG)
│ └── icon.svg # Extension icon (SVG)
├── src/
│ ├── components/ # React components
│ │ ├── TimerDisplay.tsx
│ │ ├── TimerControls.tsx
│ │ ├── HourglassDisplay.tsx
│ │ ├── PermissionWrapper.tsx
│ │ ├── TimerErrorBoundary.tsx
│ │ └── TwoIconToggleGroup.tsx
│ ├── hooks/ # Custom React hooks
│ │ ├── useOwlbearSDK.ts
│ │ ├── useTimer.ts
│ │ ├── useTimerSync.ts
│ │ ├── useLeaderElection.ts
│ │ └── usePlayerRole.ts
│ ├── services/ # Owlbear + sync services
│ │ ├── contextMenu.ts
│ │ ├── timerSync.ts
│ │ ├── leaderElection.ts
│ │ └── statePersistence.ts
│ ├── store/ # Zustand store
│ │ └── timerStore.ts
│ ├── theme/ # MUI theme
│ │ └── shadowdarkTheme.ts
│ ├── types/ # TypeScript definitions
│ │ └── index.ts
│ ├── utils/ # Helper functions
│ │ └── timeUtils.ts
│ ├── test/ # Test helpers + mocks
│ ├── App.tsx # Main application component
│ ├── main.tsx # Application entry point
│ ├── index.css # Global styles
│ └── App.css # App styles
├── index.html # HTML template
├── package.json
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts
- Node.js 18+
- npm, yarn, or pnpm
-
Install dependencies:
npm install
-
Development server:
npm run dev
-
Vite config notes (CORS + host for Owlbear embedding):
// vite.config.ts import react from "@vitejs/plugin-react"; import path from "path"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], server: { host: true, port: 5174, cors: { origin: "*", methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], credentials: true, }, }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), "@/components": path.resolve(__dirname, "./src/components"), "@/hooks": path.resolve(__dirname, "./src/hooks"), "@/store": path.resolve(__dirname, "./src/store"), "@/types": path.resolve(__dirname, "./src/types"), "@/utils": path.resolve(__dirname, "./src/utils"), }, }, });
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build- Use Zustand for global timer state
- Store: countdown duration, isRunning, isVisible, displayMode
- Persist state in a shared store (and optionally across sessions if enabled via persistence services)
- Initialize via
OBR.onReady(...)and guard onOBR.isAvailablefor non-embedded/dev mode - Use
OBR.contextMenu.create(...)for timer-related context menu actions - Use
OBR.broadcast.sendMessage(event, payload)+OBR.broadcast.onMessage(event, handler)for real-time sync - Handle role-based permissions via player role checks (GM / leader election, depending on feature)
- Material-UI custom theme inspired by Shadowdark core book
- Dark color palette with parchment accents
- Gothic/mystical typography where appropriate
- Shows remaining time in MM:SS format
- Supports both numeric and hourglass display modes
- Visibility/display mode is controlled via store + permissions
- Start/Pause/Reset controls
- Time adjustment inputs (+/- minutes)
- Permission-based rendering (GM only vs players)
PermissionWrapper: centralizes permission-gated UITimerErrorBoundary: prevents UI crashes and can report/broadcast errorsHourglassDisplay: alternate visualization for the timerTwoIconToggleGroup: small UI primitive used by settings/controls
public/manifest.json:
{
"name": "Dark Torch",
"description": "Real-time torch timer for Shadowdark RPG",
"version": "1.0.0",
"manifest_version": 1,
"action": {
"title": "Dark Torch",
"icon": "/icon.png",
"popover": "/",
"width": 320,
"height": 440
}
}- Render - Free tier available, used in official docs
- Vercel - Excellent for React apps
- Netlify - Simple static site hosting
- GitHub Pages - Free for public repositories
- Run
npm run build - Deploy
distfolder to chosen platform - Ensure
manifest.jsonis accessible at root URL - Test installation via Owlbear Rodeo extension menu
This project follows the Conventional Commits v1.0.0 specification.
<type>[optional scope]: <description>
- The description must be lowercase and written in imperative mood (e.g. "add feature", not "added feature" or "Adding feature").
- Breaking changes are indicated by appending
!after the type/scope (e.g.feat!: remove legacy timer API).
| Type | Purpose |
|---|---|
feat |
A new feature |
fix |
A bug fix |
docs |
Documentation-only changes |
style |
Code style changes (formatting, no logic change) |
chore |
Maintenance tasks, dependency bumps, releases |
infra |
Infrastructure and CI/CD changes |
| Scope | Usage |
|---|---|
release |
Release-related commits (e.g. version bumps) |
feat: add hourglass display mode
fix: token contextual menu
docs: update tags to meet ORB extension requirements
chore(release): v1.1.1
infra: add github actions workflow
feat!: drop support for legacy timer format