[@layout]: ../docs.layout.md

# Personal Sources and device settings

Saved responses, progress, and other committed work belong to Sources. Device
preferences and draft recovery are disposable: clearing them does not remove saved work.

The host stores Sources on disk or in a browser's IndexedDB repository; ordinary
Views admit and read them in either environment.

---

<!-- id: storage-requirements -->

## Before saving personal work

Saving a response, progress, or an attachment requires an admitted Actor and a
writable Home. Use the [local account setup](../accounts/auth.reference.md#local-actor)
for development, then configure credentials and persistent writable storage for
shared hosting. A [browser publication](./core.reference.md#browser-publications)
provides a local Actor and writable Home on one device without sign-in; its saved
files do not synchronize to a server. An ordinary static export does not provide
a writable Source host. Disposable drafts and device settings are separate from
committed work.

The Home directory's canonical index Source owns its privacy claim with
`access: { type: home, owner: /canonical/actor.id }`. The Actor Source itself does
not carry a `home` field. State writers resolve the current claim through the
installed access policy, so a missing or Home-free Actor cannot create personal
State accidentally.

---

## Save Input and progress in the browser

Install Completion in the project root:

```sh
bun add @pathmx/completion@beta
```

Create `plugins/personal.plugin.ts`:

```ts
import { createInputPlugin } from "@pathmx/core/plugins"
import { createCompletionPlugin } from "@pathmx/completion"

const input = createInputPlugin()
const completion = createCompletionPlugin({
  storage: "actor",
  derived: [input.completion],
})
export default [input.plugin, completion.plugin]
```

Use the normal [Input controls](../authoring/input.reference.md) and
[Completion components](../learning/completion.reference.md). Configure a
[browser publication](./core.reference.md#browser-publications) with a local Actor
and writable Home. The publication runs the same Source Actions and reads as a
server, without a sign-in or Action server. Saved work stays in that browser and
works offline after installation; it does not synchronize across devices.

The host owns the repository name, local Actor, and Home. Keep those stable across
publication updates. Domain Plugins do not select a separate browser persistence
mode. Source-shared Completion uses `storage: "source"` to edit authored tasks;
`storage: "actor"` keeps each reader's progress in their Home.

A browser publication downloads its Sources and selected runtime Plugins. Ordinary
static export remains suitable for reading and disposable browser interactions;
committed personal work requires a host that can write Sources.

---

## Export and restore saved files

In a browser publication, include:

```md
<x-browser-files-tools />
```

The controls export, import, or reset the configured personal folder. The archive
preserves Markdown Sources and binary files, excludes published files, and belongs
to that named browser repository. Import validates its folder and file entries
before atomically replacing saved files; other tabs refresh from the new Sources.
Archives are limited to 64 MB and 10,000 files. Export a copy before resetting or
replacing your work. Browser storage is not a remote backup.

Draft recovery and device preferences are separate from this archive. Clearing
those disposable records does not erase a submitted response or completed task.

---

## Add device settings

Install `@pathmx/ui@beta` and its React peer dependencies:

```sh
bun add @pathmx/ui@beta react@^19 react-dom@^19
```

Create `plugins/settings.plugin.ts`:

```ts
import { createSettingsPlugin } from "@pathmx/core/settings"
import SettingsUIPlugin from "@pathmx/ui/settings"

export default [
  createSettingsPlugin({ namespace: "my-site" }).plugin,
  SettingsUIPlugin(),
]
```

Add `<x-settings-ui-trigger />` to a page or layout. The UI reads setting schemas
supplied by installed Plugins, including Theme and Player. A root index Source is
required. Use a stable site namespace to keep independent sites' overrides separate.

These lightweight device overrides work in ordinary static exports. They survive
reloads when browser storage permits; if persistence fails, the current document
retains the preference in memory. Resetting or losing them restores authored/default
preferences. Account-level preferences should be modeled as Sources.

---

## Define a setting

Use the Settings setup above. Run `bun add zod`, then create
`plugins/reading/index.plugin.ts`:

```ts
import { definePlugin } from "@pathmx/core"
import { z } from "zod"

export default definePlugin({
  id: "reading",
  schema: {
    settings: {
      personal: z.object({ compact: z.boolean().default(false) }),
    },
  },
  async documentAssets(ctx) {
    await ctx.addBrowserBundle(new URL("./client.ts", import.meta.url), "reading-settings")
  },
})
```

---

## Apply the setting to a visible reader

Create `plugins/reading/client.ts`:

```ts
import { browserSettings } from "@pathmx/core/settings/browser"
import { mountPathMXBehavior } from "@pathmx/core/controls/browser"

mountPathMXBehavior("[data-reading-preferences]", (root, { signal }) => {
  const settings = browserSettings()
  const definition = settings?.definition("reading")
  if (!settings || !definition) return
  const refresh = () => {
    const current = settings.read(definition)
    const input = root.querySelector<HTMLInputElement>("input")
    const sample = root.querySelector<HTMLElement>("[data-reading-sample]")
    if (input) input.checked = current.value.compact === true
    if (sample) sample.style.lineHeight = current.value.compact ? "1.4" : "1.9"
  }
  root.addEventListener("change", (event) => {
    if (!(event.target instanceof HTMLInputElement)) return
    const current = settings.read(definition)
    settings.set(definition, { compact: event.target.checked }, { revision: current.revision })
    refresh()
  }, { signal })
  return { update: refresh, dispose: settings.subscribe(definition, refresh) }
})
```

Create `paths/preferences.page.md`:

```md
# Reading preferences

<div data-reading-preferences>
  <label><input type="checkbox" /> Compact line spacing</label>
  <p data-reading-sample>
    Give each idea room to breathe.<br />
    Compare these lines at both settings.<br />
    Choose the spacing you prefer.
  </p>
</div>
```

Run `bun run dev`, open `/preferences.page`, and switch Compact line spacing on.
The three sample lines move closer together. Reload: the checked preference and
spacing return when browser storage is available. Open another tab on that page
and change the preference to see subscribed readers update. Losing disposable
storage restores the default spacing; no submitted work is stored here.

`browserSettings()` returns `undefined` without Settings. A stale revision is
rejected rather than silently replacing another tab's change. The browser module
applies the setting; declaring a schema alone does not change the interface.

---

## Style the first paint

A setting that changes layout should be correct before the page appears. List
those fields under `document` in `plugins/reading/index.plugin.ts`:

```ts
schema: {
  settings: {
    personal: z.object({ compact: z.boolean().default(false) }),
    document: ["compact"],
  },
},
```

Settings now writes the saved value, or the default, to `<html>` before the body
renders: `<html data-reading-compact="true">`. Style from that attribute in a
stylesheet attached to the page or its layout:

```css
:root[data-reading-compact="true"] [data-reading-sample] { line-height: 1.4; }
```

Remove the line-height assignment from `client.ts`; the checkbox still saves the
preference. Reload with Compact line spacing on: the sample lines are close
together in the first frame. The attribute is `data-<plugin>-<field>`, with
camelCase fields in kebab case. Only boolean and string-enum fields can be listed.
A saved value outside the schema shows the default. Changes in this tab or
another tab update the attribute immediately, and exported static pages behave
the same way.

A Markdown component can change a setting without a browser module:

```js
on("button", "click", async () => {
  const { compact } = await settings.get("reading")
  await settings.set("reading", { compact: !compact })
})
```

---

## Define an Actor state domain

Start with the [local Ada account](../accounts/auth.reference.md#local-actor).
Create `plugins/bookmarks.plugin.ts` with the following definition.
`defineActorState` from `@pathmx/core/state` describes one domain's readable
State data and creates an isolated index for each Plugin instance:

```ts
import { defineComponent, definePlugin, moduleRef } from "@pathmx/core"
import { defineActorState } from "@pathmx/core/state"

const Bookmarks = defineActorState({
  plugin: "bookmarks",
  read(source, app) {
    if (typeof source.data.saved !== "boolean") {
      return app.invalid(source, "Bookmark State requires a saved boolean.")
    }
    return { saved: source.data.saved }
  },
})

export default function BookmarksPlugin() {
  const states = Bookmarks.create()
  return definePlugin({
    id: "bookmarks",
    requires: ["state"],
    components: [
      defineComponent({
        tag: "x-bookmarks-status",
        load({ page, view }) {
          const record = view.principal.type === "actor"
            ? states.resolve(view.principal.actor, page.id)
            : undefined
          return record?.data.saved === true
        },
        render: moduleRef(() => import("./bookmarks.render.ts")),
      }),
    ],
    source(transition, app) {
      return {
        targets: states.source(transition, app).map(record => ({
          type: "actor" as const,
          actors: [record.actorId],
        })),
      }
    },
  })
}
```

The loader reads State on the host. Create `plugins/bookmarks.render.ts` for the
portable renderer:

```ts
export default function renderBookmark({ data: saved }: { data: boolean }) {
  return saved ? "<span>Bookmarked</span>" : ""
}
```

Create `paths/ada/state/bookmarks/lesson.page.state.md`:

```md
---
plugin: bookmarks
for: /lesson.page
saved: true
---
```

Create `paths/lesson.page.md` with `# Read closely` and `<x-bookmarks-status />`.
Run `bun run dev`, sign in as Ada, and open `/lesson.page`: it shows Bookmarked.
Change `saved` to `false` in the State file and save: the label disappears.
Restore `true` and reload to verify the record is read again. Another Actor
resolves their own record. This example reads authored bookmark State; use the
Action contract below when a control should create or update that State.
State requires a canonical `for` Source id and belongs under the owner's Home.
Duplicate records for one Actor, domain, and origin are invalid.

The store exposes `resolve(actor, origin)`, `get(stateId)`, and `records()` for
trusted Plugin code. Its `write(tx, { actorHome, origin, current, data })` and
`delete(tx, record)` methods participate in a Source transaction; declare the
corresponding create/update/delete effects in an [Action](./actions.reference.md).
They do not replace the Action's authorization or target validation. Feed Source
transitions into `states.source` so its index follows edits, writes, and deletion.
