@brass-build/client 0.4.0 → 0.5.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/AGENTS.md +142 -41
- package/CHANGELOG.md +93 -4
- package/README.md +666 -179
- package/dist/acting-organization.d.ts.map +1 -1
- package/dist/acting-organization.js +6 -11
- package/dist/acting-organization.js.map +1 -1
- package/dist/api-fetch.d.ts +8 -0
- package/dist/api-fetch.d.ts.map +1 -1
- package/dist/api-fetch.js +35 -8
- package/dist/api-fetch.js.map +1 -1
- package/dist/batched-get.d.ts +11 -0
- package/dist/batched-get.d.ts.map +1 -0
- package/dist/batched-get.js +92 -0
- package/dist/batched-get.js.map +1 -0
- package/dist/call-service.d.ts +4 -0
- package/dist/call-service.d.ts.map +1 -1
- package/dist/call-service.js +66 -3
- package/dist/call-service.js.map +1 -1
- package/dist/client.d.ts +51 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +982 -124
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +25 -2
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +258 -61
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-commands.d.ts.map +1 -1
- package/dist/document-commands.js +5 -3
- package/dist/document-commands.js.map +1 -1
- package/dist/document-contents.d.ts +10 -0
- package/dist/document-contents.d.ts.map +1 -1
- package/dist/document-contents.js +23 -1
- package/dist/document-contents.js.map +1 -1
- package/dist/document-set-subscription.d.ts +56 -0
- package/dist/document-set-subscription.d.ts.map +1 -0
- package/dist/document-set-subscription.js +240 -0
- package/dist/document-set-subscription.js.map +1 -0
- package/dist/document-signals.d.ts +6 -0
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +31 -1
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +36 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +69 -18
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +12 -2
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +103 -27
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts +92 -10
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +285 -39
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +173 -8
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1778 -310
- package/dist/documents.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch-retry.d.ts +1 -0
- package/dist/fetch-retry.d.ts.map +1 -1
- package/dist/fetch-retry.js +2 -0
- package/dist/fetch-retry.js.map +1 -1
- package/dist/file-choice.d.ts +2 -0
- package/dist/file-choice.d.ts.map +1 -0
- package/dist/file-choice.js +27 -0
- package/dist/file-choice.js.map +1 -0
- package/dist/guest-link.d.ts +18 -1
- package/dist/guest-link.d.ts.map +1 -1
- package/dist/guest-link.js +329 -1
- package/dist/guest-link.js.map +1 -1
- package/dist/handle-types.d.ts +67 -5
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +15 -19
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +231 -76
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +61 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +94 -42
- package/dist/hosted-ui.js.map +1 -1
- package/dist/import-contract.d.ts +74 -0
- package/dist/import-contract.d.ts.map +1 -0
- package/dist/import-contract.js +163 -0
- package/dist/import-contract.js.map +1 -0
- package/dist/import-guest.d.ts +9 -0
- package/dist/import-guest.d.ts.map +1 -0
- package/dist/import-guest.js +155 -0
- package/dist/import-guest.js.map +1 -0
- package/dist/import-report.d.ts +17 -0
- package/dist/import-report.d.ts.map +1 -0
- package/dist/import-report.js +7 -0
- package/dist/import-report.js.map +1 -0
- package/dist/importer-wait.d.ts +2 -0
- package/dist/importer-wait.d.ts.map +1 -0
- package/dist/importer-wait.js +5 -0
- package/dist/importer-wait.js.map +1 -0
- package/dist/importer.d.ts +18 -4
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +322 -105
- package/dist/importer.js.map +1 -1
- package/dist/in-development.d.ts +2 -0
- package/dist/in-development.d.ts.map +1 -0
- package/dist/in-development.js +10 -0
- package/dist/in-development.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/init-keys.d.ts +1 -1
- package/dist/init-keys.d.ts.map +1 -1
- package/dist/init-keys.js +6 -1
- package/dist/init-keys.js.map +1 -1
- package/dist/input-documents.d.ts +67 -0
- package/dist/input-documents.d.ts.map +1 -0
- package/dist/input-documents.js +693 -0
- package/dist/input-documents.js.map +1 -0
- package/dist/inputs-status.d.ts +80 -0
- package/dist/inputs-status.d.ts.map +1 -0
- package/dist/inputs-status.js +460 -0
- package/dist/inputs-status.js.map +1 -0
- package/dist/inputs.d.ts +2 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +793 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/internal.js.map +1 -1
- package/dist/join-content.d.ts +33 -0
- package/dist/join-content.d.ts.map +1 -0
- package/dist/join-content.js +114 -0
- package/dist/join-content.js.map +1 -0
- package/dist/join-stamp.d.ts +3 -0
- package/dist/join-stamp.d.ts.map +1 -0
- package/dist/join-stamp.js +12 -0
- package/dist/join-stamp.js.map +1 -0
- package/dist/manifest-drift.d.ts +9 -0
- package/dist/manifest-drift.d.ts.map +1 -0
- package/dist/manifest-drift.js +46 -0
- package/dist/manifest-drift.js.map +1 -0
- package/dist/open-document.d.ts +109 -2
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +1822 -811
- package/dist/open-document.js.map +1 -1
- package/dist/page-restore.d.ts +12 -0
- package/dist/page-restore.d.ts.map +1 -0
- package/dist/page-restore.js +23 -0
- package/dist/page-restore.js.map +1 -0
- package/dist/placement.d.ts +13 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +34 -0
- package/dist/placement.js.map +1 -0
- package/dist/presence.d.ts +1 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +16 -4
- package/dist/presence.js.map +1 -1
- package/dist/read-only-document.d.ts +87 -0
- package/dist/read-only-document.d.ts.map +1 -0
- package/dist/read-only-document.js +4 -0
- package/dist/read-only-document.js.map +1 -0
- package/dist/record-paging.d.ts +63 -6
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +122 -29
- package/dist/record-paging.js.map +1 -1
- package/dist/references.d.ts +40 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +135 -0
- package/dist/references.js.map +1 -0
- package/dist/request-lane.d.ts +5 -0
- package/dist/request-lane.d.ts.map +1 -0
- package/dist/request-lane.js +48 -0
- package/dist/request-lane.js.map +1 -0
- package/dist/sealed-arrow.d.ts +170 -0
- package/dist/sealed-arrow.d.ts.map +1 -0
- package/dist/sealed-arrow.js +769 -0
- package/dist/sealed-arrow.js.map +1 -0
- package/dist/sealed-cache.d.ts +21 -0
- package/dist/sealed-cache.d.ts.map +1 -0
- package/dist/sealed-cache.js +247 -0
- package/dist/sealed-cache.js.map +1 -0
- package/dist/sealed-encode.d.ts +87 -0
- package/dist/sealed-encode.d.ts.map +1 -0
- package/dist/sealed-encode.js +876 -0
- package/dist/sealed-encode.js.map +1 -0
- package/dist/sealed-handles.d.ts +45 -0
- package/dist/sealed-handles.d.ts.map +1 -0
- package/dist/sealed-handles.js +408 -0
- package/dist/sealed-handles.js.map +1 -0
- package/dist/sealed-store.d.ts +242 -0
- package/dist/sealed-store.d.ts.map +1 -0
- package/dist/sealed-store.js +1155 -0
- package/dist/sealed-store.js.map +1 -0
- package/dist/serializer.d.ts +6 -15
- package/dist/serializer.d.ts.map +1 -1
- package/dist/session-toolbar.d.ts +20 -5
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +419 -67
- package/dist/session-toolbar.js.map +1 -1
- package/dist/session.d.ts +38 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -79
- package/dist/session.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +74 -0
- package/dist/sha256.js.map +1 -0
- package/dist/stable-handles.d.ts +19 -0
- package/dist/stable-handles.d.ts.map +1 -0
- package/dist/stable-handles.js +193 -0
- package/dist/stable-handles.js.map +1 -0
- package/dist/staged-seal.d.ts +2 -0
- package/dist/staged-seal.d.ts.map +1 -0
- package/dist/staged-seal.js +76 -0
- package/dist/staged-seal.js.map +1 -0
- package/dist/subscriber-delivery.d.ts +8 -0
- package/dist/subscriber-delivery.d.ts.map +1 -0
- package/dist/subscriber-delivery.js +31 -0
- package/dist/subscriber-delivery.js.map +1 -0
- package/dist/tail-op-import.d.ts +10 -0
- package/dist/tail-op-import.d.ts.map +1 -0
- package/dist/tail-op-import.js +33 -0
- package/dist/tail-op-import.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +6 -0
- package/dist/topic-lifecycle.d.ts.map +1 -1
- package/dist/topic-lifecycle.js +32 -2
- package/dist/topic-lifecycle.js.map +1 -1
- package/dist/trial-replace-dialog.d.ts +10 -0
- package/dist/trial-replace-dialog.d.ts.map +1 -0
- package/dist/trial-replace-dialog.js +153 -0
- package/dist/trial-replace-dialog.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +60 -5
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +344 -75
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts +1 -0
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +2 -1
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +21 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +192 -8
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts +1 -0
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +121 -17
- package/docs/custom-chrome.md +24 -16
- package/docs/hosted-ui-advanced.md +38 -6
- package/docs/imported-documents.md +104 -51
- package/docs/presence.md +16 -4
- package/docs/schemas.md +8 -5
- package/docs/services.md +6 -6
- package/docs/streams.md +139 -150
- package/docs/text-fields.md +26 -19
- package/package.json +5 -2
- package/src/acting-organization.ts +6 -12
- package/src/api-fetch.ts +53 -3
- package/src/auth-flow.ts +1 -1
- package/src/batched-get.ts +117 -0
- package/src/call-service.ts +78 -3
- package/src/client.ts +1092 -121
- package/src/document-checkpoints.ts +307 -65
- package/src/document-commands.ts +5 -5
- package/src/document-contents.ts +27 -1
- package/src/document-set-subscription.ts +274 -0
- package/src/document-signals.ts +47 -5
- package/src/document-streams.ts +85 -16
- package/src/document-thumbnail-capture.ts +118 -32
- package/src/documents-namespace.ts +539 -84
- package/src/documents.ts +2017 -348
- package/src/errors.ts +17 -6
- package/src/fetch-retry.ts +5 -0
- package/src/file-choice.ts +26 -0
- package/src/guest-link.ts +362 -1
- package/src/handle-types.ts +73 -47
- package/src/handles.ts +253 -83
- package/src/hosted-ui.ts +211 -50
- package/src/import-contract.ts +227 -0
- package/src/import-guest.ts +193 -0
- package/src/import-report.ts +24 -0
- package/src/importer-wait.ts +4 -0
- package/src/importer.ts +386 -131
- package/src/in-development.ts +10 -0
- package/src/index.ts +76 -16
- package/src/init-keys.ts +6 -1
- package/src/input-documents.ts +819 -0
- package/src/inputs-status.ts +598 -0
- package/src/inputs.ts +1090 -0
- package/src/internal.ts +7 -0
- package/src/join-content.ts +135 -0
- package/src/join-stamp.ts +12 -0
- package/src/manifest-drift.ts +54 -0
- package/src/open-document.ts +2346 -930
- package/src/page-restore.ts +38 -0
- package/src/placement.ts +54 -0
- package/src/presence.ts +21 -5
- package/src/read-only-document.ts +133 -0
- package/src/record-paging.ts +147 -25
- package/src/references.ts +207 -0
- package/src/request-lane.ts +59 -0
- package/src/sealed-arrow.ts +843 -0
- package/src/sealed-cache.ts +267 -0
- package/src/sealed-encode.ts +1015 -0
- package/src/sealed-handles.ts +486 -0
- package/src/sealed-store.ts +1330 -0
- package/src/serializer.ts +8 -20
- package/src/session-toolbar.ts +463 -72
- package/src/session.ts +740 -99
- package/src/sha256.ts +65 -0
- package/src/stable-handles.ts +235 -0
- package/src/staged-seal.ts +90 -0
- package/src/subscriber-delivery.ts +37 -0
- package/src/tail-op-import.ts +30 -0
- package/src/topic-lifecycle.ts +38 -2
- package/src/trial-replace-dialog.ts +163 -0
- package/src/types.ts +6 -6
- package/src/undo.ts +386 -73
- package/src/version-bar.ts +5 -1
- package/src/version.ts +1 -1
- package/src/ws-client.ts +208 -16
- package/src/ws-frame-limit.ts +1 -0
package/docs/app-manifest.md
CHANGED
|
@@ -14,8 +14,8 @@ build: a bundler that skips dot-directories drops it silently.
|
|
|
14
14
|
|
|
15
15
|
`brass publish` reads that same path out of the directory it publishes
|
|
16
16
|
(`<dir>/.well-known/brass-app.json`), falling back to `brass-app.json` in
|
|
17
|
-
the working directory,
|
|
18
|
-
|
|
17
|
+
the working directory, and refuses to publish a manifest whose `$ref`
|
|
18
|
+
entries do not resolve.
|
|
19
19
|
|
|
20
20
|
The platform re-reads the manifest after each deploy, so publishing
|
|
21
21
|
your bundle is what registers it. If Brass hosts your bundle
|
|
@@ -25,8 +25,12 @@ the bundle from your own origin, the platform fetches
|
|
|
25
25
|
`<origin>/.well-known/brass-app.json` as an anonymous client, so serve
|
|
26
26
|
that path publicly even when the rest of your app requires sign-in.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
Brass applies your published manifest, including to your app running
|
|
29
|
+
on your own machine, so publish after every change to it.
|
|
30
|
+
|
|
31
|
+
Brass uses `opens`, `inputs` and `creates` below to decide where to
|
|
32
|
+
offer your app. Declaring them grants your app no access to any
|
|
33
|
+
document.
|
|
30
34
|
|
|
31
35
|
## What goes in it
|
|
32
36
|
|
|
@@ -47,15 +51,57 @@ no access, so you declare them yourself.
|
|
|
47
51
|
|
|
48
52
|
Every field is optional, so ship only the ones your app uses.
|
|
49
53
|
|
|
50
|
-
- **`opens`** lists the
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
- **`opens`** lists the families of documents your app opens as
|
|
55
|
+
the document a user is working in. Brass offers your app in "Open
|
|
56
|
+
with" for them, and a user can make it their default. Declaring a `schema`
|
|
57
|
+
already folds its family in, so most apps list nothing extra here.
|
|
58
|
+
- **`inputs`** lists the schemas your app reads as inputs, such as the
|
|
59
|
+
models and drawings a dashboard counts. Each entry is a schema,
|
|
60
|
+
`{ "family", "streams" }`. The hosted picker presents a document to the
|
|
61
|
+
user when it matches one of these schemas (see
|
|
62
|
+
[What an app can open](./schemas.md#what-an-app-can-open)). For files that
|
|
63
|
+
have not been imported yet, the picker evaluates compatibility against the
|
|
64
|
+
schema declared by the file's importer. A schema declared only under
|
|
65
|
+
`inputs` does not add your app to the "Open with" menu for those documents.
|
|
66
|
+
Declare a schema under `inputs` rather than its family under `opens`
|
|
67
|
+
when your app reads documents through `Brass.chooseInputs` and never
|
|
68
|
+
edits them. Extract the schema from an existing document with
|
|
69
|
+
`npx @brass-build/cli schema pull --doc <docId> --out input-schema.json`,
|
|
70
|
+
which writes to that file while leaving your manifest's own `schema`
|
|
71
|
+
intact. Copy that file's `schema` into an `inputs` entry, then trim it to
|
|
72
|
+
only the required fields your code reads, since fewer requirements match
|
|
73
|
+
a wider set of documents.
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"inputs": [
|
|
78
|
+
{
|
|
79
|
+
"family": "ifc",
|
|
80
|
+
"streams": { "default": { "type": "object", "required": ["instances"] } }
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **`imports`** lists the file types your app imports. Brass offers your app
|
|
87
|
+
to whoever uploads a matching file and passes the file to your
|
|
88
|
+
`Brass.registerImporter` callback. Each entry has these fields:
|
|
89
|
+
|
|
90
|
+
| Field | |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `extension` | The file extension to match, without the dot, in any case, such as `"csv"`. |
|
|
93
|
+
| `content_type` | The media type to match exactly, such as `"text/csv"`. |
|
|
94
|
+
| `schema` | Optional. The schema of the document your importer makes from a matching file. Without it, your app's `schema` applies. |
|
|
95
|
+
|
|
96
|
+
An entry needs `extension` or `content_type`, and a file matches an entry
|
|
97
|
+
when it matches every field the entry sets. A file whose entry has no
|
|
98
|
+
`schema`, in an app with no `schema`, is not imported.
|
|
99
|
+
- **`creates`** is the family Brass stamps on the documents your
|
|
100
|
+
app makes, so peer apps that declare `opens` for that family can open
|
|
101
|
+
them. It folds into your own `opens` as well. When you declare a
|
|
102
|
+
`schema`, Brass stamps `schema.family` instead, so either leave
|
|
103
|
+
`creates` out or set it to the same family. `brass publish` warns
|
|
104
|
+
when the two differ.
|
|
59
105
|
- **`exports`** names the file format your app writes a document out
|
|
60
106
|
to, and your `Brass.registerSerializer` callback produces the bytes.
|
|
61
107
|
Declaring it makes those documents downloadable. Declare it when your
|
|
@@ -74,6 +120,64 @@ Every field is optional, so ship only the ones your app uses.
|
|
|
74
120
|
- **`schema`** declares the record shape your app works with, whether
|
|
75
121
|
you designed it or adopted an existing one. See
|
|
76
122
|
[`schemas.md`](./schemas.md).
|
|
123
|
+
- **`shared_schemas`** defines schemas by name for other fields to refer
|
|
124
|
+
to. See [Shared schemas](#shared-schemas) below.
|
|
125
|
+
- **`access`** says who may use your app. Declare `"create": "anyone"`
|
|
126
|
+
when a visitor who is not signed in can do something in your app, so
|
|
127
|
+
they can try it before creating an account. A visitor belongs to no
|
|
128
|
+
organization, so `Brass.chooseInputs`, `Brass.chooseInput` and calls to
|
|
129
|
+
a service your manifest declares are not available to them.
|
|
130
|
+
|
|
131
|
+
| Field | Values |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `create` | Who may start a new document in your app. `false` when your app makes no new documents of its own, such as a viewer of files other apps import: Brass then offers no New for it, and if your app calls `Brass.createDocument` anyway, the call throws a `BrassError` whose `code` is `"create_not_offered"`. Brass still opens uploaded and imported files in it. `"signed-in"` for people who are signed in. `"anyone"` adds visitors without an account, who can create, upload and edit temporary documents. For `"anyone"`, publish the app with `--gate off --visibility public`. |
|
|
134
|
+
| `open` | Who may open an organization's public link in your app: `"anyone"`, or `"signed-in"` to ask a visitor to sign in first. |
|
|
135
|
+
|
|
136
|
+
When you leave `create` out, Brass uses `"anyone"` for a public app
|
|
137
|
+
whose load gate is off and `"signed-in"` otherwise, so declare it to
|
|
138
|
+
keep the same answer when the gate changes. When you leave `open` out,
|
|
139
|
+
Brass uses `"anyone"`.
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{ "access": { "create": "anyone", "open": "anyone" } }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The hosted load gate (`brass publish --gate on|off`) is separate from
|
|
146
|
+
`access`: it decides whether your hosted bundle loads for a visitor who
|
|
147
|
+
is not signed in, before any of your code or this manifest runs.
|
|
148
|
+
|
|
149
|
+
## Shared schemas
|
|
150
|
+
|
|
151
|
+
Define schemas by name in `shared_schemas`. To use one, write
|
|
152
|
+
`{ "$ref": "#/shared_schemas/<name>" }` in place of a schema in `schema`, a
|
|
153
|
+
`inputs` entry, or an `imports` entry's `schema`. Brass treats a reference as
|
|
154
|
+
the schema it names.
|
|
155
|
+
|
|
156
|
+
This app writes `tracker` documents, reads as inputs any tracker that has
|
|
157
|
+
`rows`, and imports CSV files as `csv-table` documents that it also reads
|
|
158
|
+
as inputs:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"shared_schemas": {
|
|
163
|
+
"tracker": {
|
|
164
|
+
"family": "tracker",
|
|
165
|
+
"streams": { "default": { "type": "object", "required": ["rows", "columns"] } }
|
|
166
|
+
},
|
|
167
|
+
"tracker-input": {
|
|
168
|
+
"family": "tracker",
|
|
169
|
+
"streams": { "default": { "type": "object", "required": ["rows"] } }
|
|
170
|
+
},
|
|
171
|
+
"csv-table": {
|
|
172
|
+
"family": "csv-table",
|
|
173
|
+
"streams": { "default": { "type": "object", "required": ["header", "rows"] } }
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"schema": { "$ref": "#/shared_schemas/tracker" },
|
|
177
|
+
"inputs": [{ "$ref": "#/shared_schemas/tracker-input" }, { "$ref": "#/shared_schemas/csv-table" }],
|
|
178
|
+
"imports": [{ "extension": "csv", "schema": { "$ref": "#/shared_schemas/csv-table" } }]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
77
181
|
|
|
78
182
|
## Services, for calling third-party APIs
|
|
79
183
|
|
|
@@ -132,7 +236,7 @@ because that name belongs to whoever created the bucket.
|
|
|
132
236
|
|
|
133
237
|
### Credential fields
|
|
134
238
|
|
|
135
|
-
A field is one value the
|
|
239
|
+
A field is one value the user connecting a credential is asked for. Each one
|
|
136
240
|
is a box on the form they fill in.
|
|
137
241
|
|
|
138
242
|
Some authentication methods declare their own fields, and each method below
|
|
@@ -142,7 +246,7 @@ lists the ones it declares. Declare any others in `fields`:
|
|
|
142
246
|
| --- | --- |
|
|
143
247
|
| `name` | Required. Identifies the field. |
|
|
144
248
|
| `secret` | `true` by default. Brass encrypts the value and shows only its last few characters afterwards. `false` stores it in the clear and shows it in full. |
|
|
145
|
-
| `label` | What the form calls the field. Use the vendor's own word for it, since that is what the
|
|
249
|
+
| `label` | What the form calls the field. Use the vendor's own word for it, since that is what the user is reading from. Defaults to the name. |
|
|
146
250
|
| `readable` | `false` by default. `true` returns the value to your app with the service, for a value your app has to include in the request. Such a field is not secret, so `secret` defaults to `false` for it and declaring both is refused. See [Values the app reads](./services.md#values-the-app-reads). |
|
|
147
251
|
|
|
148
252
|
```json
|
|
@@ -200,9 +304,9 @@ The fields are `username` and `password`.
|
|
|
200
304
|
{ "host": "api.twilio.com", "auth": { "type": "basic" } }
|
|
201
305
|
```
|
|
202
306
|
|
|
203
|
-
Some services take a fixed word as the
|
|
307
|
+
Some services take a fixed word as the username rather than an account name,
|
|
204
308
|
such as `api` for Mailgun. Set it as `username` on the `auth` block and the
|
|
205
|
-
|
|
309
|
+
form asks for the password alone.
|
|
206
310
|
|
|
207
311
|
```json
|
|
208
312
|
{ "host": "api.mailgun.net", "auth": { "type": "basic", "username": "api" } }
|
package/docs/custom-chrome.md
CHANGED
|
@@ -44,7 +44,9 @@ Each piece renders one cluster wired to `Brass.session` and returns
|
|
|
44
44
|
- `Brass.mountDocumentTitle` renders the document title plus a
|
|
45
45
|
location chip; both open the hosted location editor (rename / move /
|
|
46
46
|
file). On a non-default branch the title carries a branch chip.
|
|
47
|
-
- `Brass.mountUndoRedo` renders undo / redo as
|
|
47
|
+
- `Brass.mountUndoRedo` renders undo / redo as icon buttons once either
|
|
48
|
+
can act, and nothing before then, so leave its container free to
|
|
49
|
+
collapse.
|
|
48
50
|
- `Brass.mountPresenceRoster` renders the who's-here roster of
|
|
49
51
|
coloured initials. It is the receiver half only: still publish your
|
|
50
52
|
own presence state (see the README's "Presence").
|
|
@@ -55,17 +57,19 @@ Each piece renders one cluster wired to `Brass.session` and returns
|
|
|
55
57
|
- `Brass.mountAccountMenu` renders the account avatar menu (email, an
|
|
56
58
|
Open Brass dashboard link, and sign out).
|
|
57
59
|
|
|
58
|
-
`mountHeader`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
`mountHeader` draws two strips under the bar. One tells a visitor whose
|
|
61
|
+
document was made without an account that it is temporary, and offers
|
|
62
|
+
sign-in. The other is
|
|
63
|
+
the historical-version preview banner (the return-to-live strip shown
|
|
64
|
+
while a document is previewing an earlier version). A multi-pane app on
|
|
65
|
+
the header shell gets both without another mount. A fully custom shell (next section, no
|
|
62
66
|
`mountHeader`) mounts it itself with `Brass.mountVersionPreview(el)`,
|
|
63
67
|
which renders nothing until there is something to show, like the drive
|
|
64
68
|
badge.
|
|
65
69
|
|
|
66
|
-
Mount the chrome once, when the user signs in
|
|
67
|
-
file menu has to exist
|
|
68
|
-
fresh user has no New / Open (the same rule as `mountToolbar`).
|
|
70
|
+
Mount the chrome once, when the user signs in or becomes a guest, not
|
|
71
|
+
per document: the file menu has to exist before any document is open or
|
|
72
|
+
a fresh user has no New / Open (the same rule as `mountToolbar`).
|
|
69
73
|
|
|
70
74
|
## Fully custom shells: the command list as data
|
|
71
75
|
|
|
@@ -87,8 +91,8 @@ The full built-in set when a doc is bound, in list order:
|
|
|
87
91
|
|
|
88
92
|
| `id` | `group` | `surface` | `label` | Enabled when |
|
|
89
93
|
| ---------- | ---------- | --------- | ------------------ | --------------------------------------- |
|
|
90
|
-
| `new` | `session` | `menu` | New | Signed in.
|
|
91
|
-
| `open` | `session` | `menu` | Open...
|
|
94
|
+
| `new` | `session` | `menu` | New | Signed in, or signed out where `access.create` in your manifest resolves to `"anyone"`. Never when it is `false`. |
|
|
95
|
+
| `open` | `session` | `menu` | Open... / Upload... (signed out) | Signed in, or signed out where `access.create` in your manifest resolves to `"anyone"`. |
|
|
92
96
|
| `history` | `document` | `menu` | Version history... | A document is open. |
|
|
93
97
|
| `branches` | `document` | `menu` | Branches... | A document is open. Has `children`. |
|
|
94
98
|
| `share` | `document` | `menu` | Share... / Share / Download... | A document is open. |
|
|
@@ -98,8 +102,8 @@ The full built-in set when a doc is bound, in list order:
|
|
|
98
102
|
|
|
99
103
|
The order runs use, then manage, then destructive: `new` / `open` put a
|
|
100
104
|
different document on screen, `history` / `branches` put a different
|
|
101
|
-
state of this one there, `share`
|
|
102
|
-
file, and `manage` (the entry
|
|
105
|
+
state of this one there, `share` shares the document with someone or
|
|
106
|
+
exports it to a file, and `manage` (the entry that offers delete) comes last. Render the
|
|
103
107
|
list in the order you receive it.
|
|
104
108
|
|
|
105
109
|
Each command carries `enabled` (read on every notification),
|
|
@@ -108,8 +112,8 @@ affordance), and `run()` (always resolves; failures route to
|
|
|
108
112
|
`Brass.onError`, never throw).
|
|
109
113
|
|
|
110
114
|
Read `label` on every notification too. `share` relabels once the SDK
|
|
111
|
-
knows whether the document has
|
|
112
|
-
one
|
|
115
|
+
knows whether the document has an exportable file, since downloading is
|
|
116
|
+
one option in the share dialog rather than a command of its own.
|
|
113
117
|
|
|
114
118
|
Draw a rule where `group` changes: `'session'` (a different document),
|
|
115
119
|
`'document'` (this document), `'edit'` (undo / redo). The widgets do
|
|
@@ -132,7 +136,11 @@ invoke, and the SDK owns the behavior:
|
|
|
132
136
|
|
|
133
137
|
- **`new`** creates a document and binds it, asking the platform where
|
|
134
138
|
to put it (`Brass.session.create()` under the hood).
|
|
135
|
-
- **`open`** opens the hosted picker and
|
|
139
|
+
- **`open`** opens the hosted picker and opens the document the user chooses.
|
|
140
|
+
Without an account it reads `Upload...` and creates a document from a file
|
|
141
|
+
the visitor chooses (`Brass.session.open()` under the hood). Before either
|
|
142
|
+
command replaces a document the visitor created without an account and has
|
|
143
|
+
edited, it prompts whether to sign in to save it to their account.
|
|
136
144
|
- **`history`** drives version preview end to end. During a preview
|
|
137
145
|
`doc.readOnly` is `true`, so gate your own edit controls on it;
|
|
138
146
|
returning to live is the chrome's job (the `mountVersionPreview`
|
|
@@ -142,7 +150,7 @@ invoke, and the SDK owns the behavior:
|
|
|
142
150
|
invoke a child's `run()`; a flat-list shell can call the parent's
|
|
143
151
|
`run()` to open the hosted picker instead.
|
|
144
152
|
- **`share`** opens the hosted share dialog: who can open the document,
|
|
145
|
-
and, where
|
|
153
|
+
and, where an exportable file is available, the download action.
|
|
146
154
|
- **`manage`** opens the same dialog on the document itself: its name,
|
|
147
155
|
where it sits, Duplicate, and Delete.
|
|
148
156
|
- **`undo` / `redo`** call `doc.undo()` / `doc.redo()` (see "Undo,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Hosted UI: advanced
|
|
2
2
|
|
|
3
3
|
Brass owns some surfaces: you call one method and Brass renders the UI
|
|
4
|
-
in a modal iframe over your page. `Brass.session`
|
|
4
|
+
in a modal iframe over your page. `Brass.session` runs the common ones
|
|
5
5
|
(`history`, `share`, `manage`) behind the file menu. This page covers
|
|
6
6
|
the rest and how they behave in browsers that block the iframe.
|
|
7
7
|
|
|
@@ -9,9 +9,34 @@ the rest and how they behave in browsers that block the iframe.
|
|
|
9
9
|
|
|
10
10
|
| Method | Returns |
|
|
11
11
|
| --- | --- |
|
|
12
|
-
| `Brass.selectDocument()` | `Promise<SelectDocumentResult \| null>`: opens the hosted document picker; resolves to the picked document, or `null` if the user dismisses it. |
|
|
13
12
|
| `Brass.shareDocument(docId)` | `Promise<void>`: opens the hosted share dialog for the document. |
|
|
14
13
|
|
|
14
|
+
## Narrowing a choice to the files you need
|
|
15
|
+
|
|
16
|
+
`Brass.chooseInputs(families, options?)` is how a user chooses files,
|
|
17
|
+
folders and projects for your app to read (see the README). `accept`
|
|
18
|
+
narrows the files of those families, for one call, by name or extension. Pass it once your app knows which files it
|
|
19
|
+
needs. An app that has just parsed a federation file asks for the models
|
|
20
|
+
that file names:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
await Brass.chooseInputs(['ifc'], {
|
|
24
|
+
prompt: `Models in ${federation.name}`,
|
|
25
|
+
accept: { names: federation.modelFileNames },
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The picker renders `prompt` as a subtitle beneath its title. Pass a prompt
|
|
30
|
+
when narrowing the list with `accept`, explaining why only specific files appear.
|
|
31
|
+
On a request you do not narrow, omit the prompt as the picker already shows
|
|
32
|
+
all documents your app can open.
|
|
33
|
+
|
|
34
|
+
`names` matches both the document's name and the original filename it was imported
|
|
35
|
+
from, and `extensions` matches either name's extension. A document in the specified
|
|
36
|
+
families passes when it matches any given name or extension. When a user selects a
|
|
37
|
+
folder or project, this filtering persists, so listing the container returns only
|
|
38
|
+
the matching documents.
|
|
39
|
+
|
|
15
40
|
Version history is a built-in file-menu command (`history` in
|
|
16
41
|
`session.commands` and the SDK toolbar's file menu) rather than a
|
|
17
42
|
launcher method, so an app on `mountToolbar` or `mountFileMenu` gets it
|
|
@@ -22,13 +47,18 @@ with no code.
|
|
|
22
47
|
Where the browser blocks the Hosted UI iframe (Safari, Firefox, Chrome
|
|
23
48
|
with third-party cookies off), these surfaces open as a full-page
|
|
24
49
|
redirect to Brass and return to your app when the user is done. The
|
|
25
|
-
picker returns with the chosen document
|
|
50
|
+
Open picker (`Brass.session.open()`) returns with the chosen document
|
|
51
|
+
opened into `Brass.session`.
|
|
26
52
|
|
|
27
53
|
So render the active document from the session (or `mountToolbar`): on
|
|
28
54
|
the redirect path the page reloads, so the code after `await
|
|
29
|
-
Brass.
|
|
30
|
-
session instead.
|
|
31
|
-
|
|
55
|
+
Brass.session.open()` never runs, and the picked document opens into the
|
|
56
|
+
session on reload instead.
|
|
57
|
+
|
|
58
|
+
`chooseInputs` and `chooseInput` do not redirect. Where the browser blocks
|
|
59
|
+
their iframe they reject with a `BrassError` whose `code` is
|
|
60
|
+
`'iframe_unavailable'`. Show the error's `message` and keep the references
|
|
61
|
+
your app already has.
|
|
32
62
|
|
|
33
63
|
## `BrassError` `.code` values
|
|
34
64
|
|
|
@@ -38,6 +68,8 @@ The hosted-UI launchers reject with `BrassError` carrying a `.code`
|
|
|
38
68
|
- `'embed_error'`: the hosted page loaded but couldn't proceed (e.g.
|
|
39
69
|
the user isn't signed in to Brass in this context).
|
|
40
70
|
- `'not_in_browser'`: called outside a browser environment.
|
|
71
|
+
- `'iframe_unavailable'`: the browser blocked the iframe and the surface
|
|
72
|
+
has no redirect to fall back on (`chooseInputs`, `chooseInput`).
|
|
41
73
|
|
|
42
74
|
## Theming
|
|
43
75
|
|
|
@@ -11,67 +11,123 @@ editor: declare the document's schema in `brass-app.json` so peers can
|
|
|
11
11
|
open it, and read the records through the ordinary handle surface. See
|
|
12
12
|
[`schemas.md`](./schemas.md).
|
|
13
13
|
|
|
14
|
-
**Any other file.**
|
|
15
|
-
|
|
14
|
+
**Any other file.** Nothing imports the file until your app registers an
|
|
15
|
+
importer for its type. Register the parse half with
|
|
16
16
|
`Brass.registerImporter` and the inverse with `Brass.registerSerializer`.
|
|
17
17
|
|
|
18
18
|
## Registering an importer
|
|
19
19
|
|
|
20
20
|
Register your parse half once at boot, and Brass runs it when an import
|
|
21
|
-
is needed:
|
|
21
|
+
is needed. Call `Brass.registerImporter` at startup, unconditionally: not
|
|
22
|
+
after sign-in, and not after a document opens. Brass may load your app only
|
|
23
|
+
to run its importer, in a page where nobody signs in and no document opens,
|
|
24
|
+
and an importer registered behind either step is never registered there.
|
|
22
25
|
|
|
23
26
|
```ts
|
|
24
|
-
Brass.registerImporter(async (
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const parsed =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
Brass.registerImporter(async (file) => {
|
|
28
|
+
const parsed = parseMyFormat(await file.bytes()); // your own parser
|
|
29
|
+
const instances: Record<string, unknown> = {};
|
|
30
|
+
for (const r of parsed) instances[r.id] = { type: r.type, params: r.body };
|
|
31
|
+
return {
|
|
32
|
+
streams: {
|
|
33
|
+
default: { instances, instance_order: parsed.map((r) => r.id) },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
33
36
|
});
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
The importer
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
The importer takes a file and returns JSON, so a test can call it
|
|
40
|
+
directly with a file the test builds. `DocumentImporter` types the
|
|
41
|
+
callback, `ImportFile` types the file it receives (`name`,
|
|
42
|
+
`content_type`, `byte_size`, and `bytes()`, which fetches the file), and
|
|
43
|
+
`ImportResult` types what it returns.
|
|
44
|
+
|
|
45
|
+
`streams` is the document's content, keyed by stream name. The
|
|
46
|
+
document opens on `default`. Your app opens any other stream with
|
|
47
|
+
`doc.stream(name)`, so put content there that a user can skip while
|
|
48
|
+
reading the rest (a heavy geometry layer beside the elements). Each stream
|
|
49
|
+
is a JSON object whose top-level values are objects, arrays, or strings. A
|
|
50
|
+
stream name uses lowercase letters, digits, `.`, `-`, and `_`. The
|
|
51
|
+
`schema` in your `brass-app.json` describes this shape, and
|
|
52
|
+
`npx @brass-build/cli contents --doc <docId>` writes the content of a
|
|
53
|
+
document your importer produced to `contents.json` (`--stdout` prints it
|
|
54
|
+
instead).
|
|
55
|
+
|
|
56
|
+
Brass decides when a document needs an import and runs your importer
|
|
57
|
+
once. Every client with the document open then receives the whole result
|
|
58
|
+
together. Declare the `schema` of the records your importer returns in
|
|
59
|
+
`brass-app.json`. The import publishes that schema and stamps it on the
|
|
60
|
+
document.
|
|
61
|
+
|
|
62
|
+
When your importer throws, or returns no streams or only empty
|
|
63
|
+
collections, Brass reports the error through `Brass.onError`. Brass also
|
|
64
|
+
records the failure on the document and does not run your importer on
|
|
65
|
+
that file again until the file changes. Everyone who opens the document
|
|
66
|
+
sees your error's message after your app's name, so write it for that
|
|
67
|
+
user: "Row 3 has no date column" rather than a stack trace.
|
|
52
68
|
|
|
53
69
|
Key each record by the format's own stable id. When the format has no
|
|
54
70
|
native id, derive one from the record's own content rather than from its
|
|
55
71
|
position in the file, which is not stable across a re-import.
|
|
56
72
|
|
|
57
|
-
##
|
|
73
|
+
## Reporting what the import skipped or assumed
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
with its own download URL, so an app fetches exactly the streams it
|
|
64
|
-
needs, in parallel. A single-stream file has exactly one entry, so code
|
|
65
|
-
that iterates `streams` handles both cases in one path:
|
|
75
|
+
When your importer reads only part of a file, return an `ImportReport`
|
|
76
|
+
beside the streams that says what it left out and what it filled in.
|
|
77
|
+
Brass stores the report with the import and shows it, in your words, on
|
|
78
|
+
the document's page:
|
|
66
79
|
|
|
67
80
|
```ts
|
|
68
|
-
|
|
69
|
-
const
|
|
81
|
+
Brass.registerImporter(async (file) => {
|
|
82
|
+
const rows = parseCsv(new TextDecoder().decode(await file.bytes()));
|
|
83
|
+
const content = rowsToContent(rows); // your own mapping
|
|
84
|
+
const findings: ImportFinding[] = [];
|
|
85
|
+
if (rows.ragged > 0) {
|
|
86
|
+
findings.push({
|
|
87
|
+
kind: 'skipped',
|
|
88
|
+
message: `Skipped ${rows.ragged} rows with more fields than the header.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!rows.delimiterDeclared) {
|
|
92
|
+
findings.push({
|
|
93
|
+
kind: 'assumed',
|
|
94
|
+
message: 'The file does not say how its fields are separated, so commas were used.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return findings.length > 0
|
|
98
|
+
? { streams: { default: content }, report: { summary: `Read ${rows.length} rows.`, findings } }
|
|
99
|
+
: { streams: { default: content } };
|
|
100
|
+
});
|
|
70
101
|
```
|
|
71
102
|
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
`summary` is one optional sentence stating what the import read, which Brass
|
|
104
|
+
shows above the findings.
|
|
105
|
+
|
|
106
|
+
Each finding's `message` is one sentence, and the user sees it as you
|
|
107
|
+
wrote it. Its `kind` says what the sentence describes:
|
|
108
|
+
|
|
109
|
+
- `skipped` is content in the file that the import did not read.
|
|
110
|
+
- `assumed` is a value the file did not give, which the import supplied.
|
|
111
|
+
- `missing` is something the file names that did not arrive (a texture a
|
|
112
|
+
model refers to, a drawing it references).
|
|
113
|
+
|
|
114
|
+
Return a report only when your importer has findings. Report only what
|
|
115
|
+
your importer can establish from the file, and do not tell the user a
|
|
116
|
+
file was read completely when the format cannot confirm it.
|
|
117
|
+
|
|
118
|
+
An app reads an open document's report from `doc.importReport` (`null`
|
|
119
|
+
when there is none), whichever importer wrote it. A `skipped` or `missing` finding means the document
|
|
120
|
+
contains only part of its file. An `assumed` finding means the importer
|
|
121
|
+
supplied some of the document's values rather than reading them from the
|
|
122
|
+
file.
|
|
123
|
+
|
|
124
|
+
## Reading the source bytes
|
|
125
|
+
|
|
126
|
+
Code outside the importer that needs the original bytes (a "view
|
|
127
|
+
original" affordance) calls `doc.sourceBlob()`. It resolves the stored
|
|
128
|
+
file as `{ url, filename, content_type, byte_size }`, where `url` is a
|
|
129
|
+
short-lived download URL. It rejects with a `BrassError` for a document
|
|
130
|
+
that has no stored original file.
|
|
75
131
|
|
|
76
132
|
## Registering a serializer
|
|
77
133
|
|
|
@@ -83,23 +139,20 @@ after `Brass.init`:
|
|
|
83
139
|
```ts
|
|
84
140
|
Brass.registerSerializer((doc) => {
|
|
85
141
|
const records = doc.orderedMap('instances', 'instance_order');
|
|
86
|
-
const text = myFormatToText(records.
|
|
142
|
+
const text = myFormatToText(records.entries()); // inverse of your parser
|
|
87
143
|
return new TextEncoder().encode(text);
|
|
88
144
|
});
|
|
89
145
|
```
|
|
90
146
|
|
|
91
147
|
The serializer returns the bytes (`Uint8Array`, or a promise of one).
|
|
92
|
-
`DocumentSerializer` types the callback. It
|
|
93
|
-
|
|
94
|
-
containers whole: `StoredDocument` types that document, and its `map` /
|
|
95
|
-
`list` return a `StoredMap` / `StoredList`, which add `read()` to the
|
|
96
|
-
handle an app holds.
|
|
148
|
+
`DocumentSerializer` types the callback. It receives the same document
|
|
149
|
+
your app reads, and walks each container with `entries()` or `values()`.
|
|
97
150
|
|
|
98
151
|
Your app exports so that software outside Brass can read its work, so
|
|
99
|
-
write a format that software already
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
152
|
+
write a format that outside software already supports. If your app imports a file
|
|
153
|
+
format, export that same format back out. If your app creates documents from
|
|
154
|
+
scratch, determine which formats users need to open in outside tools and export
|
|
155
|
+
that format.
|
|
103
156
|
|
|
104
157
|
## Declaring what your app handles
|
|
105
158
|
|
package/docs/presence.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
`doc.presence` is the "who is here right now" layer over an open
|
|
4
4
|
document: the roster of other live participants plus a small per-user
|
|
5
5
|
`state` (cursor / camera / selection) and an app-namespaced `ext` blob.
|
|
6
|
-
It is non-null on any open
|
|
6
|
+
It is non-null on any live open, editable or `{ readOnly: true }`,
|
|
7
|
+
including a signed-out visitor's open through a shared link, so a viewer
|
|
8
|
+
shows up in the roster beside the editors, and `null` on a
|
|
9
|
+
`{ frozen: true }` copy.
|
|
7
10
|
Presence is fully ephemeral: nothing survives a disconnect.
|
|
8
11
|
|
|
9
12
|
```ts
|
|
@@ -16,6 +19,8 @@ const doc = await Brass.openDocumentById('doc_abc');
|
|
|
16
19
|
// (e.g. on every pointermove).
|
|
17
20
|
doc.presence?.set({ state: { cursor: { x, y, z } } });
|
|
18
21
|
doc.presence?.set({ ext: { myApp: { tool: 'select' } } });
|
|
22
|
+
// A key set to `undefined` is removed, for you and for every peer.
|
|
23
|
+
doc.presence?.set({ state: { selection: undefined } });
|
|
19
24
|
|
|
20
25
|
// React to everyone else. `roster` is the other participants; each
|
|
21
26
|
// carries a server-stamped `displayName` + `color` (CSS-ready string,
|
|
@@ -78,8 +83,8 @@ interface PresenceState {
|
|
|
78
83
|
|
|
79
84
|
A caret belongs in `state` rather than in `ext` for the same reason a
|
|
80
85
|
selection does: a document of prose is often open in more than one app,
|
|
81
|
-
and
|
|
82
|
-
app
|
|
86
|
+
and each user wants to see where the others are working, whichever
|
|
87
|
+
app each is in. Publish it on every selection change, which the
|
|
83
88
|
10 Hz debounce already paces.
|
|
84
89
|
|
|
85
90
|
App-specific state (the current tool, a chat bubble, a typing
|
|
@@ -122,7 +127,14 @@ is a `PresenceParticipant` with
|
|
|
122
127
|
`{ connectionId, sub, displayName, avatarUrl, color, joinedAt, lastSeen,
|
|
123
128
|
state, ext }`. `displayName`, `avatarUrl`, and `color` are always
|
|
124
129
|
present as strings, and `avatarUrl` is `''` when the user has no
|
|
125
|
-
picture.
|
|
130
|
+
picture. A signed-out visitor on a shared link has `sub: ''` and a
|
|
131
|
+
`displayName` such as `Guest 1a2b3c`, so when you group participants
|
|
132
|
+
into people by `sub`, show each participant with an empty `sub` on its
|
|
133
|
+
own, keyed by `connectionId`. A visitor's second tab is a second guest.
|
|
134
|
+
On that visitor's page, every signed-in participant also arrives with
|
|
135
|
+
`sub: ''`, an empty `avatarUrl` and a `displayName` such as
|
|
136
|
+
`Member 4d5e6f`.
|
|
137
|
+
Both types are exported from `@brass-build/client`, alongside
|
|
126
138
|
`PresenceSetInput` and `PresenceChangeListener`, for annotating your
|
|
127
139
|
handlers and roster state.
|
|
128
140
|
|