# Setup, options and services

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from PkOptions.cs, PkLogging.cs, PkTimeZone.cs, PkRuntime.cs, PkAssets.cs and the extension methods. Do not edit: change the source and run the generator.

Register the services, add `<PkStyles />` once, first in the `<head>` of `App.razor` (a plain in-place link: it must come before the app's own stylesheets), add `<PkScripts />` at the end of `<body>` (one external module script that starts the elements, CSP-safe, no hand-written import), and add the assembly to the router only when you serve the dev tools page. `AddPlainKit(Action<PkOptions>?)` registers `PkRuntime`, `IPkLog`, the options and the dev tools services. `AddPlainKitDevTools()` (chained after `MapRazorComponents`) makes `/_plainkit` routable. At startup the runtime compares the JavaScript version with `PkAssets.Version` and logs one warning (`ILogger` category `PlainKit.blazor`, SDK scope `blazor`) when they differ. The toolkit is served as static web assets under `_content/PlainKit.Blazor/plainkit/`.

## PkOptions

| Member | Description |
|---|---|
| `public bool? DevTools` | Whether the dev tools page (`/_plainkit`) is served and the `PkDevTools` dock renders anything. Null, the default, means only when the app runs in the Development environment (a dock left in a layout is inert in production); set true or false to decide explicitly. |
| `public string? SourceRoot` | The folder the dev tools' Files tab browses. Null means the app's content root. Server-side rendering only. |
| `public Func<string, bool>? SourceExclude` | Skips a folder (relative path, forward slashes) when the Files tab is built. Return true to skip. |
| `public IReadOnlyList<PkScoreTarget>? ScoreTargets` | What the dev tools' Scorecard tab scores. Null means the toolkit's own page templates. |
| `public PkLoggingOptions Logging` | The SDK logger's settings and the ILogger bridge: level, per-scope levels, routes, and whether SDK entries also go to ILogger. |
| `public PkTimeZoneOptions TimeZone` | The organization's display timezone (IANA id, Windows id and DST-fallback rules), resolved by IPkTimeZoneResolver. Unset, the resolved zone is the synthetic fallback with a zero offset. |

## PkLoggingOptions

| Member | Description |
|---|---|
| `public PkLogLevel? Level` | The SDK's global level, applied at startup with `configureLogging`. Null leaves what the page decided (default `warn`, or the URL, page attribute or saved settings). |
| `public IDictionary<string, PkLogLevel> Scopes` | A level per scope (`loader`, `pk-dialog`, ...), which wins over Level for that scope. |
| `public IDictionary<PkLogLevel, IList<string>> Routes` | Where each level is sent: the output names `console`, `toast`, `alert` or one an app registered. A level not listed keeps its routing. |
| `public bool ForwardToILogger` | Whether SDK entries are also written to ILogger (category `PlainKit.<scope>`). Off by default. |
| `public PkLogLevel ForwardMinimumLevel` | The lowest SDK level that is forwarded (default Warn). The SDK sends every entry to its sinks whatever its level, so this is the forwarder's own filter, independent of Level. |
| `public IList<string> ForwardScopes` | The scopes that are forwarded. Empty means all. An entry ending in `*` matches a prefix (`pk-*`). |
| `public IList<string> ForwardExcludeScopes` | Scopes that are never forwarded; same matching as ForwardScopes, and it wins over it. |

## IPkLog

| Member | Description |
|---|---|
| `ValueTask WriteAsync(PkLogLevel level, string scope, string message, string? detail = null)` | Writes an entry. scope names the speaker (an app's own scope, e.g. `checkout`); Silent writes nothing. |
| `ValueTask SetLevelAsync(PkLogLevel level)` | Sets the SDK's global log level. |
| `ValueTask ConfigureAsync(PkLoggingOptions settings)` | Merges settings into the SDK's logging configuration (level, per-scope levels, routes). The forwarding settings are read only at startup. |

## PkRuntime

| Member | Description |
|---|---|
| `public PkInteropLog Interop` | The calls made through the bridge so far: count, duration and errors per function. |
| `public bool IsInitialized` | True once EnsureInitializedAsync has finished. |
| `public Task EnsureInitializedAsync(ResourceAssetCollection? assets = null)` | Wires the `pk-*` elements and behaviours for the page once, however many components ask. Pass a component's own `Assets` (from `Microsoft.AspNetCore.Components.Web`, a ComponentBase member) when you have one: the bridge is then imported from its fingerprinted URL, which the host serves with a year-long, immutable `Cache-Control` once it calls `app.MapStaticAssets()`. The first caller's value wins (later, differing calls do not re-import). Omitted, the bridge loads from its plain path (still correct, just revalidated on every warm visit). |
| `public async ValueTask<string> GetSdkVersionAsync()` | The version of the Plainkit JavaScript the page loaded (`PK_VERSION` in `js/version.js`). It equals Version unless the app serves an older or newer copy of the assets. |
| `public async ValueTask<IReadOnlyDictionary<string, string>> ReadFormValuesAsync(ElementReference form)` | The values the controls of a form hold right now, by control name (what the browser would submit; a name used twice keeps its last value). A form reset gives the controls their initial values again without raising a change event, so a value your component mirrors is stale afterwards: call this from `PkForm.OnReset` (raised after the controls have their initial values) and copy the values back. <param name="form">The PkForm's Element (it wraps a native `<form>`, which is the one read), or a native form's reference.</param> |

## PkAssets

| Member | Description |
|---|---|
| `public const string Root = "_content/PlainKit.Blazor/plainkit/"` | The folder holding the toolkit's `dist`; every other path here is relative to it. |
| `public const string Css = Root + "plainkit.css"` | The page stylesheet: tokens, base, utilities and the layout the custom elements rely on. |
| `public const string CssMin = Root + "plainkit.min.css"` | The minified page stylesheet (the same rules as Css). |
| `public const string GalleryEmbed = Root + "gallery/embed.html"` | The page `<pk-gallery>` frames. |
| `public static readonly string[] PreloadPaths = ["js/init.js", "js/loader.js", "js/log.js", "js/element.js", "js/element-core.js"]` | Paths (relative to Root) worth a `<link rel="modulepreload">` hint on a page that uses `pk-*` elements: the small entry, the loader, its logger, and the base class every element shares. Without the hint the browser still fetches the same files, but only one at a time, each discovered by parsing the one before it; the hint lets it fetch them in parallel instead, so the page's first element upgrades sooner. The exact element modules are not here: which ones a page needs depends on its markup. See `PkStyles`'s `Preload` parameter. |
| `public static string CssVersioned` | Css with a content-hash cache-busting query (`?v=...`). |
| `public static string CssMinVersioned` | CssMin with a content-hash cache-busting query (`?v=...`). |
| `public static string Versioned(string path)` | The URL of one of the toolkit's files with a cache-busting query built from the file's content hash: `?v=` and the first 12 characters of its SHA-256 digest (base64url). The hash comes from the package's `manifest.json`, which travels inside the assembly and is read once, so a call does no file access and the URL changes exactly when the file's content does. Pass a path relative to Root (`plainkit.css`, `js/plainkit.js`) or a full one (Css, GalleryEmbed). A file the manifest does not know is returned unchanged. |
| `public static string? Integrity(string path)` | The Subresource Integrity value (`sha256-...`) of one of the toolkit's files, for an `integrity` attribute, or null when the manifest does not list it. Same paths as Versioned. |
| `public static string Version` | The Plainkit release this package is (SemVer with a pre-release part while the SDK is in alpha, such as `MAJOR.MINOR.PATCH-alpha.N`). The SDK and this package always share one version, taken from `core/VERSION`; GetSdkVersionAsync asks the JavaScript assets the package serves for theirs. |

## Reconnect banner

The framework's own reconnect dialog is unthemed and, after a server restart or deploy, stays on "Rejoin failed" because the old circuit can never be resumed. Set `PkOptions.Reconnect` (`AddPlainKit(o => o.Reconnect = new())`) and `PkApp` starts a themed `pk-alert` banner, or place `<PkReconnect />` once directly in `<body>` of `App.razor` (it writes the `#components-reconnect-modal` element the framework drives and one external module script, so it passes a strict CSP). It needs .NET 10, the only version the package targets, which raises `components-reconnect-state-changed`. States: reconnecting (spinner, attempt number, Reload), failed ("Cannot reach the server": Retry now and Reload), rejected or resume-failed ("This session ended": a visible 5 s countdown with Cancel, then an automatic reload), paused (Resume). `PkReconnectOptions`: `AutoReload` (`Never`, `Gone` the default, or `Always`, which after a failure also probes the same-origin `ProbeUrl` with growing pauses for about five minutes and reloads when it answers), `CountdownSeconds` (5), `Modal` (everything behind the banner becomes inert), `Overlay` (a desktop banner floats over the page instead of reserving its height, so nothing moves), `Strings` (`PkReconnectStrings`: translate any text; `{0}` is the number). On a desktop (wider than 640 px) the banner reserves its own height at the top, so it never covers the app bar (the app frame gives that height up); on a phone it is a layer over the page, and its buttons show only an icon (the label stays as the accessible name), with the text beside the icon on a desktop, every button a 44 px touch target. At most three automatic reloads per minute, so a server that keeps dropping circuits cannot loop. The policy is `plainkit/js/reconnect.js`; the C# side only passes the options.

## Reading form values after a native reset

A form reset (`<button type="reset">`, or `PkForm`'s own Reset) raises no change event, so a two-way-bound value stays stale afterwards: in the form's `OnReset` callback, call `await Runtime.ReadFormValuesAsync(form.Element)` (inject `PkRuntime`, keep the `PkForm` with `@ref`, and name every control) and copy the returned values back into your model.

## Display timezone

Inject `IPkTimeZoneResolver` to render every timestamp in one fixed, configured zone regardless of the host's own OS timezone. Set `PkOptions.TimeZone.IanaId` (tried first) and `WindowsId` (tried when the IANA id does not resolve, e.g. on a trimmed or globalization-invariant host); set `BaseUtcOffset`, `StandardName`, `DaylightName` and `AdjustmentRules` to describe a synthetic fallback zone used when neither id resolves, so the feature degrades instead of throwing. `Resolution.Source` (`Iana`, `Windows`, `Fallback`) says which path was taken, and a `Windows` or `Fallback` resolution is also logged as a warning. `ToConfiguredZone(DateTime)` converts, treating `DateTimeKind.Unspecified` as UTC (the common case for a timestamp read back from a database column with no kind attached), not the host's local kind.

## PkTimeZoneOptions

| Member | Description |
|---|---|
| `public string? IanaId` | The IANA id (e.g. `America/New_York`), tried first. |
| `public string? WindowsId` | The Windows registry id (e.g. `Eastern Standard Time`), tried when the IANA id does not resolve (a host where only the Windows database is present, or vice versa). |
| `public TimeSpan BaseUtcOffset` | The zone's standard (non-DST) UTC offset. Used only to build the Fallback zone. |
| `public string? StandardName` | The fallback zone's standard-time display name. Null defaults to IanaId (or WindowsId). |
| `public string? DaylightName` | The fallback zone's daylight-time display name. Null defaults to StandardName. |
| `public TimeZoneInfo.AdjustmentRule[]? AdjustmentRules` | The DST transition rules for the fallback zone, e.g. built with `TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule`. Null or empty means the fallback zone never observes DST (a fixed offset). |

## IPkTimeZoneResolver

| Member | Description |
|---|---|
| `DateTime ToConfiguredZone(DateTime value)` | Converts value to Resolution's zone; see ConvertToZone. |

## Shared state: store, settings, theme

Inject `IPkStore`, `IPkSettings` or `IPkTheme` (scoped; `AddPlainKit()` registers them) for per-viewer state in browser storage. Values are strings, booleans or numbers (the type of a key is the type of its default). It is persisted as JSON `{"v":1,"data":{...}}` under `pk.<moduleId>`, the same envelope and key format as the JavaScript `createStore` (js/store.js), so a JavaScript app and a Blazor app on one origin read each other's data; the theme is `pk.theme` (key `theme`), settings are `pk.settings-<module>`. Stored data is untrusted: corrupt, oversized (64 KB), wrong-type, unknown-key or other-version data gives the defaults and one logged warning, never an exception. A module sees only its own keys; other modules see its `Publish` keys through `IPkStore.Read`, a copy. Nothing reaches JavaScript until you open a module, so open them in `OnAfterRenderAsync(firstRender)`; unavailable storage keeps the state in memory. Never store secrets here: it is plain `localStorage`. There is no migration hook yet: data of another version falls back to the defaults.

```csharp
@inject IPkTheme Theme
@inject IPkSettings Settings
@inject IPkStore Store

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!firstRender) return;
    await Theme.InitializeAsync();                                   // reads pk.theme, applies data-theme
    var size = await Settings.GetAsync("reports", "page-size", 25);  // the fallback when unset or invalid
    await Settings.SetAsync("reports", "page-size", 50);
    var m = await Store.OpenAsync("reports", new PkStoreSpec { Defaults = new Dictionary<string, object?> { ["density"] = "cozy" }, Persist = ["density"] });
    m.Changed += _ => InvokeAsync(StateHasChanged);
}
```

## IPkStore

| Member | Description |
|---|---|
| `ValueTask<IPkStoreModule> OpenAsync(string id, PkStoreSpec spec)` | Opens the module id (lowercase letters, digits, `_` and `-`, up to 40). A bad or taken id throws: it is a code mistake. |
| `IReadOnlyDictionary<string, object?> Read(string id)` | Another module's published keys as a read-only copy (empty when none or unknown). |

## PkStoreSpec

| Member | Description |
|---|---|
| `public int Version` | The version written with the data. Stored data of a newer version, or of an older one without Migrate, falls back to the defaults. |
| `public Func<IReadOnlyDictionary<string, object?>, int, IReadOnlyDictionary<string, object?>?>? Migrate` | Turns stored data of an older version (data, fromVersion) into the current shape; the result is validated like any stored data. Return null or throw for defaults. |
| `public IReadOnlyDictionary<string, object?> Defaults` | Every key of the module with its default. Keys outside this list are refused. |
| `public IReadOnlyList<string> Persist` | The keys written to browser storage; the rest stay in memory. |
| `public IReadOnlyList<string> Publish` | The keys other modules can see through Read. |
| `public IReadOnlyDictionary<string, PkStoreRule> Rules` | Extra rules per key. |

## IPkStoreModule

| Member | Description |
|---|---|
| `object? Get(string key)` | The value of a key (its default when never set); null for a key the module does not have. |
| `ValueTask<bool> SetAsync(string key, object? value)` | Sets a key; false (and one warning) instead of an exception when the key is unknown or the value invalid. |
| `ValueTask ResetAsync()` | Back to the defaults, saved as such. |

## IPkSettings

| Member | Description |
|---|---|
| `ValueTask<bool> SetAsync(string module, string key, object value)` | Saves a setting; false when the module or key name is invalid or the value is not a string, boolean or number. |

## IPkTheme

| Member | Description |
|---|---|
| `ValueTask InitializeAsync()` | Reads the stored theme and applies it. Safe to call again. |
| `ValueTask<bool> SetAsync(string theme)` | Sets and saves the theme; false for anything but `light` or `dark`. |
| `ValueTask ToggleAsync()` | Switches between light and dark. |

## Notifications and dialogs

Inject `IPkNotifications` (toasts: `InfoAsync`, `SuccessAsync`, `WarnAsync`, `ErrorAsync`) and `IPkDialogs` (modals: `ConfirmAsync`, `AlertAsync`, `PromptAsync`, `OpenAsync`), the Blazor side of `ctx.notify` and `ctx.dialogs` (js/notify.js, js/dialogs.js) over the existing `pk-toast` and `pk-dialog`. Info and success last 4 s, warn 8 s, error stays until dismissed (`duration` overrides, zero = sticky); the same kind and title within 2 s is merged. One dialog is open at a time and the rest queue; Escape and the close button cancel, the backdrop only with `Backdrop`; cancelled is `false` or `null`. Disposing the dialogs service (leaving the page) cancels its dialogs. A page that derives from `PageBase` runs an event-handler action with `await TryBusyAsync(action, label)` (busy, log and danger status like `BusyAsync`, but nothing is rethrown; it returns whether it succeeded, so no empty catch; override `DescribeError(Exception)` to word the status, for example from the inner exception) and has `ShowSuccess(title, details?)`, `ShowWarning(...)` and `ShowError(...)` over the same service (`ShowError(exception, title?)` logs and shows the message of a user-facing exception as it is, any other as a generic line). A `PkRecordEditor` toasts "Saved", "Deleted" and "Could not save" or "Could not delete" (the error as details) when it is given `Notify = notifications`; `Messages = new PkRecordMessages(Saved: "Order saved", Deleted: "")` words them or silences one with an empty string. Without `Notify` it toasts nothing. A component that derives from `PkComponentBase` repeats work (a live elapsed-time label, a poll) with `await RunEveryAsync(period, async cancel => ...)` from `OnAfterRenderAsync(firstRender)` or an event handler: it re-renders after each tick, never overlaps a slow tick, logs a failing tick and keeps going, and stops when the component or the returned handle is disposed (the timer is `js/tick.js`; do not keep a timer or a `CancellationTokenSource` of your own). Text is shown as text, never markup. A confirm, prompt or open dialog takes `OnConfirm` (`Func<Task<string?>>`): it runs when the confirming button is pressed while the dialog stays open with that button busy and the rest inert; return null to close (the call answers as confirmed) or a message to keep the dialog open and show it inside (cancel or retry); an exception shows a generic message and is logged. When the action needs what was typed, set `OnConfirmValues` (`Func<PkDialogResult, Task<string?>>`) instead: it gets the trimmed values by name (a prompt text is `Values["value"]`) and the pressed button in `Action`, and a returned message keeps the dialog open with the typed text, so a server-side rejection never loses the input. A `PkDialogField` with `Type = "checkbox"` is a checkbox whose value comes back in `PkDialogResult.Values` as "true" or "false" (a prompt that also needs a toggle is an `OpenAsync` with a text field and a checkbox field). Not supported from C#: a dialog `template` or a `validate` callback (use `Required` and `MaxLength`). Call from an event handler or `OnAfterRenderAsync`: prerendering has no page, so a call does nothing and logs one warning.

```csharp
@inject IPkNotifications Notify
@inject IPkDialogs Dialogs

async Task Delete()
{
    if (!await Dialogs.ConfirmAsync(new() { Heading = "Delete order 1042?", Message = "This cannot be undone.", ConfirmLabel = "Delete", Danger = true })) return;
    await Notify.SuccessAsync("Order deleted");
}
```

## IPkNotifications

| Member | Description |
|---|---|
| `ValueTask InfoAsync(string title, string? details = null, TimeSpan? duration = null)` | An informational toast. |
| `ValueTask SuccessAsync(string title, string? details = null, TimeSpan? duration = null)` | A success toast. |
| `ValueTask WarnAsync(string title, string? details = null, TimeSpan? duration = null)` | A warning toast. |
| `ValueTask ErrorAsync(string title, string? details = null, TimeSpan? duration = null)` | An error toast (until dismissed, unless duration is given). |

## IPkDialogs

| Member | Description |
|---|---|
| `ValueTask<bool> ConfirmAsync(PkDialogOptions options)` | Asks yes or no: true when confirmed. |
| `ValueTask AlertAsync(PkDialogOptions options)` | Tells something; completes when dismissed. |
| `ValueTask<string?> PromptAsync(PkDialogOptions options)` | Asks for one line of text: the trimmed text, or null when cancelled. |
| `ValueTask<PkDialogResult?> OpenAsync(PkDialogOptions options)` | A form dialog from Fields and Actions; null when cancelled. |


## Page types with callbacks: `PkToolPage`, `PkSettingsPage` and `PkListPage<TItem>`

The Blazor side of `pk-tool-page`, `pk-settings-page` and `pk-list-page` (the `tool`, `settings` and `list` page types). `Config` (JSON) is data; the business logic is a C# delegate the component hands to the element, so a page is a config string plus one callback. `PkToolPage` `Run` gets the input values keyed by field key (`JsonElement`: `GetString()`, `GetDouble()`) and returns what the outcome shows (text, `{ value, label }` for a stat, `{ columns, rows }` for a table, code). `PkSettingsPage` `Save` gets the values the same way; set `Values` again once loaded values arrive to refresh the fields. A delegate that throws shows the element's own error state (Retry on the tool and list pages, the exception message in the settings status bar). `PkListPage<TItem>` `Load` gets a `PkListRequest` (`Page`, `PageSize`, `SortKey`, `Descending`, `Search`, `Filters`, `Skip`) and returns a `PkListResult<TItem>` (`Items`, `Total`), the types `PkDataTable` uses; column keys in `Config` are camel-case property names; it reloads on every sort, search, filter or page change; `rowHref` is not available yet. `PkListPage` `BeforeListContent` renders your markup (a status alert, a summary card) between the title bar and the list; it scrolls with the page and `stickyHeader` still pins the column header and pager. SettingsPage` `SectionContent` (a dictionary of `RenderFragment` by the section `key` in `Config`) shows your own markup inside that section card after its fields, covered by the section nav and the save bar (a control in it that raises `pk-value-change` marks the form changed; its values are not in `Save`, keep them in your own state); `PkToolPage` `ChildContent` replaces the generated input, Run button and outcome with your own content `PkDashboardPage` `Load` gets a widget key (`config.widgets[].key`), once per widget and independently, and returns what that widget draws (an object with `value` for a stat, the chart properties for a chart); `PkDocPage` `LoadItem` gets the item id and returns `{ title, summary, html }` (null shows the not-found state); a delegate that throws shows that card or the page body's error state with Retry. For selection set `selectable` (and `rowKey`, `bulkActions`: [{ id, label }]) in `Config` and handle `OnSelect` (`PkSelectEventArgs`: `Selected`, `Scope`, `Query`, `ToRequest()`) and `OnBulk` (`PkBulkEventArgs`: `Action` plus the same, and `ToRequest()`); for scope `all` act on the query, never the ids. `PkDataTable` adds `Selected`, `BulkContent` and large-selection compaction. Page events: every page with a title bar raises `OnAction` (`PkActionEventArgs.Key`, a `config.actions` entry without an href: dashboard, master-detail, record, settings, wizard, workspace); `PkRecordPage` adds `OnDirty` (`Dirty`, for an unsaved-changes guard) and `OnSave`, `PkWizardPage` `OnDirty` and `OnSubmit`; `PkWizardPage.Submit` gets the answers keyed by field name and may return `new { errors = new { field = "message" } }` to mark fields inline; `TitleContent` fills the `title` slot of `PkToolPage`, `PkNotFoundPage` and `PkStatesPage`. Not wrapped yet: the wizard Validate, Load and MountStep, the workspace Mount, the doc page `pk-navigate` and slots, typed Config and Values. `PkDocPage.Href` and the app and module components are later steps (#364, #366).

```razor
<PkToolPage Config="@_config" RunLabel="Count" Run="CountAsync" />
<PkSettingsPage Config="@_sections" Values="@_values" Save="SaveAsync" />
<PkListPage TItem="Order" Config="@_list" Load="LoadAsync" />

@code {
    // Config: { "input": [{ "key": "text", "type": "textarea", "label": "Text" }], "outcome": "stat" }
    Task<object?> CountAsync(IReadOnlyDictionary<string, JsonElement> v) => Task.FromResult<object?>(new { value = v["text"].GetString()!.Split(' ').Length, label = "Words" });
    async Task SaveAsync(IReadOnlyDictionary<string, JsonElement> v) => await Settings.SetAsync("app", "density", v["density"].GetString());
    async Task<PkListResult<Order>> LoadAsync(PkListRequest r) => await Orders.PageAsync(r.Search, r.SortKey, r.Descending, r.Skip, r.PageSize);
}
```
