@cratis/pi 7.37.0 → 7.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package/corpus/harnesses/copilot/instructions/ai-distribution.instructions.md +109 -0
  2. package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +81 -0
  3. package/package/corpus/harnesses/copilot/instructions/capability-is-not-authority.instructions.md +45 -0
  4. package/package/corpus/harnesses/copilot/instructions/code-quality.csharp.instructions.md +92 -0
  5. package/package/corpus/harnesses/copilot/instructions/code-quality.instructions.md +83 -0
  6. package/package/corpus/harnesses/copilot/instructions/code-quality.typescript.instructions.md +90 -0
  7. package/package/corpus/harnesses/copilot/instructions/components.instructions.md +300 -0
  8. package/package/corpus/harnesses/copilot/instructions/concepts.instructions.md +138 -0
  9. package/package/corpus/harnesses/copilot/instructions/cratis-folder-is-configuration.instructions.md +29 -0
  10. package/package/corpus/harnesses/copilot/instructions/csharp.instructions.md +357 -0
  11. package/package/corpus/harnesses/copilot/instructions/dialogs.instructions.md +278 -0
  12. package/package/corpus/harnesses/copilot/instructions/documentation-structure-and-formatting.instructions.md +162 -0
  13. package/package/corpus/harnesses/copilot/instructions/documentation.instructions.md +90 -0
  14. package/package/corpus/harnesses/copilot/instructions/editing-cratis-docs.instructions.md +74 -0
  15. package/package/corpus/harnesses/copilot/instructions/efcore.instructions.md +84 -0
  16. package/package/corpus/harnesses/copilot/instructions/efcore.specs.instructions.md +45 -0
  17. package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +50 -0
  18. package/package/corpus/harnesses/copilot/instructions/exit-codes-and-wrappers.instructions.md +34 -0
  19. package/package/corpus/harnesses/copilot/instructions/framework.instructions.md +55 -0
  20. package/package/corpus/harnesses/copilot/instructions/frontend-quality.instructions.md +60 -0
  21. package/package/corpus/harnesses/copilot/instructions/frontend-testing.instructions.md +127 -0
  22. package/package/corpus/harnesses/copilot/instructions/git-commits.instructions.md +139 -0
  23. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +87 -0
  24. package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +63 -0
  25. package/package/corpus/harnesses/copilot/instructions/go-engineering.instructions.md +61 -0
  26. package/package/corpus/harnesses/copilot/instructions/go.instructions.md +116 -0
  27. package/package/corpus/harnesses/copilot/instructions/guards-and-fuses.instructions.md +61 -0
  28. package/package/corpus/harnesses/copilot/instructions/java.instructions.md +138 -0
  29. package/package/corpus/harnesses/copilot/instructions/kotlin.instructions.md +175 -0
  30. package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +56 -0
  31. package/package/corpus/harnesses/copilot/instructions/new-repository-intake.instructions.md +35 -0
  32. package/package/corpus/harnesses/copilot/instructions/orleans.instructions.md +97 -0
  33. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +403 -0
  34. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +176 -0
  35. package/package/corpus/harnesses/copilot/instructions/react.instructions.md +203 -0
  36. package/package/corpus/harnesses/copilot/instructions/reactors.instructions.md +336 -0
  37. package/package/corpus/harnesses/copilot/instructions/rtk.instructions.md +48 -0
  38. package/package/corpus/harnesses/copilot/instructions/specs.csharp.instructions.md +147 -0
  39. package/package/corpus/harnesses/copilot/instructions/specs.instructions.md +133 -0
  40. package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +281 -0
  41. package/package/corpus/harnesses/copilot/instructions/specs.typescript.instructions.md +141 -0
  42. package/package/corpus/harnesses/copilot/instructions/storybook.instructions.md +85 -0
  43. package/package/corpus/harnesses/copilot/instructions/typescript.instructions.md +151 -0
  44. package/package/corpus/harnesses/copilot/instructions/verification-discipline.instructions.md +27 -0
  45. package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +414 -0
  46. package/package/corpus/harnesses/copilot/instructions/web-fetching.instructions.md +12 -0
  47. package/package/corpus/harnesses/copilot/instructions/writing-correct-examples.instructions.md +57 -0
  48. package/package/corpus/harnesses/copilot/instructions/writing-cratis-docs.instructions.md +72 -0
  49. package/package/corpus/product-sync.json +2 -2
  50. package/package/corpus/skills/cratis-screenplay-discovery/SKILL.md +11 -6
  51. package/package/corpus/skills/cratis-screenplay-discovery/references/facilitation.md +9 -5
  52. package/package/corpus/skills/cratis-screenplay-discovery/references/human-workshop.md +1 -1
  53. package/package/corpus/skills/cratis-screenplay-discovery/references/plotting-and-handoff.md +28 -4
  54. package/package/corpus/skills/cratis-screenplay-discovery/references/storming-loop.md +2 -1
  55. package/package/corpus/skills/cratis-screenplay-discovery/verification.json +5 -0
  56. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +23 -29
  57. package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-loop.md +15 -1
  58. package/package/corpus/skills/cratis-screenplay-model-authoring/references/model-notes.md +208 -0
  59. package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +20 -1
  60. package/package/corpus/skills/cratis-screenplay-model-review/SKILL.md +19 -36
  61. package/package/corpus/skills/cratis-screenplay-model-review/references/report-template.md +26 -0
  62. package/package/corpus/skills/cratis-screenplay-model-review/verification.json +7 -0
  63. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/SKILL.md +14 -16
  64. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/core-rules-digest.md +8 -3
  65. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/handoff-template.md +5 -2
  66. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/intake.md +21 -0
  67. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/reasoning-notes.md +10 -6
  68. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/stop-or-assume.md +32 -1
  69. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/worked-example.md +5 -2
  70. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/verification.json +5 -0
  71. package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +4 -0
  72. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.json +1716 -183
  73. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.version +1 -1
  74. package/package/corpus/skills/cratis-screenplay-toolchain/references/versions.md +40 -4
  75. package/package/corpus/skills/cratis-screenplay-toolchain/verification.json +6 -0
  76. package/package.json +1 -1
@@ -0,0 +1,357 @@
1
+ ---
2
+ applyTo: "**/*.cs"
3
+ paths:
4
+ - "**/*.cs"
5
+ ---
6
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
7
+
8
+
9
+ # C# Conventions
10
+
11
+ The goal is minimal ceremony, maximum clarity. Modern C# (13+) gives us records, primary constructors, pattern matching, and file-scoped namespaces — use them everywhere. The less boilerplate in a file, the faster a reader can understand what it *does*.
12
+
13
+ ## Building
14
+
15
+ - Use `dotnet build` from the command line.
16
+ - Use `dotnet format` to format code.
17
+ - Use `dotnet test` to run tests.
18
+
19
+ ## Formatting
20
+
21
+ These rules exist so that every file in the codebase reads the same way. When formatting is consistent, code review focuses on logic, not style.
22
+
23
+ - Apply code-formatting style defined in `.editorconfig`.
24
+ - Use file-scoped namespace declarations — one less level of indentation for the entire file.
25
+ - Use single-line `using` directives, sorted alphabetically.
26
+ - Never qualify a type that is already unambiguously in scope via a `using` directive. When two `using` directives introduce conflicting type names, qualify only the conflicting occurrences using the shortest unambiguous path (e.g. `Concepts.Events.Foo` or `Contracts.Events.Foo`) — do not add `using` aliases for every conflicting type.
27
+ - Insert a blank line before the opening `{` of every code block (`if`, `for`, `foreach`, `try`, `using`, etc.).
28
+ - Ensure the final `return` statement of a method is on its own line.
29
+ - Use pattern matching and switch expressions wherever possible — they are more readable and the compiler verifies exhaustiveness.
30
+ - Use `nameof` instead of string literals — it survives refactoring.
31
+ - Place private class declarations at the bottom of the file — public API first, implementation details last.
32
+
33
+ ## Language — American English Only
34
+
35
+ All identifiers, comments, XML docs, and string literals must use **American English** spelling (initialize, serialize, behavior, color, organization, center, modeling, dialog, license, judgment, gray). See [general.md](../../../rules/general.md) for the full guidance.
36
+
37
+ ## Naming
38
+
39
+ - PascalCase for type names, method names, and public members.
40
+ - camelCase for private fields and local variables.
41
+ - Prefix private fields with `_` (e.g. `_myField`).
42
+ - Prefix interface names with `I` (e.g. `IMyService`).
43
+
44
+ ## Code Style
45
+
46
+ Every rule here reduces noise. `var` avoids redundant type repetition. Expression bodies eliminate braces for trivial members. Primary constructors remove the constructor-plus-field ceremony.
47
+
48
+ - Prefer `var` over explicit types — the right side of the assignment already tells you the type.
49
+ - Use expression-bodied members for simple methods and properties.
50
+ - Favor primary constructors for all types — they eliminate field declarations for injected dependencies.
51
+ - Use string interpolation instead of `string.Format()` or concatenation.
52
+ - Favor collection initializers and object initializers.
53
+ - Use `IEnumerable<T>` for collections that are not modified; never return mutable collections from public APIs.
54
+ - Prefer LINQ (`.Where`, `.Any`, `.Select`, `.FirstOrDefault`) over a `foreach` that filters inside its body with an `if`/`continue` or an early `return` — put the filter in the query so the intent is explicit (`items.Where(predicate)`, `return items.Any(predicate);`). Reserve `foreach` for genuine iteration with side effects. (Filtering inside a loop is what the analyzers flag as a "missed opportunity to use Where".)
55
+ - Don't use regions — they hide code instead of organizing it. If a file needs regions, it needs refactoring.
56
+ - Never add postfixes like `Async`, `Impl`, `Service` to class names — they add noise without information.
57
+ - Name a method without the `Async` suffix unless a synchronous method of the same name also exists, never write an explicit `private` modifier (property setters excepted), and never put a `.Features.` segment in a namespace.
58
+ - The `Cratis.Architecture.CodeAnalysis` analyzer package (26 diagnostics, `CRARCH0001`–`CRARCH0026`; verified at `v1.0.2`) enforces these and others: `CRARCH0003` postfixes, `0004` `.Features.` namespaces, `0005` regions, `0006`/`0024` `LoggerMessage`, `0007` `IServiceProvider` injection, `0008` `is null`, `0017` namespace matches folder, `0019` `Async` suffix, `0022` explicit `private`, `0023` typed logger category; `0012` (`async void`) and `0014` (test types in production) are errors. When a repository has the package, a `CRARCH` warning is the house convention, not noise.
59
+ - For types with no implementation body, omit the braces (e.g. `public interface IMyInterface;`).
60
+ - Prefer `record` types for immutable data structures (events, commands, read models, concepts) — they give you value equality, immutability, and concise syntax for free.
61
+
62
+ ## Nullable Reference Types
63
+
64
+ Embrace the type system — it is the first line of defense against null-related bugs. When it says something cannot be null, trust it.
65
+
66
+ - Use `is null` / `is not null` — never `== null` / `!= null`.
67
+ - Trust the C# null annotations; don't add defensive null checks when the type system guarantees a value.
68
+ - Add `!` operator where nullability warnings occur and you are certain the value is non-null.
69
+ - Use `is not null` checks before dereferencing potentially null values.
70
+
71
+ ## XML Documentation
72
+
73
+ XML doc comments are the public API's first impression. They must be multiline — never cram `<summary>` onto a single line. Every public type, method, property, and operator must have XML docs.
74
+
75
+ - Always use **multiline** `<summary>` tags — opening and closing tags on their own lines:
76
+
77
+ ```csharp
78
+ /// <summary>
79
+ /// Represents the unique identifier of a project.
80
+ /// </summary>
81
+ ```
82
+
83
+ - **Never** use single-line summaries:
84
+
85
+ ```csharp
86
+ // ❌ Wrong
87
+ /// <summary>Represents the unique identifier of a project.</summary>
88
+ ```
89
+
90
+ - Every method or operator with parameters **must** include `<param name="...">` for each parameter.
91
+ - Every method or operator that returns a value (non-void) **must** include `<returns>`.
92
+ - Every method that throws must document the exception with `<exception cref="...">` tags.
93
+ - Use `<see cref="..."/>` and `<paramref name="..."/>` to cross-reference types and parameters.
94
+ - Keep summaries concise and purposeful — only document when it adds understanding beyond the name itself.
95
+
96
+ Example:
97
+
98
+ ```csharp
99
+ /// <summary>
100
+ /// Represents an instance of <see cref="ICommandFilters"/>.
101
+ /// </summary>
102
+ /// <param name="filters">The collection of <see cref="ICommandFilter"/> to use for filtering commands.</param>
103
+ [Singleton]
104
+ public class CommandFilters(IInstancesOf<ICommandFilter> filters) : ICommandFilters
105
+ {
106
+ /// <summary>
107
+ /// Filters the command execution through all registered command filters.
108
+ /// </summary>
109
+ /// <param name="context">The <see cref="CommandContext"/> to filter.</param>
110
+ /// <returns>A <see cref="CommandResult"/> representing the aggregated filter outcome.</returns>
111
+ public async Task<CommandResult> OnExecution(CommandContext context)
112
+ {
113
+ // ...
114
+ }
115
+ }
116
+ ```
117
+
118
+ ## Exceptions
119
+
120
+ Every exception type in the codebase should communicate *what went wrong* in domain terms. Built-in types like `InvalidOperationException` tell you nothing about the problem — a custom `AuthorAlreadyRegistered` tells you everything.
121
+
122
+ - **Exceptions are for exceptional, unrecoverable state only — never for control flow.** "Unrecoverable" means the caller has no correct next step to take other than to stop: a violated invariant, a bug, a dependency that is simply gone. If a caller is expected to `catch` a specific type and then do something sensible and ordinary in response — retry, record an impediment, try a different branch, return a different response to a user — that outcome is not exceptional, it is a normal result of the operation, and it belongs in the method's return type, not in its throw list.
123
+ - **The tell: a caller-side `catch` block whose body is not "log and rethrow" or "crash louder".** A `catch (SomeException)` that goes on to do real application work — write a different event, set a different HTTP status, schedule a retry, record why something didn't happen — is exception-driven flow control wearing a `try`/`catch` costume. This applies even when the exception type is well-named and richly documented; a beautifully named exception thrown for an anticipated, recoverable outcome is still the wrong tool.
124
+ - **Wrong** — `IWorkerRuntime.Start` throwing `WorkerIsAlreadyRunning`/`WorkerIsStillGoingAway`/`WorkerLaunchWasRefused` for three entirely anticipated outcomes of trying to launch a worker, with the caller structured as one `try` and three `catch` blocks each doing real, different application work (recording a specific impediment, deciding whether to keep the callback token):
125
+
126
+ ```csharp
127
+ try
128
+ {
129
+ await workerRuntime.Start(job, cancellationToken);
130
+ }
131
+ catch (WorkerIsStillGoingAway stillGoingAway)
132
+ {
133
+ await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, "The previous worker is still shutting down"));
134
+ return false;
135
+ }
136
+ catch (WorkerIsAlreadyRunning alreadyRunning)
137
+ {
138
+ await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, "A worker for this work is already running"));
139
+ return false;
140
+ }
141
+ ```
142
+
143
+ - **Right** — a `Cratis.Monads` return type naming every real outcome, so the compiler forces every caller to handle each one and nothing is discoverable only by reading a `<exception>` tag or by triggering it at runtime:
144
+
145
+ ```csharp
146
+ public enum WorkerLaunchOutcome { Started, StillGoingAway, AlreadyRunning, RefusedByCluster }
147
+
148
+ Task<Result<WorkerLaunchOutcome, ClusterFailure>> Start(WorkerJob job, CancellationToken cancellationToken = default);
149
+
150
+ // caller:
151
+ var outcome = await workerRuntime.Start(job, cancellationToken);
152
+ if (!outcome.TryGetResult(out var launched))
153
+ {
154
+ // genuinely exceptional - the cluster itself failed in a way retry logic doesn't cover
155
+ throw new WorkerLaunchIrrecoverablyFailed(work.Id, outcome);
156
+ }
157
+
158
+ var impediment = launched switch
159
+ {
160
+ WorkerLaunchOutcome.StillGoingAway => "The previous worker is still shutting down",
161
+ WorkerLaunchOutcome.AlreadyRunning => "A worker for this work is already running",
162
+ _ => null
163
+ };
164
+ if (impediment is not null)
165
+ {
166
+ await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, impediment));
167
+ return false;
168
+ }
169
+ ```
170
+
171
+ A single-outcome case uses `Result<TResult, TError>` directly rather than an enum; `Option<TValue>` is the equivalent for "a value, or nothing" with no error to describe. Reach for these from `Cratis.Monads` before reaching for a custom exception type whenever the "failure" is something a caller is meant to branch on rather than merely propagate.
172
+ - Always create a custom exception type that derives from `Exception`.
173
+ - Never use built-in exception types (`InvalidOperationException`, `ArgumentException`, etc.).
174
+ - Never suffix exception class names with `Exception` — `AuthorNotFound` reads better than `AuthorNotFoundException`.
175
+ - Always provide a meaningful message when throwing.
176
+ - Add XML doc on the exception type starting with "The exception that is thrown when ...".
177
+ - Never write an empty or silently-swallowing `catch` block. Handle the exception, log it, or let it propagate. When ignoring is genuinely correct, use an exception filter (`catch (SomeException) when (…)`) with a body that states the decision (a comment and/or a fallback) — never a bare `catch { }`.
178
+
179
+ ## Dependency Injection
180
+
181
+ The framework discovers and wires dependencies by convention. Explicit registration is the exception, not the rule.
182
+
183
+ - Prefer constructor injection; avoid `IServiceProvider` directly (service locator anti-pattern).
184
+ - For singletons, use the `[Singleton]` attribute — no explicit registration needed.
185
+ - Systems with a convention of `IFoo → Foo` do not need to be registered explicitly.
186
+ - Command/query `Handle()` method parameters are automatically resolved from DI by type.
187
+
188
+ ### Service lifetimes — anything taking a scoped dependency is scoped or transient, never a singleton
189
+
190
+ **The rule, before any of the reasoning:**
191
+
192
+ > **A type that takes a scoped dependency is itself scoped or transient. If you are reaching for `[Singleton]` on something that needs the event store, a database, or a read model, the answer is to not make it a singleton.**
193
+
194
+ `[Singleton]` is the exception, not the default. It is for what is genuinely process-wide *and* holds nothing belonging to a tenant, a user, or a request. Everything else takes the convention (transient) or `[Scoped]`, and inherits the resolving scope — and therefore the right tenant — for free.
195
+
196
+ **Assume every application you build is multi-tenant.** Not "design for it later" — assume it now, even when the deployment ships with a single tenant and no tenant resolution configured. A single-tenant application is a multi-tenant one with one tenant in it, and the code shape that serves both is the same shape. The code shape that serves only one has to be found and rewritten later, from the far side of a data migration, under production. The same reasoning applies to the signed-in user: an application always has one, and a service that remembers *which* one will eventually answer for the wrong person.
197
+
198
+ So the rule has a second face:
199
+
200
+ > **A singleton may not depend on anything that belongs to a tenant, a user, or a request.**
201
+
202
+ These resolve **per scope**, and the scope is what carries the tenant — so none of them may be injected into a `[Singleton]`:
203
+
204
+ | Off limits in a singleton | Why |
205
+ | --- | --- |
206
+ | `IEventStore` — and everything off it: `IEventLog`, `IReadModels`, `IConstraints`, `IEventTypes`, `IProjections`, `IReducers`, `IPII` | resolved for the scope's namespace |
207
+ | `IMongoCollection<T>`, `IMongoDatabase`, `IMongoClient` | the database name is resolved per scope from the current tenant |
208
+ | An EF Core `DbContext` | scoped for the same reason, plus it is not thread-safe |
209
+ | A read model injected directly by key | same scope, same binding |
210
+ | Any held tenant, principal, claims, correlation id, or `HttpContext` **value** | belongs to one request and outlives it in a singleton |
211
+
212
+ A `[Singleton]` taking one of these is a **captive dependency**: the container hands it the *root* scope's instance and keeps it for process lifetime. The root scope has no request, so it resolves no tenant — every read and write goes to the default namespace forever, regardless of who is asking.
213
+
214
+ **It does not throw. It returns nothing.** A query against the wrong namespace hits a database that exists and is empty, so the caller gets an empty collection, a `null` read model, or a default-valued options object, and carries on. The application starts, the pages render, the build is green, and the configuration a tenant spent an afternoon entering is simply not there. It is also invisible while there is only one tenant — every symptom appears on the day a second one arrives.
215
+
216
+ **What to do instead — in this order.**
217
+
218
+ **1. Drop `[Singleton]`.** This is the answer almost every time. Delete the attribute and let the type be transient by convention, or mark it `[Scoped]` when one request should share one instance. Nothing else changes: the constructor keeps the collaborator it wanted, and now gets the caller's tenant instead of the root scope's.
219
+
220
+ ```csharp
221
+ // ❌ Wrong — IEventStore is scoped; this captures the root scope's default namespace forever.
222
+ [Singleton]
223
+ public class DigestSources(IEventStore eventStore) : IDigestSources
224
+ {
225
+ public Task<DigestConfiguration?> GetCurrent() =>
226
+ eventStore.ReadModels.GetInstanceById<DigestConfiguration>(DigestId.Default);
227
+ }
228
+
229
+ // ✅ Right — no attribute at all. Transient by convention, so it resolves in the caller's scope
230
+ // and reads that caller's tenant. There was never a reason for this to be process-wide.
231
+ public class DigestSources(IEventStore eventStore) : IDigestSources
232
+ {
233
+ public Task<DigestConfiguration?> GetCurrent() =>
234
+ eventStore.ReadModels.GetInstanceById<DigestConfiguration>(DigestId.Default);
235
+ }
236
+ ```
237
+
238
+ **2. Only when the lifetime is forced on you, open a scope per unit of work.** A hosted service or `BackgroundService` is resolved once by the host, so it *is* a singleton whether or not you asked — and it runs with no request to inherit a scope from. That, and only that, is what `IServiceScopeFactory` is for:
239
+
240
+ ```csharp
241
+ // ✅ Right for a hosted service — a scope per unit of work, so collaborators bind to a real scope.
242
+ public class DigestDispatcher(IServiceScopeFactory scopeFactory) : BackgroundService
243
+ {
244
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
245
+ {
246
+ while (!cancellationToken.IsCancellationRequested)
247
+ {
248
+ using var scope = scopeFactory.CreateScope();
249
+ var eventStore = scope.ServiceProvider.GetRequiredService<IEventStore>();
250
+ await Dispatch(eventStore, cancellationToken);
251
+ }
252
+ }
253
+ }
254
+ ```
255
+
256
+ ⚠️ **`IServiceScopeFactory` is not a way to keep `[Singleton]` on a service that had no reason to be one.** It is more code, it hides the lifetime question behind a scope nobody asked for, and — because a scope with no request still resolves no tenant — it does not by itself make an off-request flow tenant-correct. Reaching for it first is how a codebase ends up with dozens of these — a sweep of a real application found several dozen singletons holding a scoped service, not one of which needed to be a singleton at all. If the type is not the host's own, the fix is the attribute, not the factory.
257
+
258
+ `IChronicleClient` **is** singleton-safe, and is the right collaborator when a flow knows which namespace it means and has no scope to resolve one from — it names the event store and namespace explicitly: `await chronicleClient.GetEventStore("MyStore", tenantId.Value)`. Naming the namespace is a deliberate, readable statement that this code crosses a tenant boundary; capturing a scoped service is the same crossing made by accident.
259
+
260
+ **The current user is not process-wide either.** Never keep the signed-in user, their principal, claims, roles, or anything derived from them in a singleton. The distinction that matters: *the accessor is fine, the value is not.* `IHttpContextAccessor` is itself a singleton and safe to inject; reading a value out of it once and keeping it is not. A current-user service may be a singleton only when every method reads through the accessor on each call and stores nothing. Anything that derives something per user and wants to keep it holds a cache **keyed by the user**, never a single field.
261
+
262
+ **Off-request work carries its tenant.** Reactors, hosted services, background dispatch and scheduled jobs run with no HTTP request, so there is nothing for a tenant resolver to read. Chronicle observers are themselves instantiated per namespace, but the collaborators they call are not — a reactor that reaches a tenant-blind singleton has left its namespace behind without saying so. Such a flow states its tenant explicitly rather than inheriting whatever the root scope happens to be.
263
+
264
+ **Caching.** A process-wide cache of tenant data is the same bug wearing a performance justification. If a singleton caches, the tenant (and where relevant the user) is part of the key. The same holds for `static` fields: a `static` cache of anything tenant-scoped is shared by every tenant in the process.
265
+
266
+ **Enforce it, do not remember it.** This failure is silent, so review will not reliably catch it. Two gates, and an application wants both:
267
+
268
+ **Turn .NET's own scope validation on in every environment, not just Development.** `ServiceProviderOptions.ValidateScopes` rejects resolving a scoped service from the root provider, and `ValidateOnBuild` walks every registration at startup so a captive dependency fails the host immediately rather than at whichever request first happens to need it. The host enables both in Development by default and **neither outside it** — which is exactly backwards for a failure whose whole character is that it stays quiet:
269
+
270
+ ```csharp
271
+ builder.Host.UseDefaultServiceProvider(options =>
272
+ {
273
+ options.ValidateScopes = true;
274
+ options.ValidateOnBuild = true;
275
+ });
276
+ ```
277
+
278
+ Enable it on an existing codebase in this order, or it will simply refuse to start: turn it on locally first, fix everything it names, and only then let it reach the deployed environments. Turning it on *before* the sweep converts a silent multi-tenant bug into a production outage, which is a worse trade, not a braver one.
279
+
280
+ **And add an architecture spec**, because validation only catches what a run actually resolves. Reflect over the assembly, find every `[Singleton]` whose constructor takes a scope-bound service, and assert the set is empty. It is a few dozen lines, it names every offender in one pass rather than one per restart, and it holds for the types no startup path touches.
281
+
282
+ ### Discovering multiple implementations — use `IInstancesOf<T>`, never `IEnumerable<T>`
283
+
284
+ When a type needs every implementation of an abstraction (handlers, strategies, filters, validators, formatters), inject `IInstancesOf<TInterface>` from `Cratis.Types`. The framework discovers and instantiates every implementation by convention — no `services.AddSingleton<TInterface, Impl1>()` calls anywhere.
285
+
286
+ ```csharp
287
+ // ❌ Wrong — requires hand-maintained registrations for every implementation.
288
+ // Adding a new IReactorSideEffectHandler somewhere else in the codebase silently
289
+ // does nothing until someone remembers to register it here, and dead registrations
290
+ // linger after types are removed.
291
+ services.AddSingleton<IReactorSideEffectHandler, EventResultHandler>();
292
+ services.AddSingleton<IReactorSideEffectHandler, EventsResultHandler>();
293
+ services.AddSingleton<IReactorSideEffectHandlers, ReactorSideEffectHandlers>();
294
+
295
+ public class ReactorSideEffectHandlers(IEnumerable<IReactorSideEffectHandler> handlers) : IReactorSideEffectHandlers { ... }
296
+
297
+ // ✅ Right — implementations discovered automatically. Mark singletons with [Singleton].
298
+ [Singleton]
299
+ public class EventResultHandler(IEventTypes eventTypes) : IReactorSideEffectHandler { ... }
300
+
301
+ [Singleton]
302
+ public class EventsResultHandler(IEventTypes eventTypes) : IReactorSideEffectHandler { ... }
303
+
304
+ [Singleton]
305
+ public class ReactorSideEffectHandlers(IInstancesOf<IReactorSideEffectHandler> handlers) : IReactorSideEffectHandlers { ... }
306
+ ```
307
+
308
+ **Rules:**
309
+
310
+ - Never inject `IEnumerable<TInterface>` to enumerate implementations of an abstraction. That signature only works if every implementation is hand-registered, which defeats convention-based discovery. Use `IInstancesOf<TInterface>` instead.
311
+ - Never write `services.AddSingleton<TInterface, Impl>()` or `services.AddTransient<TInterface, Impl>()` for a type that exists to be discovered. Mark the implementation with `[Singleton]` (for singletons) or rely on the `IFoo → Foo` convention (for transients) and remove the registration line.
312
+ - `IInstancesOf<T>` fixes its type set at construction and resolves instances on each enumeration through the consumer's service provider (Fundamentals 7.23.0). It does not discover late-loaded implementations on access. A singleton consumer captures the root provider; it must not enumerate scoped implementations. Resolve a scoped or transient consumer inside a scope when any implementation is scoped.
313
+ - `IEnumerable<T>` is still the right type to **return** from a method that yields a sequence of values. The rule applies only when the goal is to enumerate **implementations** of an abstraction.
314
+
315
+ ## Logging
316
+
317
+ - Use structured logging with named parameters.
318
+ - Use `ILogger<T>` where `T` is the class name.
319
+ - Keep log messages in a separate `<ClassName>Logging.cs` partial static internal class.
320
+ - Use `[LoggerMessage]` attribute (without `eventId`).
321
+
322
+ ## Async
323
+
324
+ - Use `async`/`await` for asynchronous programming.
325
+ - Use `Task` and `Task<T>` for asynchronous methods.
326
+
327
+ ## Chronicle & Arc — Key API Types
328
+
329
+ These are the building blocks. Each type has a specific role in the vertical slice architecture — using the right type in the right place means the framework handles discovery, wiring, and proxy generation automatically.
330
+
331
+ | Type | Purpose |
332
+ | --- | --- |
333
+ | `ConceptAs<T>` | Strongly-typed domain *value* wrapper (see [concepts.md](./concepts.instructions.md)) |
334
+ | `EventSourceId<T>` | Strongly-typed *identity* base — derive event-source ids from this, not `ConceptAs<T>` |
335
+ | `[EventType]` | Marks a record as a Chronicle event — **never** pass arguments for a new event |
336
+ | `[Command]` | Marks a record as a model-bound command — define `Handle()` directly on the record |
337
+ | `[ReadModel]` | Marks a record as a model-bound query — define static query methods on the record |
338
+ | `CommandValidator<T>` | FluentValidation validator for commands |
339
+ | `IProjectionFor<T>` | Fluent projection definition — AutoMap is on by default, never call `.AutoMap()` |
340
+ | `IReducerFor<T>` | Imperative reducer — receives current state, returns new state |
341
+ | `IReactor` | Marker interface for side-effect observers — method dispatch by event type parameter |
342
+ | `IConstraint` | Constraint definition — enforced server-side by Chronicle at append time |
343
+ | `AggregateRoot` | Chronicle aggregate root with `Apply()` and `Commit()` |
344
+ | `ICommandPipeline` | Programmatic command execution from reactors or other code |
345
+ | `ICommandOperation` | `Cratis.Arc.Commands` marker for returned inline work with `Execute` and optional `Compensate`; see **cratis-arc-command-operation** |
346
+ | `CommandOperations` | Explicit ordered server-only operation batch; use instead of bare operation arrays/enumerables |
347
+ | `EventContext` | Event metadata: `Occurred`, `SequenceNumber`, `CorrelationId`, `EventSourceId`, etc. |
348
+ | `ISubject<T>` | Observable query return type — enables real-time push |
349
+ | `IMongoCollection<T>` | MongoDB collection — use `.Observe()` for reactive queries |
350
+
351
+ **Key conventions:**
352
+
353
+ - Prefer `ConceptAs<T>` over raw primitives in all domain models, commands, events, and queries; derive identity concepts from `EventSourceId<T>`. See [concepts.md](./concepts.instructions.md) for details.
354
+ - Projections join **events**, never read models — projections rebuild state from the event stream, not from other projections.
355
+ - For fluent projections, AutoMap is on by default — call `.From<EventType>()` without `.AutoMap()` and without manually mapping every matching property.
356
+ - Use model-bound projection attributes (`[FromEvent<T>]`, `[SetFrom<T>]`, etc.) when possible; fall back to `IProjectionFor<T>` for complex cases.
357
+ - Full slice anatomy lives in [vertical-slices.md](./vertical-slices.instructions.md).
@@ -0,0 +1,278 @@
1
+ ---
2
+ applyTo: "**/*.tsx"
3
+ profile: application
4
+ paths:
5
+ - "**/*.tsx"
6
+ ---
7
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
8
+
9
+ # Using Dialogs
10
+
11
+ The Cratis dialogs handle command execution, validation timing, busy state, focus, dismissal and footer buttons consistently. A vendor dialog, or a hand-rolled modal, bypasses all of this and leads to inconsistent UX — and a second focus trap around a Components dialog breaks keyboard and screen-reader behavior.
12
+
13
+ ## Choose the Correct Dialog Type
14
+
15
+ - If confirm executes a command, use `CommandDialog` from `@cratis/components/CommandDialog`.
16
+ - If no command is executed on confirm, use `Dialog` from `@cratis/components/Dialogs`.
17
+ - If you are **asking the user to confirm** or **showing that something is in progress**, do not build a dialog at all — raise the host-rendered one through its hook (below).
18
+ - **Never** use a vendor or hand-rolled modal. Dialogs are Components-owned; the optional renderer adapters do not replace them.
19
+
20
+ ## Confirmations and busy indicators are host-rendered — register once, raise by hook
21
+
22
+ `ConfirmationDialog` and `BusyIndicatorDialog` are **not** instantiated in a slice's JSX. They are
23
+ registered once at the app root and raised from anywhere through a hook, so every confirmation and
24
+ every busy indicator in the application looks and behaves identically:
25
+
26
+ ```tsx
27
+ import { DialogComponents } from '@cratis/arc.react/dialogs';
28
+ import { BusyIndicatorDialog, ConfirmationDialog } from '@cratis/components/Dialogs';
29
+
30
+ export const App = () => (
31
+ <DialogComponents confirmation={ConfirmationDialog} busyIndicator={BusyIndicatorDialog}>
32
+ <YourApp />
33
+ </DialogComponents>
34
+ );
35
+ ```
36
+
37
+ ```tsx
38
+ import { DialogButtons, DialogResult, useConfirmationDialog, useBusyIndicator } from '@cratis/arc.react/dialogs';
39
+
40
+ const [confirm] = useConfirmationDialog();
41
+ const answer = await confirm('Delete this alert?', `"${alert.title}" disappears permanently.`, DialogButtons.YesNo);
42
+ if (answer !== DialogResult.Yes) return;
43
+
44
+ const [showBusy, closeBusy] = useBusyIndicator('Importing', 'This takes a moment.');
45
+ showBusy();
46
+ try { await doTheSlowThing(); } finally { closeBusy(); }
47
+ ```
48
+
49
+ **Rules:**
50
+ - Register both in **exactly one** place — the app root. A second registration, or a slice building its own confirm/busy dialog, is how two of them end up looking different.
51
+ - Reach them only via `useConfirmationDialog` / `useBusyIndicator`. Never hand-roll a Yes/No `Dialog`, and never use `window.confirm`.
52
+ - `showConfirm()` resolves to a `DialogResult` — branch on the enum member, never on button text.
53
+ - Always pair `showBusy()` with `closeBusy()` in a `finally`; the busy dialog is deliberately non-dismissible, so a missed close leaves the user stuck.
54
+ - **A busy indicator is a modal for work that blocks the user.** For a quick command behind a button, an in-flight/disabled button (eventual-consistency rule 9) is the better control — a modal that flashes for 200 ms is worse than no modal. Use the busy dialog when the user genuinely cannot proceed.
55
+ - From a view model, use the injectable `IDialogs` abstraction (`@cratis/arc.react.mvvm/dialogs`) rather than the hooks — see [react.md](./react.instructions.md).
56
+
57
+ ## When Using `CommandDialog`
58
+
59
+ - Pass the command constructor to `command={}`. `CommandDialog` handles instantiation, execution, and confirm/cancel buttons.
60
+ - Use command form fields (`InputTextField`, `TextAreaField`, etc. from `@cratis/components/CommandForm`) for user-input values.
61
+ - `CommandDialog` automatically disables confirm while the command executes.
62
+ - Any value that must be present for the form to be considered valid (i.e. passes `validateRequiredProperties`) must be supplied via `initialValues`, **not** via `onBeforeExecute`.
63
+ - `onBeforeExecute` fires only at execution time — the command is already validated before it runs, so values set there never influence `isValid` and the OK/Submit button will remain permanently disabled.
64
+ - Use `initialValues` for injected context values (e.g. a parent entity id passed as a prop).
65
+ - Use `onBeforeExecute` only for transformations that should not affect form validity (e.g. generated IDs).
66
+
67
+ ```tsx
68
+ import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs';
69
+ import { CommandDialog } from '@cratis/components/CommandDialog';
70
+ import { InputTextField } from '@cratis/components/CommandForm';
71
+ import { RegisterProject } from './Registration';
72
+ import { Guid } from '@cratis/fundamentals';
73
+
74
+ export const AddProject = ({ closeDialog }: DialogProps) => {
75
+ return (
76
+ <CommandDialog<RegisterProject>
77
+ command={RegisterProject}
78
+ title="Add Project"
79
+ okLabel="Add"
80
+ cancelLabel="Cancel"
81
+ onBeforeExecute={(values) => {
82
+ values.projectId = Guid.create(); // generated, not user input
83
+ return values;
84
+ }}>
85
+ <InputTextField<RegisterProject>
86
+ value={instance => instance.name}
87
+ title="Project name"
88
+ placeholder="My Project"
89
+ />
90
+ </CommandDialog>
91
+ );
92
+ };
93
+ ```
94
+
95
+ To await the result from the parent:
96
+
97
+ ```tsx
98
+ const [AddProjectDialog, showAddProjectDialog] = useDialog(AddProject);
99
+
100
+ const [result] = await showAddProjectDialog();
101
+ if (result === DialogResult.Ok) {
102
+ // Dialog confirmed and command executed successfully
103
+ }
104
+ ```
105
+
106
+ ### `onSuccess` vs `onConfirm`
107
+
108
+ - **`onSuccess(response)`** fires only after the command succeeds and receives the typed command response — use it for `closeDialog(DialogResult.Ok, response)`, refreshing a query, or a toast.
109
+ - **`onConfirm()`** receives **no** command result. It is a close gate after successful execution: return `true` to let the wrapper close, `false`/`undefined` to keep it open. **Do not use `onConfirm` as a command-result handler.**
110
+
111
+ ### `onBeforeExecute` is a transformer
112
+
113
+ It receives the current command values and **must return them** (mutated or not). **Returning `void` does not execute with `undefined`** — the current values are kept and a `console.warn` is logged; still, always return the values. It runs only on submit — never use it to seed *required* values (validation runs against pre-transform state, so a value seeded here never makes the form valid and the submit button stays permanently disabled). Seed required values via `initialValues`; reserve `onBeforeExecute` for transforms that don't affect validity (e.g. a generated id).
114
+
115
+ ### CommandForm fields
116
+
117
+ Use built-in `CommandForm` fields (from `@cratis/components/CommandForm`) for every user-input value — a raw control (a `Common` `TextInput`, a native `<input>`) inside a command dialog is not bound to the command, so validation never re-runs and the submit button stays **permanently disabled**. Catalog: `InputTextField`, `PasswordField`, `NumberField`, `NumberInputField`, `DropdownField`, `CheckboxField`, `ToggleSwitchField`, `TextAreaField`, `CalendarField`, `RadioButtonField`, `RadioGroupField`, `ChipsField`, `MultiSelectField`, `ColorPickerField`, `SliderField`, `RatingField`. `AutoCommandForm` generates the field list from the command's properties (`string` → `InputTextField`, `number` → `NumberField`, `boolean` → `CheckboxField`, `Date` → `CalendarField`; other types need a registered field-type provider) — use it for plain forms, hand-write fields when the layout or a concept type needs it. Several `RadioButtonField`s bound to one property need the same explicit `name`.
118
+
119
+ - The `value={c => c.name}` **accessor lambda doubles as the binding and type-checked field selection** — renaming a command property surfaces a compile error at every binding. The binding is inferred from the accessor's *source text*, so a computed accessor (`c => c[descriptor.name]`) cannot be inferred — name it explicitly with `fieldName="…"` (Arc ≥ 22.16.0).
120
+ - **`RadioGroupField<T>`** renders a whole group from data (`options`/`optionLabel`/`optionValue`, `layout='horizontal'|'vertical'`); **`RadioButtonField<T>`** is one component per option (each takes a `buttonValue`). Both infer the value type from the accessor — no `as string` casts.
121
+ - **`asCommandFormField(Component, opts)`** (`asCommandFormField`, `WrappedFieldProps` from `@cratis/arc.react/commands`) wraps a custom input so it participates in `CommandForm` like a built-in. `WrappedFieldProps<T>` gives `{ value, onChange, invalid, required, errors }` (`errors` is `string[]` → `errors.join(', ')`); options are `{ defaultValue, extractValue: e => ... }`.
122
+ - **`useCommandInstance<TCommand>()`** (`@cratis/arc.react/commands`, no argument — reads the enclosing form's context) returns the live reactive instance the form is bound to — **read** it to drive dependent fields (e.g. read `command.country` to choose a `DropdownField`'s options); never mutate (mutations go through field bindings).
123
+
124
+ ### Opening dialogs — `useDialog` / `useDialogContext`
125
+
126
+ `useDialog<TResponse, TInput>(Component)` returns `[Wrapper, showFn, context]` (the third element is rarely needed): render `<Wrapper />` in JSX and call `showFn(input)` to open it; it resolves to `[DialogResult, TResponse?]` when the dialog closes. For a new dialog, prefer reading input as **plain typed props** (`<Name>Input`) and obtaining `closeDialog` from **`useDialogContext<TRequest, TResponse>()`** (request type first) — rather than declaring a props interface that extends `DialogProps` to thread both input and `closeDialog`. (Existing dialogs that destructure `closeDialog` from `DialogProps` remain valid.) Signal the outcome with `closeDialog(DialogResult.Ok | Cancelled, response?)`.
127
+
128
+ ### Multi-step wizards — `StepperCommandDialog`
129
+
130
+ For a command split across named steps use `StepperCommandDialog` (`@cratis/components/CommandDialog`) — see the **cratis-components-stepper-command-dialog** skill. Conditional steps written as `{condition && <StepperPanel/>}` are supported: only the steps that actually render are counted, so Next and Submit appear where the user expects them, and the active step is clamped if a late-resolving condition removes a step the user had passed. ⚠️ A `<>…</>` fragment wrapping several panels counts as **one** step — give each step its own `StepperPanel` child. Stepper parts are Cratis-owned (`root`, `list`, `step`, `header`, `number`, `title`, `separator`, `panels`, `panel`; `StepperParts` from `@cratis/components/CommandDialog`). When the wizard belongs inline in a page region rather than in a modal, use the sibling `CommandStepper` (`@cratis/components/CommandStepper`) — same steps, same command execution, no dialog chrome.
131
+
132
+ ## When Using `Dialog`
133
+
134
+ Use this for dialogs that collect data and return it without executing a command (e.g. confirmation prompts, pure data-entry dialogs). `Dialog` defaults to OK + Cancel buttons. Use `isValid` to control confirm button state, `okLabel`/`cancelLabel` to customize button text.
135
+
136
+ ```tsx
137
+ import { useState } from 'react';
138
+ import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs';
139
+ import { Dialog } from '@cratis/components/Dialogs';
140
+ import { TextInput } from '@cratis/components/Common';
141
+
142
+ export const AddProject = ({ closeDialog }: DialogProps<{ name: string }>) => {
143
+ const [name, setName] = useState('');
144
+ const isValid = name.trim().length > 0;
145
+
146
+ return (
147
+ <Dialog
148
+ title="Add Project"
149
+ width='32rem'
150
+ isValid={isValid}
151
+ onConfirm={() => closeDialog(DialogResult.Ok, { name })}
152
+ onCancel={() => closeDialog(DialogResult.Cancelled)}
153
+ >
154
+ <TextInput
155
+ value={name}
156
+ onChange={value => setName(value)}
157
+ aria-label="Project name"
158
+ />
159
+ </Dialog>
160
+ );
161
+ };
162
+ ```
163
+
164
+ ## Prefer `DialogButtons` over Custom Button JSX
165
+
166
+ Use the built-in `DialogButtons` enum instead of rendering manual `<Button>` elements in the `buttons` prop:
167
+
168
+ ```tsx
169
+ import { DialogButtons, DialogResult, useDialogContext } from '@cratis/arc.react/dialogs';
170
+ ```
171
+
172
+ ### Available Button Sets
173
+
174
+ | `buttons` value | Shows |
175
+ |---|---|
176
+ | `DialogButtons.OkCancel` | Ok + Cancel |
177
+ | `DialogButtons.YesNo` | Yes + No |
178
+ | `DialogButtons.YesNoCancel` | Yes + No + Cancel |
179
+ | `DialogButtons.Ok` | Ok only |
180
+ | `null` | No buttons (content-only dialog) |
181
+
182
+ ## Customizing Built-in Buttons
183
+
184
+ Use `okLabel`/`cancelLabel` to rename the buttons, and `isValid` to disable the confirm button:
185
+
186
+ ```tsx
187
+ <Dialog
188
+ title="Import Orders"
189
+ visible={true}
190
+ buttons={DialogButtons.OkCancel}
191
+ okLabel="Upload"
192
+ isValid={!!file && !isUploading}
193
+ onConfirm={handleUpload}
194
+ onCancel={() => closeDialog(DialogResult.Cancelled)}
195
+ >
196
+ ```
197
+
198
+ ## Validation Guard — Keep Dialog Open on Failure
199
+
200
+ When `onConfirm` needs to keep the dialog open (e.g. a command fails), annotate the handler as `Promise<boolean>` and return `false` to block the close. Return `true` to let the Dialog close itself:
201
+
202
+ ```tsx
203
+ const handleConfirm = async (): Promise<boolean> => {
204
+ const result = await myCommand.execute();
205
+ if (!result.isSuccess) return false; // dialog stays open
206
+ return true; // dialog closes
207
+ };
208
+ ```
209
+
210
+ > **TypeScript note:** Always annotate the function as `Promise<boolean>`. Without it TypeScript infers `Promise<false | void>` which does not satisfy the `ConfirmCallback` type.
211
+
212
+ ## Passing Result Data on Confirm
213
+
214
+ When the dialog must return data to its caller (e.g. a postal code lookup result), use `onClose` and call `closeDialog` manually, returning `false` to prevent the Dialog from calling it a second time:
215
+
216
+ ```tsx
217
+ <Dialog
218
+ title="Confirm Location"
219
+ visible={true}
220
+ buttons={DialogButtons.OkCancel}
221
+ isValid={isValid}
222
+ onClose={(result) => {
223
+ if (result === DialogResult.Ok) {
224
+ closeDialog(DialogResult.Ok, { postalCode, city, latitude, longitude } as MyResult);
225
+ return false; // prevent Dialog from calling closeDialog(Ok) again
226
+ }
227
+ // Cancelled: return undefined so the Dialog calls closeDialog(Cancelled)
228
+ }}
229
+ >
230
+ ```
231
+
232
+ ## Content-only Dialogs (No Action Buttons)
233
+
234
+ Use `buttons={null}` for dialogs that contain their own internal actions (e.g. a menu + data table) and don't need a confirm/cancel footer:
235
+
236
+ ```tsx
237
+ <Dialog
238
+ title="Hubs"
239
+ visible={true}
240
+ width="50vw"
241
+ buttons={null}
242
+ onCancel={() => closeDialog(DialogResult.Cancelled)}
243
+ >
244
+ <Menubar model={menuItems} />
245
+ <Listing configurationId={configurationId} />
246
+ </Dialog>
247
+ ```
248
+
249
+ ## Props Reference
250
+
251
+ | Prop | Type | Notes |
252
+ |---|---|---|
253
+ | `title` | `string` | Header text |
254
+ | `subtitle` | `ReactNode` | Secondary header content |
255
+ | `placement` | `'center' \| 'start' \| 'end'` | Defaults to center; start/end are RTL-aware side sheets |
256
+ | `closeIcon` | `ReactNode` | Header dismissal glyph; `null` hides only the glyph |
257
+ | `visible` | `boolean` | Controlled open state; defaults to `true` |
258
+ | `buttons` | `DialogButtons \| ReactNode \| null` | Prefer `DialogButtons` enum; `null` for no footer |
259
+ | `isValid` | `boolean` | Disables the confirm button when `false` |
260
+ | `isBusy` | `boolean` | Disables every action **and every dismissal path** while work is in flight |
261
+ | `initialFocus` | `DialogInitialFocus` | Where focus lands on open; defaults to the confirmation action — the **cratis-components-accessibility** skill covers the armed-Enter hazard |
262
+ | `okLabel` / `cancelLabel` / `yesLabel` / `noLabel` | `string` | Per-dialog label; falls back to the provider's `messages.dialog`, then English |
263
+ | `closeAriaLabel` | `string` | Accessible name of the header close button; same fallback chain |
264
+ | `onConfirm` | `() => boolean \| void \| Promise<boolean> \| Promise<void>` | Called when Ok is clicked; return `false` to keep dialog open, `true` to close |
265
+ | `onCancel` | `() => boolean \| void \| Promise<boolean> \| Promise<void>` | Called when Cancel is clicked |
266
+ | `onClose` | `(result: DialogResult) => boolean \| void \| Promise<...>` | Combined handler for both Ok and Cancel |
267
+ | `width` | `string` | Dialog width (e.g. `'50vw'`); defaults to `450px` |
268
+ | `dismissable` | `boolean` | Header close, Escape and backdrop dismissal (when not busy) |
269
+ | `style` / `contentStyle` / `className` | | Modal-root and content-region styling |
270
+ | `pt` | `DialogParts` | Per-part attributes: `backdrop`, `positioner`, `root`, `header`, `title`, `close`, `content`, `footer`, `confirm`, `cancel` |
271
+
272
+ `CommandDialog` forwards these Dialog presentation props (since Components 4.14.1).
273
+ It also accepts `confirmBeforeExecute(values) => boolean | Promise<boolean>`:
274
+ after validation and `onBeforeExecute`, only `true` executes. Fields are disabled
275
+ while confirmation is pending; changed command values discard approval without executing.
276
+ This is a pre-execution gate, unlike the post-success `onConfirm` close gate.
277
+
278
+ `resizable`, `ptOptions` and `unstyled` are accepted for source compatibility and do nothing. There is no `modal`, `draggable`, `footer` or `onHide` — the dialog is always modal, the footer comes from `buttons`, and closing is reported through `onCancel`/`onClose`. Style the dialog through its parts (`pt`, or `[data-cratis-part='root']` selectors), never through React Aria class names.