@openchamber/sdk 2.2.0 → 2.3.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.
package/API.md CHANGED
@@ -326,6 +326,8 @@ Use `mountPopoverAnchor` from `@openchamber/sdk/ui` for DOM anchors. It measures
326
326
  | `MODEL_FAILED` | The Small Model returned an error |
327
327
  | `UNSUPPORTED` | This host surface cannot do that (for example `openCommit` without a Diff view) |
328
328
  | `SERVICE_FAILED` | Service crashed or never became ready |
329
+ | `REQUEST_FAILED` | `serviceRequest` was sent but got no usable answer; the service may have acted on it |
330
+ | `RESPONSE_TOO_LARGE` | `request` / `serviceRequest` answer over 8 000 000 bytes; the call ran, ask for a smaller page |
329
331
 
330
332
 
331
333
  ### 1.4 Field limits (client clamps before send)
@@ -343,7 +345,7 @@ Use `mountPopoverAnchor` from `@openchamber/sdk/ui` for DOM anchors. It measures
343
345
  | Branch name | 200 |
344
346
  | Request path | 2 000 |
345
347
  | Request body | 64 000 |
346
- | Request response | 256 000 |
348
+ | Request response (bytes) | 8 000 000 |
347
349
  | Request timeout | 20 000 ms |
348
350
  | `resolve` answer (host waits) | 20 000 ms |
349
351
  | Background action loading and execution | 20 000 ms |
package/DOCUMENTATION.md CHANGED
@@ -30,10 +30,11 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
30
30
  - `contributes.statusSection` is `true` or `{ entry, title?, height?, defaultExpanded?, requiresProject? }` (`StatusSectionContribution`). `true` reuses `panel.entry` and fails as `invalid-status-section` without one; the object form's `entry` is package-local `.html`, `title` 1 to `GUEST_STATUS_SECTION_TITLE_MAX` (60), `height` `GUEST_STATUS_SECTION_HEIGHT_MIN`..`MAX` (24..320). `defaultExpanded` defaults to true and only applies when the user has no saved expansion choice. `requiresProject` defaults to false; when true, the actual Work Status directory controls both presence and body rendering. `resolveStatusSectionEntry` resolves shorthand. The object form is a frame of its own: without `panel.entry` or `background.entry` it permits `capabilities`, `service`, `integration`, and `filesystem`, and refuses `page`, `attach`, `actions`, and `commands` as `invalid-panel`, because those are opened or invoked elsewhere. The server validates the HTML/scripts, exposes `statusEntry`/`statusTitle`/`statusHeight`, and counts `statusEntry` as a runtime entry for serving and storage. The host mounts `PluginPane surface="status"` inside the Work Status panel (`components/chat/work-status/WorkStatusExtensionSection.tsx`, see that folder's `DOCUMENTATION.md`): the same sandbox, guest-scoped token, context, grants, approval, and pause gates as a panel. Guest `resize` (`host.setHeight`, `ResizeRequest { height }`, integer 0..`GUEST_FRAME_HEIGHT_MAX`) is a request answered with an empty result on every surface; only `status` sizes its frame from it, through `clampStatusSectionHeight`, and a page docked to a shared surface sizes its dock from it (`DockedGuestPage` in `ContextPanel`: thickness across the dock edge, at least `GUEST_SURFACE_DOCK_SIZE_MIN`, at most half the panel through CSS). Each frame's `setHeight` reaches only its own pane. A status frame never registers a command resolver, clears a badge, or closes rail tabs. Host-rendered controls exist only after a mounted, expanded status frame calls `setStatusControls`; folding, hiding, or retiring the frame removes them. A project change clears header controls; publish them again from `onDirectory`. Workspace subscriptions remain active.
31
31
  - `openCommit(sha)` posts `open-commit { sha }`. `sha` is 7 to 64 hex characters (`GUEST_COMMIT_SHA`); the client refuses anything else as `HOST_REJECTED` before posting and the host schema drops it. The host (`lib/guests/open-commit.ts`) reads the commit itself with `git.getGitLog(directory, { maxCount: 1, to: sha })` in the frame's current directory, checks the returned hash starts with the requested one (`NOT_FOUND` otherwise), selects it in `useCommitSelectionStore` under the Diff view's key (directory, current branch, git runtime key), and opens the context panel's Diff tab in `commit` scope. No project is `NO_DIRECTORY`. VS Code and mobile answer `UNSUPPORTED` explicitly; they load no guests today, so this is the contract for when they do. No capability is needed: it only shows the user their own history.
32
32
  - Scrollbars in guest documents follow the app's overlay behavior: hidden at rest, shown on hover and for one second after the element last scrolled. `GUEST_SCROLLBAR_CSS` does the hiding and hover; `installGuestScrollbarActivity` (one capturing passive `scroll` listener per document) marks the scrolling element with `data-oc-scrolling`. The server appends both to every served guest HTML as `GUEST_SCROLLBAR_SCRIPT` (the function's own source), so already installed bundles get it without a rebuild; the UI kit installs it too for previews. An authored CSP that blocks inline scripts keeps the hover-only behavior. Forced-colors mode keeps system colors.
33
+ - The UI kit's look follows the running app; its geometry follows the bundle. `src/ui/style.ts` splits the kit CSS into sizes (display, dimensions, spacing, type scale), which ship in each bundle, and `GUEST_UI_LOOK_CSS` (colours, fonts, borders, radii, shadows, hover/pressed/selected/focus), exported from `@openchamber/sdk/ui`. The server appends the look to the same `data-openchamber-guest-styles` element after the authored document, so it follows the kit's own `<style>` in the head and wins at equal specificity: an installed extension takes a restyle without a rebuild, while a size change reaches it only when its author rebuilds. Look selectors keep the specificity of the rules they override in older bundles, and `style.test.ts` fails when a geometry property enters the look. Rules for markup an older bundle lacks are keyed on what newer markup adds (a select option carries `data-mark` with its check mark; an option without it keeps the selection fill). Authors override kit rules with a more specific selector; an equal-specificity override in the head loses to the host's look.
33
34
  - `contributes.fileEditors` is 1 to `GUEST_FILE_EDITORS_MAX` (8) editors, each `{ id, title, match, entry }` (`FileEditorContribution`, `src/file-editor.ts`): unique kebab-case `id`, `title` 1 to `GUEST_FILE_EDITOR_TITLE_MAX` (60), `match` 1 to `GUEST_FILE_EDITOR_PATTERNS_MAX` (16) file-name globs (`isFileEditorPattern`: 1 to 128 characters, no `/`, `\` or NUL, not only wildcards; `matchesFileEditorPattern` compares the name case-insensitively, `*` any run, `?` one character), `entry` package-local `.html`, optional `content` `text` (default) or `binary` (`FileEditorContentKind`); anything else is `invalid-file-editors`. Like the status section, editors are frames of their own: without `panel.entry` or `background.entry` a package may still declare `capabilities`, `service`, `integration`, and `filesystem`, and `page`, `attach`, `actions`, `commands` are `invalid-panel`. The server checks each entry's HTML and built scripts at install, carries `fileEditors` on the catalog row, and counts them as runtime entries for serving and storage (`hasGuestFrame` in `server/lib/guests/catalog.js`). The host owns the file. Wire: host → guest `file-open { path, name, readOnly, encoding: 'text', content } | { ..., encoding: 'binary', bytes }` (a push, sent on `hello` and iframe load, never on other host-state pushes; `connectHost` drops a repeat of the same file), `file-snapshot { id, payload: { purpose: 'save' | 'handoff' } }` answered by guest `file-snapshot-result { id, payload: { snapshot: { content, version } | { bytes, version } } | { error } }` with no `result` back, and `file-saved { version }`; guest → host fire-and-forget `file-change { dirty, edited }`, `file-save`, and `file-unsupported` (no `id`, no answer). Bytes are a `Uint8Array<ArrayBuffer>`, copied by structured clone, so the host schema's `z.instanceof(Uint8Array)` holds in the receiving realm; `sameFileEditorDocument` compares them byte for byte for the repeat-push check. Content on both sides is capped at `GUEST_FILE_EDITOR_CONTENT_MAX` (20 000 000 characters or bytes, `fileEditorPayloadSize`); the guest refuses an oversized snapshot with an error instead of letting the host schema drop it. `onFileOpen` registration installs one capturing `keydown` listener that turns Cmd/Ctrl+S into `file-save`, because key events inside the frame never reach the host. `createFileSaveTracker` is the pure dirty bookkeeping: a written snapshot's version is marked saved, so edits made during the write stay dirty. The host side is `packages/ui/src/lib/guests/file-editor-channel.ts` (one channel per mounted editor; disconnect rejects every outstanding snapshot request), `PluginPane surface="file"`, and `components/views/files/GuestFileEditor.tsx`; the Files view contract is in that folder's `DOCUMENTATION.md`. VS Code and mobile keep the catalog empty, so files there stay with the host's own viewers.
34
35
  - `contributes.page` is `true` or `{ entry, title? }`, requires `panel.entry`, and adds a user-opened full-screen page. `resolvePageEntry` resolves shorthand; the server validates its HTML/scripts and exposes `pageEntry`/`pageTitle`. `PluginPane surface="page"` uses the same sandbox. `useUIStore.openGuestPageId` is transient and mutually exclusive with other main pages. The sidebar menu opens it; `close()` or explicit session navigation closes it. Pages are not context-rail surfaces.
35
36
  - `workspace.ts` owns project/worktree/session snapshots and storage types; `workspace-schemas.ts` validates their wire contracts. The existing `sessions` grant covers lists, subscriptions, opening sessions, and targeted creation. `lib/guests/workspace.ts` projects shared project, topology, session, and live-status stores; reads perform no network or git discovery. A registered observer is shared per runtime/extension/query and stops with its last listener. `PluginPane` limits each frame to 32 subscriptions and clears them on unmount, grant withdrawal, and runtime switch. Initial snapshots and updates have explicit loading/error coverage. `HostClient.onProjects/onWorktrees/onSessions` return promises of unsubscribe functions, so approval failures reach the caller.
36
- - `shells.ts` owns the running-shell contract: the `shells` declared capability, the snapshot and result types, and `GUEST_SHELL_ID_MAX` (128), `GUEST_SHELLS_MAX` (200), and `GUEST_SHELL_OUTPUT_TAIL_MAX` (65536). Wire: guest `shells-subscribe` `{ subscriptionId, scope }` with scope `{ kind: 'session', sessionId }`, `{ kind: 'project', projectId }`, or `{ kind: 'global' }`, `shells-unsubscribe` `{ subscriptionId }`, `shell-output` `{ shellId, cursor?, tailBytes? }`, and `shell-stop` `{ shellId }`; host push `shells` `{ subscriptionId, snapshot }` where the snapshot carries `kind: 'shells'`, an echo of the requested `scope`, the running `shells`, and `ended` (`GuestEndedShell`: a running shell plus `status` from `GUEST_SHELL_END_STATUSES`, `endedAt`, optional integer `exit`). `lib/guests/shells.ts` projects `useBackgroundShellsStore` (the requested scope: a session and its subagents, a registered project's sessions matched by directory rather than by `projectID`, or every session; running oldest start first, ended oldest end first keeping the newest `GUEST_SHELLS_MAX`) from memory, so a read never hits the network or git; `observeGuestShells` shares one observer per runtime and scope. The store's `ended` list is the app's own observation of `shell.exited` / `shell.deleted` (see `sync/DOCUMENTATION.md`), so it is empty after a reload. `readGuestShellOutput` reads a running or ended shell in the directory it ran in, and answers `NOT_FOUND` for a shell the store never saw, a `stopped` one (OpenCode unlinks its output on removal), and an OpenCode 404 (evicted past the 25 retained exited commands per directory); other failures pass through. `PluginPane` wires the four effects, checks the `shells` grant per call, counts shell subscriptions against the frame's 32-subscription limit, and answers `stopShell` on a shell that is not running with `NOT_FOUND`. `opencodeClient.stopBackgroundShell` writes the agent's cancellation note before it kills the command and kills nothing when the note fails.
37
+ - `shells.ts` owns the running-shell contract: the `shells` declared capability, the snapshot and result types, and `GUEST_SHELL_ID_MAX` (128), `GUEST_SHELLS_MAX` (200), and `GUEST_SHELL_OUTPUT_TAIL_MAX` (65536). Wire: guest `shells-subscribe` `{ subscriptionId, scope }` with scope `{ kind: 'session', sessionId }`, `{ kind: 'project', projectId }`, or `{ kind: 'global' }`, `shells-unsubscribe` `{ subscriptionId }`, `shell-output` `{ shellId, cursor?, tailBytes? }`, and `shell-stop` `{ shellId }`; host push `shells` `{ subscriptionId, snapshot }` where the snapshot carries `kind: 'shells'`, an echo of the requested `scope`, the running `shells`, and `ended` (`GuestEndedShell`: a running shell plus `status` from `GUEST_SHELL_END_STATUSES`, `endedAt`, optional integer `exit`). `lib/guests/shells.ts` projects `useBackgroundShellsStore` (the requested scope: a session and its subagents, a registered project's sessions matched by directory rather than by `projectID`, or every session; running oldest start first, ended oldest end first keeping the newest `GUEST_SHELLS_MAX`) from memory, so a read never hits the network or git; `observeGuestShells` shares one observer per runtime and scope. The store's `ended` list is the app's own observation of `shell.exited` / `shell.deleted` (see `sync/DOCUMENTATION.md`), so it is empty after a reload. `readGuestShellOutput` reads a running or ended shell in the directory it ran in, and answers `NOT_FOUND` for a shell the store never saw, a `stopped` one (OpenCode unlinks its output on removal), and an OpenCode 404 (evicted past the 25 retained exited commands per directory); other failures pass through. `PluginPane` wires the four effects, checks the `shells` grant per call, counts shell subscriptions against the frame's 32-subscription limit, and answers `stopShell` on a shell that is not running with `NOT_FOUND`. `opencodeClient.stopBackgroundShells` writes the agent's cancellation note before it kills the command and kills nothing when the note fails.
37
38
  - `storage` is instance-owned JSON under the guest's namespace, without a new capability. `storage.js` serializes authorization, read-modify-write, and removal per namespace; writes use unique temporary files and rename. Missing keys differ from stored null; corrupt files and failed writes fail without clearing data. Device scope is an explicit SDK opt-in only when ready features advertise `deviceStorage`; it uses the host runtime key plus guest ID and server-issued installation `storageId`, and has no instance fallback. Different runtime identities remain separate even if a server duplicates a storage ID. URL-based runtime keys separate address aliases; saved-host and relay keys retain their existing identity rules. IndexedDB limits are 2 MiB per namespace and 16 MiB of serialized preference data across the database; browser metadata has separate overhead. A confirmed uninstall makes the namespace inaccessible, but an offline profile retains its bytes until site data is removed. Device storage does not promise secret or SSO storage. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
38
39
 
39
40
  - Manifest `apiVersion` is `1`. Wire envelope `v` stays `1`. Any other value is `unsupported-api-version`. Optional `engines.openchamber` is `1.22.0` or `>=1.22.0`. Install refuses with `host-too-old` when this OpenChamber build is older. Package installs also need a semver `version` on `package.json` (`1.0.0`); the catalog exposes it and Settings → Extensions shows `v1.0.0` on the card. Already-installed packages without `version` still list; new installs without it fail as `invalid-manifest`.
@@ -42,7 +43,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
42
43
  - Extra manifest keys are dropped, not forwarded.
43
44
  - Theme text fields are required on `apiVersion: 1`: `primaryText`, `successText`, `warningText`, `errorText`, `infoText`. Optional syntax fields are `syntaxKeyword`, `syntaxString`, `syntaxNumber`, `syntaxFunction`, `syntaxType`, `syntaxComment`, `syntaxVariable`, and `syntaxOperator`. Host calculation and UI-kit application are documented in `src/ui/DOCUMENTATION.md`. Keep the wire schema, TypeScript contract and iframe mapping in sync.
44
45
  - `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
45
- - Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`, `UNSUPPORTED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
46
+ - Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `REQUEST_FAILED`, `RESPONSE_TOO_LARGE`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`, `UNSUPPORTED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
46
47
  - `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
47
48
  - The host parses iframe `event.data` with `guestMessageSchema` (`@openchamber/sdk/schemas`). The guest reads host pushes with `readHostMessage` in `contract.ts`: envelope and discriminant only, because the sender is the verified parent frame. `protocol.ts` asserts the hand-written `HostMessage` / `GuestMessage` types against the zod schemas at compile time, so the two cannot drift.
48
49
  - `ready.item` is `GuestItem | null`: the composer chip (`AttachIssueRequest`, unchanged, `kind` still `issue` / `pull`), or a `GuestMessageItem` / `GuestSessionItem` from a declared action, discriminated by `kind: 'message' | 'session'` and carrying the manifest `action` id. A message item's `text` is what `formatMessageRecordText` renders for the export, capped at `GUEST_ITEM_MESSAGE_TEXT_MAX` (200 000). A session item carries `messages` only when the action declared `payload: ["messages"]` and `conversation` is granted; `buildGuestSessionItem` in `packages/ui/src/lib/guests/actions.ts` drops the oldest messages until the serialized item is under `GUEST_ITEM_SESSION_MAX` (2 000 000) and sets `truncated`. The host schema refuses a larger item. `isGuestAttachItem` / `isGuestMessageItem` / `isGuestSessionItem` narrow it.
@@ -62,7 +63,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
62
63
  - `prompt` is `{ text, send? }`. `send` omitted or `false` replace-composes on the current session. `send: true` sends on that session with the same model path as `startSession`. No session is `NO_SESSION`. A busy session on send is `SESSION_BUSY`. Compose still works while busy. The result is `{ sent }`. The guest does not pick a model or agent.
63
64
  - `sessionLink` is the same attach fields on the current session. It does not create a session. No project or no session is a refusal.
64
65
  - `onSessionLifecycle` is a host push. Live `session_status` `busy` and `retry` are `started`. `idle` is `completed`. An unknown status type is `failure`. That is not abort or crash. A late listener gets the last phase, from `ready.session.busy` when no dedicated push has arrived.
65
- - `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
66
+ - `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. An answer over `GUEST_REQUEST_RESPONSE_MAX` (8 000 000 bytes) is `RESPONSE_TOO_LARGE`; the host never hands over a cut-off body. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
66
67
  - `contributes.service` names a host-spawned entry on `apiVersion: 1`. `serviceRequest` / `serviceStatus` proxy to that process on loopback. See `GUEST_SERVICES.md`. Permissions are grant UI only in phase 1. No `host.docker`.
67
68
 
68
69
  ## Host hole
package/GUEST_SERVICES.md CHANGED
@@ -111,6 +111,7 @@ Error codes:
111
111
  - `DISABLED` — extension paused in Settings → Extensions (service stopped; tokens/grants stay)
112
112
  - `SERVICE_FAILED` — process crashed or never became ready; nothing was sent to it
113
113
  - `REQUEST_FAILED` — the request was sent but got no usable answer (timeout, dropped connection); the service may have acted on it
114
+ - `RESPONSE_TOO_LARGE` — the service answered with more than `GUEST_REQUEST_RESPONSE_MAX` bytes; it acted on the request, the answer is dropped whole
114
115
  - Existing: `HOST_TIMEOUT`, `HOST_REJECTED`, `HOST_UNAVAILABLE`, `BAD_PATH`
115
116
 
116
117
  Server routes (authenticated UI session):
package/README.md CHANGED
@@ -256,6 +256,8 @@ host.onReady((ctx) => {
256
256
 
257
257
  OpenChamber supplies thin, theme-aware native scrollbars inside extension documents, including nested lists, tabs, and textareas. Like the app's own, they stay hidden until you hover or scroll. The UI kit includes the same defaults for development previews. Existing installed bundles get the host stylesheet without rebuilding. Custom rendering hosts can use `GUEST_SCROLLBAR_CSS` from `@openchamber/sdk`. Authors can override these default rules; an extension's CSP still applies.
258
258
 
259
+ The kit's look comes from the OpenChamber version that runs your extension: colours, borders, corner radii, and hover, selected and focus states follow the app after an update, with no rebuild on your side. Sizes and spacing stay the ones your bundle was built with, so a layout built around them doesn't shift; they change when you rebuild against a newer SDK. The app adds its look after your document, so to restyle a kit control, use a selector more specific than the kit's, such as `#root .oc-sdk-btn`. Custom rendering hosts can use `GUEST_UI_LOOK_CSS` from `@openchamber/sdk/ui`.
260
+
259
261
  ## Schemas
260
262
 
261
263
  `@openchamber/sdk/schemas` exports the zod schemas for the manifest and the messages, for tools that validate extensions. The main entry has no zod dependency, so a page bundle stays small.
@@ -335,7 +335,8 @@ export declare const GUEST_SESSION_AGENT_MAX = 80;
335
335
  export declare const GUEST_SETTING_VALUE_MAX = 2000;
336
336
  export declare const GUEST_REQUEST_PATH_MAX = 2000;
337
337
  export declare const GUEST_REQUEST_BODY_MAX = 64000;
338
- export declare const GUEST_REQUEST_RESPONSE_MAX = 256000;
338
+ /** Bytes of a `request` / `serviceRequest` answer; a larger one is `RESPONSE_TOO_LARGE`, never cut off. */
339
+ export declare const GUEST_REQUEST_RESPONSE_MAX = 8000000;
339
340
  export declare const GUEST_REQUEST_TIMEOUT_MS = 20000;
340
341
  /** Guest file path, in characters. */
341
342
  export declare const GUEST_FILE_PATH_MAX = 1024;
@@ -363,7 +364,7 @@ export declare const GUEST_BADGE_MAX = 999;
363
364
  export declare const GUEST_FRAME_HEIGHT_MAX = 10000;
364
365
  /** Characters in a `resolve-result` error string. */
365
366
  export declare const GUEST_RESOLVE_ERROR_MAX = 500;
366
- export declare const HOST_REQUEST_ERROR_CODES: readonly ["HOST_UNAVAILABLE", "HOST_TIMEOUT", "HOST_REJECTED", "DISCONNECTED", "DISABLED", "BAD_PATH", "NO_INTEGRATION", "NO_SERVICE", "SERVICE_FAILED", "NO_SESSION", "SESSION_BUSY", "NOT_GRANTED", "NO_DIRECTORY", "NOT_FOUND", "FILE_TOO_LARGE", "DENIED", "NO_MODEL", "MODEL_FAILED", "UNSUPPORTED"];
367
+ export declare const HOST_REQUEST_ERROR_CODES: readonly ["HOST_UNAVAILABLE", "HOST_TIMEOUT", "HOST_REJECTED", "DISCONNECTED", "DISABLED", "BAD_PATH", "NO_INTEGRATION", "NO_SERVICE", "SERVICE_FAILED", "REQUEST_FAILED", "RESPONSE_TOO_LARGE", "NO_SESSION", "SESSION_BUSY", "NOT_GRANTED", "NO_DIRECTORY", "NOT_FOUND", "FILE_TOO_LARGE", "DENIED", "NO_MODEL", "MODEL_FAILED", "UNSUPPORTED"];
367
368
  export declare const SERVICE_STATUS_VALUES: readonly ["stopped", "starting", "ready", "failed"];
368
369
  export type ServiceStatus = (typeof SERVICE_STATUS_VALUES)[number];
369
370
  export type ServiceStatusResult = {
package/dist/contract.js CHANGED
@@ -33,7 +33,8 @@ export const GUEST_SESSION_AGENT_MAX = 80;
33
33
  export const GUEST_SETTING_VALUE_MAX = 2_000;
34
34
  export const GUEST_REQUEST_PATH_MAX = 2_000;
35
35
  export const GUEST_REQUEST_BODY_MAX = 64_000;
36
- export const GUEST_REQUEST_RESPONSE_MAX = 256_000;
36
+ /** Bytes of a `request` / `serviceRequest` answer; a larger one is `RESPONSE_TOO_LARGE`, never cut off. */
37
+ export const GUEST_REQUEST_RESPONSE_MAX = 8_000_000;
37
38
  export const GUEST_REQUEST_TIMEOUT_MS = 20_000;
38
39
  /** Guest file path, in characters. */
39
40
  export const GUEST_FILE_PATH_MAX = 1_024;
@@ -71,6 +72,8 @@ export const HOST_REQUEST_ERROR_CODES = [
71
72
  'NO_INTEGRATION',
72
73
  'NO_SERVICE',
73
74
  'SERVICE_FAILED',
75
+ 'REQUEST_FAILED',
76
+ 'RESPONSE_TOO_LARGE',
74
77
  'NO_SESSION',
75
78
  'SESSION_BUSY',
76
79
  'NOT_GRANTED',
@@ -730,7 +730,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
730
730
  id: string;
731
731
  ok: false;
732
732
  error: string;
733
- code: "HOST_UNAVAILABLE" | "HOST_TIMEOUT" | "HOST_REJECTED" | "DISCONNECTED" | "DISABLED" | "BAD_PATH" | "NO_INTEGRATION" | "NO_SERVICE" | "SERVICE_FAILED" | "NO_SESSION" | "SESSION_BUSY" | "NOT_GRANTED" | "NO_DIRECTORY" | "NOT_FOUND" | "FILE_TOO_LARGE" | "DENIED" | "NO_MODEL" | "MODEL_FAILED" | "UNSUPPORTED";
733
+ code: "HOST_UNAVAILABLE" | "HOST_TIMEOUT" | "HOST_REJECTED" | "DISCONNECTED" | "DISABLED" | "BAD_PATH" | "NO_INTEGRATION" | "NO_SERVICE" | "SERVICE_FAILED" | "REQUEST_FAILED" | "RESPONSE_TOO_LARGE" | "NO_SESSION" | "SESSION_BUSY" | "NOT_GRANTED" | "NO_DIRECTORY" | "NOT_FOUND" | "FILE_TOO_LARGE" | "DENIED" | "NO_MODEL" | "MODEL_FAILED" | "UNSUPPORTED";
734
734
  payload?: undefined;
735
735
  }, {
736
736
  type: "result";
@@ -1,4 +1,4 @@
1
- export declare const GUEST_SCROLLBAR_CSS = "\n:root {\n --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 40%, transparent);\n --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 65%, transparent);\n scrollbar-gutter: stable;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: transparent transparent;\n}\n:hover, [data-oc-scrolling] {\n scrollbar-color: var(--oc-scrollbar-thumb) transparent;\n}\n/* Chromium's standard scrollbar properties otherwise override its pseudo-elements. */\n@supports selector(::-webkit-scrollbar) {\n *, :hover, [data-oc-scrolling] { scrollbar-width: auto; scrollbar-color: auto; }\n ::-webkit-scrollbar { width: 6px; height: 6px; background: transparent; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb {\n background: transparent;\n border-radius: 999px;\n min-width: 24px;\n min-height: 24px;\n }\n :hover::-webkit-scrollbar-thumb, [data-oc-scrolling]::-webkit-scrollbar-thumb { background: var(--oc-scrollbar-thumb); }\n ::-webkit-scrollbar-thumb:hover { background: var(--oc-scrollbar-thumb-hover); }\n ::-webkit-scrollbar-corner { background: transparent; }\n ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }\n}\n@media (forced-colors: active) {\n *, :hover, [data-oc-scrolling] { scrollbar-color: auto; }\n ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background: CanvasText; }\n}\n";
1
+ export declare const GUEST_SCROLLBAR_CSS = "\n:root {\n /* Quiet at rest like the app's own thumbs; hover gives a clear grab target. */\n --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 18%, transparent);\n --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 36%, transparent);\n scrollbar-gutter: stable;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: transparent transparent;\n}\n:hover, [data-oc-scrolling] {\n scrollbar-color: var(--oc-scrollbar-thumb) transparent;\n}\n/* Chromium's standard scrollbar properties otherwise override its pseudo-elements. */\n@supports selector(::-webkit-scrollbar) {\n *, :hover, [data-oc-scrolling] { scrollbar-width: auto; scrollbar-color: auto; }\n ::-webkit-scrollbar { width: 6px; height: 6px; background: transparent; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb {\n background: transparent;\n border-radius: 999px;\n min-width: 24px;\n min-height: 24px;\n }\n :hover::-webkit-scrollbar-thumb, [data-oc-scrolling]::-webkit-scrollbar-thumb { background: var(--oc-scrollbar-thumb); }\n ::-webkit-scrollbar-thumb:hover { background: var(--oc-scrollbar-thumb-hover); }\n ::-webkit-scrollbar-corner { background: transparent; }\n ::-webkit-scrollbar-button { display: none; width: 0; height: 0; }\n}\n@media (forced-colors: active) {\n *, :hover, [data-oc-scrolling] { scrollbar-color: auto; }\n ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background: CanvasText; }\n}\n";
2
2
  /**
3
3
  * Marks whichever element scrolls (the root for document scrolling) with
4
4
  * `GUEST_SCROLLING_ATTRIBUTE` and clears it once scrolling has been quiet for
@@ -14,8 +14,9 @@
14
14
  const GUEST_SCROLLING_ATTRIBUTE = 'data-oc-scrolling';
15
15
  export const GUEST_SCROLLBAR_CSS = `
16
16
  :root {
17
- --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 40%, transparent);
18
- --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 65%, transparent);
17
+ /* Quiet at rest like the app's own thumbs; hover gives a clear grab target. */
18
+ --oc-scrollbar-thumb: color-mix(in srgb, var(--oc-muted, currentColor) 18%, transparent);
19
+ --oc-scrollbar-thumb-hover: color-mix(in srgb, var(--oc-muted, currentColor) 36%, transparent);
19
20
  scrollbar-gutter: stable;
20
21
  }
21
22
  * {
@@ -1,5 +1,6 @@
1
1
  export { applyHostReady, applyHostTheme } from './theme.ts';
2
2
  export type { ThemeRoot } from './theme.ts';
3
+ export { GUEST_UI_LOOK_CSS } from './style.ts';
3
4
  export type { Handle } from './dom.ts';
4
5
  export { mountButton } from './button.ts';
5
6
  export type { ButtonHandle, ButtonProps, ButtonSize, ButtonVariant } from './button.ts';
package/dist/ui/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { applyHostReady, applyHostTheme } from "./theme.js";
2
+ export { GUEST_UI_LOOK_CSS } from "./style.js";
2
3
  export { mountButton } from "./button.js";
3
4
  export { mountTextField } from "./field.js";
4
5
  export { mountSearchField } from "./search.js";
package/dist/ui/option.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { button, el, setAttr } from "./dom.js";
2
+ import { icon } from "./icons.js";
2
3
  const optionId = (uid, id) => `${uid}-${id ?? ''}`;
3
4
  export const createOption = (uid, role, spec, on) => {
4
5
  const node = button('oc-sdk-option');
@@ -18,6 +19,13 @@ export const createOption = (uid, role, spec, on) => {
18
19
  hint.textContent = spec.hint;
19
20
  node.append(hint);
20
21
  }
22
+ if (role === 'option') {
23
+ // The host's styles drop the selection fill only for options that carry the mark.
24
+ node.dataset.mark = 'check';
25
+ const check = el('span', 'oc-sdk-option-check');
26
+ check.append(icon('check', 14));
27
+ node.append(check);
28
+ }
21
29
  node.addEventListener('pointerenter', on.hover);
22
30
  node.addEventListener('click', on.pick);
23
31
  return node;
@@ -1 +1,10 @@
1
+ /**
2
+ * Appearance: colours, fonts, borders, corner radii, shadows, and the hover,
3
+ * pressed, selected and focus states. The kit carries it for previews outside
4
+ * the host, and the server appends the running app's copy to every served
5
+ * extension document, after the kit's own style element, so installed bundles
6
+ * follow the app's look without a rebuild. Selectors here keep the specificity
7
+ * of the rules they override in older bundles.
8
+ */
9
+ export declare const GUEST_UI_LOOK_CSS: string;
1
10
  export declare const UI_CSS: string;
package/dist/ui/style.js CHANGED
@@ -48,6 +48,9 @@ const errorText = v('error-text', 'inherit');
48
48
  const font = v('font-sans', 'inherit');
49
49
  const mono = v('font-mono', 'monospace');
50
50
  const radius = v('radius', '9px');
51
+ /** The app's controls round a step below the host radius: 7px against its 9px. */
52
+ const controlRadius = `calc(${radius} - 2px)`;
53
+ const smallRadius = `calc(${radius} - 4px)`;
51
54
  const mix = (color, pct, base = 'transparent') => (`color-mix(in srgb, ${color} ${pct}%, ${base})`);
52
55
  const focusRing = `box-shadow: 0 0 0 2px ${focus};`;
53
56
  const tone = (name) => {
@@ -55,23 +58,125 @@ const tone = (name) => {
55
58
  return `
56
59
  .oc-sdk[data-tone="${name}"], .oc-sdk [data-tone="${name}"] { --oc-sdk-tone: ${color}; --oc-sdk-tone-text: ${v(`${name}-text`, 'inherit')}; }`;
57
60
  };
58
- export const UI_CSS = `
59
- ${GUEST_SCROLLBAR_CSS}
60
- .oc-sdk { box-sizing: border-box; color: ${fg}; font-family: ${font}; font-size: 0.875rem; line-height: 1.45; }
61
+ /**
62
+ * Geometry: display, sizes, spacing, type scale, positioning. It ships inside
63
+ * each extension's bundle and changes only when the author rebuilds, so a
64
+ * layout built around these sizes never shifts under an installed extension.
65
+ */
66
+ const UI_SIZE_CSS = `
67
+ .oc-sdk { box-sizing: border-box; font-size: 0.875rem; line-height: 1.45; }
61
68
  .oc-sdk *, .oc-sdk *::before, .oc-sdk *::after { box-sizing: border-box; }
62
69
  /* :where() keeps the reset at zero specificity so every primitive class below overrides it. */
63
70
  :where(.oc-sdk) :where(button, input, textarea), :where(button.oc-sdk, input.oc-sdk, textarea.oc-sdk) { font: inherit; color: inherit; margin: 0; }
64
71
  :where(.oc-sdk) :where(button), :where(button.oc-sdk) { cursor: pointer; background: none; border: 0; padding: 0; }
65
72
  .oc-sdk button:disabled, button.oc-sdk:disabled, .oc-sdk[aria-disabled="true"], .oc-sdk [aria-disabled="true"] { opacity: .5; pointer-events: none; }
73
+
74
+ .oc-sdk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 32px; padding: 0 12px; border: 1px solid transparent; font-size: 0.875rem; font-weight: 500; line-height: 1; white-space: nowrap; }
75
+ .oc-sdk-btn[data-size="sm"] { gap: 6px; height: 28px; padding: 0 10px; font-size: 0.8125rem; }
76
+ .oc-sdk-btn[data-size="xs"] { gap: 4px; height: 24px; padding: 0 8px; font-size: 0.75rem; }
77
+ .oc-sdk-btn[data-loading="true"] { opacity: .5; pointer-events: none; }
78
+ .oc-sdk-btn > .oc-sdk-spinner-ring { width: 14px; height: 14px; }
79
+
80
+ .oc-sdk-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
81
+ .oc-sdk-field-label { font-size: 0.8125rem; font-weight: 500; }
82
+ .oc-sdk-field-note { font-size: 0.75rem; }
83
+ .oc-sdk-input { display: block; width: 100%; min-width: 0; height: 32px; padding: 0 10px; border: 0; font-size: 0.875rem; line-height: 1.45; appearance: none; }
84
+ textarea.oc-sdk-input { height: auto; padding: 8px 10px; resize: vertical; }
85
+
86
+ .oc-sdk-search { position: relative; min-width: 0; }
87
+ .oc-sdk-search .oc-sdk-input { padding-left: 34px; padding-right: 34px; }
88
+ .oc-sdk-search-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); pointer-events: none; }
89
+ .oc-sdk-search-clear { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: none; align-items: center; justify-content: center; width: 24px; height: 24px; }
90
+ .oc-sdk-search[data-active="true"] .oc-sdk-search-clear { display: inline-flex; }
91
+
92
+ .oc-sdk-select { position: relative; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
93
+ .oc-sdk-trigger { display: inline-flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: 28px; padding: 0 10px; border: 1px solid transparent; font-size: 0.8125rem; text-align: left; }
94
+ .oc-sdk-trigger-value { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
95
+ .oc-sdk-trigger-chevron { flex: 0 0 auto; }
96
+ .oc-sdk-popup { position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 2px; min-width: 160px; max-width: calc(100vw - 16px); max-height: min(320px, calc(100vh - 16px)); overflow: auto; padding: 4px; border: 1px solid transparent; }
97
+ .oc-sdk-popup-search { flex: 0 0 auto; padding: 2px 2px 4px; }
98
+ .oc-sdk-popup-search .oc-sdk-input { height: 28px; font-size: 0.8125rem; }
99
+ .oc-sdk-option { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; font-size: 0.8125rem; text-align: left; }
100
+ .oc-sdk-option-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
101
+ .oc-sdk-option-hint { flex: 0 0 auto; font-size: 0.75rem; }
102
+ .oc-sdk-option-check { flex: 0 0 auto; display: inline-flex; width: 14px; visibility: hidden; }
103
+ .oc-sdk-option[aria-selected="true"] > .oc-sdk-option-check { visibility: visible; }
104
+ .oc-sdk-popup-empty { padding: 8px; font-size: 0.8125rem; }
105
+
106
+ .oc-sdk-check { display: inline-flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left; }
107
+ .oc-sdk-check-box { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin-top: 3px; border: 1px solid transparent; }
108
+ .oc-sdk-check-box > svg { display: none; }
109
+ .oc-sdk-check[aria-checked="true"] .oc-sdk-check-box > svg { display: block; }
110
+ .oc-sdk-check-thumb { flex: 0 0 auto; position: relative; width: 36px; height: 20px; }
111
+ .oc-sdk-check-thumb::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; }
112
+ .oc-sdk-check-text { display: flex; flex-direction: column; min-width: 0; }
113
+ .oc-sdk-check-label { font-size: 0.875rem; }
114
+ .oc-sdk-check-desc { font-size: 0.75rem; }
115
+
116
+ .oc-sdk-tabs { display: inline-flex; gap: 2px; padding: 2px; max-width: 100%; overflow: auto; }
117
+ .oc-sdk-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid transparent; font-size: 0.8125rem; font-weight: 500; white-space: nowrap; }
118
+ .oc-sdk-tab-count { font-size: 0.75rem; font-variant-numeric: tabular-nums; }
119
+
120
+ .oc-sdk-badge { display: inline-flex; align-items: center; padding: 1px 6px; font-size: 11px; font-weight: 500; line-height: 16px; white-space: nowrap; }
121
+
122
+ .oc-sdk-list { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
123
+ .oc-sdk-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; text-align: left; }
124
+ .oc-sdk-row-lead { flex: 0 0 auto; width: 64px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; }
125
+ .oc-sdk-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
126
+ .oc-sdk-row-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
127
+ .oc-sdk-row-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; }
128
+ .oc-sdk-row-meta { flex: 0 0 auto; font-size: 0.75rem; font-variant-numeric: tabular-nums; }
129
+ .oc-sdk-list-empty { padding: 16px 8px; text-align: center; font-size: 0.8125rem; }
130
+
131
+ .oc-sdk-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 40px 16px; text-align: center; }
132
+ .oc-sdk-empty-title { margin: 0; font-size: 0.8125rem; font-weight: 600; }
133
+ .oc-sdk-empty-body { margin: 0; max-width: 32rem; font-size: 0.8125rem; }
134
+ .oc-sdk-empty-action { margin-top: 12px; }
135
+
136
+ @keyframes oc-sdk-spin { to { transform: rotate(360deg); } }
137
+ .oc-sdk-spinner { display: inline-flex; align-items: center; gap: 8px; font-size: 0.8125rem; }
138
+ .oc-sdk-spinner-ring { width: 16px; height: 16px; border: 2px solid transparent; animation: oc-sdk-spin .8s linear infinite; }
139
+ .oc-sdk-spinner[data-size="sm"] .oc-sdk-spinner-ring { width: 12px; height: 12px; }
140
+
141
+ .oc-sdk-banner { display: flex; align-items: flex-start; gap: 12px; padding: 8px 12px; border: 1px solid transparent; }
142
+ .oc-sdk-banner-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
143
+ .oc-sdk-banner-title { font-size: 0.8125rem; font-weight: 500; }
144
+ .oc-sdk-banner-body { font-size: 0.8125rem; }
145
+ .oc-sdk-banner-action { flex: 0 0 auto; }
146
+
147
+ .oc-sdk-separator { display: flex; align-items: center; gap: 8px; width: 100%; margin: 8px 0; font-size: 0.75rem; }
148
+ .oc-sdk-separator::before, .oc-sdk-separator::after { content: ""; flex: 1 1 auto; height: 1px; }
149
+ .oc-sdk-separator[data-labeled="false"]::after { display: none; }
150
+ .oc-sdk-popup > .oc-sdk-separator { margin: 4px 0; }
151
+
152
+ .oc-sdk-progress { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
153
+ .oc-sdk-progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; font-variant-numeric: tabular-nums; }
154
+ .oc-sdk-progress-track { height: 6px; overflow: hidden; }
155
+ .oc-sdk-progress-fill { height: 100%; transform-origin: left; transition: transform 200ms ease-out; }
156
+
157
+ .oc-sdk-menu { position: relative; display: inline-flex; }
158
+
159
+ .oc-sdk-text { white-space: pre-wrap; overflow-wrap: anywhere; }
160
+ .oc-sdk-text img { display: block; max-width: 100%; margin: 8px 0; }
161
+ `;
162
+ /**
163
+ * Appearance: colours, fonts, borders, corner radii, shadows, and the hover,
164
+ * pressed, selected and focus states. The kit carries it for previews outside
165
+ * the host, and the server appends the running app's copy to every served
166
+ * extension document, after the kit's own style element, so installed bundles
167
+ * follow the app's look without a rebuild. Selectors here keep the specificity
168
+ * of the rules they override in older bundles.
169
+ */
170
+ export const GUEST_UI_LOOK_CSS = `
171
+ .oc-sdk { color: ${fg}; font-family: ${font}; }
66
172
  .oc-sdk :focus-visible { outline: none; ${focusRing} }
67
173
  .oc-sdk-mono { font-family: ${mono}; }
68
174
  .oc-sdk-muted { color: ${muted}; }
69
175
  ${tone('success')}${tone('warning')}${tone('error')}${tone('info')}
70
176
  .oc-sdk[data-tone="primary"], .oc-sdk [data-tone="primary"] { --oc-sdk-tone: ${primary}; --oc-sdk-tone-text: ${primaryText}; }
71
177
 
72
- .oc-sdk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 36px; padding: 0 14px; border: 1px solid transparent; border-radius: ${radius}; font-size: 0.875rem; font-weight: 500; line-height: 1; white-space: nowrap; transition: background 150ms ease-out, color 150ms ease-out; }
73
- .oc-sdk-btn[data-size="sm"] { height: 32px; padding: 0 10px; font-size: 0.8125rem; }
74
- .oc-sdk-btn[data-size="xs"] { height: 24px; padding: 0 8px; font-size: 0.75rem; border-radius: 6px; }
178
+ .oc-sdk-btn { border-radius: ${controlRadius}; transition: background 150ms ease-out, color 150ms ease-out; }
179
+ .oc-sdk-btn[data-size="xs"] { border-radius: ${smallRadius}; }
75
180
  .oc-sdk-btn[data-variant="default"] { color: ${primaryText}; background: ${mix(primary, 10, bg)}; border-color: ${mix(primary, 12)}; }
76
181
  .oc-sdk-btn[data-variant="default"]:hover { background: ${mix(primary, 16, bg)}; }
77
182
  .oc-sdk-btn[data-variant="default"]:active { background: ${mix(primary, 22, bg)}; }
@@ -87,15 +192,10 @@ ${tone('success')}${tone('warning')}${tone('error')}${tone('info')}
87
192
  .oc-sdk-btn[data-variant="destructive"] { --oc-sdk-tone: ${v('status-error', 'red')}; color: ${errorText}; background: ${mix('var(--oc-sdk-tone)', 7, bg)}; border-color: ${mix('var(--oc-sdk-tone)', 12)}; }
88
193
  .oc-sdk-btn[data-variant="destructive"]:hover { background: ${mix('var(--oc-sdk-tone)', 9, bg)}; }
89
194
  .oc-sdk-btn[data-variant="destructive"]:active { background: ${mix('var(--oc-sdk-tone)', 11, bg)}; }
90
- .oc-sdk-btn[data-loading="true"] { opacity: .5; pointer-events: none; }
91
- .oc-sdk-btn > .oc-sdk-spinner-ring { width: 14px; height: 14px; }
92
195
 
93
- .oc-sdk-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
94
- .oc-sdk-field-label { font-size: 0.8125rem; font-weight: 500; }
95
- .oc-sdk-field-note { font-size: 0.75rem; color: ${muted}; }
196
+ .oc-sdk-field-note { color: ${muted}; }
96
197
  .oc-sdk-field[data-invalid="true"] .oc-sdk-field-note { color: ${errorText}; }
97
- .oc-sdk-input { display: block; width: 100%; min-width: 0; height: 36px; padding: 0 12px; border: 0; border-radius: ${radius}; background: ${elevated}; color: ${elevatedFg}; font-size: 0.875rem; line-height: 1.45; appearance: none; box-shadow: inset 0 0 0 1px ${mix(border, 60)}; transition: background 150ms ease-out, box-shadow 150ms ease-out; }
98
- textarea.oc-sdk-input { height: auto; padding: 8px 12px; resize: vertical; }
198
+ .oc-sdk-input { border-radius: ${controlRadius}; background: ${elevated}; color: ${elevatedFg}; box-shadow: inset 0 0 0 1px ${mix(border, 60)}; transition: background 150ms ease-out, box-shadow 150ms ease-out; }
99
199
  .oc-sdk-input::placeholder { color: ${muted}; }
100
200
  .oc-sdk-input:hover:not(:focus) { background-image: linear-gradient(${hover}, ${hover}); }
101
201
  .oc-sdk-input:focus, .oc-sdk-input:focus-visible { box-shadow: inset 0 0 0 2px ${focus}; }
@@ -103,100 +203,78 @@ textarea.oc-sdk-input { height: auto; padding: 8px 12px; resize: vertical; }
103
203
  .oc-sdk-field[data-invalid="true"] .oc-sdk-input:focus { box-shadow: inset 0 0 0 2px ${v('status-error', 'red')}; }
104
204
  .oc-sdk-input[data-mono="true"] { font-family: ${mono}; }
105
205
 
106
- .oc-sdk-search { position: relative; min-width: 0; }
107
- .oc-sdk-search .oc-sdk-input { padding-left: 34px; padding-right: 34px; }
108
- .oc-sdk-search-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: ${muted}; pointer-events: none; }
206
+ .oc-sdk-search-icon { color: ${muted}; }
109
207
  .oc-sdk-search[data-active="true"] .oc-sdk-search-icon { color: ${primary}; }
110
- .oc-sdk-search-clear { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: none; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 6px; color: ${muted}; }
111
- .oc-sdk-search[data-active="true"] .oc-sdk-search-clear { display: inline-flex; }
208
+ .oc-sdk-search-clear { border-radius: ${smallRadius}; color: ${muted}; }
112
209
  .oc-sdk-search-clear:hover { background: ${hover}; color: ${fg}; }
113
210
 
114
- .oc-sdk-select { position: relative; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
115
- .oc-sdk-trigger { display: inline-flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: 32px; padding: 0 8px 0 10px; border: 1px solid ${border}; border-radius: 6px; background: ${elevated}; color: ${elevatedFg}; font-size: 0.8125rem; text-align: left; transition: background 150ms ease-out; }
211
+ .oc-sdk-trigger { border-color: ${border}; border-radius: ${controlRadius}; background: ${elevated}; color: ${elevatedFg}; transition: background 150ms ease-out; }
116
212
  .oc-sdk-trigger:hover { background-image: linear-gradient(${hover}, ${hover}); }
117
213
  .oc-sdk-trigger[aria-expanded="true"] { background-image: linear-gradient(${active}, ${active}); }
118
- .oc-sdk-trigger-value { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
119
214
  .oc-sdk-trigger-value[data-empty="true"] { color: ${muted}; }
120
- .oc-sdk-trigger-chevron { flex: 0 0 auto; color: ${muted}; }
121
- .oc-sdk-popup { --surface-foreground: ${elevatedFg}; position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 2px; min-width: 160px; max-width: calc(100vw - 16px); max-height: min(320px, calc(100vh - 16px)); overflow: auto; padding: 4px; border: 1px solid ${mix(border, 60)}; border-radius: 12px; background: ${elevated}; color: ${elevatedFg}; box-shadow: 0 8px 24px ${mix(fg, 12)}; }
122
- .oc-sdk-popup-search { flex: 0 0 auto; padding: 2px 2px 4px; }
123
- .oc-sdk-popup-search .oc-sdk-input { height: 32px; font-size: 0.8125rem; }
124
- .oc-sdk-option { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 8px; font-size: 0.8125rem; text-align: left; }
215
+ .oc-sdk-trigger-chevron { color: ${muted}; }
216
+ .oc-sdk-popup { --surface-foreground: ${elevatedFg}; border-color: ${mix(border, 60)}; border-radius: 12px; background: ${elevated}; color: ${elevatedFg}; box-shadow: 0 8px 24px ${mix(fg, 12)}; }
217
+ .oc-sdk-option { border-radius: 8px; }
125
218
  .oc-sdk-option[data-active="true"] { background: ${hover}; }
126
- .oc-sdk-option[aria-selected="true"] { background: ${selection}; color: ${selectionFg}; }
219
+ /* A selected option shows a check mark in the text colour, like the app's own pickers. Options without the mark come
220
+ from bundles built before it existed and keep the selection fill as their only sign of the choice. */
221
+ .oc-sdk-option[aria-selected="true"]:not([data-mark]) { background: ${selection}; color: ${selectionFg}; }
222
+ .oc-sdk-option-check { color: ${fg}; }
127
223
  .oc-sdk-option[data-destructive="true"] { color: ${errorText}; }
128
224
  .oc-sdk-option[data-destructive="true"][data-active="true"] { background: ${mix(v('status-error', 'red'), 10)}; }
129
- .oc-sdk-option-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
130
- .oc-sdk-option-hint { flex: 0 0 auto; font-size: 0.75rem; color: ${muted}; }
131
- .oc-sdk-option-check { flex: 0 0 auto; width: 12px; }
132
- .oc-sdk-popup-empty { padding: 8px; font-size: 0.8125rem; color: ${muted}; }
225
+ .oc-sdk-option-hint { color: ${muted}; }
226
+ .oc-sdk-popup-empty { color: ${muted}; }
133
227
 
134
- .oc-sdk-check { display: inline-flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left; }
135
- .oc-sdk-check-box { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin-top: 3px; border: 1px solid ${border}; border-radius: 4px; color: ${primary}; transition: border-color 150ms ease-out; }
136
- .oc-sdk-check[aria-checked="true"] .oc-sdk-check-box { border-color: ${mix(primary, 65, border)}; }
137
- .oc-sdk-check-box > svg { display: none; }
138
- .oc-sdk-check[aria-checked="true"] .oc-sdk-check-box > svg { display: block; }
139
- .oc-sdk-check-thumb { flex: 0 0 auto; position: relative; width: 36px; height: 20px; border-radius: 9999px; background: ${border}; transition: background 150ms ease-out; }
140
- .oc-sdk-check-thumb::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 9999px; background: ${bg}; transition: transform 150ms ease-out; }
228
+ /* Checked stays neutral: the mark carries the state, and the primary colour stays with primary actions. */
229
+ .oc-sdk-check-box { border-color: ${border}; border-radius: 4px; color: ${fg}; transition: border-color 150ms ease-out; }
230
+ .oc-sdk-check[aria-checked="true"] .oc-sdk-check-box { border-color: ${mix(fg, 60, border)}; }
231
+ .oc-sdk-check-thumb { border-radius: 9999px; background: ${border}; transition: background 150ms ease-out; }
232
+ .oc-sdk-check-thumb::after { border-radius: 9999px; background: ${bg}; transition: transform 150ms ease-out; }
141
233
  .oc-sdk-check[aria-checked="true"] .oc-sdk-check-thumb { background: ${primary}; }
142
234
  .oc-sdk-check[aria-checked="true"] .oc-sdk-check-thumb::after { transform: translateX(16px); }
143
235
  .oc-sdk-check:focus-visible { box-shadow: none; }
144
236
  .oc-sdk-check:focus-visible .oc-sdk-check-box, .oc-sdk-check:focus-visible .oc-sdk-check-thumb { ${focusRing} }
145
- .oc-sdk-check-text { display: flex; flex-direction: column; min-width: 0; }
146
- .oc-sdk-check-label { font-size: 0.875rem; }
147
- .oc-sdk-check-desc { font-size: 0.75rem; color: ${muted}; }
237
+ .oc-sdk-check-desc { color: ${muted}; }
148
238
 
149
- .oc-sdk-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 10px; max-width: 100%; overflow: auto; }
239
+ .oc-sdk-tabs { border-radius: 10px; }
150
240
  .oc-sdk-tabs[data-track="true"] { background: ${mix(fg, 4)}; }
151
- .oc-sdk-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid transparent; border-radius: 8px; font-size: 0.8125rem; font-weight: 500; color: ${muted}; white-space: nowrap; transition: color 150ms ease-out, background 150ms ease-out; }
241
+ .oc-sdk-tab { border-radius: 8px; color: ${muted}; transition: color 150ms ease-out, background 150ms ease-out; }
152
242
  .oc-sdk-tab:hover { color: ${fg}; }
153
243
  .oc-sdk-tab[aria-selected="true"] { color: ${selectionFg}; background: ${selection}; border-color: ${border}; }
154
- .oc-sdk-tab-count { font-size: 0.75rem; font-variant-numeric: tabular-nums; color: ${muted}; }
244
+ .oc-sdk-tab-count { color: ${muted}; }
155
245
 
156
- .oc-sdk-badge { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 9999px; font-size: 11px; font-weight: 500; line-height: 16px; white-space: nowrap; background: ${hover}; color: ${muted}; }
246
+ .oc-sdk-badge { border-radius: 9999px; background: ${hover}; color: ${muted}; }
157
247
  .oc-sdk-badge[data-tone] { color: var(--oc-sdk-tone-text, var(--oc-sdk-tone)); background: ${mix('var(--oc-sdk-tone)', 15)}; }
158
248
 
159
- .oc-sdk-list { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
160
- .oc-sdk-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 6px; text-align: left; transition: background 120ms ease-out; }
249
+ .oc-sdk-row { border-radius: 6px; transition: background 120ms ease-out; }
161
250
  .oc-sdk-row:hover, .oc-sdk-row[data-active="true"] { background: ${hover}; }
162
251
  .oc-sdk-row[aria-selected="true"] { background: ${selection}; color: ${selectionFg}; }
163
- .oc-sdk-row-lead { flex: 0 0 auto; width: 64px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ${mono}; font-size: 0.75rem; color: ${muted}; }
164
- .oc-sdk-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
165
- .oc-sdk-row-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
166
- .oc-sdk-row-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; color: ${muted}; }
167
- .oc-sdk-row-meta { flex: 0 0 auto; font-size: 0.75rem; font-variant-numeric: tabular-nums; color: ${muted}; }
252
+ .oc-sdk-row-lead { font-family: ${mono}; color: ${muted}; }
253
+ .oc-sdk-row-sub { color: ${muted}; }
254
+ .oc-sdk-row-meta { color: ${muted}; }
168
255
  .oc-sdk-row[aria-selected="true"] .oc-sdk-row-lead, .oc-sdk-row[aria-selected="true"] .oc-sdk-row-sub, .oc-sdk-row[aria-selected="true"] .oc-sdk-row-meta { color: inherit; opacity: .75; }
169
- .oc-sdk-list-empty { padding: 16px 8px; text-align: center; font-size: 0.8125rem; color: ${muted}; }
256
+ .oc-sdk-list-empty { color: ${muted}; }
170
257
 
171
- .oc-sdk-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 40px 16px; text-align: center; }
172
- .oc-sdk-empty-title { margin: 0; font-size: 0.8125rem; font-weight: 600; }
173
- .oc-sdk-empty-body { margin: 0; max-width: 32rem; font-size: 0.8125rem; color: ${muted}; }
174
- .oc-sdk-empty-action { margin-top: 12px; }
175
-
176
- @keyframes oc-sdk-spin { to { transform: rotate(360deg); } }
177
- .oc-sdk-spinner { display: inline-flex; align-items: center; gap: 8px; font-size: 0.8125rem; color: ${muted}; }
178
- .oc-sdk-spinner-ring { width: 16px; height: 16px; border: 2px solid ${border}; border-top-color: ${primary}; border-radius: 9999px; animation: oc-sdk-spin .8s linear infinite; }
179
- .oc-sdk-spinner[data-size="sm"] .oc-sdk-spinner-ring { width: 12px; height: 12px; }
258
+ .oc-sdk-empty-body { color: ${muted}; }
180
259
 
181
- .oc-sdk-banner { display: flex; align-items: flex-start; gap: 12px; padding: 8px 12px; border: 1px solid ${mix('var(--oc-sdk-tone)', 40)}; border-radius: 8px; background: ${mix('var(--oc-sdk-tone)', 10)}; }
182
- .oc-sdk-banner-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
183
- .oc-sdk-banner-title { font-size: 0.8125rem; font-weight: 500; color: var(--oc-sdk-tone-text, var(--oc-sdk-tone)); }
184
- .oc-sdk-banner-body { font-size: 0.8125rem; color: ${muted}; }
185
- .oc-sdk-banner-action { flex: 0 0 auto; }
260
+ .oc-sdk-spinner { color: ${muted}; }
261
+ .oc-sdk-spinner-ring { border-color: ${border}; border-top-color: ${primary}; border-radius: 9999px; }
186
262
 
187
- .oc-sdk-separator { display: flex; align-items: center; gap: 8px; width: 100%; margin: 8px 0; font-size: 0.75rem; color: ${muted}; }
188
- .oc-sdk-separator::before, .oc-sdk-separator::after { content: ""; flex: 1 1 auto; height: 1px; background: ${mix(border, 40)}; }
189
- .oc-sdk-separator[data-labeled="false"]::after { display: none; }
190
- .oc-sdk-popup > .oc-sdk-separator { margin: 4px 0; }
263
+ .oc-sdk-banner { border-color: ${mix('var(--oc-sdk-tone)', 40)}; border-radius: 8px; background: ${mix('var(--oc-sdk-tone)', 10)}; }
264
+ .oc-sdk-banner-title { color: var(--oc-sdk-tone-text, var(--oc-sdk-tone)); }
265
+ .oc-sdk-banner-body { color: ${muted}; }
191
266
 
192
- .oc-sdk-progress { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
193
- .oc-sdk-progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: ${muted}; font-variant-numeric: tabular-nums; }
194
- .oc-sdk-progress-track { height: 6px; border-radius: 9999px; background: ${border}; overflow: hidden; }
195
- .oc-sdk-progress-fill { height: 100%; border-radius: 9999px; background: var(--oc-sdk-tone, ${primary}); transform-origin: left; transition: transform 200ms ease-out; }
267
+ .oc-sdk-separator { color: ${muted}; }
268
+ .oc-sdk-separator::before, .oc-sdk-separator::after { background: ${mix(border, 40)}; }
196
269
 
197
- .oc-sdk-menu { position: relative; display: inline-flex; }
270
+ .oc-sdk-progress-label { color: ${muted}; }
271
+ .oc-sdk-progress-track { border-radius: 9999px; background: ${border}; }
272
+ .oc-sdk-progress-fill { border-radius: 9999px; background: var(--oc-sdk-tone, ${primary}); }
198
273
 
199
- .oc-sdk-text { white-space: pre-wrap; overflow-wrap: anywhere; }
200
274
  .oc-sdk-text a { color: ${primaryText}; text-decoration: underline; text-underline-offset: 2px; }
201
- .oc-sdk-text img { display: block; max-width: 100%; margin: 8px 0; border-radius: 8px; border: 1px solid ${mix(border, 60)}; }
275
+ .oc-sdk-text img { border-radius: 8px; border: 1px solid ${mix(border, 60)}; }
202
276
  `;
277
+ export const UI_CSS = `
278
+ ${GUEST_SCROLLBAR_CSS}
279
+ ${UI_SIZE_CSS}
280
+ ${GUEST_UI_LOOK_CSS}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Contract for OpenChamber guests: manifest parse and iframe host bridge.",
5
5
  "private": false,
6
6
  "license": "MIT",