@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.
Files changed (4) hide show
  1. package/README.md +207 -116
  2. package/index.js +38 -38
  3. package/package.json +2 -3
  4. package/CHANGELOG.md +0 -220
package/README.md CHANGED
@@ -1,210 +1,301 @@
1
1
  # @roughapp/feature
2
2
 
3
- Embeddable Web Components SDK for integrating Rough Features into a host
4
- product. It ships compiled, minified ESM plus a stylesheet, and registers a set
5
- of custom elements (including `<rough-surface>`).
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
- > **Pre-1.0.** The API surface may change between minor versions during the
8
- > `0.x` series. Pin an exact version if you need stability.
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
- pnpm add @roughapp/feature zod
31
+ npm install @roughapp/feature zod
14
32
  ```
15
33
 
16
- This package targets **npm + a modern bundler** (Vite, Webpack, Rspack, …). It
17
- is ESM-only and does not support direct `<script>`/CDN usage in this release.
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
- Install `zod` directly because your tool definitions import it. The package
20
- installs its other runtime dependencies automatically.
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
- ## Import the stylesheet
41
+ Install `zod` directly because your surface definitions import it. Other
42
+ runtime dependencies are installed with the package.
23
43
 
24
- The package does not inject a global CSS reset. Import the stylesheet once, near
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
- Component styles are registered automatically by the custom elements at runtime.
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
- ## Recommended API
54
+ ## Quick start
34
55
 
35
- ### Define a surface
56
+ ### 1. Define a surface
36
57
 
37
- A surface describes a place in your product where Rough features appear, plus
38
- the tools the feature can call. Import `z` from `zod` directly for tool schemas:
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 main message inbox.',
73
+ description: 'The signed-in user’s message inbox.',
53
74
  tools: [
54
75
  new Query({
55
76
  id: 'listMessages',
56
- name: 'List Messages',
57
- description: 'List messages in the inbox.',
58
- inputSchema: z.object({ limit: z.number().default(20) }),
59
- outputSchema: z.array(z.object({ id: z.string(), subject: z.string() })),
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
- /* return messages */
63
- return []
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
- `Query`, `Mutation`, and `Subscription` are re-exported from
73
- `@roughapp/bridge` for convenience.
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 surface and its tools array.
76
- The definition holds no client, project id or connection state, so you can hand
77
- the same value to clients for two different projects at once.
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
- `createRoughClient()` returns synchronously and immediately starts that
82
- project's authentication, local database and first sync. You own the client
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 client = createRoughClient({
119
+ export const roughClient = createRoughClient({
89
120
  projectId: 'proj_123',
90
- fetchUserToken: async () => myAppSession.getRoughToken(),
91
- // baseUrl is optional; defaults to the Rough production API.
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
- Create the client at whatever owns the project in your app: a route, a provider
96
- component, a page controller. Share that one client with everything below it
97
- rather than creating a second. Within one JavaScript realm, create only one live
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
- await client.destroy()
143
+ import { whenRoughClientReady } from '@roughapp/feature'
144
+
145
+ await whenRoughClientReady({ client: roughClient })
105
146
  ```
106
147
 
107
- `destroy()` is the only lifecycle method, and it works everywhere. It is
108
- idempotent, returns the same promise every time, closes every subscription and
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
- The client implements no disposal protocol. Teardown here is genuinely
114
- asynchronous, and there is one way to ask for it.
151
+ ### 3. Render the surface
115
152
 
116
- Startup runs in the background. If you want to observe it:
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 { whenRoughClientReady } from '@roughapp/feature'
158
+ import '@roughapp/feature'
159
+
160
+ const element = document.createElement('rough-surface')
161
+ element.client = roughClient
162
+ element.surface = inboxSurface
120
163
 
121
- await whenRoughClientReady({ client })
164
+ document.querySelector('#rough-slot')?.append(element)
122
165
  ```
123
166
 
124
- A client that fails startup keeps that error and replays it from every later
125
- operation. It is not restarted; destroy it and create a new one.
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
- ### Render a surface
170
+ ## Open the Feature Builder
128
171
 
129
- Use the `<rough-surface>` custom element, or the `RoughSurface` component
130
- export. Set both the client and the surface:
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 el = document.createElement('rough-surface')
136
- el.client = client
137
- el.surface = inboxSurface
138
- document.querySelector('#rough-slot')?.append(el)
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
- ### List features and open the create flow
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
- Operations are direct exports and take the client in their options. Neither
144
- requires anything to have mounted first.
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, openRoughCreate } from '@roughapp/feature'
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 for the first feature batch.
212
+ // Optional: wait until the first feature list has been delivered.
162
213
  await subscription.ready
163
214
 
164
- // Open the "create a feature" modal.
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
- Every cleanup handle returns a promise, is safe to call twice, and returns the
178
- same promise on the second call.
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
- ## Exports
223
+ ## Client lifetime
181
224
 
182
- Recommended, stable-ish customer API:
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
- - `createRoughClient`, `whenRoughClientReady` (+ `RoughClient`,
185
- `RoughClientOptions` types)
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
- Advanced / incidental exports are **not** intended as stable customer
197
- dependencies during the pre-1.0 series:
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
- - Custom-element components: `RoughCreateModal` (`<rough-create-modal>`),
200
- `RoughEditButton` (`<rough-edit-button>`), `RoughFeature` (`<rough-feature>`)
201
- (the `<rough-edit-modal>` element registers transitively).
202
- - UI building blocks: `PrimaryButton`, `SecondaryButton`, `ResizeHandle`,
203
- `SpriteBuildMenu`, `SpriteFrame`.
204
- - Types: `Sprite`, `SpriteBuild`, `JsonValue`, `RoughCleanup`,
205
- `FetchUserTokenFn`, `SpriteFrameDatastore`, `SpriteFrameDatastoreContext`.
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; use
300
+ `UNLICENSED`. This package is publicly installable but is not open source. Use
210
301
  is governed by your Rough customer terms.