@tappify/extension-sdk 2.0.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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-2DBSVKAU.cjs +65 -0
  3. package/dist/chunk-2DBSVKAU.cjs.map +1 -0
  4. package/dist/chunk-3YWVYA42.cjs +142 -0
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/chunk-4C5Z3CIK.mjs +26 -0
  7. package/dist/chunk-4C5Z3CIK.mjs.map +1 -0
  8. package/dist/chunk-5AAPD5WV.cjs +250 -0
  9. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  10. package/dist/chunk-7KIKOH5H.cjs +26 -0
  11. package/dist/chunk-7KIKOH5H.cjs.map +1 -0
  12. package/dist/chunk-BDCX6LRQ.mjs +250 -0
  13. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  14. package/dist/chunk-CSTYBKZC.mjs +65 -0
  15. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  16. package/dist/chunk-IGK2AP6J.mjs +142 -0
  17. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  18. package/dist/chunk-MNDVYPEP.mjs +302 -0
  19. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  20. package/dist/chunk-OPPYEA7F.cjs +33 -0
  21. package/dist/chunk-OPPYEA7F.cjs.map +1 -0
  22. package/dist/chunk-PYJMJF44.cjs +302 -0
  23. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  24. package/dist/chunk-QHM7XNRK.mjs +33 -0
  25. package/dist/chunk-QHM7XNRK.mjs.map +1 -0
  26. package/dist/chunk-RMUWRESK.cjs +365 -0
  27. package/dist/chunk-RMUWRESK.cjs.map +1 -0
  28. package/dist/chunk-ZT35X7KL.mjs +365 -0
  29. package/dist/chunk-ZT35X7KL.mjs.map +1 -0
  30. package/dist/codegen.cjs +300 -0
  31. package/dist/codegen.cjs.map +1 -0
  32. package/dist/codegen.d.cts +432 -0
  33. package/dist/codegen.d.ts +432 -0
  34. package/dist/codegen.mjs +272 -0
  35. package/dist/codegen.mjs.map +1 -0
  36. package/dist/constants-DJ_KgZpN.d.cts +449 -0
  37. package/dist/constants-DJ_KgZpN.d.ts +449 -0
  38. package/dist/context-Dt6xh_5t.d.cts +30 -0
  39. package/dist/context-Ml7k_V14.d.ts +30 -0
  40. package/dist/errors-COsBh7kd.d.cts +88 -0
  41. package/dist/errors-COsBh7kd.d.ts +88 -0
  42. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  43. package/dist/handler-kU9Jf36K.d.cts +227 -0
  44. package/dist/host.cjs +24 -0
  45. package/dist/host.cjs.map +1 -0
  46. package/dist/host.d.cts +24 -0
  47. package/dist/host.d.ts +24 -0
  48. package/dist/host.mjs +24 -0
  49. package/dist/host.mjs.map +1 -0
  50. package/dist/index.cjs +698 -0
  51. package/dist/index.cjs.map +1 -0
  52. package/dist/index.d.cts +1016 -0
  53. package/dist/index.d.ts +1016 -0
  54. package/dist/index.mjs +698 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/manifest.cjs +872 -0
  57. package/dist/manifest.cjs.map +1 -0
  58. package/dist/manifest.d.cts +432 -0
  59. package/dist/manifest.d.ts +432 -0
  60. package/dist/manifest.mjs +872 -0
  61. package/dist/manifest.mjs.map +1 -0
  62. package/dist/matchers-Cz93AK1n.d.cts +74 -0
  63. package/dist/matchers-Cz93AK1n.d.ts +74 -0
  64. package/dist/server.cjs +210 -0
  65. package/dist/server.cjs.map +1 -0
  66. package/dist/server.d.cts +243 -0
  67. package/dist/server.d.ts +243 -0
  68. package/dist/server.mjs +210 -0
  69. package/dist/server.mjs.map +1 -0
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +887 -0
  72. package/dist/testing/index.cjs.map +1 -0
  73. package/dist/testing/index.d.cts +512 -0
  74. package/dist/testing/index.d.ts +512 -0
  75. package/dist/testing/index.mjs +887 -0
  76. package/dist/testing/index.mjs.map +1 -0
  77. package/dist/testing/jest.cjs +17 -0
  78. package/dist/testing/jest.cjs.map +1 -0
  79. package/dist/testing/jest.d.cts +12 -0
  80. package/dist/testing/jest.d.ts +12 -0
  81. package/dist/testing/jest.mjs +17 -0
  82. package/dist/testing/jest.mjs.map +1 -0
  83. package/dist/testing/vitest.cjs +15 -0
  84. package/dist/testing/vitest.cjs.map +1 -0
  85. package/dist/testing/vitest.d.cts +10 -0
  86. package/dist/testing/vitest.d.ts +10 -0
  87. package/dist/testing/vitest.mjs +15 -0
  88. package/dist/testing/vitest.mjs.map +1 -0
  89. package/dist/types-DDAGLpov.d.ts +715 -0
  90. package/dist/types-DrpQSv_2.d.cts +715 -0
  91. package/dist/vite.cjs +1344 -0
  92. package/dist/vite.cjs.map +1 -0
  93. package/dist/vite.d.cts +434 -0
  94. package/dist/vite.d.ts +434 -0
  95. package/dist/vite.mjs +1314 -0
  96. package/dist/vite.mjs.map +1 -0
  97. package/package.json +268 -0
  98. package/schema/extension-v2.json +1500 -0
@@ -0,0 +1,1016 @@
1
+ export { u as useTap } from './context-Dt6xh_5t.cjs';
2
+ export { T as TAP_ERROR_CODES, a as TapError, b as TapErrorCode, c as TapServerError } from './errors-COsBh7kd.cjs';
3
+ import { T as Tap, a as TapFilters, b as TapSize, c as TapHostEvents, d as TapQuery, e as TapResult, f as TapProcedureName, g as TapProcedureInput, h as TapProcedureOutput, i as TapSingletonName, j as TapSingletonDocument } from './types-DrpQSv_2.cjs';
4
+ export { A as ActionId, k as ActionInput, P as PromptId, l as PromptInput, m as TapActionMap, n as TapActionRun, o as TapApp, p as TapChangeKey, q as TapChatCard, r as TapCollection, s as TapComparisonSide, t as TapConfirmOptions, u as TapCrashIssue, v as TapCrashesResult, w as TapCredentialValues, x as TapCredentials, y as TapDateRange, z as TapFormatters, B as TapHostEventMap, C as TapInternals, D as TapKeyword, E as TapKeywordsResult, F as TapListing, G as TapListingResult, H as TapPageProps, I as TapPath, J as TapPlatform, K as TapPlatformFilter, L as TapProcedureMap, M as TapProcedures, N as TapProjectResult, O as TapPromptMap, Q as TapRelease, R as TapRevenueResult, S as TapReview, U as TapReviewsResult, V as TapRowActionProps, W as TapSeriesMetric, X as TapSeriesPoint, Y as TapSeriesResult, Z as TapSettings, _ as TapSettingsProps, $ as TapSettingsValues, a0 as TapSingleton, a1 as TapStorage, a2 as TapStorageMap, a3 as TapTabProps, a4 as TapTableColumn, a5 as TapTelemetryEvents, a6 as TapToastTone, a7 as TapToolMap, a8 as TapWebhookMap, a9 as TapWidgetProps, aa as TelemetryEvent, ab as ToolId, ac as ToolInput, ad as WebhookName, ae as WebhookPayload } from './types-DrpQSv_2.cjs';
5
+ import { ClassValue as ClassValue$1 } from 'clsx';
6
+ import { ButtonHTMLAttributes, ReactElement, ReactNode, InputHTMLAttributes, TextareaHTMLAttributes, SelectHTMLAttributes } from 'react';
7
+ import { J as JsonSchema } from './constants-DJ_KgZpN.cjs';
8
+ export { D as DataQueryKind, H as HostEventName, a as HostPage, S as ScopeKey, b as SlotId } from './constants-DJ_KgZpN.cjs';
9
+
10
+ /**
11
+ * Returns the install token, the signed-in teammate, the workspace id, the
12
+ * granted scopes and `can(scope)`.
13
+ *
14
+ * @remarks
15
+ * Re-renders the component when the host rotates the install token, and when the
16
+ * signed-in teammate's role arrives after the mount. `scopes` and `can` report the
17
+ * grant the mount was built with. Throws `TAP_OUTSIDE_HOST` outside a mount.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { TapEmptyState, useTapAuth } from '@tappify/extension-sdk';
22
+ *
23
+ * function RevenuePanel() {
24
+ * const auth = useTapAuth();
25
+ * if (!auth.can('revenue:read')) {
26
+ * return <TapEmptyState title="Revenue is not shared with this extension" />;
27
+ * }
28
+ * return <span>Signed in as {auth.user.id}</span>;
29
+ * }
30
+ * ```
31
+ */
32
+ declare function useTapAuth(): Tap['auth'];
33
+ /**
34
+ * Returns the project the mount renders in, with its apps, releases and tracked
35
+ * keywords.
36
+ *
37
+ * @remarks
38
+ * Re-renders when the host loads or reloads the project. Throws
39
+ * `TAP_OUTSIDE_HOST` outside a mount. An extension installed on the workspace
40
+ * rather than a project still renders inside one project at a time.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { useTapProject } from '@tappify/extension-sdk';
45
+ *
46
+ * function Platforms() {
47
+ * const project = useTapProject();
48
+ * return <span>{project.name} ships to {project.platforms.join(' and ')}</span>;
49
+ * }
50
+ * ```
51
+ */
52
+ declare function useTapProject(): Tap['project'];
53
+ /**
54
+ * Returns the date range, platform and country the owner's filter bar is set to.
55
+ *
56
+ * @remarks
57
+ * Re-renders on every filter change, which is how a surface follows the owner's
58
+ * picker. Pass `range` into a `series`, `reviews`, `crashes` or `revenue` query
59
+ * yourself; the host adds only the country to a data read, while a procedure call
60
+ * carries all three. Throws `TAP_OUTSIDE_HOST` outside a mount.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * import { useTapFilters, useTapQuery } from '@tappify/extension-sdk';
65
+ *
66
+ * function Downloads() {
67
+ * const filters = useTapFilters();
68
+ * const downloads = useTapQuery({
69
+ * kind: 'series',
70
+ * metric: 'downloads',
71
+ * range: filters.range,
72
+ * });
73
+ * return <span>{downloads.data?.points.length ?? 0} days</span>;
74
+ * }
75
+ * ```
76
+ */
77
+ declare function useTapFilters(): TapFilters;
78
+ /**
79
+ * Returns the mode the host is drawing in, as `{ mode: 'light' | 'dark' }`.
80
+ *
81
+ * @remarks
82
+ * Re-renders when the owner switches theme. The host's CSS variables already
83
+ * follow the theme, so reach for this only where a component has to branch in
84
+ * code rather than restyle, such as a chart's series colours. Throws
85
+ * `TAP_OUTSIDE_HOST` outside a mount.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * import { useTapTheme } from '@tappify/extension-sdk';
90
+ *
91
+ * function SeriesLine() {
92
+ * const theme = useTapTheme();
93
+ * const stroke = theme.mode === 'dark' ? '#a5b4fc' : '#4f46e5';
94
+ * return <svg><line x1="0" y1="0" x2="40" y2="10" stroke={stroke} /></svg>;
95
+ * }
96
+ * ```
97
+ */
98
+ declare function useTapTheme(): Tap['theme'];
99
+ /**
100
+ * Returns how much room the mount has: `slot`, `panel` or `page`.
101
+ *
102
+ * @remarks
103
+ * Expanding a widget mounts a second copy of the component in the panel rather
104
+ * than resizing the one in the slot, so the change this hook reports is the
105
+ * panel's own Widen and Narrow toggle moving between `panel` and `page`. Size
106
+ * against the mount with a container query where CSS can do the work; this hook is
107
+ * for the cases that need different markup. Throws `TAP_OUTSIDE_HOST` outside a
108
+ * mount.
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * import { TapTable, useTapSize } from '@tappify/extension-sdk';
113
+ *
114
+ * function Keywords({ rows }: { rows: { term: string }[] }) {
115
+ * const size = useTapSize();
116
+ * const shown = size === 'slot' ? rows.slice(0, 3) : rows;
117
+ * return (
118
+ * <TapTable
119
+ * columns={[{ key: 'term', label: 'Term' }]}
120
+ * rows={shown}
121
+ * rowKey={row => row.term}
122
+ * />
123
+ * );
124
+ * }
125
+ * ```
126
+ */
127
+ declare function useTapSize(): TapSize;
128
+ /**
129
+ * Returns the route of the extension's own page: the page id, the whole sub-path,
130
+ * and one key per segment.
131
+ *
132
+ * @remarks
133
+ * The host builds `pageId`, `path` and then `0`, `1` and so on, one per segment of
134
+ * the sub-path. Only a page entry is routed, so this is empty for a widget, a tab,
135
+ * a row action and a settings panel. Re-renders when the owner navigates inside the
136
+ * page. Throws `TAP_OUTSIDE_HOST` outside a mount.
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * import { useTapParams } from '@tappify/extension-sdk';
141
+ *
142
+ * function FunnelDetail() {
143
+ * const params = useTapParams();
144
+ * const funnelId = params['1'];
145
+ * return <span>{funnelId ?? params.path}</span>;
146
+ * }
147
+ * ```
148
+ */
149
+ declare function useTapParams(): Record<string, string>;
150
+ /**
151
+ * Returns what the surface handed the mount: the payload Expand carried in, or
152
+ * the row behind a row action.
153
+ *
154
+ * @remarks
155
+ * Re-renders when the host hands the mount a new context. A row action reads the
156
+ * row from its own `row` prop as well; this hook is the same value for a
157
+ * component further down the tree. Throws `TAP_OUTSIDE_HOST` outside a mount.
158
+ *
159
+ * @example
160
+ * ```tsx
161
+ * import { useTapContext } from '@tappify/extension-sdk';
162
+ *
163
+ * function ContextNote() {
164
+ * const context = useTapContext();
165
+ * const reviewId = context.reviewId;
166
+ * return <span>{typeof reviewId === 'string' ? reviewId : 'No review'}</span>;
167
+ * }
168
+ * ```
169
+ */
170
+ declare function useTapContext(): Record<string, unknown>;
171
+ /**
172
+ * Returns one value from the in-memory store every mount of the install shares,
173
+ * with a setter for it.
174
+ *
175
+ * @remarks
176
+ * The value is `undefined` until something sets it, and setting it re-renders
177
+ * every mount reading the same key, including a sibling widget and the expand
178
+ * panel. The store lives in the browser session: it is not persisted, not shared
179
+ * between teammates or tabs, and never reaches Tappify or your server — use
180
+ * `useTapStorage` for a value that has to come back tomorrow. Throws
181
+ * `TAP_OUTSIDE_HOST` outside a mount.
182
+ *
183
+ * @example
184
+ * ```tsx
185
+ * import { TapButton, useTapState } from '@tappify/extension-sdk';
186
+ *
187
+ * function StepPicker() {
188
+ * const [step, setStep] = useTapState<string>('selectedStep');
189
+ * return (
190
+ * <TapButton onClick={() => setStep('install')}>
191
+ * {step ?? 'Pick a step'}
192
+ * </TapButton>
193
+ * );
194
+ * }
195
+ * ```
196
+ */
197
+ declare function useTapState<T>(key: string): [T | undefined, (value: T) => void];
198
+
199
+ /** The third argument of `useTapQuery` and `useTapServer`. */
200
+ interface TapQueryOptions {
201
+ /** Holds the call back while this is `false`. Defaults to `true`. */
202
+ enabled?: boolean;
203
+ /** Tappify events that mark this result stale and refetch it when they arrive. */
204
+ invalidateOn?: (keyof TapHostEvents)[];
205
+ }
206
+ /** What `useTapQuery` and `useTapServer` return. */
207
+ interface TapQueryResult<T> {
208
+ /**
209
+ * `undefined` until the first answer for this key arrives. A later call that
210
+ * fails leaves the last answer in place and only sets `error`, so branch on
211
+ * `error` to detect a failure, never on `data`.
212
+ */
213
+ data: T | undefined;
214
+ error: Error | null;
215
+ /** `true` only while the first answer for this key is in flight. */
216
+ isLoading: boolean;
217
+ /** Asks again now, whether or not the cached answer has gone stale. */
218
+ refetch(): void;
219
+ }
220
+ /** What `useTapStorage` returns: a read of the document, plus the two writes. */
221
+ interface TapStorageResult<T> extends TapQueryResult<T | null> {
222
+ /** Replaces the whole document. */
223
+ save(document: T): Promise<void>;
224
+ /** Merges the fields in and resolves to the merged document. */
225
+ patch(partial: Partial<T>): Promise<T>;
226
+ }
227
+ /**
228
+ * Builds the cache key `useTapQuery` reads a Tappify data query under.
229
+ *
230
+ * @remarks
231
+ * The key carries the install, the query and the filters behind it, so two mounts
232
+ * asking for the same numbers share one request. Call it only to reach the query
233
+ * cache directly; the hooks build their own keys.
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * import { dataQueryKey } from '@tappify/extension-sdk';
238
+ *
239
+ * const key = dataQueryKey('ins_test', { kind: 'keywords' }, {
240
+ * range: { from: '2026-09-01', to: '2026-09-08' },
241
+ * platform: 'all',
242
+ * country: 'all',
243
+ * });
244
+ * ```
245
+ */
246
+ declare function dataQueryKey(installId: string, query: TapQuery, filters: TapFilters): unknown[];
247
+ /**
248
+ * Builds the cache key `useTapServer` reads one procedure call under.
249
+ *
250
+ * @remarks
251
+ * The input and the filters are part of the key, so the same procedure called
252
+ * with different input caches separately. Call it only to reach the query cache
253
+ * directly.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * import { procedureQueryKey } from '@tappify/extension-sdk';
258
+ *
259
+ * const key = procedureQueryKey('ins_test', 'getSummary', { days: 7 }, {
260
+ * range: { from: '2026-09-01', to: '2026-09-08' },
261
+ * platform: 'all',
262
+ * country: 'all',
263
+ * });
264
+ * ```
265
+ */
266
+ declare function procedureQueryKey(installId: string, name: string, input: unknown, filters: TapFilters): unknown[];
267
+ /**
268
+ * Builds the cache key `useTapStorage` reads one collection's document under.
269
+ *
270
+ * @remarks
271
+ * The filters are not part of the key, because a stored document does not depend
272
+ * on them. Call it only to reach the query cache directly.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { storageQueryKey } from '@tappify/extension-sdk';
277
+ *
278
+ * const key = storageQueryKey('ins_test', 'preferences');
279
+ * ```
280
+ */
281
+ declare function storageQueryKey(installId: string, collection: string): unknown[];
282
+ /**
283
+ * Builds the key prefix every cached answer of one install sits under.
284
+ *
285
+ * @remarks
286
+ * This is the prefix `tap.invalidate()` with no name invalidates.
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * import { installPrefix } from '@tappify/extension-sdk';
291
+ *
292
+ * const prefix = installPrefix('ins_test');
293
+ * ```
294
+ */
295
+ declare function installPrefix(installId: string): unknown[];
296
+ /**
297
+ * Builds the key prefix every cached call of one procedure sits under, whatever
298
+ * its input.
299
+ *
300
+ * @remarks
301
+ * This is the prefix `tap.invalidate(name)` invalidates.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * import { procedurePrefix } from '@tappify/extension-sdk';
306
+ *
307
+ * const prefix = procedurePrefix('ins_test', 'getSummary');
308
+ * ```
309
+ */
310
+ declare function procedurePrefix(installId: string, name: string): unknown[];
311
+ /**
312
+ * Reads one query's worth of the owner's Tappify data, cached per install, query
313
+ * and filter bar.
314
+ *
315
+ * @remarks
316
+ * Each kind needs its own scope — `project` needs `projects:read`, `series` needs
317
+ * `analytics:read`, `keywords` and `listing` need `store.metadata:read`, and
318
+ * `reviews`, `crashes` and `revenue` need their own. A kind the install has not
319
+ * granted comes back as a `TapError` with code `TAP_SCOPE_MISSING` in `error`. An
320
+ * answer stays fresh for 30 seconds and a failed read is not retried, so
321
+ * `refetch` is yours to call. The host adds only the country filter to the read:
322
+ * pass `range` into a `series`, `reviews`, `crashes` or `revenue` query yourself,
323
+ * and `platform` into a `series` query, which is the only kind that takes one.
324
+ * Throws `TAP_NO_QUERY_CLIENT` when no Tappify mount provides the query client.
325
+ *
326
+ * @example
327
+ * ```tsx
328
+ * import { TapStat, useTapFilters, useTapQuery } from '@tappify/extension-sdk';
329
+ *
330
+ * function Downloads() {
331
+ * const filters = useTapFilters();
332
+ * const downloads = useTapQuery(
333
+ * { kind: 'series', metric: 'downloads', range: filters.range },
334
+ * { invalidateOn: ['release.shipped'] },
335
+ * );
336
+ * const points = downloads.data?.points ?? [];
337
+ * const total = points.reduce((sum, point) => sum + point.value, 0);
338
+ * return <TapStat label="Downloads" value={total} />;
339
+ * }
340
+ * ```
341
+ *
342
+ * @example
343
+ * ```tsx
344
+ * import { TapTable, useTapQuery } from '@tappify/extension-sdk';
345
+ *
346
+ * function Keywords() {
347
+ * const keywords = useTapQuery({ kind: 'keywords' });
348
+ * return (
349
+ * <TapTable
350
+ * columns={[
351
+ * { key: 'term', label: 'Term' },
352
+ * { key: 'position', label: 'Rank', align: 'end' },
353
+ * ]}
354
+ * rows={keywords.data?.keywords ?? []}
355
+ * rowKey={keyword => keyword.id}
356
+ * />
357
+ * );
358
+ * }
359
+ * ```
360
+ */
361
+ declare function useTapQuery<Q extends TapQuery>(query: Q, options?: TapQueryOptions): TapQueryResult<TapResult<Q>>;
362
+ /**
363
+ * Calls one procedure on your own server, cached per install, name, input and
364
+ * filter bar.
365
+ *
366
+ * @remarks
367
+ * The name and the input are narrowed by the `TapProcedureMap` augmentation
368
+ * `tappify extension types` writes, so a procedure the manifest does not declare
369
+ * does not compile; at runtime a name the host has no route for throws
370
+ * `TAP_UNKNOWN_PROCEDURE`. The call carries the whole filter bar for you. An
371
+ * answer stays fresh for 30 seconds and a failure is not retried; an error your
372
+ * server threw arrives as a `TapServerError` with your own `code`. Throws
373
+ * `TAP_NO_QUERY_CLIENT` when no Tappify mount provides the query client.
374
+ *
375
+ * @example
376
+ * ```tsx
377
+ * import { TapButton, TapErrorState, useTapFilters, useTapServer } from '@tappify/extension-sdk';
378
+ *
379
+ * function Summary() {
380
+ * const filters = useTapFilters();
381
+ * const summary = useTapServer(
382
+ * 'getSummary',
383
+ * { days: 7, platform: filters.platform },
384
+ * { invalidateOn: ['settings.changed'] },
385
+ * );
386
+ * if (summary.error !== null) {
387
+ * return <TapErrorState title="The summary did not load" description={summary.error.message} />;
388
+ * }
389
+ * return <TapButton loading={summary.isLoading} onClick={summary.refetch}>Refresh</TapButton>;
390
+ * }
391
+ * ```
392
+ */
393
+ declare function useTapServer<N extends TapProcedureName>(name: N, input: TapProcedureInput<N>, options?: TapQueryOptions): TapQueryResult<TapProcedureOutput<N>>;
394
+ /**
395
+ * Reads and writes the one document a singleton storage collection holds.
396
+ *
397
+ * @remarks
398
+ * The argument is a collection name from the `TapStorageMap` augmentation
399
+ * `tappify extension types` writes, and the collection has to be declared with
400
+ * `"singleton": true`; an id-keyed collection throws `TAP_UNKNOWN_COLLECTION`,
401
+ * with the `tap.storage.<name>.list()` path in the message. `data` is `undefined`
402
+ * while the first read is in flight and `null` until the document is first
403
+ * written. Every storage call needs the `storage:write` scope, reads included, and
404
+ * rejects with `TAP_SCOPE_MISSING` without it. A write from another mount or
405
+ * another browser tab of the same install refetches this one.
406
+ *
407
+ * @example
408
+ * ```tsx
409
+ * import { TapButton, useTapStorage } from '@tappify/extension-sdk';
410
+ *
411
+ * function CompactToggle() {
412
+ * const preferences = useTapStorage('preferences');
413
+ * return (
414
+ * <TapButton
415
+ * loading={preferences.isLoading}
416
+ * onClick={() => void preferences.patch({ compact: true })}
417
+ * >
418
+ * Compact
419
+ * </TapButton>
420
+ * );
421
+ * }
422
+ * ```
423
+ */
424
+ declare function useTapStorage<C extends TapSingletonName>(collection: C): TapStorageResult<TapSingletonDocument<C>>;
425
+
426
+ /**
427
+ * Anything `cn` accepts: a class string, a number, an array of these, a map of
428
+ * class name to condition, or a value to drop.
429
+ */
430
+ type ClassValue = ClassValue$1;
431
+ /**
432
+ * Joins class names, dropping the ones that are `false`, `null` or `undefined`.
433
+ *
434
+ * @remarks
435
+ * This is `clsx`, re-exported so a vendor surface needs no class-name dependency
436
+ * of its own. It does not merge conflicting utility classes.
437
+ *
438
+ * @example
439
+ * ```tsx
440
+ * import { cn } from '@tappify/extension-sdk';
441
+ *
442
+ * function Row({ compact }: { compact: boolean }) {
443
+ * return <div className={cn('starter-row', compact && 'starter-row--compact')} />;
444
+ * }
445
+ * ```
446
+ */
447
+ declare function cn(...values: ClassValue[]): string;
448
+
449
+ /** The props `TapButton` takes, on top of the native button attributes. */
450
+ interface TapButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
451
+ /** Defaults to `primary`. */
452
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
453
+ /** Defaults to `md`. */
454
+ size?: 'sm' | 'md';
455
+ /** Disables the button and marks it `aria-busy`. */
456
+ loading?: boolean;
457
+ }
458
+ /**
459
+ * Renders a button in the host's styling, in one of four variants.
460
+ *
461
+ * @remarks
462
+ * Draws on `--tap-accent` and `--tap-accent-fg` for `primary`, `--bg-raised`,
463
+ * `--fg-default` and `--divider` for `secondary`, `--fg-muted` for `ghost` and
464
+ * `--tap-danger` for `danger`, and takes its radius from `--tap-radius`. The
465
+ * element defaults to `type="button"`, so a button inside a `TapForm` does not
466
+ * submit it unless you pass `type="submit"`. `loading` disables the button, so a
467
+ * click cannot land twice.
468
+ *
469
+ * @example
470
+ * ```tsx
471
+ * import { TapButton } from '@tappify/extension-sdk';
472
+ *
473
+ * function ExportRow({ busy, onExport }: { busy: boolean; onExport: () => void }) {
474
+ * return (
475
+ * <TapButton variant="secondary" size="sm" loading={busy} onClick={onExport}>
476
+ * Export
477
+ * </TapButton>
478
+ * );
479
+ * }
480
+ * ```
481
+ */
482
+ declare function TapButton({ variant, size, loading, disabled, className, children, ...rest }: TapButtonProps): ReactElement;
483
+ /** The props `TapCard` takes. */
484
+ interface TapCardProps {
485
+ /** Rendered in the header, which appears only with a `title` or an `action`. */
486
+ title?: ReactNode;
487
+ /** Sits at the end of the header, for a button or a link. */
488
+ action?: ReactNode;
489
+ /** Rendered under a divider below the body. */
490
+ footer?: ReactNode;
491
+ className?: string;
492
+ children: ReactNode;
493
+ }
494
+ /**
495
+ * Renders the bordered surface a widget, tab or page section sits in.
496
+ *
497
+ * @remarks
498
+ * Renders as a `section` on `--bg-raised` with a `--divider` border and the
499
+ * `--tap-radius` corner, and the footer in `--fg-muted`. The header appears only
500
+ * when `title` or `action` is given, and the body carries its own padding, so
501
+ * nest content directly rather than wrapping it in another padded box. A widget
502
+ * belongs in one card.
503
+ *
504
+ * @example
505
+ * ```tsx
506
+ * import { TapButton, TapCard, TapStat } from '@tappify/extension-sdk';
507
+ *
508
+ * function InstallSummary() {
509
+ * return (
510
+ * <TapCard
511
+ * title="Install summary"
512
+ * action={<TapButton variant="ghost" size="sm">Compact</TapButton>}
513
+ * footer={<span>Last seven days</span>}
514
+ * >
515
+ * <TapStat label="Installs" value="1,260" delta={0.087} />
516
+ * </TapCard>
517
+ * );
518
+ * }
519
+ * ```
520
+ */
521
+ declare function TapCard({ title, action, footer, className, children, }: TapCardProps): ReactElement;
522
+ /** The props `TapPageHeader` takes. */
523
+ interface TapPageHeaderProps {
524
+ title: ReactNode;
525
+ description?: ReactNode;
526
+ /** Laid out in a row at the end of the header. */
527
+ actions?: ReactNode;
528
+ }
529
+ /**
530
+ * Renders the title, description and actions at the top of a tab, a page or a
531
+ * settings panel.
532
+ *
533
+ * @remarks
534
+ * The title renders as an `h2`, because the host owns the page's `h1`. The
535
+ * description follows `--fg-muted` and the rule under the header follows
536
+ * `--divider`. Under 360 pixels of mount width the actions stack under the title.
537
+ * A widget in a slot uses `TapCard`'s own header instead.
538
+ *
539
+ * @example
540
+ * ```tsx
541
+ * import { TapButton, TapPageHeader } from '@tappify/extension-sdk';
542
+ *
543
+ * function FunnelsHeader() {
544
+ * return (
545
+ * <TapPageHeader
546
+ * title="Funnels"
547
+ * description="Every step from impression to install."
548
+ * actions={<TapButton size="sm">New funnel</TapButton>}
549
+ * />
550
+ * );
551
+ * }
552
+ * ```
553
+ */
554
+ declare function TapPageHeader({ title, description, actions, }: TapPageHeaderProps): ReactElement;
555
+ /** The props `TapSkeleton` takes. */
556
+ interface TapSkeletonProps {
557
+ /** How many bars to draw. Defaults to one. */
558
+ lines?: number;
559
+ /** A CSS width for every bar. Left out, each bar is 12% shorter than the last. */
560
+ width?: string;
561
+ className?: string;
562
+ }
563
+ /**
564
+ * Renders the placeholder bars that stand in for content while a read is in
565
+ * flight.
566
+ *
567
+ * @remarks
568
+ * The bars are `aria-hidden`, so a screen reader is not told about them, and they
569
+ * take their tone from `--bg-subtle`. The pulse stops under
570
+ * `prefers-reduced-motion`. Render this while `isLoading` is `true`, in place of
571
+ * the content rather than beside it.
572
+ *
573
+ * @example
574
+ * ```tsx
575
+ * import { TapSkeleton, TapStat, useTapQuery } from '@tappify/extension-sdk';
576
+ *
577
+ * function Keywords() {
578
+ * const keywords = useTapQuery({ kind: 'keywords' });
579
+ * if (keywords.isLoading) return <TapSkeleton lines={3} />;
580
+ * return <TapStat label="Tracked" value={keywords.data?.keywords.length ?? 0} />;
581
+ * }
582
+ * ```
583
+ */
584
+ declare function TapSkeleton({ lines, width, className, }: TapSkeletonProps): ReactElement;
585
+ /** The props `TapEmptyState` takes. */
586
+ interface TapEmptyStateProps {
587
+ title: string;
588
+ description?: string;
589
+ /** Rendered under the text, for the one thing the owner can do next. */
590
+ action?: ReactNode;
591
+ }
592
+ /**
593
+ * Renders the centred message that stands in for content when there is nothing to
594
+ * show.
595
+ *
596
+ * @remarks
597
+ * Centres its text in `--fg-muted` with the title in `--fg-default`. Use it for an
598
+ * empty result or a surface the install has not been granted; a failure belongs in
599
+ * `TapErrorState`, which carries a retry and announces itself as an alert.
600
+ *
601
+ * @example
602
+ * ```tsx
603
+ * import { TapEmptyState, useTapQuery } from '@tappify/extension-sdk';
604
+ *
605
+ * function Reviews() {
606
+ * const reviews = useTapQuery({
607
+ * kind: 'reviews',
608
+ * range: { from: '2026-09-01', to: '2026-09-08' },
609
+ * });
610
+ * if (reviews.data?.total === 0) {
611
+ * return <TapEmptyState title="No reviews in this range" />;
612
+ * }
613
+ * return <span>{reviews.data?.total ?? 0} reviews</span>;
614
+ * }
615
+ * ```
616
+ */
617
+ declare function TapEmptyState({ title, description, action, }: TapEmptyStateProps): ReactElement;
618
+ /** The props `TapErrorState` takes. */
619
+ interface TapErrorStateProps {
620
+ title: string;
621
+ /** The failure's own message, which is what a caller shows the owner. */
622
+ description?: string;
623
+ /** An error code, rendered small in `--font-mono` under the description. */
624
+ code?: string;
625
+ /** Renders a Retry button, and is called by it. */
626
+ onRetry?: () => void;
627
+ }
628
+ /**
629
+ * Renders the centred failure message a surface shows in place of its content.
630
+ *
631
+ * @remarks
632
+ * Carries `role="alert"`, so the host announces it, and draws the title in
633
+ * `--tap-danger`. The Retry button appears only with `onRetry`; wire it to the
634
+ * `refetch` of every read the surface needs. Pass the failure's own `message` as
635
+ * the description — an error from `useTapServer` carries the text your server sent
636
+ * the owner — and the `code` of a `TapError` or `TapServerError` as `code`.
637
+ *
638
+ * @example
639
+ * ```tsx
640
+ * import { TapErrorState, useTapQuery } from '@tappify/extension-sdk';
641
+ *
642
+ * function Crashes() {
643
+ * const crashes = useTapQuery({
644
+ * kind: 'crashes',
645
+ * range: { from: '2026-09-01', to: '2026-09-08' },
646
+ * });
647
+ * if (crashes.error !== null) {
648
+ * return (
649
+ * <TapErrorState
650
+ * title="Crashes did not load"
651
+ * description={crashes.error.message}
652
+ * onRetry={crashes.refetch}
653
+ * />
654
+ * );
655
+ * }
656
+ * return <span>{crashes.data?.issues.length ?? 0} issues</span>;
657
+ * }
658
+ * ```
659
+ */
660
+ declare function TapErrorState({ title, description, code, onRetry, }: TapErrorStateProps): ReactElement;
661
+ /** The props `TapStat` takes. */
662
+ interface TapStatProps {
663
+ label: string;
664
+ value: ReactNode;
665
+ /** A share of the previous period, so `0.087` renders as `+9%`. */
666
+ delta?: number;
667
+ /** A line of context under the number. */
668
+ hint?: string;
669
+ }
670
+ /**
671
+ * Renders one labelled number, with an optional change against the previous
672
+ * period.
673
+ *
674
+ * @remarks
675
+ * The label is uppercased in `--fg-muted` and the number uses tabular figures, so
676
+ * a row of stats stays aligned as values change. `delta` is a ratio, not a
677
+ * percentage: it renders as whole percent with a plus in front when it is above
678
+ * zero, and is coloured `--tap-danger` below zero and `--tap-accent` at or above
679
+ * it. Format `value`
680
+ * yourself with `tap.format.number` or `tap.format.currency`, so it follows the
681
+ * owner's locale.
682
+ *
683
+ * @example
684
+ * ```tsx
685
+ * import { TapStat, useTap } from '@tappify/extension-sdk';
686
+ *
687
+ * function Installs({ installs, delta }: { installs: number; delta: number }) {
688
+ * const tap = useTap();
689
+ * return (
690
+ * <TapStat
691
+ * label="Installs"
692
+ * value={tap.format.number(installs)}
693
+ * delta={delta}
694
+ * hint="Against the previous seven days"
695
+ * />
696
+ * );
697
+ * }
698
+ * ```
699
+ */
700
+ declare function TapStat({ label, value, delta, hint, }: TapStatProps): ReactElement;
701
+ /**
702
+ * The `TapInput` props for a single-line control, on top of the native input
703
+ * attributes minus `size`.
704
+ */
705
+ interface TapSingleLineInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
706
+ /** Rendered as a `label` bound to the control by id. */
707
+ label?: string;
708
+ /** Marks the control invalid and renders the message under it. */
709
+ error?: string;
710
+ /** A line of guidance under the control, referenced by `aria-describedby`. */
711
+ hint?: string;
712
+ }
713
+ /**
714
+ * The `TapInput` props for a multiline control, on top of the native textarea
715
+ * attributes.
716
+ */
717
+ interface TapTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
718
+ /** Rendered as a `label` bound to the control by id. */
719
+ label?: string;
720
+ /** Marks the control invalid and renders the message under it. */
721
+ error?: string;
722
+ /** A line of guidance under the control, referenced by `aria-describedby`. */
723
+ hint?: string;
724
+ }
725
+ /**
726
+ * The props `TapInput` takes: the single-line set, or the textarea set behind
727
+ * `multiline`.
728
+ */
729
+ type TapInputProps = ({
730
+ multiline?: false;
731
+ } & TapSingleLineInputProps) | ({
732
+ multiline: true;
733
+ } & TapTextareaProps);
734
+ /**
735
+ * Renders a labelled text control with its hint and error, as an input or, with
736
+ * `multiline`, a textarea.
737
+ *
738
+ * @remarks
739
+ * Generates an id when none is passed and binds the label, the hint and the error
740
+ * to the control, so the field is announced whole; an `error` also sets
741
+ * `aria-invalid` and the `--tap-danger` border. The textarea starts at four rows
742
+ * and is resizable in height. Pass `type="checkbox"` with
743
+ * `className="tap-control--checkbox"` for a boolean, which is what `TapForm`
744
+ * does.
745
+ *
746
+ * @example
747
+ * ```tsx
748
+ * import { TapInput } from '@tappify/extension-sdk';
749
+ *
750
+ * function TermField({ term, onTerm }: { term: string; onTerm: (next: string) => void }) {
751
+ * return (
752
+ * <TapInput
753
+ * label="Keyword"
754
+ * hint="One term per row in the report."
755
+ * value={term}
756
+ * onChange={event => onTerm(event.target.value)}
757
+ * />
758
+ * );
759
+ * }
760
+ * ```
761
+ */
762
+ declare function TapInput(props: TapInputProps): ReactElement;
763
+ /** The props `TapSelect` takes, on top of the native select attributes. */
764
+ interface TapSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
765
+ /** Rendered as a `label` bound to the control by id. */
766
+ label?: string;
767
+ /** Marks the control invalid and renders the message under it. */
768
+ error?: string;
769
+ /** A line of guidance under the control, referenced by `aria-describedby`. */
770
+ hint?: string;
771
+ /** Rendered in order; the component adds no empty option of its own. */
772
+ options: {
773
+ value: string;
774
+ label: string;
775
+ }[];
776
+ }
777
+ /**
778
+ * Renders a labelled select over a list of options, with its hint and error.
779
+ *
780
+ * @remarks
781
+ * Carries the same label, hint, error and `aria-describedby` wiring as
782
+ * `TapInput`. Add your own empty option when the field starts unset — `TapForm`
783
+ * puts a `Choose one` option in front of a schema's enum values.
784
+ *
785
+ * @example
786
+ * ```tsx
787
+ * import { TapSelect } from '@tappify/extension-sdk';
788
+ *
789
+ * function PlatformPicker({ value, onPick }: { value: string; onPick: (next: string) => void }) {
790
+ * return (
791
+ * <TapSelect
792
+ * label="Platform"
793
+ * value={value}
794
+ * options={[
795
+ * { value: 'ios', label: 'iOS' },
796
+ * { value: 'android', label: 'Android' },
797
+ * ]}
798
+ * onChange={event => onPick(event.target.value)}
799
+ * />
800
+ * );
801
+ * }
802
+ * ```
803
+ */
804
+ declare function TapSelect({ label, error, hint, options, className, id, ...rest }: TapSelectProps): ReactElement;
805
+ /** One column of a `TapTable`. */
806
+ interface TapTableColumnDef<T> {
807
+ /** The key read from each row when there is no `render`. */
808
+ key: string;
809
+ label: string;
810
+ /** `end` right-aligns the column and gives it tabular figures. */
811
+ align?: 'start' | 'end';
812
+ /** Draws the cell. Without it the cell shows the row's own string or number. */
813
+ render?(row: T): ReactNode;
814
+ }
815
+ /** The props `TapTable` takes. */
816
+ interface TapTableProps<T> {
817
+ columns: TapTableColumnDef<T>[];
818
+ rows: T[];
819
+ /** Returns a stable key per row, which React uses for reconciliation. */
820
+ rowKey: (row: T) => string;
821
+ /** Rendered in place of the whole table when `rows` is empty. */
822
+ empty?: ReactNode;
823
+ /** Makes each row clickable and focusable, and is called on click or Enter. */
824
+ onRowClick?: (row: T) => void;
825
+ }
826
+ /**
827
+ * Renders rows under column headers, scrolling sideways inside the mount rather
828
+ * than widening it.
829
+ *
830
+ * @remarks
831
+ * A cell with no `render` shows the row's own value under `key` when that value is
832
+ * a string, number, boolean or bigint, and nothing otherwise, so an object or an
833
+ * array needs a `render`. With `empty` and no rows, the table is replaced whole by
834
+ * that node. With `onRowClick` each row becomes focusable and answers Enter and
835
+ * Space as well as a click, and hovers on `--bg-subtle`; the headers and rules
836
+ * follow `--fg-muted` and `--divider`.
837
+ *
838
+ * @example
839
+ * ```tsx
840
+ * import { TapEmptyState, TapTable, useTapQuery } from '@tappify/extension-sdk';
841
+ *
842
+ * function Keywords() {
843
+ * const keywords = useTapQuery({ kind: 'keywords' });
844
+ * return (
845
+ * <TapTable
846
+ * columns={[
847
+ * { key: 'term', label: 'Term' },
848
+ * { key: 'position', label: 'Rank', align: 'end' },
849
+ * ]}
850
+ * rows={keywords.data?.keywords ?? []}
851
+ * rowKey={keyword => keyword.id}
852
+ * empty={<TapEmptyState title="No keywords tracked yet" />}
853
+ * />
854
+ * );
855
+ * }
856
+ * ```
857
+ */
858
+ declare function TapTable<T>({ columns, rows, rowKey, empty, onRowClick, }: TapTableProps<T>): ReactElement;
859
+
860
+ /** The props `TapDialog` takes. */
861
+ interface TapDialogProps {
862
+ /** The dialog renders nothing while this is `false`. */
863
+ open: boolean;
864
+ /** Rendered in the header, and used as the dialog's accessible name. */
865
+ title: string;
866
+ /** Called by Escape, by a click on the scrim, and by the host's close event. */
867
+ onClose: () => void;
868
+ /** Rendered under a divider at the end, for the dialog's buttons. */
869
+ footer?: ReactNode;
870
+ children: ReactNode;
871
+ /** The id of your own heading, when the accessible name is not the title. */
872
+ labelledBy?: string;
873
+ }
874
+ /**
875
+ * Renders a modal dialog into the mount's own portal node, so it covers the
876
+ * surface without leaving the extension.
877
+ *
878
+ * @remarks
879
+ * Rendered outside a Tappify mount it throws `TAP_OUTSIDE_HOST`, from the `useTap`
880
+ * above it. Inside a mount whose shadow root has no portal node yet, opening it
881
+ * throws `TapError` with code `TAP_NO_PORTAL`; `renderWithTap` provides that node
882
+ * in tests. While open it traps Tab
883
+ * inside itself, moves focus to the first focusable element and returns focus
884
+ * where it was on close, and answers Escape and a click on the scrim with
885
+ * `onClose`. The scrim follows `--tap-scrim` and the panel `--bg-raised`,
886
+ * `--divider` and `--tap-radius`; the panel is at most 520 pixels wide. The
887
+ * component keeps no open state of its own.
888
+ *
889
+ * @example
890
+ * ```tsx
891
+ * import { TapButton, TapDialog } from '@tappify/extension-sdk';
892
+ * import { useState } from 'react';
893
+ *
894
+ * function ExportDialog() {
895
+ * const [open, setOpen] = useState(false);
896
+ * return (
897
+ * <>
898
+ * <TapButton onClick={() => setOpen(true)}>Export</TapButton>
899
+ * <TapDialog
900
+ * open={open}
901
+ * title="Export keywords"
902
+ * onClose={() => setOpen(false)}
903
+ * footer={<TapButton onClick={() => setOpen(false)}>Done</TapButton>}
904
+ * >
905
+ * <span>The report covers the range in the filter bar.</span>
906
+ * </TapDialog>
907
+ * </>
908
+ * );
909
+ * }
910
+ * ```
911
+ */
912
+ declare function TapDialog({ open, title, onClose, footer, children, labelledBy, }: TapDialogProps): ReactElement | null;
913
+
914
+ /** The control a schema property maps to. */
915
+ type TapFormFieldKind = 'text' | 'textarea' | 'date' | 'number' | 'boolean' | 'enum';
916
+ /** One control `TapForm` renders, as `formFields` reads it off a schema. */
917
+ interface TapFormField {
918
+ /** The property name in the schema, and the key in the submitted value. */
919
+ name: string;
920
+ /** The property's `title`, or the name spaced and capitalised. */
921
+ label: string;
922
+ kind: TapFormFieldKind;
923
+ /** Whether the schema lists the property under `required`. */
924
+ required: boolean;
925
+ /** The property's `description`. */
926
+ hint?: string;
927
+ /** Set for an `enum` property: one option per string value. */
928
+ options?: {
929
+ value: string;
930
+ label: string;
931
+ }[];
932
+ }
933
+ /**
934
+ * Reads a JSON Schema object and returns the list of controls `TapForm` renders
935
+ * from it.
936
+ *
937
+ * @remarks
938
+ * One field per declared property, in the order the schema lists them; a schema
939
+ * with no `properties` object returns an empty list, and a property that is not an
940
+ * object is skipped. An `enum` becomes a select and keeps only its string values;
941
+ * otherwise the kind comes from `type` — `boolean`, `number` and `integer` — and
942
+ * then from `format`, where `textarea` and `date` are recognised and anything else
943
+ * falls back to a single-line text control. Nested objects and arrays get a text
944
+ * control, so render those yourself. Call it when you want the fields without the
945
+ * form.
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * import { formFields } from '@tappify/extension-sdk';
950
+ *
951
+ * const fields = formFields({
952
+ * type: 'object',
953
+ * properties: { refreshMinutes: { type: 'integer', title: 'Refresh minutes' } },
954
+ * required: ['refreshMinutes'],
955
+ * });
956
+ * const names = fields.map(field => field.name);
957
+ * ```
958
+ */
959
+ declare function formFields(schema: JsonSchema): TapFormField[];
960
+ /** The props `TapForm` takes. */
961
+ interface TapFormProps<T> {
962
+ /** The object schema to render, one control per declared property. */
963
+ schema: JsonSchema;
964
+ /** The value the fields start at; later changes to it do not reset the form. */
965
+ value?: Partial<T>;
966
+ /** Defaults to `Submit`. */
967
+ submitLabel?: string;
968
+ /** Defaults to `Cancel`. */
969
+ cancelLabel?: string;
970
+ /** Marks the submit button busy and disables it while a submit is in flight. */
971
+ busy?: boolean;
972
+ /** Called with the cleaned value once validation passes. */
973
+ onSubmit: (value: T) => void | Promise<void>;
974
+ /** Renders a cancel button next to submit, and is called by it. */
975
+ onCancel?: () => void;
976
+ }
977
+ /**
978
+ * Renders a JSON Schema as a form in the host's styling, validates it on submit
979
+ * and hands back the value.
980
+ *
981
+ * @remarks
982
+ * Point it at the same schema file the manifest references, so the form and the
983
+ * handler change together. Submit drops the fields left blank, then checks what
984
+ * remains against the whole schema, so a `minimum`, `pattern` or `maxLength`
985
+ * lands under the control it belongs to; a required field left empty is reported
986
+ * without reaching the schema. A schema this SDK's Zod version cannot compile
987
+ * still renders and still enforces required fields. `onSubmit` runs only when
988
+ * nothing failed, and the form holds its own values, so a change to `value` after
989
+ * the first render does not reset it. A form with no `value` has nothing to infer
990
+ * `T` from, so annotate the handler's parameter.
991
+ *
992
+ * @example
993
+ * ```tsx
994
+ * import { TapForm, type TapSettingsProps } from '@tappify/extension-sdk';
995
+ *
996
+ * const schema = {
997
+ * type: 'object',
998
+ * properties: { refreshMinutes: { type: 'integer', title: 'Refresh minutes' } },
999
+ * required: ['refreshMinutes'],
1000
+ * };
1001
+ *
1002
+ * function Settings({ values, onChange }: TapSettingsProps) {
1003
+ * return (
1004
+ * <TapForm
1005
+ * schema={schema}
1006
+ * value={values}
1007
+ * submitLabel="Save"
1008
+ * onSubmit={next => onChange(next)}
1009
+ * />
1010
+ * );
1011
+ * }
1012
+ * ```
1013
+ */
1014
+ declare function TapForm<T>({ schema, value, submitLabel, cancelLabel, busy, onSubmit, onCancel, }: TapFormProps<T>): ReactElement;
1015
+
1016
+ export { type ClassValue, Tap, TapButton, type TapButtonProps, TapCard, type TapCardProps, TapDialog, type TapDialogProps, TapEmptyState, type TapEmptyStateProps, TapErrorState, type TapErrorStateProps, TapFilters, TapForm, type TapFormField, type TapFormFieldKind, type TapFormProps, TapHostEvents, TapInput, type TapInputProps, TapPageHeader, type TapPageHeaderProps, TapProcedureInput, TapProcedureName, TapProcedureOutput, TapQuery, type TapQueryOptions, type TapQueryResult, TapResult, TapSelect, type TapSelectProps, type TapSingleLineInputProps, TapSingletonDocument, TapSingletonName, TapSize, TapSkeleton, type TapSkeletonProps, TapStat, type TapStatProps, type TapStorageResult, TapTable, type TapTableColumnDef, type TapTableProps, type TapTextareaProps, cn, dataQueryKey, formFields, installPrefix, procedurePrefix, procedureQueryKey, storageQueryKey, useTapAuth, useTapContext, useTapFilters, useTapParams, useTapProject, useTapQuery, useTapServer, useTapSize, useTapState, useTapStorage, useTapTheme };