@fias/arche-sdk 2.12.0 → 2.14.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/dist/bridge.d.ts +27 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +58 -0
- package/dist/bridge.js.map +1 -1
- package/dist/bridge.test.js +93 -0
- package/dist/bridge.test.js.map +1 -1
- package/dist/fias.d.ts +9 -4
- package/dist/fias.d.ts.map +1 -1
- package/dist/fias.js +15 -8
- package/dist/fias.js.map +1 -1
- package/dist/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +2 -0
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +56 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +93 -4
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +93 -0
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +559 -20
- package/dist/panels.d.ts +213 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/panels.js +422 -0
- package/dist/panels.js.map +1 -0
- package/dist/panels.test.d.ts +2 -0
- package/dist/panels.test.d.ts.map +1 -0
- package/dist/panels.test.js +127 -0
- package/dist/panels.test.js.map +1 -0
- package/dist/types.d.ts +174 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +135 -1
- package/templates/default/CLAUDE.md +135 -1
|
@@ -744,6 +744,7 @@ if ('canceled' in picked) {
|
|
|
744
744
|
const granted = await userDocs.list(); // all granted docs
|
|
745
745
|
const { document, content } = await userDocs.get(id, { includeContent: true }); // text docs
|
|
746
746
|
const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
|
|
747
|
+
const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binary docs
|
|
747
748
|
```
|
|
748
749
|
|
|
749
750
|
**The consent model (what to tell your users):**
|
|
@@ -752,7 +753,55 @@ const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
|
|
|
752
753
|
- After revocation, `get`/`getDownloadUrl` reject with `DOCUMENT_NOT_FOUND` — handle that path gracefully (drop the document from your UI; do not retry in a loop).
|
|
753
754
|
- `proprietary`-sensitivity documents are never grantable (they don't even appear in the picker).
|
|
754
755
|
- Every read is audited by the platform; a new document _version_ is a new documentId, so a re-pick is needed after the user replaces a document.
|
|
755
|
-
-
|
|
756
|
+
- `getDownloadUrl` is for DISPLAY (`<img src>`); your iframe cannot fetch the URL (CSP). To process binary content (parse a PDF, transform an image), use `getBytes(id)` — size-capped at 15 MB (`CONSENTED_DOCUMENT_TOO_LARGE` names the cap; ask the user to open the file manually instead). `getBytes` has a tighter per-minute budget than the rest of the family because it can move megabytes: read sequentially, and surface a rate-limit error rather than retrying in a loop.
|
|
757
|
+
- Revocation behaves differently per method: `getBytes` and `get` re-check the grant on every call, so they start failing immediately. A URL already handed out by `getDownloadUrl` keeps working until it expires (1 hour) — that is the documented contract, not a bug.
|
|
758
|
+
- **Testable in the dev harness (mock mode).** `pick()` grants a small fixture set — a text file, a real PDF, and one deliberately over the 15 MB cap — and `list` / `get` / `getDownloadUrl` / `getBytes` all resolve against it, including the refusals (`BINARY_DOCUMENT` on a text read of a PDF, `CONSENTED_DOCUMENT_TOO_LARGE`, `DOCUMENT_NOT_FOUND`). Run the harness with `FIAS_HARNESS_VAULT_PICK=canceled` to exercise the cancel branch. Not available in the builder preview — `pick()` resolves `{ canceled: true }` there.
|
|
759
|
+
|
|
760
|
+
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
761
|
+
|
|
762
|
+
**Permission:** `ai:actions`
|
|
763
|
+
|
|
764
|
+
The platform assistant (the Fias AI button in the host header) can perform actions inside your plugin — but only ones you declare. Declared actions become the assistant's tools while your plugin is open; a user can say "rotate the selected pages" and the assistant calls your handler.
|
|
765
|
+
|
|
766
|
+
There are two declaration lanes, one shape:
|
|
767
|
+
|
|
768
|
+
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
|
|
769
|
+
- **Runtime (this hook, trusted arches only):** declare/extend actions from code. The host silently ignores runtime declarations from non-trusted arches — manifest actions still work there, and this hook still handles their dispatches.
|
|
770
|
+
|
|
771
|
+
```tsx
|
|
772
|
+
import { useFiasAIActions } from '@fias/arche-sdk';
|
|
773
|
+
|
|
774
|
+
useFiasAIActions({
|
|
775
|
+
description: 'Once a document is open, the assistant can rotate its pages.',
|
|
776
|
+
actions: [
|
|
777
|
+
{
|
|
778
|
+
id: 'rotate_pages', // lowercase snake_case — becomes the tool name
|
|
779
|
+
description: 'Rotate the selected pages 90° clockwise.',
|
|
780
|
+
parameterSchema: { type: 'object', properties: {} },
|
|
781
|
+
availableWhen: { documentOpen: true }, // hidden until the state says so
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
state: { documentOpen: doc !== null }, // small, flag-shaped — NOT your data
|
|
785
|
+
onAction: async (actionId, params) => {
|
|
786
|
+
if (actionId === 'rotate_pages') {
|
|
787
|
+
if (!doc) throw new Error('No document is open.'); // ALWAYS re-check
|
|
788
|
+
await rotateSelected();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
throw new Error(`Unknown action ${actionId}`);
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Rules that matter:
|
|
797
|
+
|
|
798
|
+
- **Throw to report failure.** The assistant awaits `onAction` before telling the model the action applied — a batch ("do this to each file") stops on the failing step only if you throw.
|
|
799
|
+
- **Re-check preconditions in `onAction`.** `availableWhen` filtering is presentation; a stale turn can still dispatch a hidden verb.
|
|
800
|
+
- **Keep `state` tiny** (`{documentOpen: true}`, `hasPages: true`) — it is embedded in the assistant's prompt every turn and oversized snapshots are dropped.
|
|
801
|
+
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
802
|
+
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
803
|
+
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
804
|
+
- The assistant is not present in the builder preview or the dev harness — declarations are accepted but nothing dispatches until the published plugin runs with the Fias AI window.
|
|
756
805
|
|
|
757
806
|
### `useArcheAssets()` — Contributor-published asset library
|
|
758
807
|
|
|
@@ -979,6 +1028,91 @@ await fias.dataStore.listCollections();
|
|
|
979
1028
|
await fias.dataStore.deleteCollection('scores');
|
|
980
1029
|
```
|
|
981
1030
|
|
|
1031
|
+
### Panels (dialogs, reward popups, empty states)
|
|
1032
|
+
|
|
1033
|
+
A **panel** is a bounded block of declarative content — a title, an ordered list of leaf
|
|
1034
|
+
elements, some buttons. The platform ships a shared renderer, `@fias/panel-kit`, from its own
|
|
1035
|
+
CDN, so you get the same dialog/reward-popup look as first-party arches without writing modal,
|
|
1036
|
+
scrim or focus-trap code.
|
|
1037
|
+
|
|
1038
|
+
**Opt in** with a permission and a dependency (the platform serves the module; do NOT install it
|
|
1039
|
+
from npm — it isn't there):
|
|
1040
|
+
|
|
1041
|
+
```json
|
|
1042
|
+
// fias-plugin.json
|
|
1043
|
+
{
|
|
1044
|
+
"permissions": ["sandbox:vendored-libraries"],
|
|
1045
|
+
"dependencies": { "@fias/panel-kit": "0.3.0" }
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
The build **pins the version from the platform registry and rejects a manifest that declares a
|
|
1050
|
+
different one**, with the exact string to write. When a new panel-kit ships, bump this on your
|
|
1051
|
+
next submission — already-published builds keep resolving the version they were built against.
|
|
1052
|
+
|
|
1053
|
+
**Render one.** Inline is just a component; modal goes through the host:
|
|
1054
|
+
|
|
1055
|
+
```tsx
|
|
1056
|
+
import { Panel, PanelHost, usePanel } from '@fias/panel-kit';
|
|
1057
|
+
|
|
1058
|
+
// Inline — in the page's normal flow.
|
|
1059
|
+
<Panel definition={definition} context={{ values: { score }, items }} onAction={handleAction} />;
|
|
1060
|
+
|
|
1061
|
+
// Modal — mount <PanelHost> once near your app root, then:
|
|
1062
|
+
const { showPanel } = usePanel();
|
|
1063
|
+
const outcome = await showPanel(definition, { context: { values, items } });
|
|
1064
|
+
if (outcome.type === 'emit' && outcome.token === 'restart') restart();
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
`onAction` / the resolved outcome hands you the parsed intent — the renderer never navigates. A
|
|
1068
|
+
button's `action` is a closed namespace: `close`, `emit:<token>` (your own verb, handed back to
|
|
1069
|
+
you), `open_arche:arc_<32 hex>`, `open_url:https://…`.
|
|
1070
|
+
|
|
1071
|
+
**Elements** are leaf kinds only — no nesting, no layout algebra, no conditionals beyond one
|
|
1072
|
+
truthiness check:
|
|
1073
|
+
|
|
1074
|
+
| Kind | What it draws |
|
|
1075
|
+
| ----------------- | --------------------------------------------------------------------------------- |
|
|
1076
|
+
| `text` | Plain text; `{placeholder}` tokens fill from `context.values`. Never HTML. |
|
|
1077
|
+
| `conditionalText` | The same, only when `context.values[showIf]` is truthy. `showIf` is a value NAME. |
|
|
1078
|
+
| `image` | A platform storage key the host resolves. |
|
|
1079
|
+
| `assetImage` | An arche asset-library id (`as_…`). |
|
|
1080
|
+
| `items` | `context.items` — the list you pass at render time. |
|
|
1081
|
+
| `slottedItems` | `context.itemsBySlot[slot]` — several independent lists in one panel. |
|
|
1082
|
+
| `button` | Label + action. |
|
|
1083
|
+
| `spacer` | Vertical whitespace. |
|
|
1084
|
+
|
|
1085
|
+
Both text kinds take `joinNext: true` to share a line with the element after them
|
|
1086
|
+
(`Base Reward: [icon] Fabricanse ×1`).
|
|
1087
|
+
|
|
1088
|
+
**Theming.** A `theme` sets colours, font, radii, spacing and the quantity format. Pass it to the
|
|
1089
|
+
presenter — not to a wrapping element — because the modal portals out of your subtree:
|
|
1090
|
+
|
|
1091
|
+
```tsx
|
|
1092
|
+
<PanelHost theme={{ bg: '#F6EAD2', accent: '#D8B24A', amountFormat: '×{n}' }} />
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Values are grammars, not free CSS: hex or `rgb()/rgba()` colours, `px`/`rem`/`em` lengths, a font
|
|
1096
|
+
stack with no parentheses. Anything else is dropped (a `url()` in a custom property would make
|
|
1097
|
+
every panel you draw fetch a third-party asset).
|
|
1098
|
+
|
|
1099
|
+
**Panels as DATA.** If your panels live in storage rather than in code — a `dataStore` document,
|
|
1100
|
+
an admin-edited blob — validate on the WRITE, using the same rules the platform enforces:
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
import { validatePanelCatalog } from '@fias/arche-sdk';
|
|
1104
|
+
|
|
1105
|
+
const result = validatePanelCatalog(draft); // { panels: [...], theme?: {...} }
|
|
1106
|
+
if (!result.ok) return showErrors(result.issues); // one message per problem, path-prefixed
|
|
1107
|
+
await fias.dataStore.put('panels', 'catalog', draft);
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
That is the whole pattern for server-driven panel content: **your existing storage plus a shared
|
|
1111
|
+
validator** — no new permission, no platform call. Changing a panel's wording then means editing
|
|
1112
|
+
a document, not shipping a release. `validatePanelDefinition` and `validatePanelTheme` validate
|
|
1113
|
+
the parts individually; the types (`PanelDefinition`, `PanelElement`, `PanelTheme`, `PanelCatalog`)
|
|
1114
|
+
come from `@fias/arche-sdk` too, so the shape you store is the shape the renderer takes.
|
|
1115
|
+
|
|
982
1116
|
### Advanced exports (rarely needed)
|
|
983
1117
|
|
|
984
1118
|
The SDK also exports the following for advanced use cases. Most plugins don't need them.
|
|
@@ -744,6 +744,7 @@ if ('canceled' in picked) {
|
|
|
744
744
|
const granted = await userDocs.list(); // all granted docs
|
|
745
745
|
const { document, content } = await userDocs.get(id, { includeContent: true }); // text docs
|
|
746
746
|
const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
|
|
747
|
+
const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binary docs
|
|
747
748
|
```
|
|
748
749
|
|
|
749
750
|
**The consent model (what to tell your users):**
|
|
@@ -752,7 +753,55 @@ const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
|
|
|
752
753
|
- After revocation, `get`/`getDownloadUrl` reject with `DOCUMENT_NOT_FOUND` — handle that path gracefully (drop the document from your UI; do not retry in a loop).
|
|
753
754
|
- `proprietary`-sensitivity documents are never grantable (they don't even appear in the picker).
|
|
754
755
|
- Every read is audited by the platform; a new document _version_ is a new documentId, so a re-pick is needed after the user replaces a document.
|
|
755
|
-
-
|
|
756
|
+
- `getDownloadUrl` is for DISPLAY (`<img src>`); your iframe cannot fetch the URL (CSP). To process binary content (parse a PDF, transform an image), use `getBytes(id)` — size-capped at 15 MB (`CONSENTED_DOCUMENT_TOO_LARGE` names the cap; ask the user to open the file manually instead). `getBytes` has a tighter per-minute budget than the rest of the family because it can move megabytes: read sequentially, and surface a rate-limit error rather than retrying in a loop.
|
|
757
|
+
- Revocation behaves differently per method: `getBytes` and `get` re-check the grant on every call, so they start failing immediately. A URL already handed out by `getDownloadUrl` keeps working until it expires (1 hour) — that is the documented contract, not a bug.
|
|
758
|
+
- **Testable in the dev harness (mock mode).** `pick()` grants a small fixture set — a text file, a real PDF, and one deliberately over the 15 MB cap — and `list` / `get` / `getDownloadUrl` / `getBytes` all resolve against it, including the refusals (`BINARY_DOCUMENT` on a text read of a PDF, `CONSENTED_DOCUMENT_TOO_LARGE`, `DOCUMENT_NOT_FOUND`). Run the harness with `FIAS_HARNESS_VAULT_PICK=canceled` to exercise the cancel branch. Not available in the builder preview — `pick()` resolves `{ canceled: true }` there.
|
|
759
|
+
|
|
760
|
+
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
761
|
+
|
|
762
|
+
**Permission:** `ai:actions`
|
|
763
|
+
|
|
764
|
+
The platform assistant (the Fias AI button in the host header) can perform actions inside your plugin — but only ones you declare. Declared actions become the assistant's tools while your plugin is open; a user can say "rotate the selected pages" and the assistant calls your handler.
|
|
765
|
+
|
|
766
|
+
There are two declaration lanes, one shape:
|
|
767
|
+
|
|
768
|
+
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
|
|
769
|
+
- **Runtime (this hook, trusted arches only):** declare/extend actions from code. The host silently ignores runtime declarations from non-trusted arches — manifest actions still work there, and this hook still handles their dispatches.
|
|
770
|
+
|
|
771
|
+
```tsx
|
|
772
|
+
import { useFiasAIActions } from '@fias/arche-sdk';
|
|
773
|
+
|
|
774
|
+
useFiasAIActions({
|
|
775
|
+
description: 'Once a document is open, the assistant can rotate its pages.',
|
|
776
|
+
actions: [
|
|
777
|
+
{
|
|
778
|
+
id: 'rotate_pages', // lowercase snake_case — becomes the tool name
|
|
779
|
+
description: 'Rotate the selected pages 90° clockwise.',
|
|
780
|
+
parameterSchema: { type: 'object', properties: {} },
|
|
781
|
+
availableWhen: { documentOpen: true }, // hidden until the state says so
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
state: { documentOpen: doc !== null }, // small, flag-shaped — NOT your data
|
|
785
|
+
onAction: async (actionId, params) => {
|
|
786
|
+
if (actionId === 'rotate_pages') {
|
|
787
|
+
if (!doc) throw new Error('No document is open.'); // ALWAYS re-check
|
|
788
|
+
await rotateSelected();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
throw new Error(`Unknown action ${actionId}`);
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Rules that matter:
|
|
797
|
+
|
|
798
|
+
- **Throw to report failure.** The assistant awaits `onAction` before telling the model the action applied — a batch ("do this to each file") stops on the failing step only if you throw.
|
|
799
|
+
- **Re-check preconditions in `onAction`.** `availableWhen` filtering is presentation; a stale turn can still dispatch a hidden verb.
|
|
800
|
+
- **Keep `state` tiny** (`{documentOpen: true}`, `hasPages: true`) — it is embedded in the assistant's prompt every turn and oversized snapshots are dropped.
|
|
801
|
+
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
802
|
+
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
803
|
+
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
804
|
+
- The assistant is not present in the builder preview or the dev harness — declarations are accepted but nothing dispatches until the published plugin runs with the Fias AI window.
|
|
756
805
|
|
|
757
806
|
### `useArcheAssets()` — Contributor-published asset library
|
|
758
807
|
|
|
@@ -979,6 +1028,91 @@ await fias.dataStore.listCollections();
|
|
|
979
1028
|
await fias.dataStore.deleteCollection('scores');
|
|
980
1029
|
```
|
|
981
1030
|
|
|
1031
|
+
### Panels (dialogs, reward popups, empty states)
|
|
1032
|
+
|
|
1033
|
+
A **panel** is a bounded block of declarative content — a title, an ordered list of leaf
|
|
1034
|
+
elements, some buttons. The platform ships a shared renderer, `@fias/panel-kit`, from its own
|
|
1035
|
+
CDN, so you get the same dialog/reward-popup look as first-party arches without writing modal,
|
|
1036
|
+
scrim or focus-trap code.
|
|
1037
|
+
|
|
1038
|
+
**Opt in** with a permission and a dependency (the platform serves the module; do NOT install it
|
|
1039
|
+
from npm — it isn't there):
|
|
1040
|
+
|
|
1041
|
+
```json
|
|
1042
|
+
// fias-plugin.json
|
|
1043
|
+
{
|
|
1044
|
+
"permissions": ["sandbox:vendored-libraries"],
|
|
1045
|
+
"dependencies": { "@fias/panel-kit": "0.3.0" }
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
The build **pins the version from the platform registry and rejects a manifest that declares a
|
|
1050
|
+
different one**, with the exact string to write. When a new panel-kit ships, bump this on your
|
|
1051
|
+
next submission — already-published builds keep resolving the version they were built against.
|
|
1052
|
+
|
|
1053
|
+
**Render one.** Inline is just a component; modal goes through the host:
|
|
1054
|
+
|
|
1055
|
+
```tsx
|
|
1056
|
+
import { Panel, PanelHost, usePanel } from '@fias/panel-kit';
|
|
1057
|
+
|
|
1058
|
+
// Inline — in the page's normal flow.
|
|
1059
|
+
<Panel definition={definition} context={{ values: { score }, items }} onAction={handleAction} />;
|
|
1060
|
+
|
|
1061
|
+
// Modal — mount <PanelHost> once near your app root, then:
|
|
1062
|
+
const { showPanel } = usePanel();
|
|
1063
|
+
const outcome = await showPanel(definition, { context: { values, items } });
|
|
1064
|
+
if (outcome.type === 'emit' && outcome.token === 'restart') restart();
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
`onAction` / the resolved outcome hands you the parsed intent — the renderer never navigates. A
|
|
1068
|
+
button's `action` is a closed namespace: `close`, `emit:<token>` (your own verb, handed back to
|
|
1069
|
+
you), `open_arche:arc_<32 hex>`, `open_url:https://…`.
|
|
1070
|
+
|
|
1071
|
+
**Elements** are leaf kinds only — no nesting, no layout algebra, no conditionals beyond one
|
|
1072
|
+
truthiness check:
|
|
1073
|
+
|
|
1074
|
+
| Kind | What it draws |
|
|
1075
|
+
| ----------------- | --------------------------------------------------------------------------------- |
|
|
1076
|
+
| `text` | Plain text; `{placeholder}` tokens fill from `context.values`. Never HTML. |
|
|
1077
|
+
| `conditionalText` | The same, only when `context.values[showIf]` is truthy. `showIf` is a value NAME. |
|
|
1078
|
+
| `image` | A platform storage key the host resolves. |
|
|
1079
|
+
| `assetImage` | An arche asset-library id (`as_…`). |
|
|
1080
|
+
| `items` | `context.items` — the list you pass at render time. |
|
|
1081
|
+
| `slottedItems` | `context.itemsBySlot[slot]` — several independent lists in one panel. |
|
|
1082
|
+
| `button` | Label + action. |
|
|
1083
|
+
| `spacer` | Vertical whitespace. |
|
|
1084
|
+
|
|
1085
|
+
Both text kinds take `joinNext: true` to share a line with the element after them
|
|
1086
|
+
(`Base Reward: [icon] Fabricanse ×1`).
|
|
1087
|
+
|
|
1088
|
+
**Theming.** A `theme` sets colours, font, radii, spacing and the quantity format. Pass it to the
|
|
1089
|
+
presenter — not to a wrapping element — because the modal portals out of your subtree:
|
|
1090
|
+
|
|
1091
|
+
```tsx
|
|
1092
|
+
<PanelHost theme={{ bg: '#F6EAD2', accent: '#D8B24A', amountFormat: '×{n}' }} />
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Values are grammars, not free CSS: hex or `rgb()/rgba()` colours, `px`/`rem`/`em` lengths, a font
|
|
1096
|
+
stack with no parentheses. Anything else is dropped (a `url()` in a custom property would make
|
|
1097
|
+
every panel you draw fetch a third-party asset).
|
|
1098
|
+
|
|
1099
|
+
**Panels as DATA.** If your panels live in storage rather than in code — a `dataStore` document,
|
|
1100
|
+
an admin-edited blob — validate on the WRITE, using the same rules the platform enforces:
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
import { validatePanelCatalog } from '@fias/arche-sdk';
|
|
1104
|
+
|
|
1105
|
+
const result = validatePanelCatalog(draft); // { panels: [...], theme?: {...} }
|
|
1106
|
+
if (!result.ok) return showErrors(result.issues); // one message per problem, path-prefixed
|
|
1107
|
+
await fias.dataStore.put('panels', 'catalog', draft);
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
That is the whole pattern for server-driven panel content: **your existing storage plus a shared
|
|
1111
|
+
validator** — no new permission, no platform call. Changing a panel's wording then means editing
|
|
1112
|
+
a document, not shipping a release. `validatePanelDefinition` and `validatePanelTheme` validate
|
|
1113
|
+
the parts individually; the types (`PanelDefinition`, `PanelElement`, `PanelTheme`, `PanelCatalog`)
|
|
1114
|
+
come from `@fias/arche-sdk` too, so the shape you store is the shape the renderer takes.
|
|
1115
|
+
|
|
982
1116
|
### Advanced exports (rarely needed)
|
|
983
1117
|
|
|
984
1118
|
The SDK also exports the following for advanced use cases. Most plugins don't need them.
|