@roughapp/feature 0.4.0 → 0.4.2
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/README.md +207 -116
- package/index.js +38 -38
- package/package.json +2 -3
- package/CHANGELOG.md +0 -220
package/README.md
CHANGED
|
@@ -1,210 +1,301 @@
|
|
|
1
1
|
# @roughapp/feature
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
product.
|
|
5
|
-
|
|
3
|
+
`@roughapp/feature` is the browser SDK for adding Rough Features to your
|
|
4
|
+
product. Your app defines where features can appear (a **surface**) and the
|
|
5
|
+
typed tools available there. The SDK renders published features and opens the
|
|
6
|
+
Feature Builder where users create them.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
The package ships as compiled ESM and Web Components, with TypeScript
|
|
9
|
+
declarations and a separate stylesheet.
|
|
10
|
+
|
|
11
|
+
> **Pre-1.0:** minor releases may contain breaking changes. Pin
|
|
12
|
+
> `@roughapp/feature` to an exact version if you want to control when you
|
|
13
|
+
> upgrade. Review the hosted [Feature SDK changelog](https://rough.app/docs/changelog)
|
|
14
|
+
> before updating.
|
|
15
|
+
|
|
16
|
+
## Before you start
|
|
17
|
+
|
|
18
|
+
You will need:
|
|
19
|
+
|
|
20
|
+
- a Rough project ID;
|
|
21
|
+
- a Rough signing key, with its private key kept on your server; and
|
|
22
|
+
- a backend endpoint that returns a short-lived Rough identity token for the
|
|
23
|
+
signed-in user.
|
|
24
|
+
|
|
25
|
+
Never put the private key in browser code. `fetchUserToken` should call your
|
|
26
|
+
backend, which authenticates the current user and mints the identity token.
|
|
9
27
|
|
|
10
28
|
## Install
|
|
11
29
|
|
|
12
30
|
```bash
|
|
13
|
-
|
|
31
|
+
npm install @roughapp/feature zod
|
|
14
32
|
```
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
The package is ESM-only and expects a modern browser and bundler such as Vite,
|
|
35
|
+
Webpack or Rspack. Direct `<script>` and CDN usage is not supported.
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
`@roughapp/feature` is browser-only: its root module registers custom elements
|
|
38
|
+
when it is evaluated. In an SSR app, import it and create the client from a
|
|
39
|
+
client-only boundary rather than from server-rendered code.
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
Install `zod` directly because your surface definitions import it. Other
|
|
42
|
+
runtime dependencies are installed with the package.
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
your app entry, to get the Rough theme variables:
|
|
44
|
+
Import the stylesheet once, near your app entry:
|
|
26
45
|
|
|
27
46
|
```ts
|
|
28
47
|
import '@roughapp/feature/style.css'
|
|
29
48
|
```
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
It provides Rough's theme variables but does not apply a global CSS reset.
|
|
51
|
+
Component styles are registered by the custom elements when the JavaScript
|
|
52
|
+
package loads.
|
|
32
53
|
|
|
33
|
-
##
|
|
54
|
+
## Quick start
|
|
34
55
|
|
|
35
|
-
### Define a surface
|
|
56
|
+
### 1. Define a surface
|
|
36
57
|
|
|
37
|
-
A surface
|
|
38
|
-
|
|
58
|
+
A surface represents one place in your product where Rough Features can appear.
|
|
59
|
+
Give it a stable key and describe the tools a feature may call there.
|
|
39
60
|
|
|
40
61
|
```ts
|
|
41
|
-
import {
|
|
42
|
-
defineRoughSurface,
|
|
43
|
-
Query,
|
|
44
|
-
Mutation,
|
|
45
|
-
Subscription,
|
|
46
|
-
} from '@roughapp/feature'
|
|
62
|
+
import { defineRoughSurface, Query } from '@roughapp/feature'
|
|
47
63
|
import { z } from 'zod'
|
|
48
64
|
|
|
65
|
+
const Message = z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
subject: z.string(),
|
|
68
|
+
})
|
|
69
|
+
|
|
49
70
|
export const inboxSurface = defineRoughSurface({
|
|
50
71
|
key: 'inbox',
|
|
51
72
|
name: 'Inbox',
|
|
52
|
-
description: 'The
|
|
73
|
+
description: 'The signed-in user’s message inbox.',
|
|
53
74
|
tools: [
|
|
54
75
|
new Query({
|
|
55
76
|
id: 'listMessages',
|
|
56
|
-
name: 'List
|
|
57
|
-
description: '
|
|
58
|
-
inputSchema: z.object({
|
|
59
|
-
|
|
77
|
+
name: 'List messages',
|
|
78
|
+
description: 'Return the newest messages in the inbox.',
|
|
79
|
+
inputSchema: z.object({
|
|
80
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
81
|
+
}),
|
|
82
|
+
outputSchema: z.array(Message),
|
|
60
83
|
outputSample: [{ id: 'msg_1', subject: 'Welcome' }],
|
|
61
84
|
implementation: async ({ limit }) => {
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
const response = await fetch(`/api/messages?limit=${limit}`)
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error('Unable to load messages')
|
|
88
|
+
}
|
|
89
|
+
return response.json()
|
|
64
90
|
},
|
|
65
91
|
}),
|
|
66
|
-
// Mutation uses the same fields. A Subscription implementation receives
|
|
67
|
-
// a callback before the input.
|
|
68
92
|
],
|
|
69
93
|
})
|
|
70
94
|
```
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
Tool inputs and outputs are validated against their Zod schemas at runtime.
|
|
97
|
+
`outputSample` should be representative, but must not contain real customer
|
|
98
|
+
data or secrets.
|
|
99
|
+
|
|
100
|
+
Use `Query` for reads and `Mutation` for writes; both implementations receive
|
|
101
|
+
the parsed input and return a promise. A `Subscription` implementation receives
|
|
102
|
+
an output callback first and the parsed input second, then returns a promise for
|
|
103
|
+
an unsubscribe function. All three classes are exported from
|
|
104
|
+
`@roughapp/feature`.
|
|
74
105
|
|
|
75
|
-
`defineRoughSurface()` validates and freezes the
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
`defineRoughSurface()` validates and freezes the definition. The definition has
|
|
107
|
+
no project or connection state, so the same value can be used with clients for
|
|
108
|
+
different projects. Within one client, a surface key identifies one contract;
|
|
109
|
+
reuse the same definition anywhere that surface is active.
|
|
78
110
|
|
|
79
|
-
### Create a client
|
|
111
|
+
### 2. Create a client
|
|
80
112
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
until you destroy it.
|
|
113
|
+
Create one client at the part of your app that owns the Rough project: a route,
|
|
114
|
+
provider, page controller or similar boundary.
|
|
84
115
|
|
|
85
116
|
```ts
|
|
86
117
|
import { createRoughClient } from '@roughapp/feature'
|
|
87
118
|
|
|
88
|
-
const
|
|
119
|
+
export const roughClient = createRoughClient({
|
|
89
120
|
projectId: 'proj_123',
|
|
90
|
-
fetchUserToken: async () =>
|
|
91
|
-
|
|
121
|
+
fetchUserToken: async () => {
|
|
122
|
+
const response = await fetch('/api/rough/user-token', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
credentials: 'include',
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error('Unable to authenticate with Rough')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { token } = (await response.json()) as { token: string }
|
|
132
|
+
return token
|
|
133
|
+
},
|
|
134
|
+
// baseUrl is optional. Omit it when using the Rough production API.
|
|
92
135
|
})
|
|
93
136
|
```
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
client for each `<baseUrl, projectId, personId>` identity. If you create another,
|
|
99
|
-
its startup fails with `RoughReplicacheIdentityConflictError`.
|
|
100
|
-
|
|
101
|
-
### Destroy it when you are done
|
|
138
|
+
`createRoughClient()` returns immediately, then starts authentication, the local
|
|
139
|
+
browser database and the first sync in the background. Operations wait for
|
|
140
|
+
startup themselves. If your UI needs an explicit ready state, use:
|
|
102
141
|
|
|
103
142
|
```ts
|
|
104
|
-
|
|
143
|
+
import { whenRoughClientReady } from '@roughapp/feature'
|
|
144
|
+
|
|
145
|
+
await whenRoughClientReady({ client: roughClient })
|
|
105
146
|
```
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
modal the client still owns, and does not resolve until all of that has
|
|
110
|
-
finished. A framework unmount hook may use `void client.destroy()`; tests and
|
|
111
|
-
controlled route transitions should await it.
|
|
148
|
+
A failed client is not restarted. Destroy it and create a new one after the
|
|
149
|
+
underlying problem has been handled.
|
|
112
150
|
|
|
113
|
-
|
|
114
|
-
asynchronous, and there is one way to ask for it.
|
|
151
|
+
### 3. Render the surface
|
|
115
152
|
|
|
116
|
-
|
|
153
|
+
Importing `@roughapp/feature` registers its custom elements. Set the `client`
|
|
154
|
+
and `surface` properties on `<rough-surface>`; these are JavaScript values, not
|
|
155
|
+
HTML attributes.
|
|
117
156
|
|
|
118
157
|
```ts
|
|
119
|
-
import
|
|
158
|
+
import '@roughapp/feature'
|
|
159
|
+
|
|
160
|
+
const element = document.createElement('rough-surface')
|
|
161
|
+
element.client = roughClient
|
|
162
|
+
element.surface = inboxSurface
|
|
120
163
|
|
|
121
|
-
|
|
164
|
+
document.querySelector('#rough-slot')?.append(element)
|
|
122
165
|
```
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
|
|
167
|
+
The package also exports `RoughSurface` for Svelte apps. As with the custom
|
|
168
|
+
element, import and render it only on the client when the app uses SSR.
|
|
126
169
|
|
|
127
|
-
|
|
170
|
+
## Open the Feature Builder
|
|
128
171
|
|
|
129
|
-
|
|
130
|
-
|
|
172
|
+
`openRoughCreate()` opens the create flow without requiring a mounted surface.
|
|
173
|
+
It returns a handle you can close during route teardown.
|
|
131
174
|
|
|
132
175
|
```ts
|
|
133
|
-
import '@roughapp/feature'
|
|
176
|
+
import { openRoughCreate } from '@roughapp/feature'
|
|
134
177
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
document.
|
|
178
|
+
const modal = await openRoughCreate({
|
|
179
|
+
client: roughClient,
|
|
180
|
+
surface: inboxSurface,
|
|
181
|
+
// Defaults to document.body. Use a target when your Rough theme is scoped
|
|
182
|
+
// to a subtree.
|
|
183
|
+
target: document.querySelector('#rough-root') ?? undefined,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// If the host needs to close it programmatically:
|
|
187
|
+
await modal.close()
|
|
139
188
|
```
|
|
140
189
|
|
|
141
|
-
|
|
190
|
+
The user can also close the modal from the UI. Calling `close()` again is safe.
|
|
191
|
+
|
|
192
|
+
## Subscribe to published features
|
|
142
193
|
|
|
143
|
-
|
|
144
|
-
|
|
194
|
+
Use `getRoughFeatures()` when you need the feature list rather than the rendered
|
|
195
|
+
`<rough-surface>`. The callback runs with the initial list, even when it is
|
|
196
|
+
empty, and runs again as published features change.
|
|
145
197
|
|
|
146
198
|
```ts
|
|
147
|
-
import { getRoughFeatures
|
|
199
|
+
import { getRoughFeatures } from '@roughapp/feature'
|
|
148
200
|
|
|
149
|
-
// Subscribe to the published features for a surface.
|
|
150
201
|
const subscription = getRoughFeatures({
|
|
151
|
-
client,
|
|
202
|
+
client: roughClient,
|
|
152
203
|
surface: inboxSurface,
|
|
153
204
|
onFeatures: (features) => {
|
|
154
205
|
console.log(features)
|
|
155
206
|
},
|
|
156
207
|
onError: (error) => {
|
|
157
|
-
console.error(error)
|
|
208
|
+
console.error('Unable to load Rough Features', error)
|
|
158
209
|
},
|
|
159
210
|
})
|
|
160
211
|
|
|
161
|
-
// Optional: wait
|
|
212
|
+
// Optional: wait until the first feature list has been delivered.
|
|
162
213
|
await subscription.ready
|
|
163
214
|
|
|
164
|
-
//
|
|
165
|
-
const modal = await openRoughCreate({
|
|
166
|
-
client,
|
|
167
|
-
surface: inboxSurface,
|
|
168
|
-
// Where the modal attaches. Defaults to document.body. Pass the element that
|
|
169
|
-
// scopes your Rough theme if you scope it to a subtree.
|
|
170
|
-
target: document.querySelector('#rough-root') ?? undefined,
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
await modal.close()
|
|
215
|
+
// Later:
|
|
174
216
|
await subscription.unsubscribe()
|
|
175
217
|
```
|
|
176
218
|
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
Both `getRoughFeatures()` and `openRoughCreate()` also accept an `AbortSignal`.
|
|
220
|
+
Their cleanup methods are asynchronous, idempotent and return the same promise
|
|
221
|
+
when called more than once.
|
|
179
222
|
|
|
180
|
-
##
|
|
223
|
+
## Client lifetime
|
|
181
224
|
|
|
182
|
-
|
|
225
|
+
Share one client with everything that uses the same Rough project and signed-in
|
|
226
|
+
person. In one JavaScript realm, only one live client may own a given
|
|
227
|
+
`<baseUrl, projectId, personId>` identity. Starting a duplicate client fails
|
|
228
|
+
with `RoughReplicacheIdentityConflictError`.
|
|
183
229
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- `defineRoughSurface` (+ `RoughSurfaceDefinition` type)
|
|
187
|
-
- `getRoughFeatures` (+ `GetRoughFeaturesOptions`,
|
|
188
|
-
`RoughFeatureSubscription` types)
|
|
189
|
-
- `openRoughCreate` (+ `OpenRoughCreateOptions`, `RoughModalHandle` types)
|
|
190
|
-
- `RoughSurface` / `<rough-surface>`
|
|
191
|
-
- `Query`, `Mutation`, `Subscription` (re-exported from `@roughapp/bridge`)
|
|
192
|
-
- Errors worth branching on: `RoughClientDestroyedError`,
|
|
193
|
-
`RoughReplicacheIdentityConflictError`, `RoughSurfaceContractConflictError`,
|
|
194
|
-
`RoughClientDestroyError`, `RoughInvalidClientError`
|
|
230
|
+
Destroy the client when its owning route or provider goes away, or before
|
|
231
|
+
replacing it after the user or project changes:
|
|
195
232
|
|
|
196
|
-
|
|
197
|
-
|
|
233
|
+
```ts
|
|
234
|
+
await roughClient.destroy()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`destroy()` closes any subscriptions and modals still owned by the client and
|
|
238
|
+
waits for cleanup to finish. It is safe to call more than once. Framework
|
|
239
|
+
unmount hooks that cannot await cleanup may use `void roughClient.destroy()`;
|
|
240
|
+
tests and controlled transitions should await it.
|
|
241
|
+
|
|
242
|
+
## Theming
|
|
243
|
+
|
|
244
|
+
The stylesheet defines a light theme on `:root`. Add `rough-dark` to an
|
|
245
|
+
ancestor to use the bundled dark values:
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<div id="rough-root" class="rough-dark"></div>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Override variables on `:root` or on the subtree containing your Rough elements:
|
|
252
|
+
|
|
253
|
+
```css
|
|
254
|
+
#rough-root {
|
|
255
|
+
--rough-primary: oklch(0.52 0.2 264);
|
|
256
|
+
--rough-primary-foreground: white;
|
|
257
|
+
--rough-brand: #635bff;
|
|
258
|
+
--rough-radius: 0.75rem;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
198
261
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
262
|
+
Common variables include `--rough-background`, `--rough-foreground`,
|
|
263
|
+
`--rough-card`, `--rough-primary`, `--rough-secondary`, `--rough-muted`,
|
|
264
|
+
`--rough-border`, `--rough-ring`, `--rough-brand` and `--rough-radius`.
|
|
265
|
+
|
|
266
|
+
Create and edit modals attach to `document.body` by default. If your overrides
|
|
267
|
+
live below `body`, pass that themed element as the modal's `target` so the modal
|
|
268
|
+
inherits them.
|
|
269
|
+
|
|
270
|
+
## Public API
|
|
271
|
+
|
|
272
|
+
The recommended integration surface is:
|
|
273
|
+
|
|
274
|
+
- `createRoughClient`, `whenRoughClientReady` and the `RoughClient`,
|
|
275
|
+
`RoughClientOptions` types;
|
|
276
|
+
- `defineRoughSurface` and the `RoughSurfaceDefinition` type;
|
|
277
|
+
- `getRoughFeatures` and the `GetRoughFeaturesOptions`,
|
|
278
|
+
`RoughFeatureSubscription` types;
|
|
279
|
+
- `openRoughCreate` and the `OpenRoughCreateOptions`, `RoughModalHandle` types;
|
|
280
|
+
- `RoughSurface` / `<rough-surface>`;
|
|
281
|
+
- `Query`, `Mutation`, `Subscription`; and
|
|
282
|
+
- `RoughClientDestroyedError`, `RoughReplicacheIdentityConflictError`,
|
|
283
|
+
`RoughSurfaceContractConflictError`, `RoughClientDestroyError` and
|
|
284
|
+
`RoughInvalidClientError`.
|
|
285
|
+
|
|
286
|
+
The package also exports lower-level components and data types used to build its
|
|
287
|
+
own UI. During the pre-1.0 series, do not treat these as a stable integration
|
|
288
|
+
surface:
|
|
289
|
+
|
|
290
|
+
- `RoughCreateModal` (`<rough-create-modal>`), `RoughEditButton`
|
|
291
|
+
(`<rough-edit-button>`) and `RoughFeature` (`<rough-feature>`). The
|
|
292
|
+
`<rough-edit-modal>` element is registered transitively.
|
|
293
|
+
- `PrimaryButton`, `SecondaryButton`, `ResizeHandle`, `SpriteBuildMenu` and
|
|
294
|
+
`SpriteFrame`.
|
|
295
|
+
- `Sprite`, `SpriteBuild`, `JsonValue`, `RoughCleanup`, `FetchUserTokenFn`,
|
|
296
|
+
`SpriteFrameDatastore` and `SpriteFrameDatastoreContext`.
|
|
206
297
|
|
|
207
298
|
## License
|
|
208
299
|
|
|
209
|
-
`UNLICENSED`. This package is publicly installable but is not open source
|
|
300
|
+
`UNLICENSED`. This package is publicly installable but is not open source. Use
|
|
210
301
|
is governed by your Rough customer terms.
|