@foir/sdk 0.28.0 → 0.30.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.
@@ -1,3 +1,4 @@
1
+ import type { Ref } from 'react';
1
2
  import type { ProgressiveImageSource, ProgressiveVideoSource, VariantTierKey } from './media';
2
3
  export interface ProgressiveMediaProps {
3
4
  image?: ProgressiveImageSource;
@@ -25,6 +26,31 @@ export interface ProgressiveMediaProps {
25
26
  muted?: boolean;
26
27
  /** Video playback — loop on end. Default true. */
27
28
  loop?: boolean;
29
+ /**
30
+ * Video playback: show the browser's controls. Defaults to `!autoplay`. A
31
+ * video with controls shows from the start, its poster standing until it
32
+ * plays, so the controls can always be reached.
33
+ */
34
+ controls?: boolean;
35
+ /** Video: how much the browser fetches before it plays (`<video preload>`). */
36
+ preload?: 'none' | 'metadata' | 'auto';
37
+ /**
38
+ * Where the media sits in its box when it is cropped to fit, as CSS
39
+ * `object-position` (`'30% 60%'`), for a focal point. Applies to the image,
40
+ * the video and its thumbnail. Default centre.
41
+ */
42
+ objectPosition?: string;
43
+ /**
44
+ * The `<video>` element, once it mounts: for a caller's own play and pause
45
+ * controls, and the events that keep them in step.
46
+ */
47
+ videoRef?: Ref<HTMLVideoElement>;
48
+ /**
49
+ * Called once when a video's source cannot be played. The video is then
50
+ * removed, and its thumbnail, labelled with `alt`, stands in for it over
51
+ * the colour and blurhash layers.
52
+ */
53
+ onVideoError?: () => void;
28
54
  /**
29
55
  * When false, skip loading the full media layer (no `<video>`/full `<img>`
30
56
  * mount, no HLS attach, no full-image preload). The lower layers — colour,
@@ -53,4 +79,4 @@ export interface ProgressiveMediaProps {
53
79
  */
54
80
  variantTiers?: readonly VariantTierKey[];
55
81
  }
56
- export declare function ProgressiveMedia({ image, video, alt, className, fill, fit, placeholder, autoplay, muted, loop, active, sizes, priority, variantTiers, }: ProgressiveMediaProps): import("react/jsx-runtime").JSX.Element | null;
82
+ export declare function ProgressiveMedia({ image, video, alt, className, fill, fit, placeholder, autoplay, muted, loop, controls, preload, objectPosition, videoRef: externalVideoRef, onVideoError, active, sizes, priority, variantTiers, }: ProgressiveMediaProps): import("react/jsx-runtime").JSX.Element | null;
package/dist/media.js CHANGED
@@ -213,6 +213,11 @@ function ProgressiveMedia({
213
213
  autoplay = true,
214
214
  muted = true,
215
215
  loop = true,
216
+ controls,
217
+ preload,
218
+ objectPosition,
219
+ videoRef: externalVideoRef,
220
+ onVideoError,
216
221
  active = true,
217
222
  sizes = "100vw",
218
223
  priority = false,
@@ -225,7 +230,13 @@ function ProgressiveMedia({
225
230
  const source = video ?? image;
226
231
  const isVideo = !!video;
227
232
  const [wrapperRef, inViewport] = useInViewport("300px", priority);
228
- const videoActive = isVideo && active && (priority || inViewport);
233
+ const [failedUrl, setFailedUrl] = useState2(void 0);
234
+ const videoFailed = isVideo && failedUrl !== void 0 && failedUrl === video?.url;
235
+ const [videoSize, setVideoSize] = useState2(
236
+ null
237
+ );
238
+ const videoActive = isVideo && active && !videoFailed && (priority || inViewport);
239
+ const showControls = controls ?? !autoplay;
229
240
  const imageActive = !isVideo && active;
230
241
  const hlsUrl = video?.hlsUrl;
231
242
  const useHls = isVideo && !!hlsUrl;
@@ -282,6 +293,8 @@ function ProgressiveMedia({
282
293
  cleanupRef.current = null;
283
294
  }
284
295
  videoRef.current = el;
296
+ if (typeof externalVideoRef === "function") externalVideoRef(el);
297
+ else if (externalVideoRef) externalVideoRef.current = el;
285
298
  if (!el) return;
286
299
  if (el.readyState >= 3) {
287
300
  onFullReady();
@@ -299,7 +312,7 @@ function ProgressiveMedia({
299
312
  el.removeEventListener("playing", handler);
300
313
  };
301
314
  },
302
- [onFullReady]
315
+ [onFullReady, externalVideoRef]
303
316
  );
304
317
  if (!source) {
305
318
  if (!placeholder) return null;
@@ -314,8 +327,9 @@ function ProgressiveMedia({
314
327
  );
315
328
  }
316
329
  const effective = !isVideo ? effectiveImageDimensions(image) : null;
317
- const w = effective?.width ?? source.width;
318
- const h = effective?.height ?? source.height;
330
+ const w = effective?.width ?? source.width ?? videoSize?.w;
331
+ const h = effective?.height ?? source.height ?? videoSize?.h;
332
+ const positioned = objectPosition ? { objectPosition } : void 0;
319
333
  const aspect = w && h ? w / h : 16 / 10;
320
334
  return /* @__PURE__ */ jsxs(
321
335
  "div",
@@ -342,13 +356,14 @@ function ProgressiveMedia({
342
356
  "img",
343
357
  {
344
358
  src: thumbnailUrl,
345
- alt: "",
346
- "aria-hidden": true,
359
+ alt: videoFailed ? alt ?? "" : "",
360
+ "aria-hidden": videoFailed ? void 0 : true,
347
361
  width: w,
348
362
  height: h,
349
363
  loading: priority ? "eager" : "lazy",
350
364
  fetchPriority: priority ? "high" : void 0,
351
- className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-400 ease-in-out ${stage === "thumbnail" || stage === "full" ? "opacity-100" : "opacity-0"}`
365
+ className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-400 ease-in-out ${stage === "thumbnail" || stage === "full" ? "opacity-100" : "opacity-0"}`,
366
+ style: positioned
352
367
  }
353
368
  ),
354
369
  videoActive ? /* @__PURE__ */ jsx2(
@@ -363,8 +378,21 @@ function ProgressiveMedia({
363
378
  muted,
364
379
  loop,
365
380
  playsInline: true,
366
- controls: !autoplay,
367
- className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" ? "opacity-100" : "opacity-0"}`
381
+ controls: showControls,
382
+ preload,
383
+ "aria-label": alt || void 0,
384
+ onLoadedMetadata: (e) => {
385
+ const v = e.currentTarget;
386
+ if (!source.width && v.videoWidth && v.videoHeight) {
387
+ setVideoSize({ w: v.videoWidth, h: v.videoHeight });
388
+ }
389
+ },
390
+ onError: () => {
391
+ setFailedUrl(video?.url);
392
+ onVideoError?.();
393
+ },
394
+ className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" || showControls ? "opacity-100" : "opacity-0"}`,
395
+ style: positioned
368
396
  }
369
397
  ) : imageActive ? (
370
398
  // <picture>: browser picks AVIF when it can decode it, else WebP. The
@@ -383,7 +411,8 @@ function ProgressiveMedia({
383
411
  height: h,
384
412
  loading: priority ? "eager" : "lazy",
385
413
  fetchPriority: priority ? "high" : void 0,
386
- className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" ? "opacity-100" : "opacity-0"}`
414
+ className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" ? "opacity-100" : "opacity-0"}`,
415
+ style: positioned
387
416
  }
388
417
  )
389
418
  ] })
@@ -27,8 +27,12 @@ interface PasswordArgs {
27
27
  }
28
28
  /** Password sign-in in a route handler; persists the session to cookies. */
29
29
  export declare function foirSignInWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirUser>;
30
- /** Register a user in a route handler; persists the session to cookies. */
31
- export declare function foirRegisterWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirUser>;
30
+ /**
31
+ * Start a sign-up in a route handler: the address is mailed a code, and
32
+ * {@link foirSignInWithOtp} with it finishes the account and persists the
33
+ * session to cookies.
34
+ */
35
+ export declare function foirRegisterWithPassword({ env, email, password, }: Pick<PasswordArgs, 'env' | 'email' | 'password'>): Promise<void>;
32
36
  /** Complete an OTP sign-in in a route handler; persists the session to cookies. */
33
37
  export declare function foirSignInWithOtp({ env, store, email, otp, cookieOptions, }: {
34
38
  env: FoirEnv;
package/dist/next.js CHANGED
@@ -1127,8 +1127,8 @@ async function getSessionUser(adapter) {
1127
1127
  async function signInWithPassword(adapter, env, vars) {
1128
1128
  return saveSession(adapter, await login(env, vars));
1129
1129
  }
1130
- async function signUpWithPassword(adapter, env, vars) {
1131
- return saveSession(adapter, await register(env, vars));
1130
+ async function signUpWithPassword(env, vars) {
1131
+ await register(env, vars);
1132
1132
  }
1133
1133
  async function signInWithOTP(adapter, env, vars) {
1134
1134
  return saveSession(adapter, await loginWithOTP(env, vars));
@@ -1158,15 +1158,10 @@ function foirSignInWithPassword({
1158
1158
  }
1159
1159
  function foirRegisterWithPassword({
1160
1160
  env,
1161
- store,
1162
1161
  email,
1163
- password,
1164
- cookieOptions
1162
+ password
1165
1163
  }) {
1166
- return signUpWithPassword(createFoirCookieAdapter(store, { cookieOptions }), env, {
1167
- email,
1168
- password
1169
- });
1164
+ return signUpWithPassword(env, { email, password });
1170
1165
  }
1171
1166
  function foirSignInWithOtp({
1172
1167
  env,
package/dist/placement.js CHANGED
@@ -32,6 +32,12 @@ var GRANT_TABLE = [
32
32
  ["operations:write", "none", [], []],
33
33
  ["operations:complete", "none", [], []],
34
34
  ["status:write", "none", [], []],
35
+ ["agents:read", "none", [], []],
36
+ ["agents:execute", "agent", [], []],
37
+ ["agents:write", "none", [], []],
38
+ ["agenttools:execute", "agent_tool", [], []],
39
+ ["brief:read", "none", [], []],
40
+ ["brief:write", "none", [], []],
35
41
  ["appconfig:read", "none", [], []],
36
42
  ["schedules:read", "none", [], []],
37
43
  ["schedules:write", "none", [], []],
@@ -70,6 +76,7 @@ var GRANT_TABLE = [
70
76
  ["apikeys:write", "none", [], []],
71
77
  ["organizations:read", "none", [], []],
72
78
  ["organizations:write", "none", [], []],
79
+ ["orgsettings:write", "none", [], []],
73
80
  ["organizations:create", "none", [], []],
74
81
  ["organizations:delete", "none", [], []],
75
82
  ["organizations:reach", "none", [], []],
@@ -80,7 +87,7 @@ var GRANT_TABLE = [
80
87
  ["projects:read", "none", [], []],
81
88
  ["projects:write", "none", [], []],
82
89
  ["project:stand", "none", [], []],
83
- ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "auth:login_with_foir", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
90
+ ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "agents:read", "agents:execute", "agents:write", "agenttools:execute", "brief:read", "brief:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
84
91
  ["projects:create", "none", [], []],
85
92
  ["projects:delete", "none", [], []],
86
93
  ["tenants:read", "none", [], []],
@@ -111,7 +118,6 @@ var GRANT_TABLE = [
111
118
  ["usersecrets:list", "none", [], []],
112
119
  ["usersecrets:put", "none", [], []],
113
120
  ["usersecrets:delete", "none", [], []],
114
- ["auth:login_with_foir", "none", [], []],
115
121
  ["signinproviders:read", "none", [], []],
116
122
  ["signinproviders:write", "none", [], []]
117
123
  ];
@@ -59,6 +59,23 @@ export interface FoirFeedbackProps {
59
59
  * with a backdrop, closed by Escape, the backdrop or its Close button.
60
60
  */
61
61
  layout?: 'inline' | 'dialog';
62
+ /**
63
+ * Show the button fixed in a corner of the viewport, rather than where the
64
+ * component is placed, with the inline panel opening above it. Default: where
65
+ * it is placed.
66
+ */
67
+ launcher?: 'bottom-right' | 'bottom-left';
68
+ /**
69
+ * Let the reader point at the part of the page they mean: a field the host
70
+ * marked with `subjects.field(record, key, { label })`. The note is then filed
71
+ * against that record and names the field, so a page showing two records (a
72
+ * menu and an article) files each comment against the right one.
73
+ *
74
+ * Needs `subjects`, and is offered only while the page shows a marked field.
75
+ * A mouse points by clicking; the keyboard by Tab and Enter; Escape cancels.
76
+ * Default false.
77
+ */
78
+ pointing?: boolean;
62
79
  /**
63
80
  * The built-in styles' colours, radius and type. See
64
81
  * `FoirFeedbackAppearance`: each is also a CSS custom property, so a
@@ -121,4 +138,4 @@ export interface FoirFeedbackAppearance {
121
138
  /** `--foir-feedback-font-size`. Default: inherited. */
122
139
  fontSize?: string;
123
140
  }
124
- export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, signIn, layout, appearance, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
141
+ export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, signIn, layout, launcher, pointing, appearance, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
@@ -28,10 +28,14 @@ export interface FoirContextValue {
28
28
  email: string;
29
29
  password: string;
30
30
  }): Promise<FoirUser>;
31
+ /**
32
+ * Start a sign-up: the address is mailed a code, and `signInWithOtp` with it
33
+ * finishes the account and signs them in.
34
+ */
31
35
  register(vars: {
32
36
  email: string;
33
37
  password: string;
34
- }): Promise<FoirUser>;
38
+ }): Promise<void>;
35
39
  requestOtp(email: string): Promise<boolean>;
36
40
  signInWithOtp(vars: {
37
41
  email: string;
@@ -138,7 +142,7 @@ export declare function useFoirAuth(): {
138
142
  register: (vars: {
139
143
  email: string;
140
144
  password: string;
141
- }) => Promise<FoirUser>;
145
+ }) => Promise<void>;
142
146
  requestOtp: (email: string) => Promise<boolean>;
143
147
  signInWithOtp: (vars: {
144
148
  email: string;
@@ -2,7 +2,7 @@ import type { FoirUser } from '../auth';
2
2
  export interface SignInProps {
3
3
  /** `password` (default) or `otp` (email one-time code). Ignored when `hostedUrl` is set. */
4
4
  method?: 'password' | 'otp';
5
- /** Render a single button that redirects to a hosted login page instead of an embedded form. */
5
+ /** Render a single button that redirects to the hosted sign-in page instead of an embedded form. */
6
6
  hostedUrl?: string;
7
7
  /** Show OAuth provider buttons below the form. Default true. */
8
8
  oauth?: boolean;
@@ -15,7 +15,7 @@ export interface SignInProps {
15
15
  /**
16
16
  * A drop-in user sign-in. Renders an email/password (or OTP) form and,
17
17
  * unless disabled, OAuth provider buttons. Set `hostedUrl` to instead render a
18
- * button that redirects to a hosted login page. All state is handled through
18
+ * button that redirects to the hosted sign-in page. All state is handled through
19
19
  * {@link FoirProvider}; on success it calls `onSignIn` and optionally navigates
20
20
  * to `afterSignInUrl`.
21
21
  */
@@ -113,6 +113,8 @@ export interface FoirFeedbackSubmission {
113
113
  */
114
114
  subject: FoirSubject;
115
115
  body: string;
116
+ /** The field of the subject the reader pointed at, when they pointed at one. */
117
+ anchorField?: string;
116
118
  /** The still the visitor chose to attach, or null. */
117
119
  screenshot: FoirScreenCapture | null;
118
120
  /** Injectable for the presigned PUT. Defaults to `globalThis.fetch`. */
@@ -132,6 +134,11 @@ export interface FoirVisitorSubmission {
132
134
  /** What the note is about, as for a signed-in user's. */
133
135
  subject: FoirSubject;
134
136
  body: string;
137
+ /**
138
+ * The field of the subject the visitor pointed at. The data API takes a
139
+ * field the visitor can read and refuses any other.
140
+ */
141
+ anchorField?: string;
135
142
  /**
136
143
  * Where the project's team may reply, as the visitor typed it. Sent only
137
144
  * when it is not blank, and never verified by anyone.