@foir/sdk 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -108,6 +108,46 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
108
108
 
109
109
  Changes on `dev` that have not been published to npm.
110
110
 
111
+ ## [0.28.0] — 2026-10-20
112
+
113
+ ### Added
114
+
115
+ - **`<FoirFeedback appearance>`** (`@foir/sdk/react`): the built-in styles'
116
+ colours, radius and type, as the new `FoirFeedbackAppearance`. Each token is
117
+ also a CSS custom property on the wrapper (`--foir-feedback-text`,
118
+ `--foir-feedback-radius`, …), so a stylesheet can set it from a design
119
+ system's own variables. Unset, the widget looks as it did.
120
+ - **`<FoirFeedback layout="dialog">`**: the panel opens over the page in a
121
+ modal `<dialog>`, with a backdrop and a Close button, closed by Escape and
122
+ the backdrop too. `inline`, the default, is the panel under the button.
123
+ - **`<FoirFeedback signIn={false}>`**: a site where nobody can sign in says so,
124
+ and a visitor the project takes no notes from reads "Feedback is not open
125
+ right now" rather than "Sign in to send feedback".
126
+
127
+ ### Changed
128
+
129
+ - **Breaking: a visitor's thank-you shows no id.** Nothing opens a visitor's
130
+ note from its id, so `<FoirFeedback>` no longer renders the
131
+ `data-foir-feedback-receipt` line. The receipt, `{ id, createdAt }`, still
132
+ reaches `onVisitorSubmitted`.
133
+ - **A visitor whose note is refused because the project stopped taking
134
+ visitors' notes is shown that**, as the panel would show it on opening,
135
+ rather than asked to try again. The widget asks `noteConfig` again after a
136
+ visitor's send fails for a reason it cannot name.
137
+ - **The widget's smaller lines are sized relative to its font size**, so
138
+ `fontSize` scales them; they were fixed at 13 and 15px.
139
+
140
+ ## [0.27.1] — 2026-10-20
141
+
142
+ ### Fixed
143
+
144
+ - **`<FoirFeedback>` takes a client made without a generated module**
145
+ (`@foir/sdk/react`). `createFoirClient({ apiUrl, apiKey })` with no
146
+ `generated` prints from the manifest it fetches, and the widget worked with
147
+ one, but `FoirFeedbackClient` refused it at compile time for lacking
148
+ `notes`. `FoirFeedbackClient` is now that client or a generated one; a
149
+ generated client whose module has no notes namespace is still refused.
150
+
111
151
  ## [0.27.0] — 2026-10-20
112
152
 
113
153
  ### Added
@@ -554,8 +554,7 @@ export interface FoirNoteConfig {
554
554
  * and nothing else, because nobody is signed in to read the note back as. Ask
555
555
  * for more and the write is refused before anything is written.
556
556
  *
557
- * Show the `id` to the visitor so they can quote it. Nothing on the public
558
- * plane takes it to read the note.
557
+ * Nothing on the public plane takes the `id` to read the note.
559
558
  */
560
559
  export interface FoirNoteReceipt {
561
560
  id: string;
@@ -46,6 +46,25 @@ export interface FoirFeedbackProps {
46
46
  * button that calls it; given none, it says so and stops.
47
47
  */
48
48
  onSignIn?: () => void;
49
+ /**
50
+ * Whether a visitor can sign in on this site. Default true. `false` says
51
+ * they cannot, so where the project takes no notes from visitors who are not
52
+ * signed in, the widget says feedback is not open rather than asking them to
53
+ * sign in somewhere they cannot.
54
+ */
55
+ signIn?: boolean;
56
+ /**
57
+ * How the panel opens. `inline` (the default) opens it under the button, in
58
+ * the page's flow. `dialog` opens it over the page in a modal `<dialog>`,
59
+ * with a backdrop, closed by Escape, the backdrop or its Close button.
60
+ */
61
+ layout?: 'inline' | 'dialog';
62
+ /**
63
+ * The built-in styles' colours, radius and type. See
64
+ * `FoirFeedbackAppearance`: each is also a CSS custom property, so a
65
+ * stylesheet can set it instead. Ignored with `className`.
66
+ */
67
+ appearance?: FoirFeedbackAppearance;
49
68
  /** Called with a signed-in user's note once it is written. */
50
69
  onSubmitted?: (note: FoirUserNote) => void;
51
70
  /**
@@ -61,9 +80,45 @@ export interface FoirFeedbackProps {
61
80
  onError?: (error: unknown) => void;
62
81
  /**
63
82
  * Style it yourself. Passing a class turns OFF the built-in inline styles
64
- * entirely, so the `data-foir-feedback-*` attributes below are yours to target
65
- * without fighting anything.
83
+ * entirely, `appearance` with them, so the `data-foir-feedback-*` attributes
84
+ * are yours to target without fighting anything.
66
85
  */
67
86
  className?: string;
68
87
  }
69
- export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
88
+ /**
89
+ * The built-in styles' tokens.
90
+ *
91
+ * Each is a CSS custom property on the wrapper, `--foir-feedback-<name>`, which
92
+ * the built-in styles read with today's look as the fallback. So a token can be
93
+ * set here, or from a stylesheet, where a design system's own variables can
94
+ * feed it:
95
+ *
96
+ * ```css
97
+ * [data-foir-feedback] { --foir-feedback-radius: var(--radius); }
98
+ * ```
99
+ *
100
+ * A value is any CSS value the property takes.
101
+ */
102
+ export interface FoirFeedbackAppearance {
103
+ /** `--foir-feedback-text`. Default: inherited, or the browser's dialog text in a dialog. */
104
+ colorText?: string;
105
+ /** `--foir-feedback-muted`, the line naming what it is about. Default: the text at three quarters. */
106
+ colorMuted?: string;
107
+ /** `--foir-feedback-background`, the panel and its fields. Default: none, or the browser's dialog background in a dialog. */
108
+ colorBackground?: string;
109
+ /** `--foir-feedback-border`. Default `currentColor`. */
110
+ colorBorder?: string;
111
+ /** `--foir-feedback-primary`, the send button's fill and border. Default: no fill. */
112
+ colorPrimary?: string;
113
+ /** `--foir-feedback-primary-text`, the send button's text. Default: inherited. */
114
+ colorPrimaryText?: string;
115
+ /** `--foir-feedback-danger`, a failure. Default `#c0392b`. */
116
+ colorDanger?: string;
117
+ /** `--foir-feedback-radius`, the panel's corners; its controls take three quarters. Default `8px`. */
118
+ radius?: string;
119
+ /** `--foir-feedback-font-family`. Default: inherited. */
120
+ fontFamily?: string;
121
+ /** `--foir-feedback-font-size`. Default: inherited. */
122
+ fontSize?: string;
123
+ }
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;
@@ -7,10 +7,22 @@
7
7
  * shape (see `data/selection.ts`) — so every response here has to be read rather
8
8
  * than cast, and reading is the part worth testing on its own.
9
9
  */
10
- import type { FoirUserNote, FoirNoteConfig, FoirNoteReceipt, FoirSubject } from '../data';
10
+ import type { FoirClient, FoirSchemaDefinition, FoirUserNote, FoirNoteConfig, FoirNoteReceipt, FoirSubject } from '../data';
11
11
  import type { FoirScreenCapture } from '../media/screen-capture';
12
12
  /**
13
- * The client the widget needs, and no more of it.
13
+ * The client the widget writes through: a generated one whose module names the
14
+ * notes namespace, or one made without a generated module.
15
+ *
16
+ * A client made without a module prints from the manifest it fetches, so which
17
+ * namespaces it reaches is known only once it has asked, and its type names
18
+ * every namespace. The panel's first call is what finds out whether its
19
+ * credential reaches notes, as it is for a generated client whose key has lost
20
+ * the scope. A generated client whose module has no notes namespace is neither,
21
+ * and is refused here: it cannot send a note.
22
+ */
23
+ export type FoirFeedbackClient = FoirGeneratedFeedbackClient | FoirClient<FoirSchemaDefinition>;
24
+ /**
25
+ * A generated client, as far as the widget needs it.
14
26
  *
15
27
  * Structural rather than `FoirClient<S>`, for two reasons. `FoirClient` is
16
28
  * generic in the caller's generated schema, so naming it here would force the
@@ -23,7 +35,7 @@ import type { FoirScreenCapture } from '../media/screen-capture';
23
35
  * `Record<string, unknown>` in, `unknown` out — so a real generated client
24
36
  * assigns to this without a cast. `react/__tests__/feedback.test-d.ts` pins that.
25
37
  */
26
- export interface FoirFeedbackClient {
38
+ interface FoirGeneratedFeedbackClient {
27
39
  readonly ns: {
28
40
  readonly notes: {
29
41
  noteConfig(args?: Record<string, unknown>): Promise<unknown>;
@@ -135,3 +147,4 @@ export interface FoirVisitorSubmission {
135
147
  * visitor's note cannot carry.
136
148
  */
137
149
  export declare function submitVisitorNote(client: FoirFeedbackClient, submission: FoirVisitorSubmission): Promise<FoirNoteReceipt>;
150
+ export {};
@@ -20,7 +20,7 @@ export type { OAuthButtonsProps } from './OAuthButtons';
20
20
  export { UserButton } from './UserButton';
21
21
  export type { UserButtonProps } from './UserButton';
22
22
  export { FoirFeedback } from './FoirFeedback';
23
- export type { FoirFeedbackProps, FoirFeedbackClient } from './FoirFeedback';
23
+ export type { FoirFeedbackAppearance, FoirFeedbackClient, FoirFeedbackProps, } from './FoirFeedback';
24
24
  export { createLocalStorageAdapter } from './localStorageAdapter';
25
25
  export type { FoirAccess, FoirUser, SessionAdapter, AuthProviderPublic } from '../auth';
26
26
  export type { FoirEnv } from '../graphql/foir-request';
package/dist/react.js CHANGED
@@ -2393,16 +2393,29 @@ function screenshotFilename(mimeType) {
2393
2393
 
2394
2394
  // src/react/FoirFeedback.tsx
2395
2395
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2396
+ var TOKENS = {
2397
+ colorText: "--foir-feedback-text",
2398
+ colorMuted: "--foir-feedback-muted",
2399
+ colorBackground: "--foir-feedback-background",
2400
+ colorBorder: "--foir-feedback-border",
2401
+ colorPrimary: "--foir-feedback-primary",
2402
+ colorPrimaryText: "--foir-feedback-primary-text",
2403
+ colorDanger: "--foir-feedback-danger",
2404
+ radius: "--foir-feedback-radius",
2405
+ fontFamily: "--foir-feedback-font-family",
2406
+ fontSize: "--foir-feedback-font-size"
2407
+ };
2396
2408
  var COPY = {
2397
2409
  signIn: "Sign in to send feedback.",
2398
2410
  signInAction: "Sign in",
2411
+ closed: "Feedback is not open right now.",
2412
+ close: "Close",
2399
2413
  notEnabled: "Feedback is not available on this account.",
2400
2414
  unavailable: "Feedback is unavailable right now. Please try again later.",
2401
2415
  noSubject: "There is nothing on this page to give feedback on.",
2402
2416
  loading: "One moment\u2026",
2403
2417
  sendFailed: "Your feedback could not be sent. Please try again.",
2404
2418
  sent: "Thanks. Your feedback has been sent.",
2405
- reference: "Reference",
2406
2419
  contactEmail: "Your email, if you would like a reply (optional)",
2407
2420
  send: "Send feedback",
2408
2421
  sending: "Sending\u2026",
@@ -2413,52 +2426,98 @@ var COPY = {
2413
2426
  removeScreenshot: "Remove screenshot",
2414
2427
  about: "About"
2415
2428
  };
2429
+ var border = "1px solid var(--foir-feedback-border, currentColor)";
2430
+ var radius = "var(--foir-feedback-radius, 8px)";
2431
+ var controlRadius = `calc(${radius} * 0.75)`;
2432
+ var background = "var(--foir-feedback-background, transparent)";
2433
+ function wrapperStyle(appearance) {
2434
+ const style = {
2435
+ color: "var(--foir-feedback-text)",
2436
+ fontFamily: "var(--foir-feedback-font-family)",
2437
+ fontSize: "var(--foir-feedback-font-size)"
2438
+ };
2439
+ for (const [name, value] of Object.entries(appearance ?? {})) {
2440
+ const property = TOKENS[name];
2441
+ if (property && typeof value === "string" && value !== "") style[property] = value;
2442
+ }
2443
+ return style;
2444
+ }
2416
2445
  var stackStyle = { display: "flex", flexDirection: "column", gap: 10 };
2417
2446
  var panelStyle = {
2418
2447
  ...stackStyle,
2419
2448
  maxWidth: 340,
2449
+ marginTop: 8,
2420
2450
  padding: 16,
2421
- borderRadius: 8,
2422
- border: "1px solid currentColor",
2451
+ borderRadius: radius,
2452
+ border,
2453
+ background,
2423
2454
  font: "inherit"
2424
2455
  };
2456
+ var dialogStyle = {
2457
+ // The browser centres a modal dialog with its margin, and a CSS reset that
2458
+ // zeroes every margin (Tailwind's preflight does) pins it to the corner.
2459
+ margin: "auto",
2460
+ width: "min(420px, calc(100vw - 32px))",
2461
+ padding: 0,
2462
+ borderRadius: radius,
2463
+ border,
2464
+ background: "var(--foir-feedback-background, Canvas)",
2465
+ color: "var(--foir-feedback-text, CanvasText)",
2466
+ font: "inherit"
2467
+ };
2468
+ var dialogPanelStyle = { ...stackStyle, padding: 20 };
2469
+ var headerStyle = {
2470
+ display: "flex",
2471
+ alignItems: "center",
2472
+ justifyContent: "space-between",
2473
+ gap: 12
2474
+ };
2425
2475
  var buttonStyle = {
2426
2476
  padding: "8px 10px",
2427
- borderRadius: 6,
2428
- border: "1px solid currentColor",
2477
+ borderRadius: controlRadius,
2478
+ border,
2429
2479
  background: "transparent",
2480
+ color: "inherit",
2430
2481
  font: "inherit",
2431
2482
  cursor: "pointer"
2432
2483
  };
2433
- var submitStyle2 = { ...buttonStyle, fontWeight: 600 };
2434
- var textareaStyle = {
2435
- padding: "8px 10px",
2436
- borderRadius: 6,
2437
- border: "1px solid currentColor",
2438
- background: "transparent",
2439
- font: "inherit",
2440
- minHeight: 96,
2441
- resize: "vertical"
2484
+ var closeStyle = { ...buttonStyle, padding: "4px 8px" };
2485
+ var submitStyle2 = {
2486
+ ...buttonStyle,
2487
+ border: "1px solid var(--foir-feedback-primary, var(--foir-feedback-border, currentColor))",
2488
+ background: "var(--foir-feedback-primary, transparent)",
2489
+ color: "var(--foir-feedback-primary-text)",
2490
+ fontWeight: 600
2442
2491
  };
2443
2492
  var inputStyle2 = {
2444
2493
  padding: "8px 10px",
2445
- borderRadius: 6,
2446
- border: "1px solid currentColor",
2447
- background: "transparent",
2494
+ borderRadius: controlRadius,
2495
+ border,
2496
+ background,
2497
+ color: "inherit",
2448
2498
  font: "inherit"
2449
2499
  };
2450
- var headingStyle = { margin: 0, fontSize: 15, fontWeight: 600 };
2451
- var subjectStyle = { margin: 0, fontSize: 13, opacity: 0.75 };
2452
- var noteStyle = { margin: 0, fontSize: 13 };
2453
- var errorStyle2 = { margin: 0, fontSize: 13, color: "#c0392b" };
2500
+ var textareaStyle = { ...inputStyle2, minHeight: 96, resize: "vertical" };
2501
+ var headingStyle = { margin: 0, fontSize: "1.07em", fontWeight: 600 };
2502
+ var subjectStyle = {
2503
+ margin: 0,
2504
+ fontSize: "0.93em",
2505
+ color: "var(--foir-feedback-muted, color-mix(in srgb, currentColor 75%, transparent))"
2506
+ };
2507
+ var noteStyle = { margin: 0, fontSize: "0.93em" };
2508
+ var errorStyle2 = {
2509
+ margin: 0,
2510
+ fontSize: "0.93em",
2511
+ color: "var(--foir-feedback-danger, #c0392b)"
2512
+ };
2454
2513
  var previewStyle = {
2455
2514
  display: "block",
2456
2515
  width: "100%",
2457
2516
  height: 96,
2458
2517
  objectFit: "cover",
2459
2518
  objectPosition: "top",
2460
- borderRadius: 6,
2461
- border: "1px solid currentColor"
2519
+ borderRadius: controlRadius,
2520
+ border
2462
2521
  };
2463
2522
  function FoirFeedback({
2464
2523
  client,
@@ -2470,6 +2529,9 @@ function FoirFeedback({
2470
2529
  placeholder = "What would you like to tell us?",
2471
2530
  defaultOpen = false,
2472
2531
  onSignIn,
2532
+ signIn = true,
2533
+ layout = "inline",
2534
+ appearance,
2473
2535
  onSubmitted,
2474
2536
  onVisitorSubmitted,
2475
2537
  onError,
@@ -2479,7 +2541,6 @@ function FoirFeedback({
2479
2541
  const [probe, setProbe] = useState4({ status: "loading" });
2480
2542
  const [body, setBody] = useState4("");
2481
2543
  const [contactEmail, setContactEmail] = useState4("");
2482
- const [receipt, setReceipt] = useState4(null);
2483
2544
  const [screenshot, setScreenshot] = useState4(null);
2484
2545
  const [capturing, setCapturing] = useState4(false);
2485
2546
  const [busy, setBusy] = useState4(false);
@@ -2508,6 +2569,7 @@ function FoirFeedback({
2508
2569
  if (cancelled) return;
2509
2570
  const fault = classifyNotesFault(err);
2510
2571
  reportFault(fault, err, "the notes surface could not be read");
2572
+ announceClosed(fault, signIn);
2511
2573
  onErrorRef.current?.(err);
2512
2574
  setProbe({ status: "blocked", fault });
2513
2575
  }
@@ -2515,7 +2577,19 @@ function FoirFeedback({
2515
2577
  return () => {
2516
2578
  cancelled = true;
2517
2579
  };
2518
- }, [open, client]);
2580
+ }, [open, client, signIn]);
2581
+ const dialogRef = useRef2(null);
2582
+ useEffect3(() => {
2583
+ const dialog2 = dialogRef.current;
2584
+ if (!dialog2) return;
2585
+ if (open && !dialog2.open) {
2586
+ if (typeof dialog2.showModal === "function") dialog2.showModal();
2587
+ else dialog2.setAttribute("open", "");
2588
+ } else if (!open && dialog2.open) {
2589
+ if (typeof dialog2.close === "function") dialog2.close();
2590
+ else dialog2.removeAttribute("open");
2591
+ }
2592
+ }, [open, layout]);
2519
2593
  const capture = useCallback2(async () => {
2520
2594
  setFailure(null);
2521
2595
  setCapturing(true);
@@ -2555,7 +2629,6 @@ function FoirFeedback({
2555
2629
  });
2556
2630
  setBody("");
2557
2631
  setContactEmail("");
2558
- setReceipt(written);
2559
2632
  setSent(true);
2560
2633
  onVisitorSubmitted?.(written);
2561
2634
  }
@@ -2563,7 +2636,15 @@ function FoirFeedback({
2563
2636
  const fault = classifyNotesFault(err);
2564
2637
  reportFault(fault, err, "sending feedback failed");
2565
2638
  onErrorRef.current?.(err);
2566
- setFailure(failureCopy(fault));
2639
+ if (!probe.signedIn && fault.kind === "other") {
2640
+ const now = await probeRefusal(client);
2641
+ if (now) {
2642
+ announceClosed(now, signIn);
2643
+ setProbe({ status: "blocked", fault: now });
2644
+ return;
2645
+ }
2646
+ }
2647
+ setFailure(failureCopy(fault, signIn));
2567
2648
  } finally {
2568
2649
  setBusy(false);
2569
2650
  }
@@ -2574,157 +2655,214 @@ function FoirFeedback({
2574
2655
  return;
2575
2656
  }
2576
2657
  setSent(false);
2577
- setReceipt(null);
2578
2658
  setFailure(null);
2579
2659
  setOpen(true);
2580
2660
  };
2581
2661
  const subjectName = resolved ? label ?? resolved.label : void 0;
2582
- return /* @__PURE__ */ jsxs4("div", { className, "data-foir-feedback": true, children: [
2583
- /* @__PURE__ */ jsx5(
2584
- "button",
2585
- {
2586
- type: "button",
2587
- "data-foir-feedback-trigger": true,
2588
- "aria-expanded": open,
2589
- onClick: toggle,
2590
- style: styled ? buttonStyle : void 0,
2591
- children: triggerLabel
2592
- }
2593
- ),
2594
- open && /* @__PURE__ */ jsxs4(
2595
- "section",
2596
- {
2597
- "data-foir-feedback-panel": true,
2598
- "aria-label": title,
2599
- style: styled ? panelStyle : void 0,
2600
- children: [
2601
- /* @__PURE__ */ jsx5("h2", { "data-foir-feedback-title": true, style: styled ? headingStyle : void 0, children: title }),
2602
- subjectName && /* @__PURE__ */ jsxs4("p", { "data-foir-feedback-subject": true, style: styled ? subjectStyle : void 0, children: [
2603
- COPY.about,
2604
- ": ",
2605
- subjectName
2606
- ] }),
2607
- probe.status === "loading" && /* @__PURE__ */ jsx5(
2608
- "p",
2662
+ const dialog = layout === "dialog";
2663
+ const heading = /* @__PURE__ */ jsx5("h2", { "data-foir-feedback-title": true, style: styled ? headingStyle : void 0, children: title });
2664
+ const panel = open && /* @__PURE__ */ jsxs4(
2665
+ "section",
2666
+ {
2667
+ "data-foir-feedback-panel": true,
2668
+ "aria-label": title,
2669
+ style: styled ? dialog ? dialogPanelStyle : panelStyle : void 0,
2670
+ children: [
2671
+ dialog ? /* @__PURE__ */ jsxs4("div", { "data-foir-feedback-header": true, style: styled ? headerStyle : void 0, children: [
2672
+ heading,
2673
+ /* @__PURE__ */ jsx5(
2674
+ "button",
2609
2675
  {
2610
- role: "status",
2611
- "data-foir-feedback-status": true,
2612
- style: styled ? noteStyle : void 0,
2613
- children: COPY.loading
2676
+ type: "button",
2677
+ "data-foir-feedback-close": true,
2678
+ onClick: () => setOpen(false),
2679
+ style: styled ? closeStyle : void 0,
2680
+ children: COPY.close
2681
+ }
2682
+ )
2683
+ ] }) : heading,
2684
+ subjectName && /* @__PURE__ */ jsxs4("p", { "data-foir-feedback-subject": true, style: styled ? subjectStyle : void 0, children: [
2685
+ COPY.about,
2686
+ ": ",
2687
+ subjectName
2688
+ ] }),
2689
+ probe.status === "loading" && /* @__PURE__ */ jsx5(
2690
+ "p",
2691
+ {
2692
+ role: "status",
2693
+ "data-foir-feedback-status": true,
2694
+ style: styled ? noteStyle : void 0,
2695
+ children: COPY.loading
2696
+ }
2697
+ ),
2698
+ probe.status === "blocked" && /* @__PURE__ */ jsx5(
2699
+ BlockedNotice,
2700
+ {
2701
+ fault: probe.fault,
2702
+ onSignIn,
2703
+ signIn,
2704
+ styled
2705
+ }
2706
+ ),
2707
+ probe.status === "ready" && !resolved && /* @__PURE__ */ jsx5(
2708
+ "p",
2709
+ {
2710
+ role: "status",
2711
+ "data-foir-feedback-status": true,
2712
+ style: styled ? noteStyle : void 0,
2713
+ children: COPY.noSubject
2714
+ }
2715
+ ),
2716
+ probe.status === "ready" && resolved && /* @__PURE__ */ jsxs4("form", { onSubmit, style: styled ? stackStyle : void 0, children: [
2717
+ /* @__PURE__ */ jsx5(
2718
+ "textarea",
2719
+ {
2720
+ "data-foir-feedback-body": true,
2721
+ "aria-label": placeholder,
2722
+ placeholder,
2723
+ required: true,
2724
+ value: body,
2725
+ disabled: busy,
2726
+ onChange: (event) => {
2727
+ setBody(event.target.value);
2728
+ if (sent) setSent(false);
2729
+ },
2730
+ style: styled ? textareaStyle : void 0
2614
2731
  }
2615
2732
  ),
2616
- probe.status === "blocked" && /* @__PURE__ */ jsx5(BlockedNotice, { fault: probe.fault, onSignIn, styled }),
2617
- probe.status === "ready" && !resolved && /* @__PURE__ */ jsx5(
2618
- "p",
2733
+ !probe.signedIn && /* @__PURE__ */ jsx5(
2734
+ "input",
2619
2735
  {
2620
- role: "status",
2621
- "data-foir-feedback-status": true,
2622
- style: styled ? noteStyle : void 0,
2623
- children: COPY.noSubject
2736
+ type: "email",
2737
+ "data-foir-feedback-contact": true,
2738
+ "aria-label": COPY.contactEmail,
2739
+ placeholder: COPY.contactEmail,
2740
+ autoComplete: "email",
2741
+ value: contactEmail,
2742
+ disabled: busy,
2743
+ onChange: (event) => setContactEmail(event.target.value),
2744
+ style: styled ? inputStyle2 : void 0
2624
2745
  }
2625
2746
  ),
2626
- probe.status === "ready" && resolved && /* @__PURE__ */ jsxs4("form", { onSubmit, style: styled ? stackStyle : void 0, children: [
2747
+ probe.screenshotsEnabled && isFoirScreenCaptureSupported() && /* @__PURE__ */ jsx5(Fragment2, { children: screenshot ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
2627
2748
  /* @__PURE__ */ jsx5(
2628
- "textarea",
2749
+ "img",
2629
2750
  {
2630
- "data-foir-feedback-body": true,
2631
- "aria-label": placeholder,
2632
- placeholder,
2633
- required: true,
2634
- value: body,
2635
- disabled: busy,
2636
- onChange: (event) => {
2637
- setBody(event.target.value);
2638
- if (sent) {
2639
- setSent(false);
2640
- setReceipt(null);
2641
- }
2642
- },
2643
- style: styled ? textareaStyle : void 0
2644
- }
2645
- ),
2646
- !probe.signedIn && /* @__PURE__ */ jsx5(
2647
- "input",
2648
- {
2649
- type: "email",
2650
- "data-foir-feedback-contact": true,
2651
- "aria-label": COPY.contactEmail,
2652
- placeholder: COPY.contactEmail,
2653
- autoComplete: "email",
2654
- value: contactEmail,
2655
- disabled: busy,
2656
- onChange: (event) => setContactEmail(event.target.value),
2657
- style: styled ? inputStyle2 : void 0
2751
+ "data-foir-feedback-screenshot": true,
2752
+ src: screenshot.dataUrl,
2753
+ alt: COPY.screenshotAlt,
2754
+ style: styled ? previewStyle : void 0
2658
2755
  }
2659
2756
  ),
2660
- probe.screenshotsEnabled && isFoirScreenCaptureSupported() && /* @__PURE__ */ jsx5(Fragment2, { children: screenshot ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
2661
- /* @__PURE__ */ jsx5(
2662
- "img",
2663
- {
2664
- "data-foir-feedback-screenshot": true,
2665
- src: screenshot.dataUrl,
2666
- alt: COPY.screenshotAlt,
2667
- style: styled ? previewStyle : void 0
2668
- }
2669
- ),
2670
- /* @__PURE__ */ jsx5(
2671
- "button",
2672
- {
2673
- type: "button",
2674
- "data-foir-feedback-remove": true,
2675
- onClick: () => setScreenshot(null),
2676
- disabled: busy,
2677
- style: styled ? buttonStyle : void 0,
2678
- children: COPY.removeScreenshot
2679
- }
2680
- )
2681
- ] }) : /* @__PURE__ */ jsx5(
2757
+ /* @__PURE__ */ jsx5(
2682
2758
  "button",
2683
2759
  {
2684
2760
  type: "button",
2685
- "data-foir-feedback-capture": true,
2686
- onClick: () => void capture(),
2687
- disabled: busy || capturing,
2761
+ "data-foir-feedback-remove": true,
2762
+ onClick: () => setScreenshot(null),
2763
+ disabled: busy,
2688
2764
  style: styled ? buttonStyle : void 0,
2689
- children: capturing ? COPY.capturing : COPY.capture
2690
- }
2691
- ) }),
2692
- failure && /* @__PURE__ */ jsx5("p", { role: "alert", "data-foir-feedback-error": true, style: styled ? errorStyle2 : void 0, children: failure }),
2693
- sent && !failure && /* @__PURE__ */ jsx5(
2694
- "p",
2695
- {
2696
- role: "status",
2697
- "data-foir-feedback-status": true,
2698
- style: styled ? noteStyle : void 0,
2699
- children: COPY.sent
2700
- }
2701
- ),
2702
- sent && !failure && receipt && /* @__PURE__ */ jsxs4("p", { "data-foir-feedback-receipt": true, style: styled ? subjectStyle : void 0, children: [
2703
- COPY.reference,
2704
- ": ",
2705
- receipt.id
2706
- ] }),
2707
- /* @__PURE__ */ jsx5(
2708
- "button",
2709
- {
2710
- type: "submit",
2711
- "data-foir-feedback-submit": true,
2712
- disabled: busy || body.trim() === "",
2713
- style: styled ? submitStyle2 : void 0,
2714
- children: busy ? COPY.sending : COPY.send
2765
+ children: COPY.removeScreenshot
2715
2766
  }
2716
2767
  )
2717
- ] })
2718
- ]
2719
- }
2720
- )
2721
- ] });
2768
+ ] }) : /* @__PURE__ */ jsx5(
2769
+ "button",
2770
+ {
2771
+ type: "button",
2772
+ "data-foir-feedback-capture": true,
2773
+ onClick: () => void capture(),
2774
+ disabled: busy || capturing,
2775
+ style: styled ? buttonStyle : void 0,
2776
+ children: capturing ? COPY.capturing : COPY.capture
2777
+ }
2778
+ ) }),
2779
+ failure && /* @__PURE__ */ jsx5("p", { role: "alert", "data-foir-feedback-error": true, style: styled ? errorStyle2 : void 0, children: failure }),
2780
+ sent && !failure && /* @__PURE__ */ jsx5(
2781
+ "p",
2782
+ {
2783
+ role: "status",
2784
+ "data-foir-feedback-status": true,
2785
+ style: styled ? noteStyle : void 0,
2786
+ children: COPY.sent
2787
+ }
2788
+ ),
2789
+ /* @__PURE__ */ jsx5(
2790
+ "button",
2791
+ {
2792
+ type: "submit",
2793
+ "data-foir-feedback-submit": true,
2794
+ disabled: busy || body.trim() === "",
2795
+ style: styled ? submitStyle2 : void 0,
2796
+ children: busy ? COPY.sending : COPY.send
2797
+ }
2798
+ )
2799
+ ] })
2800
+ ]
2801
+ }
2802
+ );
2803
+ return /* @__PURE__ */ jsxs4(
2804
+ "div",
2805
+ {
2806
+ className,
2807
+ "data-foir-feedback": true,
2808
+ "data-foir-feedback-layout": layout,
2809
+ style: styled ? wrapperStyle(appearance) : void 0,
2810
+ children: [
2811
+ /* @__PURE__ */ jsx5(
2812
+ "button",
2813
+ {
2814
+ type: "button",
2815
+ "data-foir-feedback-trigger": true,
2816
+ "aria-expanded": open,
2817
+ "aria-haspopup": dialog ? "dialog" : void 0,
2818
+ onClick: toggle,
2819
+ style: styled ? buttonStyle : void 0,
2820
+ children: triggerLabel
2821
+ }
2822
+ ),
2823
+ dialog ? /* @__PURE__ */ jsx5(
2824
+ "dialog",
2825
+ {
2826
+ ref: dialogRef,
2827
+ "data-foir-feedback-dialog": true,
2828
+ "aria-label": title,
2829
+ onClose: () => setOpen(false),
2830
+ onClick: (event) => {
2831
+ if (event.target === event.currentTarget) setOpen(false);
2832
+ },
2833
+ style: styled ? dialogStyle : void 0,
2834
+ children: panel
2835
+ }
2836
+ ) : panel
2837
+ ]
2838
+ }
2839
+ );
2840
+ }
2841
+ async function probeRefusal(client) {
2842
+ try {
2843
+ await readNoteConfig(client);
2844
+ return null;
2845
+ } catch (err) {
2846
+ const fault = classifyNotesFault(err);
2847
+ return fault.kind === "other" ? null : fault;
2848
+ }
2849
+ }
2850
+ function announceClosed(fault, signIn) {
2851
+ if (process.env.NODE_ENV === "production") return;
2852
+ if (fault.kind !== "signIn" || signIn) return;
2853
+ console.info(
2854
+ `[foir] <FoirFeedback>: the project takes no notes from visitors who are not signed in, and signIn={false} says this site has no sign-in, so the widget says feedback is not open. Turn on "Accept anonymous notes" in the project's Notes settings to take them.`
2855
+ );
2722
2856
  }
2723
2857
  function BlockedNotice({
2724
2858
  fault,
2725
2859
  onSignIn,
2860
+ signIn,
2726
2861
  styled
2727
2862
  }) {
2863
+ if (fault.kind === "signIn" && !signIn) {
2864
+ return /* @__PURE__ */ jsx5("p", { role: "status", "data-foir-feedback-status": true, style: styled ? noteStyle : void 0, children: COPY.closed });
2865
+ }
2728
2866
  if (fault.kind === "signIn") {
2729
2867
  return /* @__PURE__ */ jsxs4(Fragment2, { children: [
2730
2868
  /* @__PURE__ */ jsx5("p", { role: "status", "data-foir-feedback-status": true, style: styled ? noteStyle : void 0, children: COPY.signIn }),
@@ -2742,8 +2880,8 @@ function BlockedNotice({
2742
2880
  }
2743
2881
  return /* @__PURE__ */ jsx5("p", { role: "status", "data-foir-feedback-status": true, style: styled ? noteStyle : void 0, children: fault.kind === "scope" ? COPY.notEnabled : COPY.unavailable });
2744
2882
  }
2745
- function failureCopy(fault) {
2746
- if (fault.kind === "signIn") return COPY.signIn;
2883
+ function failureCopy(fault, signIn) {
2884
+ if (fault.kind === "signIn") return signIn ? COPY.signIn : COPY.closed;
2747
2885
  if (fault.kind === "scope") return COPY.notEnabled;
2748
2886
  return COPY.sendFailed;
2749
2887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -177,8 +177,8 @@
177
177
  "ws": "^8.18.0",
178
178
  "y-websocket": "^3.0.0",
179
179
  "yjs": "^13.6.30",
180
- "@foir/access-vocab": "0.0.0",
181
- "@foir/proto-ts": "0.109.0"
180
+ "@foir/proto-ts": "0.109.0",
181
+ "@foir/access-vocab": "0.0.0"
182
182
  },
183
183
  "engines": {
184
184
  "node": ">=18.0.0"