@-reddacted-/react-ui 0.2.3 → 0.3.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/dist/index.d.ts CHANGED
@@ -11,10 +11,11 @@ export { defaultPalette } from "./theme/palette.js";
11
11
  export { createLocalThemeStore, createHttpThemeStore, type ThemeStore } from "./theme/store.js";
12
12
  export { serializeColors, parseColors, isHexColor, randomHex } from "./theme/clipboard.js";
13
13
  export type { ThemeColors, ThemeDefinition, ThemeCatalog, ThemeDraft } from "./theme/types.js";
14
- export { PopupProvider } from "./popup/PopupProvider.js";
14
+ export { PopupProvider, type InterceptFetchMode } from "./popup/PopupProvider.js";
15
+ export { usePopup, type PopupApi } from "./popup/usePopup.js";
15
16
  export { PopupHost } from "./popup/PopupHost.js";
16
17
  export { createInterceptedFetch, type InterceptorHandlers } from "./popup/fetchInterceptor.js";
17
- export { createErrorRules, defaultMessages, type ErrorRules, type ErrorRulesOptions, type PopupMessages, } from "./popup/errorRules.js";
18
+ export { createErrorRules, createGraphqlErrorRules, defaultMessages, type ErrorRules, type ErrorRulesOptions, type GraphqlError, type GraphqlErrorRulesOptions, type PopupMessages, type FetchErrorReport, } from "./popup/errorRules.js";
18
19
  export { fetchJson } from "./net/fetchJson.js";
19
20
  export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey } from "@-reddacted-/react-hooks";
20
21
  export type { AsyncStatus, AsyncStatus as LoadStatus, AsyncResult, ElementSize } from "@-reddacted-/react-hooks";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACnE,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAC1F,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE9F,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAC9F,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACpH,YAAY,EAAE,WAAW,EAAE,WAAW,IAAI,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACnE,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAC1F,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE9F,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAC9F,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACpH,YAAY,EAAE,WAAW,EAAE,WAAW,IAAI,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA"}
package/dist/index.js CHANGED
@@ -11,9 +11,10 @@ export { defaultPalette } from "./theme/palette.js";
11
11
  export { createLocalThemeStore, createHttpThemeStore } from "./theme/store.js";
12
12
  export { serializeColors, parseColors, isHexColor, randomHex } from "./theme/clipboard.js";
13
13
  export { PopupProvider } from "./popup/PopupProvider.js";
14
+ export { usePopup } from "./popup/usePopup.js";
14
15
  export { PopupHost } from "./popup/PopupHost.js";
15
16
  export { createInterceptedFetch } from "./popup/fetchInterceptor.js";
16
- export { createErrorRules, defaultMessages, } from "./popup/errorRules.js";
17
+ export { createErrorRules, createGraphqlErrorRules, defaultMessages, } from "./popup/errorRules.js";
17
18
  export { fetchJson } from "./net/fetchJson.js";
18
19
  // Réexportés depuis react-hooks : les appelants n'ont pas à connaître le découpage.
19
20
  export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey } from "@-reddacted-/react-hooks";
@@ -1,9 +1,19 @@
1
1
  import type { ReactNode } from "react";
2
- import type { ErrorRulesOptions } from "./errorRules.js";
3
- type PopupProviderProps = ErrorRulesOptions & {
2
+ import type { FetchErrorReport, GraphqlErrorRulesOptions } from "./errorRules.js";
3
+ /** Which `window.fetch` failures raise a popup on their own; `false` leaves fetch untouched. */
4
+ export type InterceptFetchMode = false | "rest" | "graphql";
5
+ type PopupProviderProps = GraphqlErrorRulesOptions & {
4
6
  children: ReactNode;
5
7
  closeLabel?: string;
8
+ /**
9
+ * Installs a `window.fetch` interceptor that raises popups by itself: `"rest"` reads HTTP
10
+ * statuses, `"graphql"` also reads the `errors` array on `graphqlPaths`. Off by default.
11
+ * Intercepted messages clear as soon as the next request starts; pushed ones stay.
12
+ */
13
+ interceptFetch?: InterceptFetchMode;
14
+ /** Called for every intercepted failure, with the method, URL and status behind it. */
15
+ onReport?: (report: FetchErrorReport) => void;
6
16
  };
7
- export declare function PopupProvider({ children, closeLabel, ...ruleOptions }: PopupProviderProps): import("react").JSX.Element;
17
+ export declare function PopupProvider({ children, closeLabel, interceptFetch, onReport, ...ruleOptions }: PopupProviderProps): import("react").JSX.Element;
8
18
  export {};
9
19
  //# sourceMappingURL=PopupProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PopupProvider.d.ts","sourceRoot":"","sources":["../../src/popup/PopupProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,iBAAiB,CAAA;AAMpE,KAAK,kBAAkB,GAAG,iBAAiB,GAAG;IAC5C,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,EAAE,kBAAkB,+BAkCzF"}
1
+ {"version":3,"file":"PopupProvider.d.ts","sourceRoot":"","sources":["../../src/popup/PopupProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAOjF,gGAAgG;AAChG,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;AAE3D,KAAK,kBAAkB,GAAG,wBAAwB,GAAG;IACnD,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,kBAAkB,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC9C,CAAA;AAKD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAsB,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,EAAE,kBAAkB,+BAyD3H"}
@@ -1,28 +1,47 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
- import { createErrorRules } from "./errorRules.js";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { createErrorRules, createGraphqlErrorRules } from "./errorRules.js";
4
4
  import { createInterceptedFetch } from "./fetchInterceptor.js";
5
5
  import { PopupHost } from "./PopupHost.js";
6
- export function PopupProvider({ children, closeLabel, ...ruleOptions }) {
7
- const [messages, setMessages] = useState([]);
8
- // Les options arrivent en littéraux : les garder dans un ref évite de réinstaller
9
- // l'interception à chaque rendu du parent.
10
- const rulesRef = useRef(null);
11
- if (rulesRef.current === null)
12
- rulesRef.current = createErrorRules(ruleOptions);
6
+ import { PopupContext } from "./usePopup.js";
7
+ const addMessage = (message) => (current) => current.includes(message) ? current : [...current, message];
8
+ export function PopupProvider({ children, closeLabel, interceptFetch = false, onReport, ...ruleOptions }) {
9
+ const [fetchMessages, setFetchMessages] = useState([]);
10
+ const [pushedMessages, setPushedMessages] = useState([]);
11
+ // L'interception ne s'installe qu'une fois : le callback, lui, peut changer d'un rendu à
12
+ // l'autre sans avoir à la réinstaller.
13
+ const reportRef = useRef(onReport);
14
+ reportRef.current = onReport;
15
+ // Options arrive as literals: reading them through a ref keeps the interceptor from being
16
+ // reinstalled on every parent render.
17
+ const ruleOptionsRef = useRef(ruleOptions);
18
+ ruleOptionsRef.current = ruleOptions;
13
19
  useEffect(() => {
20
+ if (!interceptFetch)
21
+ return;
14
22
  const original = window.fetch;
15
23
  if (original.__popupIntercepted)
16
24
  return;
25
+ const rules = interceptFetch === "graphql"
26
+ ? createGraphqlErrorRules(ruleOptionsRef.current)
27
+ : createErrorRules(ruleOptionsRef.current);
17
28
  const intercepted = createInterceptedFetch(original, {
18
- onRequestStart: () => setMessages(current => (current.length === 0 ? current : [])),
19
- onError: message => setMessages(current => (current.includes(message) ? current : [...current, message])),
20
- }, rulesRef.current);
29
+ onRequestStart: () => setFetchMessages(current => (current.length === 0 ? current : [])),
30
+ onError: message => setFetchMessages(addMessage(message)),
31
+ onReport: report => reportRef.current?.(report),
32
+ }, rules);
21
33
  intercepted.__popupIntercepted = true;
22
34
  window.fetch = intercepted;
23
35
  return () => {
24
36
  window.fetch = original;
25
37
  };
38
+ }, [interceptFetch]);
39
+ const pushError = useCallback((message) => setPushedMessages(addMessage(message)), []);
40
+ const dismiss = useCallback(() => {
41
+ setFetchMessages([]);
42
+ setPushedMessages([]);
26
43
  }, []);
27
- return (_jsxs(_Fragment, { children: [children, _jsx(PopupHost, { messages: messages, onDismiss: () => setMessages([]), closeLabel: closeLabel })] }));
44
+ const api = useMemo(() => ({ pushError, dismiss }), [pushError, dismiss]);
45
+ const messages = useMemo(() => [...new Set([...pushedMessages, ...fetchMessages])], [pushedMessages, fetchMessages]);
46
+ return (_jsxs(PopupContext.Provider, { value: api, children: [children, _jsx(PopupHost, { messages: messages, onDismiss: dismiss, closeLabel: closeLabel })] }));
28
47
  }
@@ -6,6 +6,17 @@ export type PopupMessages = {
6
6
  requestFailed: (status: number) => string;
7
7
  };
8
8
  export declare const defaultMessages: PopupMessages;
9
+ /** What a failed request carried, for callers that record failures instead of only showing them. */
10
+ export type FetchErrorReport = {
11
+ /** The message shown to the user, already humanised by the rules. */
12
+ message: string;
13
+ method: string;
14
+ url: string;
15
+ /** Absent when the request never reached a response. */
16
+ status?: number;
17
+ /** The thrown cause, when the failure was a network one. */
18
+ cause?: unknown;
19
+ };
9
20
  export type ErrorRulesOptions = {
10
21
  /** Statuses that never raise a popup. Form input errors by default. */
11
22
  ignoredStatuses?: number[];
@@ -15,9 +26,26 @@ export type ErrorRulesOptions = {
15
26
  };
16
27
  export type ErrorRules = {
17
28
  networkFailureMessage: string;
18
- shouldReportResponse: (res: Response) => boolean;
19
29
  shouldReportFailure: (cause: unknown) => boolean;
20
- describeResponse: (res: Response) => Promise<string>;
30
+ /** The messages a response should raise; empty when it is not worth a popup. */
31
+ inspectResponse: (res: Response) => Promise<string[]>;
21
32
  };
22
33
  export declare function createErrorRules(options?: ErrorRulesOptions): ErrorRules;
34
+ /** One entry of a GraphQL response's `errors` array. */
35
+ export type GraphqlError = {
36
+ message: string;
37
+ path?: ReadonlyArray<string | number>;
38
+ extensions?: Record<string, unknown>;
39
+ };
40
+ export type GraphqlErrorRulesOptions = ErrorRulesOptions & {
41
+ /** Paths whose responses are read for a GraphQL `errors` array. */
42
+ graphqlPaths?: string[];
43
+ /** Turns one GraphQL error into the popup sentence; `null` keeps it silent. Defaults to its message. */
44
+ describeGraphqlError?: (error: GraphqlError) => string | null;
45
+ };
46
+ /**
47
+ * GraphQL reports failures in the body, usually with a 200: on the GraphQL paths the `errors`
48
+ * array wins over the status, and the HTTP rules only take over when the body carries none.
49
+ */
50
+ export declare function createGraphqlErrorRules(options?: GraphqlErrorRulesOptions): ErrorRules;
23
51
  //# sourceMappingURL=errorRules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errorRules.d.ts","sourceRoot":"","sources":["../../src/popup/errorRules.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,aAM7B,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,oBAAoB,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAA;IAChD,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IAChD,gBAAgB,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CACrD,CAAA;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAkC5E"}
1
+ {"version":3,"file":"errorRules.d.ts","sourceRoot":"","sources":["../../src/popup/errorRules.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,aAM7B,CAAA;AAED,oGAAoG;AACpG,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IAChD,gFAAgF;IAChF,eAAe,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACtD,CAAA;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CA0B5E;AAED,wDAAwD;AACxD,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG,iBAAiB,GAAG;IACzD,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,wGAAwG;IACxG,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAA;CAC9D,CAAA;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,wBAA6B,GAAG,UAAU,CAmB1F"}
@@ -9,36 +9,79 @@ export function createErrorRules(options = {}) {
9
9
  const ignoredStatuses = options.ignoredStatuses ?? [400, 422];
10
10
  const authProbePaths = options.authProbePaths ?? ["/auth/me"];
11
11
  const messages = { ...defaultMessages, ...options.messages };
12
- const isAuthProbe = (url) => {
13
- const matches = (path) => {
14
- try {
15
- return new URL(url).pathname.endsWith(path);
16
- }
17
- catch {
18
- return url.includes(path);
19
- }
20
- };
21
- return authProbePaths.some(matches);
12
+ const isAuthProbe = (url) => matchesPath(url, authProbePaths);
13
+ const shouldReportResponse = (res) => {
14
+ if (res.ok)
15
+ return false;
16
+ if (ignoredStatuses.includes(res.status))
17
+ return false;
18
+ if (res.status === 401 && isAuthProbe(res.url))
19
+ return false;
20
+ return true;
22
21
  };
23
22
  return {
24
23
  networkFailureMessage: messages.networkFailure,
25
- shouldReportResponse(res) {
26
- if (res.ok)
27
- return false;
28
- if (ignoredStatuses.includes(res.status))
29
- return false;
30
- if (res.status === 401 && isAuthProbe(res.url))
31
- return false;
32
- return true;
33
- },
34
24
  shouldReportFailure(cause) {
35
25
  return !(cause instanceof Error && cause.name === "AbortError");
36
26
  },
37
- async describeResponse(res) {
38
- return (await readBodyMessage(res)) ?? genericMessage(res.status, messages);
27
+ async inspectResponse(res) {
28
+ if (!shouldReportResponse(res))
29
+ return [];
30
+ return [(await readBodyMessage(res)) ?? genericMessage(res.status, messages)];
39
31
  },
40
32
  };
41
33
  }
34
+ /**
35
+ * GraphQL reports failures in the body, usually with a 200: on the GraphQL paths the `errors`
36
+ * array wins over the status, and the HTTP rules only take over when the body carries none.
37
+ */
38
+ export function createGraphqlErrorRules(options = {}) {
39
+ const httpRules = createErrorRules(options);
40
+ const graphqlPaths = options.graphqlPaths ?? ["/graphql"];
41
+ const describe = options.describeGraphqlError ?? (error => error.message);
42
+ return {
43
+ ...httpRules,
44
+ async inspectResponse(res) {
45
+ if (!matchesPath(res.url, graphqlPaths))
46
+ return httpRules.inspectResponse(res);
47
+ const errors = await readGraphqlErrors(res);
48
+ if (errors === null)
49
+ return httpRules.inspectResponse(res);
50
+ // An error the caller chose to silence must not resurface as a generic message.
51
+ const messages = errors.map(describe).map(message => message?.trim() ?? "").filter(message => message !== "");
52
+ return [...new Set(messages)];
53
+ },
54
+ };
55
+ }
56
+ function matchesPath(url, paths) {
57
+ return paths.some(path => {
58
+ try {
59
+ return new URL(url).pathname.endsWith(path);
60
+ }
61
+ catch {
62
+ return url.includes(path);
63
+ }
64
+ });
65
+ }
66
+ /** The errors a GraphQL body carries, or `null` when it carries none (or is not GraphQL JSON). */
67
+ async function readGraphqlErrors(res) {
68
+ try {
69
+ const body = await res.clone().json();
70
+ // A server that accepts batching answers with an array of results.
71
+ const results = Array.isArray(body) ? body : [body];
72
+ const errors = results.flatMap(result => {
73
+ const list = result?.errors;
74
+ return Array.isArray(list) ? list.filter(isGraphqlError) : [];
75
+ });
76
+ return errors.length > 0 ? errors : null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function isGraphqlError(value) {
83
+ return typeof value === "object" && value !== null && typeof value.message === "string";
84
+ }
42
85
  async function readBodyMessage(res) {
43
86
  try {
44
87
  const body = (await res.clone().json());
@@ -1,7 +1,12 @@
1
- import type { ErrorRules } from "./errorRules.js";
1
+ import type { ErrorRules, FetchErrorReport } from "./errorRules.js";
2
2
  export type InterceptorHandlers = {
3
3
  onRequestStart: () => void;
4
4
  onError: (message: string) => void;
5
+ /**
6
+ * Called for the same failures as `onError`, with what the request itself carried. A popup only
7
+ * needs a sentence; a caller that records failures needs the method, the URL and the status.
8
+ */
9
+ onReport?: (report: FetchErrorReport) => void;
5
10
  };
6
11
  export declare function createInterceptedFetch(original: typeof fetch, handlers: InterceptorHandlers, rules: ErrorRules): typeof fetch;
7
12
  //# sourceMappingURL=fetchInterceptor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetchInterceptor.d.ts","sourceRoot":"","sources":["../../src/popup/fetchInterceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC,CAAA;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,OAAO,KAAK,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,GAChB,OAAO,KAAK,CAYd"}
1
+ {"version":3,"file":"fetchInterceptor.d.ts","sourceRoot":"","sources":["../../src/popup/fetchInterceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEnE,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC9C,CAAA;AAQD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,OAAO,KAAK,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,GAChB,OAAO,KAAK,CAkBd"}
@@ -1,15 +1,26 @@
1
+ function describeRequest(input, init) {
2
+ if (typeof input === "string")
3
+ return { url: input, method: init?.method ?? "GET" };
4
+ if (input instanceof URL)
5
+ return { url: input.href, method: init?.method ?? "GET" };
6
+ return { url: input.url, method: init?.method ?? input.method ?? "GET" };
7
+ }
1
8
  export function createInterceptedFetch(original, handlers, rules) {
2
9
  return async (input, init) => {
3
10
  handlers.onRequestStart();
4
11
  try {
5
12
  const res = await original(input, init);
6
- if (rules.shouldReportResponse(res))
7
- handlers.onError(await rules.describeResponse(res));
13
+ for (const message of await rules.inspectResponse(res)) {
14
+ handlers.onError(message);
15
+ handlers.onReport?.({ ...describeRequest(input, init), message, status: res.status });
16
+ }
8
17
  return res;
9
18
  }
10
19
  catch (cause) {
11
- if (rules.shouldReportFailure(cause))
20
+ if (rules.shouldReportFailure(cause)) {
12
21
  handlers.onError(rules.networkFailureMessage);
22
+ handlers.onReport?.({ ...describeRequest(input, init), message: rules.networkFailureMessage, cause });
23
+ }
13
24
  throw cause;
14
25
  }
15
26
  };
@@ -0,0 +1,9 @@
1
+ export type PopupApi = {
2
+ /** Shows a message until the user closes the popup. Identical messages are shown once. */
3
+ pushError: (message: string) => void;
4
+ /** Closes the popup and forgets every message. */
5
+ dismiss: () => void;
6
+ };
7
+ export declare const PopupContext: import("react").Context<PopupApi | null>;
8
+ export declare function usePopup(): PopupApi;
9
+ //# sourceMappingURL=usePopup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePopup.d.ts","sourceRoot":"","sources":["../../src/popup/usePopup.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IACrB,0FAA0F;IAC1F,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,kDAAkD;IAClD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,YAAY,0CAAuC,CAAA;AAEhE,wBAAgB,QAAQ,IAAI,QAAQ,CAInC"}
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from "react";
2
+ export const PopupContext = createContext(null);
3
+ export function usePopup() {
4
+ const ctx = useContext(PopupContext);
5
+ if (!ctx)
6
+ throw new Error("usePopup must be used within PopupProvider");
7
+ return ctx;
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@-reddacted-/react-ui",
3
- "version": "0.2.3",
4
- "description": "Theming system, Modal, Button, ColorPicker, NavBar and fetch error popups for React",
3
+ "version": "0.3.0",
4
+ "description": "Theming system, Modal, Button, ColorPicker, NavBar and error popups (imperative, REST or GraphQL fetch) for React",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -1,9 +0,0 @@
1
- import "./ColorPicker.css";
2
- interface ColorPickerProps {
3
- value: string;
4
- onChange: (value: string) => void;
5
- className?: string;
6
- }
7
- export default function ColorPicker({ value, onChange, className }: ColorPickerProps): import("react/jsx-runtime").JSX.Element;
8
- export {};
9
- //# sourceMappingURL=ColorPicker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ColorPicker.d.ts","sourceRoot":"","sources":["../src/ColorPicker.tsx"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,CAAA;AAE1B,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CA2FnF"}
@@ -1,62 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState, useEffect, useLayoutEffect } from "react";
3
- import { HexColorPicker } from "react-colorful";
4
- import "./ColorPicker.css";
5
- const HEX_RE = /^#[0-9a-fA-F]{6}$/;
6
- const safe = (v) => (HEX_RE.test(v) ? v : "#000000");
7
- export default function ColorPicker({ value, onChange, className }) {
8
- const [open, setOpen] = useState(false);
9
- const [inputValue, setInputValue] = useState(value);
10
- const [pos, setPos] = useState({ top: 0, left: 0 });
11
- const [positioned, setPositioned] = useState(false);
12
- const swatchRef = useRef(null);
13
- const popoverRef = useRef(null);
14
- useEffect(() => { setInputValue(value); }, [value]);
15
- useLayoutEffect(() => {
16
- if (!open || !popoverRef.current || !swatchRef.current) {
17
- setPositioned(false);
18
- return;
19
- }
20
- const pr = popoverRef.current.getBoundingClientRect();
21
- const sr = swatchRef.current.getBoundingClientRect();
22
- const vw = window.innerWidth;
23
- const vh = window.innerHeight;
24
- const gap = 6;
25
- const top = sr.bottom + gap + pr.height > vh
26
- ? Math.max(4, sr.top - pr.height - gap)
27
- : sr.bottom + gap;
28
- const left = sr.left + pr.width > vw
29
- ? Math.max(4, vw - pr.width - 8)
30
- : sr.left;
31
- setPos({ top, left });
32
- setPositioned(true);
33
- }, [open]);
34
- const toggle = () => {
35
- if (!open)
36
- setInputValue(value);
37
- setOpen(o => !o);
38
- };
39
- useEffect(() => {
40
- if (!open)
41
- return;
42
- const handleMouseDown = (e) => {
43
- if (!popoverRef.current?.contains(e.target) &&
44
- !swatchRef.current?.contains(e.target))
45
- setOpen(false);
46
- };
47
- document.addEventListener("mousedown", handleMouseDown);
48
- return () => document.removeEventListener("mousedown", handleMouseDown);
49
- }, [open]);
50
- const handleHexInput = (e) => {
51
- const v = e.target.value;
52
- setInputValue(v);
53
- if (HEX_RE.test(v))
54
- onChange(v);
55
- };
56
- return (_jsxs("div", { className: `color-picker${className ? ` ${className}` : ""}`, children: [_jsx("button", { ref: swatchRef, type: "button", className: "color-picker__swatch", style: { background: safe(value) }, onClick: toggle, "aria-label": `Color: ${value}` }), open && (_jsxs("div", { ref: popoverRef, className: "color-picker__popover", style: {
57
- position: "fixed",
58
- top: pos.top,
59
- left: pos.left,
60
- visibility: positioned ? "visible" : "hidden",
61
- }, onMouseDown: e => e.stopPropagation(), children: [_jsx(HexColorPicker, { color: safe(value), onChange: onChange }), _jsx("input", { type: "text", className: "color-picker__hex-input", value: inputValue, onChange: handleHexInput, maxLength: 7, spellCheck: false })] }))] }));
62
- }
@@ -1,11 +0,0 @@
1
- type Props = {
2
- onConfirm: () => void;
3
- label?: string;
4
- confirmLabel?: string;
5
- className?: string;
6
- /** Delay in ms before the confirm state auto-resets (default: 10000) */
7
- resetDelay?: number;
8
- };
9
- export default function ConfirmDeleteButton({ onConfirm, label, confirmLabel, className, resetDelay, }: Props): import("react/jsx-runtime").JSX.Element;
10
- export {};
11
- //# sourceMappingURL=ConfirmDeleteButton.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ConfirmDeleteButton.d.ts","sourceRoot":"","sources":["../src/ConfirmDeleteButton.tsx"],"names":[],"mappings":"AAEA,KAAK,KAAK,GAAG;IACX,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC1C,SAAS,EACT,KAAgB,EAChB,YAA8B,EAC9B,SAAS,EACT,UAAmB,GACpB,EAAE,KAAK,2CA4BP"}
@@ -1,23 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect } from "react";
3
- export default function ConfirmDeleteButton({ onConfirm, label = "Delete", confirmLabel = "Are you sure?", className, resetDelay = 10000, }) {
4
- const [confirming, setConfirming] = useState(false);
5
- useEffect(() => {
6
- if (!confirming)
7
- return;
8
- const timer = setTimeout(() => setConfirming(false), resetDelay);
9
- return () => clearTimeout(timer);
10
- }, [confirming, resetDelay]);
11
- const handleClick = () => {
12
- if (!confirming) {
13
- setConfirming(true);
14
- return;
15
- }
16
- onConfirm();
17
- };
18
- return (_jsx("button", { type: "button", onClick: handleClick, className: className, style: {
19
- color: "var(--confirm-delete-danger-color, #e55)",
20
- borderColor: "var(--confirm-delete-danger-color, #e55)",
21
- background: confirming ? "var(--confirm-delete-danger-bg, rgba(220,50,50,0.15))" : undefined,
22
- }, children: confirming ? confirmLabel : label }));
23
- }
package/dist/Modal.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import React from "react";
2
- import type { ReactNode } from "react";
3
- export declare function openModal(id: string): void;
4
- export declare function closeModal(id: string): void;
5
- type ModalProps = {
6
- id: string;
7
- width?: string | number;
8
- height?: string | number;
9
- children: ReactNode;
10
- className?: string;
11
- style?: React.CSSProperties;
12
- preventClose?: boolean;
13
- ariaLabelledBy?: string;
14
- ariaDescribedBy?: string;
15
- };
16
- export default function Modal({ id, width, height, children, className, style, preventClose, ariaLabelledBy, ariaDescribedBy, }: ModalProps): React.ReactPortal | null;
17
- export {};
18
- //# sourceMappingURL=Modal.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../src/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAA;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAgBtC,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,QAGnC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,QAEpC;AAED,KAAK,UAAU,GAAG;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,EAC5B,EAAE,EACF,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,KAAK,EACL,YAAoB,EACpB,cAAc,EACd,eAAe,GAChB,EAAE,UAAU,4BA+KZ"}
package/dist/Modal.js DELETED
@@ -1,162 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
- import { createPortal } from "react-dom";
4
- const listeners = new Map();
5
- function on(event, cb) {
6
- if (!listeners.has(event))
7
- listeners.set(event, new Set());
8
- listeners.get(event).add(cb);
9
- return () => listeners.get(event).delete(cb);
10
- }
11
- function emit(event, payload) {
12
- listeners.get(event)?.forEach(cb => cb(payload));
13
- }
14
- export function openModal(id) {
15
- emit("modal:closeAll");
16
- emit("modal:open", id);
17
- }
18
- export function closeModal(id) {
19
- emit("modal:close", id);
20
- }
21
- export default function Modal({ id, width, height, children, className, style, preventClose = false, ariaLabelledBy, ariaDescribedBy, }) {
22
- const [isOpen, setIsOpen] = useState(false);
23
- const contentRef = useRef(null);
24
- const lastFocusedEl = useRef(null);
25
- const portalTarget = useRef(null);
26
- useEffect(() => {
27
- const offOpen = on("modal:open", (targetId) => {
28
- if (targetId === id) {
29
- lastFocusedEl.current = document.activeElement ?? null;
30
- setIsOpen(true);
31
- }
32
- });
33
- const offClose = on("modal:close", (targetId) => {
34
- if (targetId === id)
35
- setIsOpen(false);
36
- });
37
- const offCloseAll = on("modal:closeAll", () => setIsOpen(false));
38
- return () => { offOpen(); offClose(); offCloseAll(); };
39
- }, [id]);
40
- useEffect(() => {
41
- let el = document.getElementById("__modal-root__");
42
- if (!el) {
43
- el = document.createElement("div");
44
- el.id = "__modal-root__";
45
- document.body.appendChild(el);
46
- }
47
- portalTarget.current = el;
48
- }, []);
49
- useEffect(() => {
50
- if (!isOpen)
51
- return;
52
- const prevOverflow = document.body.style.overflow;
53
- const prevPadRight = document.body.style.paddingRight;
54
- const scrollbarW = window.innerWidth - document.documentElement.clientWidth;
55
- if (scrollbarW > 0)
56
- document.body.style.paddingRight = `${scrollbarW}px`;
57
- document.body.style.overflow = "hidden";
58
- const to = setTimeout(() => {
59
- const container = contentRef.current;
60
- if (!container)
61
- return;
62
- const focusables = container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
63
- (focusables[0] ?? container).focus();
64
- }, 0);
65
- const onKeyDown = (e) => {
66
- if (e.key === "Escape" && !preventClose) {
67
- closeModal(id);
68
- return;
69
- }
70
- if (e.key !== "Tab")
71
- return;
72
- const container = contentRef.current;
73
- if (!container)
74
- return;
75
- const focusables = Array.from(container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')).filter(el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"));
76
- if (focusables.length === 0)
77
- return;
78
- const first = focusables[0];
79
- const last = focusables[focusables.length - 1];
80
- const active = document.activeElement;
81
- if (e.shiftKey) {
82
- if (active === first || !container.contains(active)) {
83
- last.focus();
84
- e.preventDefault();
85
- }
86
- }
87
- else {
88
- if (active === last || !container.contains(active)) {
89
- first.focus();
90
- e.preventDefault();
91
- }
92
- }
93
- };
94
- const onFocusIn = (e) => {
95
- const container = contentRef.current;
96
- if (!container)
97
- return;
98
- if (!container.contains(e.target))
99
- container.focus();
100
- };
101
- document.addEventListener("keydown", onKeyDown);
102
- document.addEventListener("focusin", onFocusIn);
103
- return () => {
104
- document.body.style.overflow = prevOverflow;
105
- document.body.style.paddingRight = prevPadRight;
106
- clearTimeout(to);
107
- document.removeEventListener("keydown", onKeyDown);
108
- document.removeEventListener("focusin", onFocusIn);
109
- lastFocusedEl.current?.focus?.();
110
- lastFocusedEl.current = null;
111
- };
112
- }, [isOpen, id, preventClose]);
113
- if (!isOpen || !portalTarget.current)
114
- return null;
115
- const w = typeof width === "number" ? `${width}px` : width ?? undefined;
116
- const h = typeof height === "number" ? `${height}px` : height ?? undefined;
117
- const prefersReducedMotion = typeof window !== "undefined" &&
118
- window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
119
- const contentStyles = {
120
- position: "fixed",
121
- inset: 0,
122
- display: "grid",
123
- placeItems: "center",
124
- zIndex: 9999,
125
- backdropFilter: "blur(2px)",
126
- background: "rgba(0,0,0,0.35)",
127
- animation: prefersReducedMotion ? "none" : "modalFade 150ms ease-out",
128
- };
129
- const cardStyles = {
130
- maxWidth: "95vw",
131
- maxHeight: "95vh",
132
- width: w,
133
- height: h,
134
- overflow: "auto",
135
- outline: "none",
136
- background: "var(--modal-background, #1a1a1a)",
137
- borderRadius: "10px",
138
- };
139
- const overlayStyles = {
140
- position: "fixed",
141
- inset: 0,
142
- backdropFilter: "blur(2px)",
143
- background: "rgba(0,0,0,0.35)",
144
- zIndex: 9998,
145
- animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
146
- };
147
- const innerWrapper = {
148
- animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
149
- };
150
- const keyframes = (_jsx("style", { children: `
151
- @keyframes modalFade {
152
- from { opacity: 0; } to { opacity: 1; }
153
- }
154
- @keyframes modalPop {
155
- from { opacity: 0; transform: translateY(4px) scale(0.98); }
156
- to { opacity: 1; transform: translateY(0) scale(1); }
157
- }
158
- ` }));
159
- return createPortal(_jsxs(_Fragment, { children: [keyframes, _jsx("div", { "aria-hidden": "true", style: overlayStyles, onClick: () => { if (!preventClose)
160
- closeModal(id); } }), _jsx("div", { style: contentStyles, "aria-hidden": !isOpen, onMouseDown: e => { if (!preventClose && e.target === e.currentTarget)
161
- closeModal(id); }, children: _jsx("div", { style: innerWrapper, children: _jsx("div", { ref: contentRef, role: "dialog", "aria-modal": "true", "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, tabIndex: -1, className: className ?? "", style: { ...cardStyles, ...style }, onMouseDown: e => e.stopPropagation(), children: children }) }) })] }), portalTarget.current);
162
- }
@@ -1,2 +0,0 @@
1
- export declare function useDocumentTitle(title: string | undefined, siteName: string, fallback?: string): void;
2
- //# sourceMappingURL=useDocumentTitle.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useDocumentTitle.d.ts","sourceRoot":"","sources":["../../src/net/useDocumentTitle.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,QAI9F"}
@@ -1,8 +0,0 @@
1
- import { useEffect } from "react";
2
- // Without this every page shares the title of index.html in the tab, the history
3
- // and the search results.
4
- export function useDocumentTitle(title, siteName, fallback) {
5
- useEffect(() => {
6
- document.title = title ? `${title} — ${siteName}` : (fallback ?? siteName);
7
- }, [title, siteName, fallback]);
8
- }