Generic Avalonia components from books-writer
Policies that keep the org coherent
name: Generic Avalonia lifts
overview: Extract books-writer Avalonia UI patterns into existing Novolis.Avalonia.Controls, Studio, and Markdown packages as domain-agnostic primitives (no Manuscript-specific package), with unit tests and a small dogfooding demo app.
todos:
- id: controls-dialogs
content: Add ChoiceDialog, FilteredPickerDialog, MarkedListRow/Box, JobQueuePanel + IJobQueueRow to Novolis.Avalonia.Controls with unit tests status: completed
- id: studio-focus
content: Add StudioFocusMode + StudioStatusBrushes to Novolis.Avalonia.Studio status: completed
- id: markdown-highlight
content: Enrich BookAuthoring xshd + MarkdownSpanAnalyzer from writer HighlightService; unit tests status: completed
- id: dogfood-lab
content: Add StudioChromeLab dogfood app + Directory.Packages.props/slnx/README wiring status: completed
- id: publish-verify
content: Build/test Avalonia packages; publish to GPR; restore dogfood nuget-only status: completed isProject: false
Goal
Lift the reusable Avalonia UX from `D:\repos\books\tools\apps\books-writer` into existing Avalonia packages, kept domain-agnostic (no chapter/book/manuscript types in library APIs). Consumers (Manuscript Studio, StarMapLab, WireFish, etc.) supply labels and DTOs.
Out of scope: retargeting books-writer, spellcheck, print/voice settings JSON, reference catalog services, Manuscript Studio product wiring (follow-up).
In scope consumer: new dogfood app under `novolis-dogfooding` that exercises the dialogs, nav list, focus mode, and job panel with fake/IO.Processes data.
Package map (chosen)
flowchart LR
writer[books-writer UX]
controls[Avalonia.Controls]
studio[Avalonia.Studio]
markdown[Avalonia.Markdown]
dogfood[dogfooding StudioChromeLab]
writer --> controls
writer --> studio
writer --> markdown
controls --> dogfood
studio --> dogfood
markdown --> dogfood| Writer inspiration | Generic home | Public API shape |
|---|---|---|
| Recovery / Conflict / Reference picker chrome | [`Novolis.Avalonia.Controls`](d:\novolis\novolis-avalonia\src\Novolis.Avalonia.Controls) | `ChoiceDialog.ShowAsync(owner, title, message, detail?, options)` → selected option `Id` |
| Go To Chapter | same | `FilteredPickerDialog.ShowAsync<T>(owner, title, items, filter, display)` → `T?` |
| Chapter list row (dirty · # · title · words) | same | `MarkedListItemTemplate` / `MarkedListRow` model: `Marker`, `Leading`, `Primary`, `Trailing` (strings only) |
| Publish jobs list + cancel/open + log | same | `JobQueuePanel` bound to `IJobQueueRow` (no `IO.Processes` package ref) |
| Focus mode / dirty status bar color | [`Novolis.Avalonia.Studio`](d:\novolis\novolis-avalonia\src\Novolis.Avalonia.Studio) | `StudioFocusMode.Set(chromeControls, focused)`; `StudioStatusBrushes.Dirty/Clean` |
| HighlightService dialogue + metadata | [`Novolis.Avalonia.Markdown`](d:\novolis\novolis-avalonia\src\Novolis.Avalonia.Markdown) | Enrich `BookAuthoringStudio.xshd`; add optional `MarkdownSpanAnalyzer` (span list for tests / non-Edit hosts) |
No new Novolis.Avalonia.Manuscript package. Keep Controls free of Novolis.IO / Markup package references; apps adapt ProcessJob → IJobQueueRow and recovery payloads → ChoiceDialog options.
API contracts (concrete)
Controls
- `ChoiceOption`:
(string Id, string Label, bool IsDefault = false, bool IsCancel = false) - `ChoiceDialog`: code-built
Window(match existing no-XAML style of Controls/Studio). Buttons from options; Enter activates default; Esc returns cancel id ornull. - `FilteredPickerDialog`: watermark filter
TextBox+ListBox; live filter viaFunc<T, string, bool>; double-click / Enter confirms; Esc cancels. - `MarkedListRow`: record for list items; factory helper builds a
ControlDataTemplate-equivalent viaMarkedListBox.CreateItem(MarkedListRow)for code-first apps. - `IJobQueueRow`:
Title,StatusLabel,Detail,LogTail,CanCancel,CanOpenOutput,object? Tag - `JobQueuePanel`: list + selected log
TextBlock+ Cancel/Open buttons raisingCancelRequested/OpenOutputRequestedwith the row.
Recovery/Conflict become app call-sites using ChoiceDialog with ids like restore / discard / keep / reload / compare — same as writer’s string results, without library knowledge of recovery files.
Studio
- Extend chrome helpers next to `StudioChrome` / `StudioWorkspace`:
StudioFocusMode.Apply(bool focused, params Control?[] chrome)togglesIsVisibleStudioStatusBrushesstatic dirty/clean brushes (writer’s green/amber dirty bar)
Markdown
- Update `BookAuthoringStudio.xshd` for:
- double-quoted dialogue spans
> [!key]metadata lines / key tokens- TK/TODO/FIXME (if not already present)
- Add
MarkdownSpanAnalyzermirroring writer `HighlightService` kinds as a small enum + span record; unit-tested without AvaloniaEdit UI. Xshd remains the Edit path; analyzer is the portable/testable twin.
Implementation steps
- Controls: implement dialogs + marked row + job panel in
src/Novolis.Avalonia.Controls/; widen package description beyond “packet analyzers”; addtests/Novolis.Avalonia.Unit/Controls/coverage for filter predicate, choice default/cancel ids, marked row field mapping, job panel event wiring (headless where possible; dialog logic testable via extracted static helpers if Window show is awkward). - Studio: add focus-mode + status brushes; document on `Studio` README.
- Markdown: xshd + analyzer + tests under existing Markdown test folder.
- Wire repo metadata: update `Novolis.Avalonia.slnx` if needed; ensure unit project references Studio when testing focus helpers; refresh `.novolis/packages.json` paths for Studio/Markdown/Controls as required by release tooling.
- Dogfood:
novolis-dogfooding/apps/avalonia/StudioChromeLab(WinExe Avalonia) demonstrating:
- ChoiceDialog (fake recovery + conflict)
- FilteredPickerDialog over a string list
- MarkedListBox
- JobQueuePanel over in-memory rows (and optionally one real
ProcessJobvia adapter) - Focus mode toggle + dirty status brush
- Package refs only (
2026.1.*); register inDirectory.Packages.props+Novolis.Dogfooding.slnx+ README table
- Publish path: after merge, CI publishes Avalonia packages to GitHub Packages; dogfood restores floating
2026.1.*(bootstrap push2026.1.99-style only if GPR lacks versions before CI).
Non-goals / explicit skips
Novolis.Avalonia.Manuscriptpackage- ProjectReference from Avalonia → books repo or sibling
novolis-apps - SpellService, PrintSettingsService, VoiceMapService, CatalogService
- Changing StarMapLab (already has selectable jumps)
Done when
- Unit tests green for analyzer, filter, choice resolution, job panel events
StudioChromeLabbuilds/runs against GPR packagesverify-nuget-onlyclean for touched repos (no cross-repo ProjectReference)- Public APIs have XML docs; Controls/Studio/Markdown READMEs mention the new types