@ciridae/ui 1.0.2 → 1.0.4

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 (209) hide show
  1. package/dist/app/auth.d.ts +10 -0
  2. package/dist/app/auth.js +60 -0
  3. package/dist/app/error-boundary.d.ts +18 -0
  4. package/dist/app/error-boundary.js +45 -0
  5. package/dist/app/feedback.d.ts +12 -0
  6. package/dist/app/feedback.js +128 -0
  7. package/dist/app/index.d.ts +8 -0
  8. package/dist/app/index.js +8 -0
  9. package/dist/app/request-errors.d.ts +63 -0
  10. package/dist/app/request-errors.js +172 -0
  11. package/dist/app/sentry.d.ts +18 -0
  12. package/dist/app/sentry.js +52 -0
  13. package/dist/app/telemetry.d.ts +7 -0
  14. package/dist/app/telemetry.js +28 -0
  15. package/dist/app/trail.d.ts +9 -0
  16. package/dist/app/trail.js +18 -0
  17. package/dist/chat/chat-query-keys.d.ts +3 -3
  18. package/dist/chat/contract.d.ts +15 -15
  19. package/dist/chat/conversation-items-query-options.d.ts +114 -114
  20. package/dist/chat/conversation-source.d.ts +89 -0
  21. package/dist/chat/conversation-source.js +1 -0
  22. package/dist/chat/describe-tool-call.d.ts +3 -3
  23. package/dist/chat/describe-tool-call.js +3 -3
  24. package/dist/chat/message-text.d.ts +1 -1
  25. package/dist/chat/message-text.js +116 -48
  26. package/dist/chat/use-conversation-list.d.ts +6 -6
  27. package/dist/ciridae.css +16 -4
  28. package/dist/components/activity-feed.d.ts +3 -2
  29. package/dist/components/activity-feed.js +1 -0
  30. package/dist/components/agent-work.d.ts +3 -3
  31. package/dist/components/app-frame.d.ts +91 -0
  32. package/dist/components/app-frame.js +134 -0
  33. package/dist/components/app-navigation.d.ts +30 -3
  34. package/dist/components/app-navigation.js +105 -14
  35. package/dist/components/badge.d.ts +30 -5
  36. package/dist/components/badge.js +134 -15
  37. package/dist/components/brand-icons.d.ts +3 -3
  38. package/dist/components/bulk-action-bar.d.ts +1 -1
  39. package/dist/components/bulk-action-bar.js +3 -3
  40. package/dist/components/button.d.ts +12 -5
  41. package/dist/components/button.js +77 -44
  42. package/dist/components/calendar.d.ts +1 -1
  43. package/dist/components/calendar.js +44 -17
  44. package/dist/components/centered-page.d.ts +1 -1
  45. package/dist/components/centered-page.js +2 -2
  46. package/dist/components/chart.d.ts +22 -2
  47. package/dist/components/chart.js +251 -51
  48. package/dist/components/chat-client-source.d.ts +16 -0
  49. package/dist/components/chat-client-source.js +297 -0
  50. package/dist/components/chat-page.d.ts +9 -8
  51. package/dist/components/chat-page.js +7 -11
  52. package/dist/components/chat-sheet-controller.d.ts +11 -14
  53. package/dist/components/chat-sheet-controller.js +3 -3
  54. package/dist/components/chat-sheet.d.ts +18 -11
  55. package/dist/components/chat-sheet.js +188 -289
  56. package/dist/components/chat.d.ts +109 -19
  57. package/dist/components/chat.js +520 -124
  58. package/dist/components/checkbox-item.d.ts +6 -3
  59. package/dist/components/checkbox-item.js +9 -5
  60. package/dist/components/checkbox-list.d.ts +1 -1
  61. package/dist/components/chip.d.ts +6 -1
  62. package/dist/components/chip.js +27 -1
  63. package/dist/components/collapsible.d.ts +1 -1
  64. package/dist/components/color-theme.d.ts +1 -1
  65. package/dist/components/column-settings.d.ts +1 -1
  66. package/dist/components/column-settings.js +1 -1
  67. package/dist/components/command-menu.d.ts +5 -1
  68. package/dist/components/command-menu.js +98 -32
  69. package/dist/components/command-palette.d.ts +7 -3
  70. package/dist/components/command-palette.js +20 -11
  71. package/dist/components/copyable-id.d.ts +22 -5
  72. package/dist/components/copyable-id.js +46 -6
  73. package/dist/components/data-display.d.ts +39 -8
  74. package/dist/components/data-display.js +138 -19
  75. package/dist/components/data-grid.d.ts +23 -11
  76. package/dist/components/data-grid.js +61 -26
  77. package/dist/components/date-range-picker.d.ts +1 -1
  78. package/dist/components/date-range-picker.js +9 -20
  79. package/dist/components/date-time.d.ts +1 -1
  80. package/dist/components/dialog-height-context.d.ts +1 -1
  81. package/dist/components/document-viewer.d.ts +2 -2
  82. package/dist/components/drawer.d.ts +11 -6
  83. package/dist/components/drawer.js +91 -54
  84. package/dist/components/dropzone.d.ts +4 -4
  85. package/dist/components/editable-field.d.ts +12 -3
  86. package/dist/components/editable-field.js +57 -27
  87. package/dist/components/email-input.d.ts +1 -1
  88. package/dist/components/email-input.js +6 -5
  89. package/dist/components/environment-frame.d.ts +2 -2
  90. package/dist/components/environment-frame.js +37 -6
  91. package/dist/components/expandable-text.d.ts +1 -1
  92. package/dist/components/feedback-screenshot.d.ts +1 -1
  93. package/dist/components/feedback-sheet.d.ts +18 -3
  94. package/dist/components/feedback-sheet.js +216 -55
  95. package/dist/components/field-context.d.ts +1 -1
  96. package/dist/components/filter-bar.d.ts +3 -3
  97. package/dist/components/filter-bar.js +43 -29
  98. package/dist/components/form-controls.d.ts +12 -3
  99. package/dist/components/form-controls.js +46 -22
  100. package/dist/components/form-layout.d.ts +14 -10
  101. package/dist/components/form-layout.js +68 -23
  102. package/dist/components/glass-menu.d.ts +3 -0
  103. package/dist/components/glass-menu.js +63 -0
  104. package/dist/components/gradient-edge.d.ts +9 -0
  105. package/dist/components/gradient-edge.js +31 -0
  106. package/dist/components/hotkeys.d.ts +2 -2
  107. package/dist/components/icon.d.ts +1 -1
  108. package/dist/components/input.d.ts +7 -5
  109. package/dist/components/input.js +29 -17
  110. package/dist/components/keyboard-shortcuts.d.ts +2 -2
  111. package/dist/components/keycap.d.ts +10 -6
  112. package/dist/components/keycap.js +57 -19
  113. package/dist/components/layout.d.ts +13 -13
  114. package/dist/components/lightbox.d.ts +9 -3
  115. package/dist/components/lightbox.js +13 -9
  116. package/dist/components/link.d.ts +1 -1
  117. package/dist/components/locale.d.ts +42 -2
  118. package/dist/components/locale.js +43 -3
  119. package/dist/components/log-entry.d.ts +40 -0
  120. package/dist/components/log-entry.js +146 -0
  121. package/dist/components/log-list.d.ts +3 -3
  122. package/dist/components/mention-input.d.ts +2 -2
  123. package/dist/components/mention-input.js +4 -3
  124. package/dist/components/menu.d.ts +7 -3
  125. package/dist/components/menu.js +34 -37
  126. package/dist/components/meter.d.ts +10 -2
  127. package/dist/components/meter.js +64 -6
  128. package/dist/components/navigation.d.ts +21 -4
  129. package/dist/components/navigation.js +84 -19
  130. package/dist/components/notification-tray.d.ts +1 -1
  131. package/dist/components/notification-tray.js +5 -4
  132. package/dist/components/overlays.d.ts +6 -4
  133. package/dist/components/overlays.js +69 -40
  134. package/dist/components/page-frame.d.ts +4 -4
  135. package/dist/components/page-frame.js +1 -1
  136. package/dist/components/panel.d.ts +4 -4
  137. package/dist/components/panel.js +18 -12
  138. package/dist/components/profile-header.d.ts +1 -1
  139. package/dist/components/progress.d.ts +1 -1
  140. package/dist/components/project-avatar.d.ts +1 -1
  141. package/dist/components/radio-card.d.ts +2 -2
  142. package/dist/components/radio-card.js +10 -9
  143. package/dist/components/record.d.ts +29 -5
  144. package/dist/components/record.js +147 -18
  145. package/dist/components/resolved-file-url.d.ts +19 -0
  146. package/dist/components/resolved-file-url.js +49 -0
  147. package/dist/components/scroll-area.d.ts +1 -1
  148. package/dist/components/secret-dialog.d.ts +1 -1
  149. package/dist/components/segmented-bar.d.ts +4 -2
  150. package/dist/components/segmented-bar.js +16 -6
  151. package/dist/components/select.d.ts +9 -6
  152. package/dist/components/select.js +103 -127
  153. package/dist/components/settings.d.ts +4 -4
  154. package/dist/components/shell-skeleton.d.ts +1 -1
  155. package/dist/components/shell-skeleton.js +1 -1
  156. package/dist/components/shell.d.ts +21 -21
  157. package/dist/components/shell.js +43 -43
  158. package/dist/components/side-panel.d.ts +3 -3
  159. package/dist/components/sidebar.d.ts +13 -8
  160. package/dist/components/sidebar.js +55 -27
  161. package/dist/components/single-line-text.d.ts +2 -0
  162. package/dist/components/single-line-text.js +16 -0
  163. package/dist/components/site-banner.d.ts +5 -2
  164. package/dist/components/site-banner.js +17 -6
  165. package/dist/components/skeleton.d.ts +8 -7
  166. package/dist/components/skeleton.js +28 -16
  167. package/dist/components/skeletons.d.ts +17 -5
  168. package/dist/components/skeletons.js +71 -8
  169. package/dist/components/stages.d.ts +83 -11
  170. package/dist/components/stages.js +512 -156
  171. package/dist/components/stat-card.d.ts +12 -4
  172. package/dist/components/stat-card.js +92 -37
  173. package/dist/components/status-banner.d.ts +1 -1
  174. package/dist/components/status-banner.js +1 -1
  175. package/dist/components/status.d.ts +2 -2
  176. package/dist/components/steps.d.ts +1 -1
  177. package/dist/components/switch.d.ts +1 -1
  178. package/dist/components/tabs.d.ts +8 -8
  179. package/dist/components/tabs.js +26 -13
  180. package/dist/components/theme-customizer.d.ts +2 -2
  181. package/dist/components/theme-toggle.d.ts +4 -0
  182. package/dist/components/theme-toggle.js +67 -0
  183. package/dist/components/toast.d.ts +10 -3
  184. package/dist/components/toast.js +78 -29
  185. package/dist/components/use-new-version-available.d.ts +6 -0
  186. package/dist/components/use-new-version-available.js +29 -0
  187. package/dist/components/user-chip.d.ts +3 -3
  188. package/dist/components/wizard.d.ts +2 -2
  189. package/dist/feedback/contract.d.ts +136 -136
  190. package/dist/feedback/feedback-controller.d.ts +6 -6
  191. package/dist/feedback/generated/zod.gen.d.ts +28 -28
  192. package/dist/feedback/operator-contract.d.ts +11 -11
  193. package/dist/feedback/operator-controller.d.ts +32 -13
  194. package/dist/feedback/operator-controller.js +44 -11
  195. package/dist/preset.js +289 -33
  196. package/dist/styled-system/styles.css +3294 -1079
  197. package/dist/styled-system/tokens/index.mjs +598 -22
  198. package/dist/styled-system/tokens/tokens.d.ts +11 -11
  199. package/dist/styled-system/types/prop-type.d.ts +2 -2
  200. package/dist/styles.css +16 -4
  201. package/dist/theme/color.d.ts +3 -0
  202. package/dist/theme/color.js +5 -0
  203. package/dist/theme/derive.d.ts +9 -2
  204. package/dist/theme/derive.js +72 -12
  205. package/dist/theme/palettes.d.ts +49 -6
  206. package/dist/theme/palettes.js +84 -15
  207. package/dist/theme/tokens.d.ts +3 -3
  208. package/dist/theme/tokens.js +107 -32
  209. package/package.json +40 -3
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ /** Renders children for a signed-in session and sends everyone else to the auth service. */
3
+ export declare function RequireSignIn({ children, loading, }: {
4
+ children: ReactNode;
5
+ loading?: ReactNode;
6
+ }): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
7
+ /** Where the auth service returns the reader; finishes sign-in and goes back to where they were. */
8
+ export declare function SignInCallbackPage({ loading }: {
9
+ loading?: ReactNode;
10
+ }): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null;
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { parseSameOriginPath } from "@ciridae/auth/client";
3
+ import { useCiridaeAuth, useCompleteCiridaeSignIn } from "@ciridae/auth/react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { useLocation, useNavigate } from "react-router-dom";
6
+ import { CiridaeMark } from "../components/brand-icons.js";
7
+ import { icon, Spinner } from "../components/icon.js";
8
+ import { Stack } from "../components/layout.js";
9
+ import { EmptyState } from "../components/navigation.js";
10
+ function SignInLoading() {
11
+ const [visible, setVisible] = useState(false);
12
+ useEffect(() => {
13
+ const timeout = window.setTimeout(() => setVisible(true), 300);
14
+ return () => window.clearTimeout(timeout);
15
+ }, []);
16
+ if (!visible)
17
+ return null;
18
+ return (_jsxs(Stack, { role: "status", "aria-label": "Signing in", gap: "16", style: { minHeight: "100dvh", alignItems: "center", justifyContent: "center" }, children: [_jsx(CiridaeMark, { className: icon({ size: "display" }) }), _jsx(Spinner, {})] }));
19
+ }
20
+ /** Renders children for a signed-in session and sends everyone else to the auth service. */
21
+ export function RequireSignIn({ children, loading = _jsx(SignInLoading, {}), }) {
22
+ const { auth, session } = useCiridaeAuth();
23
+ const signInUrl = session.status === "signedOut"
24
+ ? auth.getSignInUrl(window.location.pathname + window.location.search)
25
+ : null;
26
+ useEffect(() => {
27
+ if (signInUrl)
28
+ window.location.assign(signInUrl);
29
+ }, [signInUrl]);
30
+ if (session.status === "signedIn")
31
+ return children;
32
+ if (session.status === "unavailable" || (session.status === "signedOut" && !signInUrl)) {
33
+ return _jsx(EmptyState, { title: "Sign-in unavailable" });
34
+ }
35
+ return loading;
36
+ }
37
+ const signInFailures = {
38
+ failed: "Sign-in failed",
39
+ missingToken: "No sign-in session found",
40
+ authUnavailable: "Sign-in unavailable",
41
+ };
42
+ /** Where the auth service returns the reader; finishes sign-in and goes back to where they were. */
43
+ export function SignInCallbackPage({ loading = _jsx(SignInLoading, {}) }) {
44
+ const navigate = useNavigate();
45
+ const parameters = new URLSearchParams(useLocation().search);
46
+ const onComplete = useCallback((path) => void navigate(path, { replace: true }), [navigate]);
47
+ const state = useCompleteCiridaeSignIn({
48
+ customToken: parameters.get("customToken"),
49
+ onComplete,
50
+ redirectPath: parseSameOriginPath(parameters.get("redirect_path")),
51
+ });
52
+ const failure = signInFailures[state.status];
53
+ useEffect(() => {
54
+ if (!failure)
55
+ return;
56
+ const timeout = window.setTimeout(() => void navigate("/", { replace: true }), 3000);
57
+ return () => window.clearTimeout(timeout);
58
+ }, [failure, navigate]);
59
+ return failure ? _jsx(EmptyState, { title: failure }) : loading;
60
+ }
@@ -0,0 +1,18 @@
1
+ import { CiridaeTelemetryContext } from "@ciridae/telemetry/react";
2
+ import { Component, type ContextType, type ErrorInfo, type ReactNode } from "react";
3
+ type ErrorBoundaryState = {
4
+ hasError: boolean;
5
+ errorId: string | null;
6
+ };
7
+ /** Reports a render crash to Sentry and PostHog and offers a reload instead of a blank page. */
8
+ export declare class AppErrorBoundary extends Component<{
9
+ children: ReactNode;
10
+ }, ErrorBoundaryState> {
11
+ static contextType: typeof CiridaeTelemetryContext;
12
+ context: ContextType<typeof CiridaeTelemetryContext>;
13
+ state: ErrorBoundaryState;
14
+ static getDerivedStateFromError(): ErrorBoundaryState;
15
+ componentDidCatch(error: Error, info: ErrorInfo): void;
16
+ render(): ReactNode;
17
+ }
18
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CiridaeTelemetryContext } from "@ciridae/telemetry/react";
3
+ import { WarningCircleIcon } from "@phosphor-icons/react";
4
+ import { Component } from "react";
5
+ import { Button } from "../components/button.js";
6
+ import { CopyableId } from "../components/copyable-id.js";
7
+ import { icon } from "../components/icon.js";
8
+ import { Cluster, Stack } from "../components/layout.js";
9
+ import { EmptyState } from "../components/navigation.js";
10
+ import { captureExceptionInSentry } from "./sentry.js";
11
+ /** Reports a render crash to Sentry and PostHog and offers a reload instead of a blank page. */
12
+ export class AppErrorBoundary extends Component {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.state = { hasError: false, errorId: null };
16
+ }
17
+ static getDerivedStateFromError() {
18
+ return { hasError: true, errorId: null };
19
+ }
20
+ componentDidCatch(error, info) {
21
+ const componentStack = info.componentStack ?? "";
22
+ void captureExceptionInSentry(error, componentStack).then((errorId) => {
23
+ if (errorId)
24
+ this.setState({ errorId });
25
+ });
26
+ const telemetry = this.context;
27
+ if (!telemetry)
28
+ return;
29
+ if (telemetry.isInitialized()) {
30
+ telemetry.captureException(error, { componentStack });
31
+ return;
32
+ }
33
+ // A crash on first render lands before PostHog starts, so it waits until it has.
34
+ const unsubscribe = telemetry.subscribeToInitialization(() => {
35
+ unsubscribe();
36
+ telemetry.captureException(error, { componentStack });
37
+ });
38
+ }
39
+ render() {
40
+ if (!this.state.hasError)
41
+ return this.props.children;
42
+ return (_jsx(EmptyState, { icon: _jsx(WarningCircleIcon, { className: icon({ size: "large", tone: "danger" }), weight: "fill" }), title: "Something went wrong", action: _jsxs(Stack, { gap: "8", children: [this.state.errorId && _jsx(CopyableId, { label: "Error ID", value: this.state.errorId }), _jsxs(Cluster, { gap: "8", children: [_jsx(Button, { onClick: () => window.location.reload(), children: "Reload" }), _jsx(Button, { variant: "ghost", nativeButton: false, render: _jsx("a", { href: "https://statuspage.incident.io/ciridae", target: "_blank", rel: "noreferrer" }), children: "Status page" })] })] }) }));
43
+ }
44
+ }
45
+ AppErrorBoundary.contextType = CiridaeTelemetryContext;
@@ -0,0 +1,12 @@
1
+ /** The Crucible project environment whose feedback inbox receives this app's feedback. */
2
+ export type FeedbackScope = {
3
+ apiUrl: string;
4
+ projectId: string;
5
+ environment: string;
6
+ };
7
+ /** Sends feedback about the app and follows up on earlier submissions. */
8
+ export declare function FeedbackDialog({ scope, open, onOpenChange, }: {
9
+ scope: FeedbackScope;
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCiridaeAuth } from "@ciridae/auth/react";
3
+ import { useCiridaeTelemetry } from "@ciridae/telemetry/react";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useMemo, useState } from "react";
6
+ import { CopyableId } from "../components/copyable-id.js";
7
+ import { FeedbackSheet, formatFeedbackDescription, } from "../components/feedback-sheet.js";
8
+ import { useToast } from "../components/toast.js";
9
+ import { createFeedbackClient } from "../feedback/client.js";
10
+ import { feedbackMaxLengths } from "../feedback/contract.js";
11
+ import { errorTraceId, reportedInlineMeta } from "./request-errors.js";
12
+ const feedbackTypeByKind = {
13
+ wrong: "bug",
14
+ idea: "feature",
15
+ question: "general",
16
+ };
17
+ /** Sends feedback about the app and follows up on earlier submissions. */
18
+ export function FeedbackDialog({ scope, open, onOpenChange, }) {
19
+ const { auth, session } = useCiridaeAuth();
20
+ const user = session.status === "signedIn" || session.status === "signingOut" ? session.user : null;
21
+ const { apiUrl, projectId, environment } = scope;
22
+ const client = useMemo(() => createFeedbackClient({
23
+ baseUrl: `${apiUrl.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(projectId)}/environments/${encodeURIComponent(environment)}`,
24
+ getBearerToken: async () => {
25
+ const token = await auth.getAccessToken();
26
+ if (!token)
27
+ throw new Error("Sign in again to send feedback.");
28
+ return token;
29
+ },
30
+ }), [auth, apiUrl, projectId, environment]);
31
+ const queryClient = useQueryClient();
32
+ const toast = useToast();
33
+ const telemetry = useCiridaeTelemetry();
34
+ const [idempotencyKey, setIdempotencyKey] = useState(() => crypto.randomUUID());
35
+ const [openItemId, setOpenItemId] = useState(null);
36
+ const queryKey = ["feedback", user?.uid ?? ""];
37
+ const items = useQuery({
38
+ meta: reportedInlineMeta,
39
+ queryKey,
40
+ queryFn: ({ signal }) => client.list({ signal }),
41
+ enabled: open && user !== null,
42
+ retry: false,
43
+ });
44
+ const item = useQuery({
45
+ meta: reportedInlineMeta,
46
+ queryKey: [...queryKey, openItemId],
47
+ queryFn: ({ signal }) => client.get(openItemId ?? "", { signal }),
48
+ enabled: openItemId !== null,
49
+ retry: false,
50
+ });
51
+ // Resolves once the refetched list and item carry the change, so the sheet never shows stale.
52
+ const refresh = () => queryClient.invalidateQueries({ queryKey });
53
+ const create = useMutation({
54
+ meta: reportedInlineMeta,
55
+ mutationFn: (values) => {
56
+ const type = feedbackTypeByKind[values.kind];
57
+ return client.create({
58
+ type,
59
+ subject: subjectFromMessage(values.whatHappened),
60
+ description: formatFeedbackDescription(values),
61
+ urgency: "normal",
62
+ }, { idempotencyKey, files: values.attachments });
63
+ },
64
+ onSuccess: (createdFeedback, values) => {
65
+ telemetry.capture("feedback_submitted", {
66
+ source: "app_shell",
67
+ type: feedbackTypeByKind[values.kind],
68
+ urgency: createdFeedback.urgency,
69
+ });
70
+ void refresh();
71
+ toast.add({ variant: "success", title: "Feedback sent" });
72
+ setIdempotencyKey(crypto.randomUUID());
73
+ },
74
+ });
75
+ const reply = useMutation({
76
+ meta: reportedInlineMeta,
77
+ mutationFn: ({ feedbackId, body }) => client.createMessage(feedbackId, body),
78
+ onSuccess: () => {
79
+ toast.add({ variant: "success", title: "Reply sent" });
80
+ return refresh();
81
+ },
82
+ });
83
+ const withdraw = useMutation({
84
+ meta: reportedInlineMeta,
85
+ mutationFn: (feedbackId) => client.withdraw(feedbackId),
86
+ onSuccess: () => refresh(),
87
+ });
88
+ if (!user)
89
+ return null;
90
+ return (_jsx(FeedbackSheet, { open: open, onOpenChange: onOpenChange,
91
+ // The page the sheet is over; it cannot change while the sheet is open.
92
+ where: {
93
+ label: document.title || "This page",
94
+ detail: window.location.href,
95
+ href: window.location.href,
96
+ }, onSubmit: async (values) => {
97
+ const createdFeedback = await create.mutateAsync(values);
98
+ return { reference: createdFeedback.id };
99
+ }, errorDetail: (error) => {
100
+ const traceId = errorTraceId(error);
101
+ return traceId && _jsx(CopyableId, { label: "Error ID", value: traceId });
102
+ }, sent: {
103
+ items: loadState(items),
104
+ onOpenItem: setOpenItemId,
105
+ item: openItemId === null ? undefined : loadState(item),
106
+ onReply: async (feedbackId, body) => {
107
+ await reply.mutateAsync({ feedbackId, body });
108
+ },
109
+ onWithdraw: async (feedbackId) => {
110
+ await withdraw.mutateAsync(feedbackId);
111
+ },
112
+ } }));
113
+ }
114
+ /** The query as the sheet reads it: loading until its first answer, then its data or failure. */
115
+ function loadState(query) {
116
+ if (query.isPending)
117
+ return { status: "loading" };
118
+ if (query.isError)
119
+ return { status: "error", error: query.error, retry: () => void query.refetch() };
120
+ return { status: "loaded", data: query.data };
121
+ }
122
+ /** The message's first line, within the contract's subject length. */
123
+ function subjectFromMessage(message) {
124
+ const firstLine = message.split("\n", 1)[0] ?? message;
125
+ return firstLine.length > feedbackMaxLengths.subject
126
+ ? `${firstLine.slice(0, feedbackMaxLengths.subject - 1)}…`
127
+ : firstLine;
128
+ }
@@ -0,0 +1,8 @@
1
+ /** Opt-in integrations for Ciridae apps. The standalone UI/frame does not load these peers. */
2
+ export { RequireSignIn, SignInCallbackPage } from "./auth.js";
3
+ export { AppErrorBoundary } from "./error-boundary.js";
4
+ export { FeedbackDialog, type FeedbackScope } from "./feedback.js";
5
+ export { errorMessage, errorStatus, errorTraceId, QueryContent, reportedInline, reportedInlineMeta, requestErrorDescription, RequestErrorToasts, RequestFieldError, requestErrorToast, withResponseDetails, } from "./request-errors.js";
6
+ export { initializeSentry, setSentryOrganization } from "./sentry.js";
7
+ export { AppTelemetryProvider } from "./telemetry.js";
8
+ export { PageTrail } from "./trail.js";
@@ -0,0 +1,8 @@
1
+ /** Opt-in integrations for Ciridae apps. The standalone UI/frame does not load these peers. */
2
+ export { RequireSignIn, SignInCallbackPage } from "./auth.js";
3
+ export { AppErrorBoundary } from "./error-boundary.js";
4
+ export { FeedbackDialog } from "./feedback.js";
5
+ export { errorMessage, errorStatus, errorTraceId, QueryContent, reportedInline, reportedInlineMeta, requestErrorDescription, RequestErrorToasts, RequestFieldError, requestErrorToast, withResponseDetails, } from "./request-errors.js";
6
+ export { initializeSentry, setSentryOrganization } from "./sentry.js";
7
+ export { AppTelemetryProvider } from "./telemetry.js";
8
+ export { PageTrail } from "./trail.js";
@@ -0,0 +1,63 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ToastOptions } from "../components/toast.js";
3
+ /** Normalize an unvalidated generated-client failure at the HTTP boundary. */
4
+ export declare function withResponseDetails(error: unknown, response: Response | undefined): Error;
5
+ /** The failed request's HTTP status, when it got a response. */
6
+ export declare function errorStatus(error: unknown): number | null;
7
+ /** The failed request's trace ID, which support looks the request up by. */
8
+ export declare function errorTraceId(error: unknown): string | null;
9
+ /** Human-readable message from a normalized request failure or another Error. */
10
+ export declare function errorMessage(error: unknown, fallback: string): string;
11
+ /**
12
+ * What a failed request's notice says under its title: the request's message when the title does
13
+ * not already say it, and its error ID to copy for support.
14
+ */
15
+ export declare function requestErrorDescription(error: unknown, title?: string): ReactNode;
16
+ /** An error toast for a failed request, carrying its error ID to copy for support. */
17
+ export declare function requestErrorToast(error: unknown, title?: string): ToastOptions;
18
+ /**
19
+ * The `meta` of a query or mutation whose failure is reported where it is used: a banner or field
20
+ * error on screen names it, so `RequestErrorToasts` does not toast it as well.
21
+ */
22
+ export declare const reportedInlineMeta: {
23
+ reportedInline: boolean;
24
+ };
25
+ /** Built options, such as a generated client's, marked with `reportedInlineMeta`. */
26
+ export declare function reportedInline<Options extends {
27
+ meta?: Record<string, unknown>;
28
+ }>(options: Options): Options;
29
+ /**
30
+ * Toasts a failed query or mutation with the request's error ID, unless something on screen
31
+ * reports it: a mounted query or a mutation whose options are wrapped in `reportedInline`. A page
32
+ * shows its own query failures in place and a form its own save failures, so the toast is only for
33
+ * a failure nothing on screen owns. An app can also exempt expected query errors such as an empty
34
+ * metrics state.
35
+ */
36
+ export declare function RequestErrorToasts({ isExpectedQueryError, }: {
37
+ isExpectedQueryError?: (error: unknown) => boolean;
38
+ }): null;
39
+ /**
40
+ * A query's three states in place: its loading placeholder, an error banner titled
41
+ * `unavailableTitle` with the request's message and error ID, or its loaded data. Build the query
42
+ * with `reportedInline` so the failure is not toasted as well. Data loaded before a failed refetch
43
+ * stays shown under the banner.
44
+ */
45
+ export declare function QueryContent<Data>({ query, loading, unavailableTitle, children, }: {
46
+ query: {
47
+ data: Data | undefined;
48
+ isError: boolean;
49
+ error: unknown;
50
+ };
51
+ /** Drawn at the loaded content's size, such as a table or list with its `loading` prop. */
52
+ loading: ReactNode;
53
+ unavailableTitle: string;
54
+ children: (data: Data) => ReactNode;
55
+ }): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
56
+ /**
57
+ * A failed request's message, or `fallback` when it has none, as a form's field error, with its
58
+ * error ID to copy for support. For a mutation built with `reportedInline`.
59
+ */
60
+ export declare function RequestFieldError({ error, fallback }: {
61
+ error: unknown;
62
+ fallback: string;
63
+ }): import("react").JSX.Element;
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { useEffect, useRef } from "react";
4
+ import { CopyableId } from "../components/copyable-id.js";
5
+ import { FieldError } from "../components/form-layout.js";
6
+ import { Stack } from "../components/layout.js";
7
+ import { StatusBanner } from "../components/status-banner.js";
8
+ import { useToast } from "../components/toast.js";
9
+ class RequestError extends Error {
10
+ constructor(message, status, traceId) {
11
+ super(message);
12
+ this.status = status;
13
+ this.traceId = traceId;
14
+ }
15
+ }
16
+ function messageFromResponse(error) {
17
+ if (error instanceof Error)
18
+ return error.message;
19
+ if (typeof error === "string")
20
+ return error;
21
+ if (!error || typeof error !== "object")
22
+ return "";
23
+ // OAuth endpoints answer with `{error, error_description}` rather than `detail`.
24
+ if ("error_description" in error && typeof error.error_description === "string")
25
+ return error.error_description;
26
+ if (!("detail" in error))
27
+ return "";
28
+ if (typeof error.detail === "string")
29
+ return error.detail;
30
+ // Domain conflicts answer with `detail: {error, message}`.
31
+ if (error.detail &&
32
+ typeof error.detail === "object" &&
33
+ "message" in error.detail &&
34
+ typeof error.detail.message === "string")
35
+ return error.detail.message;
36
+ if (!Array.isArray(error.detail))
37
+ return "";
38
+ return error.detail
39
+ .map((item) => item && typeof item === "object" && "msg" in item && typeof item.msg === "string"
40
+ ? item.msg
41
+ : "")
42
+ .filter(Boolean)
43
+ .join("; ");
44
+ }
45
+ /** Normalize an unvalidated generated-client failure at the HTTP boundary. */
46
+ export function withResponseDetails(error, response) {
47
+ if (!response && error instanceof Error)
48
+ return error;
49
+ return new RequestError(messageFromResponse(error), response?.status ?? null, response?.headers.get("X-Trace-ID") ?? null);
50
+ }
51
+ /** The failed request's HTTP status, when it got a response. */
52
+ export function errorStatus(error) {
53
+ if (error instanceof RequestError)
54
+ return error.status;
55
+ return error instanceof Error && "status" in error && typeof error.status === "number"
56
+ ? error.status
57
+ : null;
58
+ }
59
+ /** The failed request's trace ID, which support looks the request up by. */
60
+ export function errorTraceId(error) {
61
+ if (error instanceof RequestError)
62
+ return error.traceId;
63
+ return error instanceof Error && "errorId" in error && typeof error.errorId === "string"
64
+ ? error.errorId
65
+ : null;
66
+ }
67
+ /** Human-readable message from a normalized request failure or another Error. */
68
+ export function errorMessage(error, fallback) {
69
+ return error instanceof Error && error.message ? error.message : fallback;
70
+ }
71
+ /**
72
+ * What a failed request's notice says under its title: the request's message when the title does
73
+ * not already say it, and its error ID to copy for support.
74
+ */
75
+ export function requestErrorDescription(error, title) {
76
+ const traceId = errorTraceId(error);
77
+ const message = errorMessage(error, "");
78
+ const description = message && message !== title ? message : null;
79
+ return description || traceId ? (_jsxs(_Fragment, { children: [description, description && traceId && _jsx("br", {}), traceId && _jsx(CopyableId, { label: "Error ID", value: traceId })] })) : undefined;
80
+ }
81
+ /** An error toast for a failed request, carrying its error ID to copy for support. */
82
+ export function requestErrorToast(error, title) {
83
+ const shownTitle = title ?? errorMessage(error, "Request failed.");
84
+ return {
85
+ variant: "error",
86
+ title: shownTitle,
87
+ description: requestErrorDescription(error, shownTitle),
88
+ };
89
+ }
90
+ /**
91
+ * The `meta` of a query or mutation whose failure is reported where it is used: a banner or field
92
+ * error on screen names it, so `RequestErrorToasts` does not toast it as well.
93
+ */
94
+ export const reportedInlineMeta = { reportedInline: true };
95
+ /** Built options, such as a generated client's, marked with `reportedInlineMeta`. */
96
+ export function reportedInline(options) {
97
+ return { ...options, meta: { ...options.meta, ...reportedInlineMeta } };
98
+ }
99
+ function isReportedInline(meta) {
100
+ return meta?.reportedInline === true;
101
+ }
102
+ /**
103
+ * Toasts a failed query or mutation with the request's error ID, unless something on screen
104
+ * reports it: a mounted query or a mutation whose options are wrapped in `reportedInline`. A page
105
+ * shows its own query failures in place and a form its own save failures, so the toast is only for
106
+ * a failure nothing on screen owns. An app can also exempt expected query errors such as an empty
107
+ * metrics state.
108
+ */
109
+ export function RequestErrorToasts({ isExpectedQueryError, }) {
110
+ // useToast returns a new object each render; a ref keeps the subscription from restarting.
111
+ const toastApi = useToast();
112
+ const toast = useRef(toastApi);
113
+ useEffect(() => {
114
+ toast.current = toastApi;
115
+ });
116
+ const queryClient = useQueryClient();
117
+ useEffect(() => {
118
+ const showError = (error) => {
119
+ toast.current.add(requestErrorToast(error));
120
+ };
121
+ // A polled query that keeps failing toasts once, when it starts failing, not on every poll.
122
+ const failingQueries = new Set();
123
+ const stopQueries = queryClient.getQueryCache().subscribe((event) => {
124
+ if (event.type !== "updated")
125
+ return;
126
+ if (event.action.type === "error" && !failingQueries.has(event.query.queryHash)) {
127
+ failingQueries.add(event.query.queryHash);
128
+ if (isExpectedQueryError?.(event.action.error))
129
+ return;
130
+ // Any mounted reader that reports the failure in place owns it.
131
+ if (event.query.observers.some((observer) => isReportedInline(observer.options.meta)))
132
+ return;
133
+ showError(event.action.error);
134
+ }
135
+ else if (event.action.type === "success") {
136
+ failingQueries.delete(event.query.queryHash);
137
+ }
138
+ });
139
+ const stopMutations = queryClient.getMutationCache().subscribe((event) => {
140
+ if (event.type !== "updated" || event.action.type !== "error")
141
+ return;
142
+ if (isReportedInline(event.mutation.options.meta))
143
+ return;
144
+ showError(event.action.error);
145
+ });
146
+ return () => {
147
+ stopQueries();
148
+ stopMutations();
149
+ };
150
+ }, [queryClient, isExpectedQueryError]);
151
+ return null;
152
+ }
153
+ /**
154
+ * A query's three states in place: its loading placeholder, an error banner titled
155
+ * `unavailableTitle` with the request's message and error ID, or its loaded data. Build the query
156
+ * with `reportedInline` so the failure is not toasted as well. Data loaded before a failed refetch
157
+ * stays shown under the banner.
158
+ */
159
+ export function QueryContent({ query, loading, unavailableTitle, children, }) {
160
+ const failure = query.isError && (_jsx(StatusBanner, { tone: "danger", title: unavailableTitle, description: requestErrorDescription(query.error, unavailableTitle) }));
161
+ if (query.data === undefined)
162
+ return failure || loading;
163
+ return (_jsxs(_Fragment, { children: [failure, children(query.data)] }));
164
+ }
165
+ /**
166
+ * A failed request's message, or `fallback` when it has none, as a form's field error, with its
167
+ * error ID to copy for support. For a mutation built with `reportedInline`.
168
+ */
169
+ export function RequestFieldError({ error, fallback }) {
170
+ const traceId = errorTraceId(error);
171
+ return (_jsxs(Stack, { gap: "4", children: [_jsx(FieldError, { children: errorMessage(error, fallback) }), traceId && _jsx(CopyableId, { label: "Error ID", value: traceId })] }));
172
+ }
@@ -0,0 +1,18 @@
1
+ import type { TelemetryUser } from "@ciridae/telemetry/client";
2
+ export type SentryConfiguration = {
3
+ /** Reporting is off when the build has no DSN. */
4
+ dsn: string | undefined;
5
+ environment: string | undefined;
6
+ /** The commit the bundle was built from, so an error names the build it came from. */
7
+ release: string | undefined;
8
+ /** The API origin that receives trace headers. */
9
+ backendUrl: string | undefined;
10
+ };
11
+ /** Start Sentry before the first render so a crash during it is still reported. */
12
+ export declare function initializeSentry(configuration: SentryConfiguration): void;
13
+ export declare function setSentryUser(user: TelemetryUser | null): void;
14
+ export declare function setSentryOrganization(organization: {
15
+ id: string;
16
+ displayName: string;
17
+ } | null): void;
18
+ export declare function captureExceptionInSentry(error: unknown, componentStack: string): Promise<string | null>;
@@ -0,0 +1,52 @@
1
+ let sentryModule = null;
2
+ let enabled = false;
3
+ /** Start Sentry before the first render so a crash during it is still reported. */
4
+ export function initializeSentry(configuration) {
5
+ if (!configuration.dsn)
6
+ return;
7
+ enabled = true;
8
+ const { dsn, environment, release, backendUrl } = configuration;
9
+ withSentry((sentry) => {
10
+ const isProduction = environment === "prod";
11
+ sentry.init({
12
+ dsn,
13
+ environment,
14
+ release,
15
+ integrations: [
16
+ sentry.browserTracingIntegration(),
17
+ sentry.replayIntegration(),
18
+ sentry.httpClientIntegration(),
19
+ ],
20
+ replaysOnErrorSampleRate: 1,
21
+ replaysSessionSampleRate: isProduction ? 0 : 1,
22
+ tracesSampleRate: isProduction ? 0.1 : 1,
23
+ tracePropagationTargets: backendUrl
24
+ ? [new URL(backendUrl, window.location.origin).origin]
25
+ : [],
26
+ });
27
+ });
28
+ }
29
+ export function setSentryUser(user) {
30
+ withSentry((sentry) => sentry.setUser(user
31
+ ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined }
32
+ : null));
33
+ }
34
+ export function setSentryOrganization(organization) {
35
+ withSentry((sentry) => {
36
+ sentry.setContext("organization", organization);
37
+ sentry.setTag("organization_id", organization?.id);
38
+ });
39
+ }
40
+ export function captureExceptionInSentry(error, componentStack) {
41
+ return withSentry((sentry) => sentry.captureException(error, { contexts: { react: { componentStack } } }));
42
+ }
43
+ // Sentry loads on first use so it stays out of the first paint.
44
+ function withSentry(use) {
45
+ if (!enabled)
46
+ return Promise.resolve(null);
47
+ sentryModule ?? (sentryModule = import("@sentry/react"));
48
+ return sentryModule.then(use).catch((error) => {
49
+ console.error("Sentry failed to load", error);
50
+ return null;
51
+ });
52
+ }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CiridaeTelemetry } from "@ciridae/telemetry/client";
3
+ /** Identifies the signed-in reader to PostHog and Sentry, and clears them on sign-out. */
4
+ export declare function AppTelemetryProvider({ telemetry, children, }: {
5
+ telemetry: CiridaeTelemetry;
6
+ children: ReactNode;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCiridaeAuth } from "@ciridae/auth/react";
3
+ import { CiridaeTelemetryProvider } from "@ciridae/telemetry/react";
4
+ import { useEffect, useMemo } from "react";
5
+ import { useLocation } from "react-router-dom";
6
+ import { setSentryUser } from "./sentry.js";
7
+ /** Identifies the signed-in reader to PostHog and Sentry, and clears them on sign-out. */
8
+ export function AppTelemetryProvider({ telemetry, children, }) {
9
+ const { session } = useCiridaeAuth();
10
+ const { pathname } = useLocation();
11
+ const user = useMemo(() => {
12
+ if (session.status === "signedIn" || session.status === "signingOut") {
13
+ return {
14
+ id: session.user.uid,
15
+ name: session.user.displayName,
16
+ email: session.user.email,
17
+ emailVerified: session.user.emailVerified,
18
+ };
19
+ }
20
+ // Auth that failed to load leaves no one signed in, so an earlier identity is cleared too.
21
+ return session.status === "signedOut" || session.status === "unavailable" ? null : undefined;
22
+ }, [session]);
23
+ useEffect(() => {
24
+ if (user !== undefined)
25
+ setSentryUser(user);
26
+ }, [user]);
27
+ return (_jsx(CiridaeTelemetryProvider, { pathname: pathname, telemetry: telemetry, user: user, children: children }));
28
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ /** A page's breadcrumbs for `AppFrame`'s title: links to where it sits, then the page itself. */
3
+ export declare function PageTrail({ parents, children, }: {
4
+ parents?: readonly {
5
+ label: string;
6
+ to: string;
7
+ }[];
8
+ children: ReactNode;
9
+ }): import("react").JSX.Element;