Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
fbdfe18
feat(gallery): add gallery, app shot, template, and palette models
hanrw Apr 7, 2026
35d767a
feat(domain): add Codable conformance to gallery models
hanrw Apr 7, 2026
a56be28
feat(domain): add Codable conformance to ScreenTemplate
hanrw Apr 7, 2026
ca206ac
feat(gallery): add HTML rendering for gallery shots
hanrw Apr 7, 2026
5ae2d2a
feat(gallery): add aggregated gallery template repository and tests
hanrw Apr 7, 2026
7082074
feat(app-shots): add gallery templates endpoint and model
hanrw Apr 7, 2026
74a5173
feat(gallery): add HTML preview for gallery templates
hanrw Apr 7, 2026
2035369
feat(gallery): add description and background to GalleryTemplate
hanrw Apr 7, 2026
8ef184a
feat(screenshot-plans): add gallery preview with multi-panel layout
hanrw Apr 7, 2026
3ea2432
feat(gallery): enhance app shot layout with tagline, body, and badges
hanrw Apr 7, 2026
4b1eb1d
style(gallery): update panel sizing and layout styles
hanrw Apr 7, 2026
049ea7a
feat(gallery): add Codable support for Gallery and AppShot
hanrw Apr 7, 2026
6a80021
refactor(gallery): remove previewHTML from GalleryTemplate Codable
hanrw Apr 7, 2026
e7a7ee4
refactor(gallery): rename GalleryTemplate to Gallery and update APIs
hanrw Apr 7, 2026
277f56c
fix(web): update gallery-templates endpoint response format
hanrw Apr 7, 2026
701940a
test(gallery): update tests to use Gallery model instead of templates
hanrw Apr 7, 2026
5101628
refactor(domain): simplify GalleryHTMLRenderer preview rendering
hanrw Apr 7, 2026
39848d9
test(gallery): refactor tests with helper and improve coverage
hanrw Apr 7, 2026
e26b298
test(gallery): update preview tests to use real gallery JSON
hanrw Apr 7, 2026
9967e1f
refactor(gallery): rename panel rendering to screen rendering
hanrw Apr 7, 2026
647149f
refactor(gallery): use template or default device position for hero
hanrw Apr 7, 2026
6b012ab
test(domain): update gallery compose test expectations
hanrw Apr 7, 2026
c8965b1
style(layout): add min-width and overflow to main container
hanrw Apr 7, 2026
f7ad0c9
feat(screenshot-plans): unify template rendering with GalleryHTMLRend…
hanrw Apr 8, 2026
8a7cc5c
test(domain): update ScreenDesignTests to check wireframe content
hanrw Apr 8, 2026
0496445
refactor(screenshots): unify template rendering with AppShot model
hanrw Apr 8, 2026
34ba1fc
test(template): update tests to use AppShot and screenTemplate model
hanrw Apr 8, 2026
1fc336f
test(domain): update screenshot template and presentable tests
hanrw Apr 8, 2026
d6e3fe1
feat(gallery): support multiple devices in ScreenTemplate
hanrw Apr 8, 2026
caefdbd
refactor(app-shots): update screen template and output formatting
hanrw Apr 8, 2026
c32c503
refactor(screenshots): replace single device with devices array
hanrw Apr 8, 2026
1b499a5
docs(app-shots): revamp docs with new gallery and template workflows
hanrw Apr 8, 2026
5e5d886
feat(gallery): support multi-screenshot app shots and rendering
hanrw Apr 8, 2026
e34c0c8
test(gallery): add tests for hero shot image rendering
hanrw Apr 8, 2026
f2c2f35
test(gallery): update screenshot expectation to real image file
hanrw Apr 8, 2026
38a8a31
feat(app-shots): add gallery compose endpoint
hanrw Apr 8, 2026
0475d4c
refactor(web): streamline gallery screenshot processing
hanrw Apr 8, 2026
9fba514
test(domain): add GalleryApplyTests for screenshot distribution and a…
hanrw Apr 8, 2026
65ae788
fix(gallery): exclude bezel overlay from real screenshots
hanrw Apr 8, 2026
afd2312
refactor(gallery): simplify screenshot frame rendering logic
hanrw Apr 8, 2026
534759f
feat(web): support multiple screenshots in AppShotsController
hanrw Apr 8, 2026
6635836
feat(domain): add tagline and subheading to ScreenTemplate
hanrw Apr 8, 2026
952e6ed
fix(domain): remove unused sampleShot property in ScreenshotTemplate
hanrw Apr 8, 2026
7de80a8
test(domain): add TextSlotPreviewTests for preview behavior
hanrw Apr 8, 2026
5086d93
refactor(screenshot-plans): rename ScreenTemplate to ScreenLayout
hanrw Apr 8, 2026
afb8daa
refactor(appShots): rename ScreenshotTemplate to AppShotTemplate
hanrw Apr 8, 2026
6ddf03e
docs(features): update app-shots with new text slots
hanrw Apr 8, 2026
4a0a423
feat(web): support gallery templates fallback for AppShotsController
hanrw Apr 8, 2026
e0b95b0
refactor(web): simplify template retrieval in AppShotsController
hanrw Apr 8, 2026
8520106
feat(web): support AI-generated headlines for app shots
hanrw Apr 8, 2026
12d5bd0
refactor(web): unify template resolution and rendering in AppShotsCon…
hanrw Apr 8, 2026
c7cb6c5
feat(web): support template rendering from gallery in app shots contr…
hanrw Apr 8, 2026
128ae32
refactor(domain): simplify GalleryHTMLRenderer page wrapping
hanrw Apr 8, 2026
3efc2db
feat(screenshotPlans): add AppShotTemplate and ScreenLayout models
hanrw Apr 8, 2026
dee7ad2
feat(theme): add ThemeDesign generation and application support
hanrw Apr 8, 2026
f09c489
feat(theme): add ThemeDesign domain model and workflow
hanrw Apr 8, 2026
a0eaa92
refactor(screenshots): rename ScreenshotPlans to Screenshots module
hanrw Apr 8, 2026
ff40041
feat(html): add HTMLComposer for template rendering
hanrw Apr 8, 2026
5dbaf02
feat(core): include gallery resources in mock target
hanrw Apr 8, 2026
930c234
refactor(screenshots): consolidate page-wrapper style rendering
hanrw Apr 8, 2026
e6f71c2
refactor(gallery): rewrite HTML rendering to use templates with context
hanrw Apr 8, 2026
dea2c45
refactor(screenshots/gallery): simplify HTML rendering and context bu…
hanrw Apr 8, 2026
70ba61a
refactor(gallery): unify color logic with GalleryPalette
hanrw Apr 8, 2026
0266318
refactor(gallery): migrate to CSS variables for theming and simplify …
hanrw Apr 8, 2026
1b0e4e7
refactor(gallery): streamline HTML rendering and tests
hanrw Apr 8, 2026
0de0a60
refactor(screenshots): move page wrapper styles to template context
hanrw Apr 8, 2026
8bae9af
feat(html): support nested if blocks in template rendering
hanrw Apr 8, 2026
3ae0bd5
refactor(screenshots): cache bundled HTML templates in memory
hanrw Apr 8, 2026
e8b5f3e
refactor(htmlcomposer): replace custom rendering with Mustache integr…
hanrw Apr 8, 2026
3afcba9
chore(deps): add swift-mustache package for Mustache templating
hanrw Apr 8, 2026
9e3176c
refactor(gallery): extract HTML templates and use HTMLComposer
hanrw Apr 9, 2026
d7369df
chore(deps): add swift-mustache package dependency
hanrw Apr 9, 2026
ad7db78
feat(screenshots): add caching for HTML preview rendering
hanrw Apr 9, 2026
4a984f4
feat(app-shots): add ThemeDesign for deterministic theming
hanrw Apr 9, 2026
fa8a92a
feat(theme): refactor GalleryHTMLRenderer to use Mustache templates
hanrw Apr 9, 2026
4459ca8
docs(features): add fallback step for full AI restyle in themes
hanrw Apr 9, 2026
f6e4b74
feat(app-shots): add design and apply-design subcommands
hanrw Apr 9, 2026
67818d3
feat(app-shots): add gallery templates commands and tests
hanrw Apr 9, 2026
867ad3e
test(domain): update AppShotTemplate previewHTML test assertions
hanrw Apr 9, 2026
91b77bb
docs(app-shots): add gallery-templates and themes CLI commands
hanrw Apr 9, 2026
cd1e13b
refactor(tests): rename ScreenshotPlans folder to Screenshots
hanrw Apr 9, 2026
3a25077
refactor(screenshots): rename ScreenshotPlans directories to Screenshots
hanrw Apr 9, 2026
0e75a91
test(gallery,template): update preview HTML rendering tests
hanrw Apr 9, 2026
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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`ThemeDesign` domain model** — composes from Gallery-native types (`GalleryPalette` + `[Decoration]`) for structured theme output. Generated by AI once, applied deterministically to all screenshots
- **`ThemeDesignApplier`** — re-renders through `GalleryHTMLRenderer.renderScreen()` pipeline with overridden palette and merged decorations (no HTML patching)
- **`GalleryPalette.textColor`** — optional explicit text color, overrides the auto-detect heuristic
- **`Decoration.label()` shape** — text/emoji decorative elements (e.g. `Decoration(shape: .label("✨"), ...)`)
- **`DecorationAnimation`** — float, drift, pulse, spin, twinkle animations for decorations
- **`ScreenLayout.withDecorations()`** — creates a copy with additional decorations
- **`GalleryHTMLRenderer.renderDecorations()`** — renders `ScreenLayout.decorations` (previously dead code) using `cqi` units
- **`buildDesignContext()` on `ScreenTheme`** — prompt method that instructs AI to return `ThemeDesign` JSON
- **`design()` on `ThemeProvider`/`ThemeRepository`** — generate a ThemeDesign from AI in one call
- **`--design-only` / `--apply-design` CLI flags** — batch theme workflow
- **REST endpoints** — `POST /app-shots/themes/design` and `POST /app-shots/themes/apply-design`

### Changed
- **`GalleryHTMLRenderer` refactored to Mustache templates** — all HTML extracted from Swift into 7 `.mustache` template files using [swift-mustache](https://github.com/hummingbird-project/swift-mustache). The renderer only builds context dictionaries; all HTML, CSS colors (via CSS custom properties in `theme-vars.mustache`), and keyframe animations live in templates. Templates are pre-compiled at startup via `MustacheLibrary` for performance. Preview rendering is cached per template ID.
- **`DecorationShape.displayCharacter`** — computed property on the model instead of renderer logic
- **`GalleryPalette.isLight` + `headlineColor`** — theme detection and text color derivation moved from renderer to palette
- **`Decoration` extended** — new optional fields: `color`, `background`, `borderRadius`, `animation`
- **Theme selection no longer requires auto-compose** — clicking a theme applies immediately to slides with existing preview HTML via `ThemeDesign` (1 AI call for design, then deterministic apply to all slides)
- **Blitz plugin: `design()` implemented** — generates `ThemeDesign` via compose bridge `mode: "design"`, enabling the fast design→apply-design flow

---

## [0.1.65] - 2026-04-06
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let package = Package(
.package(url: "https://github.com/steipete/SweetCookieKit.git", from: "0.3.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", exact: "2.21.1"),
.package(url: "https://github.com/hummingbird-project/hummingbird-websocket.git", from: "2.6.0"),
.package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.2"),
],
targets: [
.target(
Expand All @@ -31,7 +32,9 @@ let package = Package(
name: "Domain",
dependencies: [
.product(name: "Mockable", package: "Mockable"),
.product(name: "Mustache", package: "swift-mustache"),
],
resources: [.copy("Screenshots/Gallery/Resources")],
swiftSettings: [.define("MOCKING")]
),
.target(
Expand Down
4 changes: 4 additions & 0 deletions Sources/ASCCommand/ClientProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ struct ClientProvider {
AggregateTemplateRepository.shared
}

static func makeGalleryTemplateRepository() -> AggregateGalleryTemplateRepository {
AggregateGalleryTemplateRepository.shared
}

static func makeThemeRepository() -> AggregateThemeRepository {
AggregateThemeRepository.shared
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ASCCommand/Commands/AppShots/AppShotsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ struct AppShotsCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "app-shots",
abstract: "AI-powered App Store screenshot planning and generation",
subcommands: [AppShotsTemplatesCommand.self, AppShotsThemesCommand.self, AppShotsGenerate.self, AppShotsExport.self, AppShotsConfig.self]
subcommands: [AppShotsTemplatesCommand.self, AppShotsGalleryTemplatesCommand.self, AppShotsThemesCommand.self, AppShotsGenerate.self, AppShotsExport.self, AppShotsConfig.self]
)
}
8 changes: 3 additions & 5 deletions Sources/ASCCommand/Commands/AppShots/AppShotsExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,13 @@ struct AppShotsExport: AsyncParsableCommand {
static func renderToPNG(html: String, width: Int = 1320, height: Int = 2868, renderer: any HTMLRenderer) async throws -> Data {
var htmlContent = html

// Ensure preview fills the full viewport for image export
// Themed HTML from `themes apply` uses width:320px — replace with 100%
// Convert 320px preview container to fill viewport for export
if htmlContent.contains("width:320px") {
htmlContent = htmlContent
.replacingOccurrences(of: "width:320px", with: "width:100%;height:100%")
.replacingOccurrences(of: "min-height:100vh;background:#111", with: "margin:0;overflow:hidden")
.replacingOccurrences(of: "width:320px;aspect-ratio:1320/2868;container-type:inline-size", with: "width:100%;height:100%;container-type:inline-size")
.replacingOccurrences(of: "display:flex;justify-content:center;align-items:center;min-height:100vh;background:#111", with: "margin:0;overflow:hidden")
if !htmlContent.contains("html,body{") {
htmlContent = htmlContent.replacingOccurrences(of: "box-sizing:border-box}", with: "box-sizing:border-box}html,body{width:100%;height:100%}")
htmlContent = htmlContent.replacingOccurrences(of: "box-sizing:border-box}", with: "box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import ArgumentParser
import Domain
import Foundation

struct AppShotsGalleryTemplatesCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "gallery-templates",
abstract: "Browse gallery templates (multi-screen sets)",
subcommands: [AppShotsGalleryTemplatesList.self, AppShotsGalleryTemplatesGet.self]
)
}

// MARK: - List

struct AppShotsGalleryTemplatesList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List available gallery templates"
)

@OptionGroup var globals: GlobalOptions

func run() async throws {
let repo = ClientProvider.makeGalleryTemplateRepository()
print(try await execute(repo: repo))
}

func execute(repo: any GalleryTemplateRepository, affordanceMode: AffordanceMode = .cli) async throws -> String {
let galleries = try await repo.listGalleries()
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
galleries,
headers: ["App Name", "Shots", "Template", "Ready"],
rowMapper: { [$0.appName, "\($0.shotCount)", $0.template?.name ?? "-", $0.isReady ? "✓" : "✗"] },
affordanceMode: affordanceMode
)
}
}

// MARK: - Get

struct AppShotsGalleryTemplatesGet: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "get",
abstract: "Get details of a specific gallery template"
)

@OptionGroup var globals: GlobalOptions

@Option(name: .long, help: "Gallery template ID")
var id: String

@Flag(name: .long, help: "Output self-contained HTML gallery preview page")
var preview: Bool = false

func run() async throws {
let repo = ClientProvider.makeGalleryTemplateRepository()
print(try await execute(repo: repo))
}

func execute(repo: any GalleryTemplateRepository) async throws -> String {
guard let gallery = try await repo.getGallery(templateId: id) else {
throw ValidationError("Gallery template '\(id)' not found. Run `asc app-shots gallery-templates list` to see available templates.")
}

if preview {
return gallery.previewHTML
}

let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
[gallery],
headers: ["App Name", "Shots", "Template", "Ready"],
rowMapper: { [$0.appName, "\($0.shotCount)", $0.template?.name ?? "-", $0.isReady ? "✓" : "✗"] }
)
}
}
23 changes: 15 additions & 8 deletions Sources/ASCCommand/Commands/AppShots/AppShotsTemplates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,23 +160,30 @@ struct AppShotsTemplatesApply: AsyncParsableCommand {
throw ValidationError("Template '\(id)' not found. Run `asc app-shots templates list` to see available templates.")
}

let shot = AppShot(screenshot: screenshot, type: .feature)
shot.headline = headline
shot.body = subtitle
shot.tagline = tagline

if preview == .image, let renderer {
let content = TemplateContent(headline: headline, subtitle: subtitle, tagline: tagline, screenshotFile: screenshot)
let html = template.apply(content: content, fillViewport: true)
let html = template.apply(shot: shot, fillViewport: true)
return try await renderToImage(html: html, renderer: renderer)
}

if preview == .html {
let content = TemplateContent(headline: headline, subtitle: subtitle, tagline: tagline, screenshotFile: URL(fileURLWithPath: screenshot).lastPathComponent)
return template.apply(content: content)
let shotForHTML = AppShot(screenshot: URL(fileURLWithPath: screenshot).lastPathComponent, type: .feature)
shotForHTML.headline = headline
shotForHTML.body = subtitle
shotForHTML.tagline = tagline
return template.apply(shot: shotForHTML)
}

let screen = ScreenDesign(index: 0, template: template, screenshotFile: screenshot, heading: headline, subheading: subtitle ?? "")
// Return the shot as JSON output
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
return try formatter.formatAgentItems(
[screen],
headers: ["Heading", "Screenshot", "Template", "Complete"],
rowMapper: { [$0.heading, $0.screenshotFile, $0.template?.name ?? "-", $0.isComplete ? "✓" : "✗"] }
[shot],
headers: ["Headline", "Screenshot", "Template", "Configured"],
rowMapper: { [$0.headline ?? "-", $0.screenshot, template.name, $0.isConfigured ? "✓" : "✗"] }
)
}

Expand Down
135 changes: 127 additions & 8 deletions Sources/ASCCommand/Commands/AppShots/AppShotsThemes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ struct AppShotsThemesCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "themes",
abstract: "Browse visual themes for screenshot composition",
subcommands: [AppShotsThemesList.self, AppShotsThemesGet.self, AppShotsThemesApply.self]
subcommands: [
AppShotsThemesList.self,
AppShotsThemesGet.self,
AppShotsThemesDesign.self,
AppShotsThemesApplyDesign.self,
AppShotsThemesApply.self,
]
)
}

Expand Down Expand Up @@ -76,12 +82,125 @@ struct AppShotsThemesGet: AsyncParsableCommand {
}
}

// MARK: - Apply
// MARK: - Design (generate ThemeDesign JSON — 1 AI call)

struct AppShotsThemesDesign: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "design",
abstract: "Generate a ThemeDesign (palette + decorations) from AI — one call, reusable"
)

@OptionGroup var globals: GlobalOptions

@Option(name: .long, help: "Theme ID")
var id: String

func run() async throws {
let repo = ClientProvider.makeThemeRepository()
print(try await execute(themeRepo: repo))
}

func execute(themeRepo: any ThemeRepository) async throws -> String {
let design = try await themeRepo.design(themeId: id)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(design)
return String(data: data, encoding: .utf8) ?? "{}"
}
}

// MARK: - Apply Design (deterministic — no AI)

struct AppShotsThemesApplyDesign: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "apply-design",
abstract: "Apply a cached ThemeDesign to a template — deterministic, no AI"
)

@OptionGroup var globals: GlobalOptions

@Option(name: .long, help: "Path to ThemeDesign JSON file")
var design: String

@Option(name: .long, help: "Template ID")
var template: String

@Option(name: .long, help: "Path to screenshot file")
var screenshot: String

@Option(name: .long, help: "Headline text")
var headline: String = "Your Headline"

@Option(name: .long, help: "Subtitle text")
var subtitle: String?

@Option(name: .long, help: "Tagline text")
var tagline: String?

@Option(name: .long, help: "Preview format: html (default) or image")
var preview: PreviewFormat?

@Option(name: .long, help: "Output PNG path (for --preview image)")
var imageOutput: String?

@Option(name: .long, help: "Canvas width in pixels")
var canvasWidth: Int = 1320

@Option(name: .long, help: "Canvas height in pixels")
var canvasHeight: Int = 2868

func run() async throws {
let templateRepo = ClientProvider.makeTemplateRepository()
if preview == .image {
let renderer = ClientProvider.makeHTMLRenderer()
print(try await execute(templateRepo: templateRepo, renderer: renderer))
} else {
print(try await execute(templateRepo: templateRepo))
}
}

func execute(templateRepo: any TemplateRepository, renderer: (any HTMLRenderer)? = nil) async throws -> String {
let designData = try Data(contentsOf: URL(fileURLWithPath: design))
let themeDesign = try JSONDecoder().decode(ThemeDesign.self, from: designData)

guard let tmpl = try await templateRepo.getTemplate(id: template) else {
throw ValidationError("Template '\(template)' not found.")
}

let screenshotFile = (preview == .image) ? screenshot : URL(fileURLWithPath: screenshot).lastPathComponent
let shot = AppShot(screenshot: screenshotFile, type: .feature)
shot.headline = headline
shot.body = subtitle
shot.tagline = tagline

let themedHTML = ThemeDesignApplier.apply(themeDesign, shot: shot, screenLayout: tmpl.screenLayout)
let page = ThemedPage(body: themedHTML, width: canvasWidth, height: canvasHeight, fillViewport: preview == .image)

if preview == .image, let renderer {
return try await renderToImage(html: page.html, renderer: renderer)
}

return page.html
}

private func renderToImage(html: String, renderer: any HTMLRenderer) async throws -> String {
let pngData = try await renderer.render(html: html, width: canvasWidth, height: canvasHeight)
let outputPath = imageOutput ?? ".asc/app-shots/output/screen-0.png"
let fileURL = URL(fileURLWithPath: outputPath)
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try pngData.write(to: fileURL)
let result: [String: Any] = ["exported": outputPath, "width": canvasWidth, "height": canvasHeight, "bytes": pngData.count]
let data = try JSONSerialization.data(withJSONObject: result, options: [.prettyPrinted, .sortedKeys])
return String(data: data, encoding: .utf8) ?? "{}"
}
}

// MARK: - Apply (full AI restyle — fallback)

struct AppShotsThemesApply: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "apply",
abstract: "Apply a theme to a template — renders deterministic layout, then AI restyles"
abstract: "Apply a theme via full AI restyle"
)

@OptionGroup var globals: GlobalOptions
Expand Down Expand Up @@ -133,10 +252,12 @@ struct AppShotsThemesApply: AsyncParsableCommand {
}

let screenshotFile = (preview == .image) ? screenshot : URL(fileURLWithPath: screenshot).lastPathComponent
let content = TemplateContent(headline: headline, subtitle: subtitle, tagline: tagline, screenshotFile: screenshotFile)
let shot = AppShot(screenshot: screenshotFile, type: .feature)
shot.headline = headline
shot.body = subtitle
shot.tagline = tagline

// Domain: template renders fragment, theme repo composes, ThemedPage wraps
let fragment = tmpl.renderFragment(content: content)
let fragment = tmpl.renderFragment(shot: shot)
let themedHTML = try await themeRepo.compose(themeId: theme, html: fragment, canvasWidth: canvasWidth, canvasHeight: canvasHeight)
let page = ThemedPage(body: themedHTML, width: canvasWidth, height: canvasHeight, fillViewport: preview == .image)

Expand All @@ -157,6 +278,4 @@ struct AppShotsThemesApply: AsyncParsableCommand {
let data = try JSONSerialization.data(withJSONObject: result, options: [.prettyPrinted, .sortedKeys])
return String(data: data, encoding: .utf8) ?? "{}"
}

// wrapInPage logic moved to Domain/ScreenshotPlans/ThemedPage.swift
}
Loading
Loading