Skip to content
Closed
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: 24 additions & 0 deletions docs/OPENWORLDS_DESIGN_ASSET_POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# OpenWorlds Design Asset Policy

The OpenWorlds design bundle is a local product-design reference for the native macOS app and dashboard polish. It may guide layout, interaction structure, and abstract visual tokens, but it is not a source of production game assets or canonical ClawDnD content.

## Allowed

- Reimplement abstract layout ideas such as a navigation rail, parchment panels, brass controls, campaign launcher, and CRPG-style status surfaces.
- Translate color, spacing, typography, and component intent into ClawDnD-owned code.
- Use generated or original ClawDnD assets in later PRs when their source/license is documented.
- Reference local scratch artifacts under `/Volumes/LEXAR/Codex/openworlds-design-2026-05-25/` in issue and PR bodies.

## Not Allowed

- Commit `OpenWorlds.zip`, extracted screenshots, pasted reference images, or binary mockup assets from the design bundle.
- Copy Owlcat, Baldur's Gate, D&D-private, or other proprietary UI art into the repo.
- Promote prototype/test copy from the bundle into canonical campaign, lore, world seed, item, NPC, or companion content.
- Let visual UI code write `play-state`, `qa/state`, engine snapshots, or provider state directly.

## PR Checklist

- State whether any assets were copied. The expected answer for OpenWorlds visual PRs is "no third-party/reference assets copied."
- Keep visual/design implementation scoped to `macos/`, `viewer/`, `docs/`, or build scripts unless the issue explicitly needs engine read models.
- Run `python3 scripts/license_check.py` for PRs that touch content, data, licensing docs, or bundled assets.
- Confirm `git status --short` does not include extracted design artifacts, runtime state, `.build/`, `dist/`, transcripts, or private content.
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ final class AppProcessService: ObservableObject {
endpoint.status = .stopped
viewerEndpoint = endpoint
}
activeCampaignID = nil
}

func startProviderSession(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import SwiftUI

struct OpenWorldsPanel<Content: View>: View {
let title: String?
let subtitle: String?
let icon: String?
let content: Content

init(
title: String? = nil,
subtitle: String? = nil,
icon: String? = nil,
@ViewBuilder content: () -> Content
) {
self.title = title
self.subtitle = subtitle
self.icon = icon
self.content = content()
}

var body: some View {
VStack(alignment: .leading, spacing: 14) {
if title != nil || subtitle != nil || icon != nil {
HStack(alignment: .firstTextBaseline, spacing: 10) {
if let icon {
Image(systemName: icon)
.foregroundStyle(OpenWorldsTheme.crimson)
}
VStack(alignment: .leading, spacing: 2) {
if let title {
Text(title)
.font(.title3.weight(.semibold))
.foregroundStyle(OpenWorldsTheme.ink800)
}
if let subtitle {
Text(subtitle)
.font(.caption)
.foregroundStyle(OpenWorldsTheme.ink600)
}
}
Spacer()
}
OpenWorldsDivider()
}
content
}
.padding(18)
.background(OpenWorldsParchmentBackground())
.clipShape(RoundedRectangle(cornerRadius: OpenWorldsTheme.panelRadius))
.overlay {
RoundedRectangle(cornerRadius: OpenWorldsTheme.panelRadius)
.stroke(OpenWorldsTheme.parchmentEdge.opacity(0.62), lineWidth: 1)
}
.overlay(alignment: .topLeading) {
OpenWorldsCornerOrnament()
.frame(width: 28, height: 28)
.padding(7)
}
.overlay(alignment: .topTrailing) {
OpenWorldsCornerOrnament()
.rotationEffect(.degrees(90))
.frame(width: 28, height: 28)
.padding(7)
}
.shadow(color: .black.opacity(0.24), radius: 16, x: 0, y: 8)
}
}


struct OpenWorldsDivider: View {
var body: some View {
HStack(spacing: 8) {
Rectangle()
.fill(OpenWorldsTheme.parchmentEdge.opacity(0.42))
.frame(height: 1)
Diamond()
.fill(OpenWorldsTheme.brass400)
.frame(width: 7, height: 7)
Rectangle()
.fill(OpenWorldsTheme.parchmentEdge.opacity(0.42))
.frame(height: 1)
}
}
}

struct Diamond: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
path.closeSubpath()
return path
}
}

struct OpenWorldsCornerOrnament: View {
var body: some View {
GeometryReader { proxy in
Path { path in
let w = proxy.size.width
let h = proxy.size.height
path.move(to: CGPoint(x: 2, y: h * 0.34))
path.addCurve(
to: CGPoint(x: w * 0.34, y: 2),
control1: CGPoint(x: 2, y: 8),
control2: CGPoint(x: 8, y: 2)
)
path.move(to: CGPoint(x: 2, y: h * 0.58))
path.addCurve(
to: CGPoint(x: w * 0.58, y: 2),
control1: CGPoint(x: 2, y: 10),
control2: CGPoint(x: 10, y: 2)
)
path.move(to: CGPoint(x: w * 0.22, y: h * 0.22))
path.addCurve(
to: CGPoint(x: w * 0.48, y: h * 0.12),
control1: CGPoint(x: w * 0.34, y: h * 0.24),
control2: CGPoint(x: w * 0.42, y: h * 0.18)
)
}
.stroke(OpenWorldsTheme.brass600.opacity(0.64), style: StrokeStyle(lineWidth: 1.1, lineCap: .round))
}
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}

struct OpenWorldsPill: View {
let text: String
var tone: Tone = .neutral

enum Tone {
case neutral
case live
case warning
case danger
case royal
}

var body: some View {
Text(text)
.font(.caption.weight(.semibold))
.foregroundStyle(foreground)
.padding(.horizontal, 9)
.padding(.vertical, 4)
.background(background)
.clipShape(Capsule())
}

private var foreground: Color {
switch tone {
case .neutral: OpenWorldsTheme.ink700
case .live: .white
case .warning: OpenWorldsTheme.ink800
case .danger: .white
case .royal: .white
}
}

private var background: Color {
switch tone {
case .neutral: OpenWorldsTheme.parchment300.opacity(0.9)
case .live: OpenWorldsTheme.emerald
case .warning: OpenWorldsTheme.brass300
case .danger: OpenWorldsTheme.crimson
case .royal: OpenWorldsTheme.royal
}
}
}

struct OpenWorldsBrassButtonStyle: ButtonStyle {
var prominent = false
var danger = false

func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.callout.weight(.semibold))
.foregroundStyle(danger || prominent ? Color.white : OpenWorldsTheme.ink800)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(minHeight: 32)
.background(background(configuration: configuration))
.clipShape(RoundedRectangle(cornerRadius: 5))
.overlay {
RoundedRectangle(cornerRadius: 5)
.stroke(OpenWorldsTheme.brass600.opacity(0.55), lineWidth: 1)
}
.opacity(configuration.isPressed ? 0.78 : 1)
}

private func background(configuration: Configuration) -> some View {
let base: [Color]
if danger {
base = [OpenWorldsTheme.crimson, OpenWorldsTheme.crimson.opacity(0.82)]
} else if prominent {
base = [OpenWorldsTheme.royal, OpenWorldsTheme.royal.opacity(0.82)]
} else {
base = [OpenWorldsTheme.brass100, OpenWorldsTheme.brass300]
}
return LinearGradient(colors: base, startPoint: .top, endPoint: .bottom)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import SwiftUI

enum OpenWorldsTheme {
static let parchment100 = Color(hex: 0xF6ECD2)
static let parchment200 = Color(hex: 0xF2E7CE)
static let parchment300 = Color(hex: 0xEAD9B5)
static let parchment500 = Color(hex: 0xC8B287)
static let parchmentEdge = Color(hex: 0x8A7146)

static let walnut100 = Color(hex: 0x5A3E2B)
static let walnut200 = Color(hex: 0x4B3425)
static let walnut300 = Color(hex: 0x3A281B)
static let walnut400 = Color(hex: 0x2A1D14)

static let brass100 = Color(hex: 0xF0D8A0)
static let brass200 = Color(hex: 0xD4B97A)
static let brass300 = Color(hex: 0xC9A66B)
static let brass400 = Color(hex: 0xB08D57)
static let brass600 = Color(hex: 0x5D4827)

static let ink900 = Color(hex: 0x1C130A)
static let ink800 = Color(hex: 0x2C1F17)
static let ink700 = Color(hex: 0x4A3325)
static let ink600 = Color(hex: 0x6A4D35)

static let royal = Color(hex: 0x22305E)
static let crimson = Color(hex: 0x6E1D1D)
static let emerald = Color(hex: 0x2F5A3A)
static let goldGlow = Color(hex: 0xF4D27B)

static let railWidth: CGFloat = 92
static let outerRadius: CGFloat = 8
static let panelRadius: CGFloat = 7
}


extension Color {
init(hex: UInt, opacity: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 8) & 0xff) / 255,
blue: Double(hex & 0xff) / 255,
opacity: opacity
)
}
}

struct OpenWorldsWindowBackground: View {
var body: some View {
ZStack {
LinearGradient(
colors: [
OpenWorldsTheme.walnut200,
OpenWorldsTheme.walnut300,
OpenWorldsTheme.walnut400
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
OpenWorldsTheme.ink900
.opacity(0.22)
.blendMode(.multiply)
OpenWorldsTheme.brass400
.opacity(0.26)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.mask(
RoundedRectangle(cornerRadius: OpenWorldsTheme.outerRadius)
.stroke(lineWidth: 2)
.padding(8)
)
}
.ignoresSafeArea()
}
}

struct OpenWorldsParchmentBackground: View {
var body: some View {
ZStack {
LinearGradient(
colors: [
OpenWorldsTheme.parchment100,
OpenWorldsTheme.parchment200,
OpenWorldsTheme.parchment300.opacity(0.92)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
OpenWorldsTheme.brass100.opacity(0.12)
.blendMode(.softLight)
}
}
}
Loading
Loading