@gusnips/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/dist/api-client.d.ts +139 -0
  4. package/dist/api-client.d.ts.map +1 -0
  5. package/dist/api-client.js +133 -0
  6. package/dist/api-client.js.map +1 -0
  7. package/dist/api-error.d.ts +45 -0
  8. package/dist/api-error.d.ts.map +1 -0
  9. package/dist/api-error.js +52 -0
  10. package/dist/api-error.js.map +1 -0
  11. package/dist/auth-store.d.ts +49 -0
  12. package/dist/auth-store.d.ts.map +1 -0
  13. package/dist/auth-store.js +27 -0
  14. package/dist/auth-store.js.map +1 -0
  15. package/dist/chunk-reload.d.ts +61 -0
  16. package/dist/chunk-reload.d.ts.map +1 -0
  17. package/dist/chunk-reload.js +92 -0
  18. package/dist/chunk-reload.js.map +1 -0
  19. package/dist/cn.d.ts +13 -0
  20. package/dist/cn.d.ts.map +1 -0
  21. package/dist/cn.js +16 -0
  22. package/dist/cn.js.map +1 -0
  23. package/dist/describe-error.d.ts +74 -0
  24. package/dist/describe-error.d.ts.map +1 -0
  25. package/dist/describe-error.js +70 -0
  26. package/dist/describe-error.js.map +1 -0
  27. package/dist/error-boundary.d.ts +61 -0
  28. package/dist/error-boundary.d.ts.map +1 -0
  29. package/dist/error-boundary.js +32 -0
  30. package/dist/error-boundary.js.map +1 -0
  31. package/dist/guards.d.ts +65 -0
  32. package/dist/guards.d.ts.map +1 -0
  33. package/dist/guards.js +52 -0
  34. package/dist/guards.js.map +1 -0
  35. package/dist/hydrate.d.ts +30 -0
  36. package/dist/hydrate.d.ts.map +1 -0
  37. package/dist/hydrate.js +25 -0
  38. package/dist/hydrate.js.map +1 -0
  39. package/dist/i18n.d.ts +70 -0
  40. package/dist/i18n.d.ts.map +1 -0
  41. package/dist/i18n.js +66 -0
  42. package/dist/i18n.js.map +1 -0
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +31 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/prerender-contract.d.ts +30 -0
  48. package/dist/prerender-contract.d.ts.map +1 -0
  49. package/dist/prerender-contract.js +30 -0
  50. package/dist/prerender-contract.js.map +1 -0
  51. package/dist/query.d.ts +28 -0
  52. package/dist/query.d.ts.map +1 -0
  53. package/dist/query.js +51 -0
  54. package/dist/query.js.map +1 -0
  55. package/dist/sse.d.ts +64 -0
  56. package/dist/sse.d.ts.map +1 -0
  57. package/dist/sse.js +105 -0
  58. package/dist/sse.js.map +1 -0
  59. package/dist/states.d.ts +69 -0
  60. package/dist/states.d.ts.map +1 -0
  61. package/dist/states.js +2 -0
  62. package/dist/states.js.map +1 -0
  63. package/dist/ui/class-name.d.ts +16 -0
  64. package/dist/ui/class-name.d.ts.map +1 -0
  65. package/dist/ui/class-name.js +13 -0
  66. package/dist/ui/class-name.js.map +1 -0
  67. package/dist/ui/combobox.d.ts +81 -0
  68. package/dist/ui/combobox.d.ts.map +1 -0
  69. package/dist/ui/combobox.js +70 -0
  70. package/dist/ui/combobox.js.map +1 -0
  71. package/dist/ui/dialog.d.ts +39 -0
  72. package/dist/ui/dialog.d.ts.map +1 -0
  73. package/dist/ui/dialog.js +32 -0
  74. package/dist/ui/dialog.js.map +1 -0
  75. package/dist/ui/drawer.d.ts +63 -0
  76. package/dist/ui/drawer.d.ts.map +1 -0
  77. package/dist/ui/drawer.js +55 -0
  78. package/dist/ui/drawer.js.map +1 -0
  79. package/dist/ui/glyphs.d.ts +4 -0
  80. package/dist/ui/glyphs.d.ts.map +1 -0
  81. package/dist/ui/glyphs.js +20 -0
  82. package/dist/ui/glyphs.js.map +1 -0
  83. package/dist/ui/index.d.ts +9 -0
  84. package/dist/ui/index.d.ts.map +1 -0
  85. package/dist/ui/index.js +48 -0
  86. package/dist/ui/index.js.map +1 -0
  87. package/dist/ui/input.d.ts +45 -0
  88. package/dist/ui/input.d.ts.map +1 -0
  89. package/dist/ui/input.js +108 -0
  90. package/dist/ui/input.js.map +1 -0
  91. package/dist/ui/menu.d.ts +70 -0
  92. package/dist/ui/menu.d.ts.map +1 -0
  93. package/dist/ui/menu.js +85 -0
  94. package/dist/ui/menu.js.map +1 -0
  95. package/dist/ui/select.d.ts +69 -0
  96. package/dist/ui/select.d.ts.map +1 -0
  97. package/dist/ui/select.js +48 -0
  98. package/dist/ui/select.js.map +1 -0
  99. package/dist/ui/tabs.d.ts +52 -0
  100. package/dist/ui/tabs.d.ts.map +1 -0
  101. package/dist/ui/tabs.js +62 -0
  102. package/dist/ui/tabs.js.map +1 -0
  103. package/package.json +124 -0
  104. package/src/api-client.test.ts +256 -0
  105. package/src/api-client.ts +305 -0
  106. package/src/api-error.ts +60 -0
  107. package/src/auth-store.ts +59 -0
  108. package/src/chunk-reload.test.ts +46 -0
  109. package/src/chunk-reload.ts +94 -0
  110. package/src/cn.ts +16 -0
  111. package/src/describe-error.test.ts +102 -0
  112. package/src/describe-error.ts +140 -0
  113. package/src/error-boundary.tsx +88 -0
  114. package/src/guards.tsx +99 -0
  115. package/src/hydrate.ts +36 -0
  116. package/src/i18n.test.ts +68 -0
  117. package/src/i18n.ts +101 -0
  118. package/src/index.ts +63 -0
  119. package/src/prerender-contract.ts +31 -0
  120. package/src/query.test.ts +54 -0
  121. package/src/query.ts +79 -0
  122. package/src/sse.test.ts +50 -0
  123. package/src/sse.ts +117 -0
  124. package/src/states.ts +71 -0
  125. package/src/ui/class-name.ts +23 -0
  126. package/src/ui/combobox.test.ts +32 -0
  127. package/src/ui/combobox.tsx +242 -0
  128. package/src/ui/dialog.tsx +87 -0
  129. package/src/ui/drawer.tsx +161 -0
  130. package/src/ui/focus.test.ts +82 -0
  131. package/src/ui/glyphs.tsx +35 -0
  132. package/src/ui/index.ts +145 -0
  133. package/src/ui/input.test.ts +22 -0
  134. package/src/ui/input.tsx +181 -0
  135. package/src/ui/menu.test.ts +73 -0
  136. package/src/ui/menu.tsx +186 -0
  137. package/src/ui/select.tsx +191 -0
  138. package/src/ui/tabs.tsx +95 -0
@@ -0,0 +1,27 @@
1
+ import { create } from "zustand";
2
+ /**
3
+ * Create the store.
4
+ *
5
+ * The whole reason this is a factory and not a store: `isLoading` must start FALSE where there
6
+ * is no window, and only a factory can decide that at the call site rather than at import.
7
+ *
8
+ * A session bootstrap can only be in flight in a browser. The BUILD renders this app to files
9
+ * with no window at all, so `true` there is a wait that never ends — one donor shipped a route
10
+ * guard holding its loading screen forever, and prerendered a public page as 1,174 bytes of
11
+ * `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be
12
+ * found. With no browser there is no session and never will be, which is exactly the state a
13
+ * first-time visitor arrives in, so that is what the page should render.
14
+ *
15
+ * Only one of the three donors knew this. It is invariant 7.
16
+ */
17
+ export function createAuthStore() {
18
+ return create((set) => ({
19
+ user: null,
20
+ isAuthenticated: false,
21
+ isLoading: typeof window !== "undefined",
22
+ setUser: (user) => set({ user, isAuthenticated: Boolean(user), isLoading: false }),
23
+ setLoading: (isLoading) => set({ isLoading }),
24
+ clear: () => set({ user: null, isAuthenticated: false, isLoading: false }),
25
+ }));
26
+ }
27
+ //# sourceMappingURL=auth-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqC,MAAM,SAAS,CAAC;AAkCpE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,MAAM,CAAmB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACxC,IAAI,EAAE,IAAI;QACV,eAAe,EAAE,KAAK;QACtB,SAAS,EAAE,OAAO,MAAM,KAAK,WAAW;QACxC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAClF,UAAU,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,CAAC;QAC7C,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;KAC3E,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Surviving a deploy that lands while a tab is open.
3
+ *
4
+ * Every app on this stack is a Vite SPA with `lazy()` routes, deployed on push. When a build
5
+ * replaces the one a tab is running, the next navigation asks for a chunk whose hashed name no
6
+ * longer exists — and a static host answers a missing asset with the SPA fallback, so the
7
+ * import gets HTML where it expected a module. React's lazy boundary throws, and the person
8
+ * gets a white screen on a click that worked a minute ago.
9
+ *
10
+ * One repo in the fleet had all of this. The others had none of it, and none of them knew.
11
+ */
12
+ /**
13
+ * A lazy-chunk fetch failure — recoverable by reloading.
14
+ *
15
+ * Matched by message because no browser gives it a shared type. Every phrasing in the wild is
16
+ * here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
17
+ * dynamically imported module", Safari "Importing a module script failed", webpack's
18
+ * ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
19
+ * which is the one that actually fires on a static host, because the missing `.js` is answered
20
+ * with `index.html`.
21
+ *
22
+ * A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
23
+ * handler can classify the same failure without importing a component.
24
+ */
25
+ export declare function isChunkLoadError(error: unknown): boolean;
26
+ /**
27
+ * Hard-reload once to pick up the fresh deploy. Returns false when the guard suppressed it.
28
+ *
29
+ * The guard is the part that matters. Reloading on a chunk error is obvious; reloading on a
30
+ * chunk error that the reload does not fix is an infinite loop with the user inside it. At most
31
+ * one reload per minute, so a genuinely missing chunk degrades to the error UI — which can at
32
+ * least say something — instead of flickering forever.
33
+ *
34
+ * `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament,
35
+ * and a stale flag in another tab must not suppress a reload this one needs. Wrapped because
36
+ * storage throws outright when site data is blocked, and a privacy setting must not be the
37
+ * reason somebody cannot recover.
38
+ */
39
+ export declare function reloadOnceForChunkError(): boolean;
40
+ /**
41
+ * Vite's *hint* failure, which is not the same thing and must not be treated as one.
42
+ *
43
+ * Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
44
+ * When that link fails, the import itself still runs right after and usually succeeds — so this
45
+ * one is worth swallowing, where a failure of the import is the real thing
46
+ * {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
47
+ * for a stylesheet link; script hints never reject.
48
+ */
49
+ export declare function isPreloadHintFailure(error: unknown): boolean;
50
+ /**
51
+ * Install the `vite:preloadError` listener. Call once, from the browser entry.
52
+ *
53
+ * Vite offers `preventDefault()` here so an app can own a failed asset itself — but taking that
54
+ * deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
55
+ * `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
56
+ * The user gets the crash screen and the developer gets a TypeError with only React frames in
57
+ * it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
58
+ * runs. A real module failure is left to throw, where the error boundary can recognise it.
59
+ */
60
+ export declare function installPreloadErrorHandler(): void;
61
+ //# sourceMappingURL=chunk-reload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-reload.d.ts","sourceRoot":"","sources":["../src/chunk-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAWjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Surviving a deploy that lands while a tab is open.
3
+ *
4
+ * Every app on this stack is a Vite SPA with `lazy()` routes, deployed on push. When a build
5
+ * replaces the one a tab is running, the next navigation asks for a chunk whose hashed name no
6
+ * longer exists — and a static host answers a missing asset with the SPA fallback, so the
7
+ * import gets HTML where it expected a module. React's lazy boundary throws, and the person
8
+ * gets a white screen on a click that worked a minute ago.
9
+ *
10
+ * One repo in the fleet had all of this. The others had none of it, and none of them knew.
11
+ */
12
+ const RELOAD_FLAG_KEY = "frontkit:chunk-reload-at";
13
+ const RELOAD_WINDOW_MS = 60_000;
14
+ /**
15
+ * A lazy-chunk fetch failure — recoverable by reloading.
16
+ *
17
+ * Matched by message because no browser gives it a shared type. Every phrasing in the wild is
18
+ * here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
19
+ * dynamically imported module", Safari "Importing a module script failed", webpack's
20
+ * ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
21
+ * which is the one that actually fires on a static host, because the missing `.js` is answered
22
+ * with `index.html`.
23
+ *
24
+ * A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
25
+ * handler can classify the same failure without importing a component.
26
+ */
27
+ export function isChunkLoadError(error) {
28
+ if (error instanceof Error && error.name === "ChunkLoadError")
29
+ return true;
30
+ const message = (error instanceof Error ? error.message : String(error) || "").toLowerCase();
31
+ return (message.includes("dynamically imported module") ||
32
+ message.includes("importing a module script failed") ||
33
+ message.includes("mime type") ||
34
+ message.includes("loading chunk"));
35
+ }
36
+ /**
37
+ * Hard-reload once to pick up the fresh deploy. Returns false when the guard suppressed it.
38
+ *
39
+ * The guard is the part that matters. Reloading on a chunk error is obvious; reloading on a
40
+ * chunk error that the reload does not fix is an infinite loop with the user inside it. At most
41
+ * one reload per minute, so a genuinely missing chunk degrades to the error UI — which can at
42
+ * least say something — instead of flickering forever.
43
+ *
44
+ * `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament,
45
+ * and a stale flag in another tab must not suppress a reload this one needs. Wrapped because
46
+ * storage throws outright when site data is blocked, and a privacy setting must not be the
47
+ * reason somebody cannot recover.
48
+ */
49
+ export function reloadOnceForChunkError() {
50
+ const now = Date.now();
51
+ try {
52
+ const last = Number(sessionStorage.getItem(RELOAD_FLAG_KEY) ?? 0);
53
+ if (now - last < RELOAD_WINDOW_MS)
54
+ return false;
55
+ sessionStorage.setItem(RELOAD_FLAG_KEY, String(now));
56
+ }
57
+ catch {
58
+ // Storage blocked — proceed with the reload rather than refusing to recover.
59
+ }
60
+ window.location.reload();
61
+ return true;
62
+ }
63
+ /**
64
+ * Vite's *hint* failure, which is not the same thing and must not be treated as one.
65
+ *
66
+ * Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
67
+ * When that link fails, the import itself still runs right after and usually succeeds — so this
68
+ * one is worth swallowing, where a failure of the import is the real thing
69
+ * {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
70
+ * for a stylesheet link; script hints never reject.
71
+ */
72
+ export function isPreloadHintFailure(error) {
73
+ return error instanceof Error && error.message.startsWith("Unable to preload CSS for");
74
+ }
75
+ /**
76
+ * Install the `vite:preloadError` listener. Call once, from the browser entry.
77
+ *
78
+ * Vite offers `preventDefault()` here so an app can own a failed asset itself — but taking that
79
+ * deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
80
+ * `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
81
+ * The user gets the crash screen and the developer gets a TypeError with only React frames in
82
+ * it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
83
+ * runs. A real module failure is left to throw, where the error boundary can recognise it.
84
+ */
85
+ export function installPreloadErrorHandler() {
86
+ window.addEventListener("vite:preloadError", (event) => {
87
+ const payload = event.payload;
88
+ if (isPreloadHintFailure(payload))
89
+ event.preventDefault();
90
+ });
91
+ }
92
+ //# sourceMappingURL=chunk-reload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-reload.js","sourceRoot":"","sources":["../src/chunk-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,eAAe,GAAG,0BAA0B,CAAC;AACnD,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,IAAI,CAAC;IAC3E,MAAM,OAAO,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7F,OAAO,CACL,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAC;QAC/C,OAAO,CAAC,QAAQ,CAAC,kCAAkC,CAAC;QACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,IAAI,GAAG,GAAG,IAAI,GAAG,gBAAgB;YAAE,OAAO,KAAK,CAAC;QAChD,cAAc,CAAC,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;IACD,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACzB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,2BAA2B,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B;IACxC,MAAM,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;QACrD,MAAM,OAAO,GAAI,KAAuC,CAAC,OAAO,CAAC;QACjE,IAAI,oBAAoB,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,cAAc,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC"}
package/dist/cn.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type ClassValue } from "clsx";
2
+ /**
3
+ * Compose class names, letting later Tailwind utilities win over earlier ones
4
+ * (`p-2 p-4` → `p-4`).
5
+ *
6
+ * That precedence is the whole point, and it is why this is not `clsx` alone: a caller's
7
+ * `className` has to be able to OVERRIDE a component's defaults rather than fight them at
8
+ * equal specificity, where the winner would be whichever rule Tailwind happened to emit last.
9
+ *
10
+ * Ten repos wrote this function. All ten wrote it identically.
11
+ */
12
+ export declare function cn(...inputs: ClassValue[]): string;
13
+ //# sourceMappingURL=cn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.d.ts","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C;;;;;;;;;GASG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAElD"}
package/dist/cn.js ADDED
@@ -0,0 +1,16 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ /**
4
+ * Compose class names, letting later Tailwind utilities win over earlier ones
5
+ * (`p-2 p-4` → `p-4`).
6
+ *
7
+ * That precedence is the whole point, and it is why this is not `clsx` alone: a caller's
8
+ * `className` has to be able to OVERRIDE a component's defaults rather than fight them at
9
+ * equal specificity, where the winner would be whichever rule Tailwind happened to emit last.
10
+ *
11
+ * Ten repos wrote this function. All ten wrote it identically.
12
+ */
13
+ export function cn(...inputs) {
14
+ return twMerge(clsx(inputs));
15
+ }
16
+ //# sourceMappingURL=cn.js.map
package/dist/cn.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.js","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { ApiError } from "./api-error.ts";
2
+ /**
3
+ * Turning anything thrown into copy a person can act on.
4
+ *
5
+ * The API already answers with a message written for a developer, a stable `messageKey` for the
6
+ * domain refusals, and `details` carrying the parts that make a refusal actionable. This is the
7
+ * one place that decides what the SCREEN says — which is not always the same sentence.
8
+ * "Monthly quota spent, upgrade or buy a pack" is right in a JSON body and wrong under a button
9
+ * that could just say what to do next.
10
+ *
11
+ * `cause` is what happened. `hint` is what to do about it. **A surface that shows only the first
12
+ * half is the dead end this exists to prevent** — which is why the type has two fields and not
13
+ * one, and why `hint` being absent should be a deliberate choice at each call site rather than
14
+ * the default nobody noticed.
15
+ *
16
+ * The per-code switch stays in the product: two donors' switches shared their SHAPE and almost
17
+ * no arms, because the arms are that API's vocabulary. What ships is the scaffolding around it,
18
+ * which is the part that was written twice and got subtly different both times.
19
+ */
20
+ export interface DescribedError {
21
+ /** What happened, in the reader's words. */
22
+ cause: string;
23
+ /** What to do about it. Its absence should be a decision, not an oversight. */
24
+ hint?: string;
25
+ }
26
+ /** The subset of i18next's `t` this module needs — typed here so `i18next` stays optional. */
27
+ export type Translate = (key: string, params?: Record<string, unknown>) => string;
28
+ /** Seconds until a refusal clears, when the envelope carries them. */
29
+ export declare function retryAfterSecs(error: ApiError): number | null;
30
+ /**
31
+ * "in 4 minutes" / "in 2 hours" — a wait nobody has to convert from seconds.
32
+ *
33
+ * The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
34
+ * not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
35
+ * one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` in the catalog, each with a
36
+ * `count` plural.
37
+ */
38
+ export declare function humanizeWait(t: Translate, secs: number, prefix?: string): string;
39
+ export interface ErrorDescriberOptions<Code extends string> {
40
+ t: Translate;
41
+ /**
42
+ * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
43
+ * ignored — the server names a sentence, it does not get to name any key in the app.
44
+ */
45
+ messageKeyPrefix?: string;
46
+ /**
47
+ * The keys this build actually carries, usually `Object.keys(en.serverErrors)`.
48
+ *
49
+ * Membership is checked with `hasOwnProperty` and not `in`, which would accept `"toString"`.
50
+ * A key that is absent falls back to the server's English `message`, because a deploy can land
51
+ * ahead of the bundle a tab is still running and a missing translation must degrade to
52
+ * readable rather than render the key itself at somebody.
53
+ */
54
+ knownMessageKeys?: readonly string[] | Record<string, unknown>;
55
+ /** Catalog namespace for this module's own copy. Defaults to `"errors."`. */
56
+ copyPrefix?: string;
57
+ /** Per-code copy. Everything not listed falls through to the default arm below. */
58
+ codes?: Partial<Record<Code, (ctx: ErrorContext) => DescribedError>>;
59
+ }
60
+ export interface ErrorContext {
61
+ error: ApiError;
62
+ /** The server's own localized sentence, when `messageKey` named one this build carries. */
63
+ says: string | null;
64
+ /** Seconds until it clears, already humanized. Null when the envelope did not say. */
65
+ wait: string | null;
66
+ waitSecs: number | null;
67
+ t: Translate;
68
+ }
69
+ /**
70
+ * Build the describer. Needs `network`, `networkHint`, `unexpected`, `retrySoon`,
71
+ * `waitSeconds`, `waitMinutes` and `waitHours` under `copyPrefix` in every locale.
72
+ */
73
+ export declare function createErrorDescriber<Code extends string>({ t, messageKeyPrefix, knownMessageKeys, copyPrefix, codes, }: ErrorDescriberOptions<Code>): (error: unknown) => DescribedError;
74
+ //# sourceMappingURL=describe-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;AAElF,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,CAK5E;AAED,MAAM,WAAW,qBAAqB,CAAC,IAAI,SAAS,MAAM;IACxD,CAAC,EAAE,SAAS,CAAC;IACb;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/D,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,cAAc,CAAC,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,CAAC,EAAE,SAAS,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,SAAS,MAAM,EAAE,EACxD,CAAC,EACD,gBAAkC,EAClC,gBAAgB,EAChB,UAAsB,EACtB,KAAU,GACX,EAAE,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CA2ClE"}
@@ -0,0 +1,70 @@
1
+ import { ApiError } from "./api-error.js";
2
+ /** Seconds until a refusal clears, when the envelope carries them. */
3
+ export function retryAfterSecs(error) {
4
+ const details = error.details;
5
+ if (typeof details !== "object" || details === null)
6
+ return null;
7
+ const value = details.retryAfterSecs;
8
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
9
+ }
10
+ /**
11
+ * "in 4 minutes" / "in 2 hours" — a wait nobody has to convert from seconds.
12
+ *
13
+ * The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
14
+ * not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
15
+ * one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` in the catalog, each with a
16
+ * `count` plural.
17
+ */
18
+ export function humanizeWait(t, secs, prefix = "") {
19
+ const key = (name) => `${prefix}${name}`;
20
+ if (secs < 90)
21
+ return t(key("waitSeconds"), { count: Math.max(1, Math.round(secs)) });
22
+ if (secs < 90 * 60)
23
+ return t(key("waitMinutes"), { count: Math.round(secs / 60) });
24
+ return t(key("waitHours"), { count: Math.round(secs / 3600) });
25
+ }
26
+ /**
27
+ * Build the describer. Needs `network`, `networkHint`, `unexpected`, `retrySoon`,
28
+ * `waitSeconds`, `waitMinutes` and `waitHours` under `copyPrefix` in every locale.
29
+ */
30
+ export function createErrorDescriber({ t, messageKeyPrefix = "serverErrors.", knownMessageKeys, copyPrefix = "errors.", codes = {}, }) {
31
+ const known = Array.isArray(knownMessageKeys)
32
+ ? new Set(knownMessageKeys)
33
+ : new Set(Object.keys(knownMessageKeys ?? {}));
34
+ const has = (name) => known.size === 0 || known.has(name);
35
+ const key = (name) => `${copyPrefix}${name}`;
36
+ function serverSentence(error) {
37
+ const messageKey = error.messageKey;
38
+ if (messageKey === undefined || !messageKey.startsWith(messageKeyPrefix))
39
+ return null;
40
+ const name = messageKey.slice(messageKeyPrefix.length);
41
+ return has(name) ? t(messageKey, error.params) : null;
42
+ }
43
+ return function describeError(error) {
44
+ if (!(error instanceof ApiError)) {
45
+ // Not a response at all — the request never landed. Almost always the network, and almost
46
+ // never worth showing a stack trace for.
47
+ return { cause: t(key("network")), hint: t(key("networkHint")) };
48
+ }
49
+ const waitSecs = retryAfterSecs(error);
50
+ const ctx = {
51
+ error,
52
+ says: serverSentence(error),
53
+ wait: waitSecs === null ? null : humanizeWait(t, waitSecs, copyPrefix),
54
+ waitSecs,
55
+ t,
56
+ };
57
+ const arm = error.code === undefined ? undefined : codes[error.code];
58
+ if (arm)
59
+ return arm(ctx);
60
+ // Anything unmapped: the server's own sentence is still the most specific thing we have,
61
+ // and support can act on it. A 5xx additionally gets "try again shortly", because that one
62
+ // genuinely does clear on its own — a 4xx does not, and saying so would be a lie that costs
63
+ // the reader another attempt.
64
+ return {
65
+ cause: ctx.says ?? (error.message || t(key("unexpected"))),
66
+ hint: error.status >= 500 ? t(key("retrySoon")) : undefined,
67
+ };
68
+ };
69
+ }
70
+ //# sourceMappingURL=describe-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+ /**
3
+ * The last stop before a white screen.
4
+ *
5
+ * React unmounts the whole tree when a render throws, and an app with no boundary anywhere
6
+ * answers that with a blank page: no words, no navigation, no reload — the most complete dead
7
+ * end a product can produce, and the one the never-dead-end rule cannot reach, because by then
8
+ * there is no component left to render a state from.
9
+ *
10
+ * It is deliberately dumb. No retry loop, no error reporting, no reset on a timer: it catches,
11
+ * it hands the error to a `fallback` the app draws in its own words, and it offers `reset`.
12
+ * Every donor that baked its own UI in here had to keep a second copy for its second surface;
13
+ * a render prop has no such problem, and the brand stays in the product where it belongs.
14
+ *
15
+ * A class, because `getDerivedStateFromError` has no hook equivalent — this is the one thing in
16
+ * the kit React still has no other way to express.
17
+ */
18
+ export interface ErrorBoundaryFallbackProps {
19
+ error: Error;
20
+ /** Put the children back. */
21
+ reset: () => void;
22
+ /**
23
+ * True when the throw was a failed lazy-chunk import — a deploy landed while this tab was
24
+ * open, so the fix is a reload and NOT a retry (a rejected dynamic import rethrows on
25
+ * re-render forever). Draw the "updating" screen, not the crash screen.
26
+ */
27
+ isChunkError: boolean;
28
+ }
29
+ export interface ErrorBoundaryProps {
30
+ /** Drawn instead of the children once something has thrown. */
31
+ fallback: (props: ErrorBoundaryFallbackProps) => ReactNode;
32
+ /**
33
+ * Changing this value clears the error and remounts the children — pass the route, so
34
+ * navigating away from a screen that broke actually leaves it rather than carrying its
35
+ * wreckage to every page after it.
36
+ */
37
+ resetKey?: string;
38
+ /** Called once per catch. Report to whatever the app reports to. */
39
+ onError?: (error: Error, info: ErrorInfo) => void;
40
+ /**
41
+ * Classify a chunk-load failure. Pass `isChunkLoadError` from this package; it is a
42
+ * parameter rather than a hard import so an app that does not lazy-load pays nothing.
43
+ */
44
+ isChunkError?: (error: unknown) => boolean;
45
+ children: ReactNode;
46
+ }
47
+ interface State {
48
+ error: Error | null;
49
+ /** Which `resetKey` the current error belongs to. */
50
+ key: string | undefined;
51
+ }
52
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
53
+ state: State;
54
+ static getDerivedStateFromError(error: Error): Pick<State, "error">;
55
+ static getDerivedStateFromProps(props: ErrorBoundaryProps, state: State): State | null;
56
+ componentDidCatch(error: Error, info: ErrorInfo): void;
57
+ private readonly reset;
58
+ render(): ReactNode;
59
+ }
60
+ export {};
61
+ //# sourceMappingURL=error-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../src/error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,KAAK,CAAC;IACb,6BAA6B;IAC7B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,QAAQ,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,SAAS,CAAC;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,UAAU,KAAK;IACb,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,qDAAqD;IACrD,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED,qBAAa,aAAc,SAAQ,SAAS,CAAC,kBAAkB,EAAE,KAAK,CAAC;IAC5D,KAAK,EAAE,KAAK,CAAmC;IAExD,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC;IAInE,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI;IAQ7E,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAK/D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8D;IAE3E,MAAM,IAAI,SAAS;CAS7B"}
@@ -0,0 +1,32 @@
1
+ import { Component } from "react";
2
+ export class ErrorBoundary extends Component {
3
+ state = { error: null, key: undefined };
4
+ static getDerivedStateFromError(error) {
5
+ return { error };
6
+ }
7
+ static getDerivedStateFromProps(props, state) {
8
+ if (state.error === null)
9
+ return { error: null, key: props.resetKey };
10
+ // The error belongs to the screen it happened on. Somewhere else is a fresh start, and
11
+ // staying broken there would strand somebody who already did the sensible thing and
12
+ // navigated away.
13
+ return state.key === props.resetKey ? null : { error: null, key: props.resetKey };
14
+ }
15
+ componentDidCatch(error, info) {
16
+ this.props.onError?.(error, info);
17
+ if (!this.props.onError)
18
+ console.error("Unhandled render error", error, info.componentStack);
19
+ }
20
+ reset = () => this.setState({ error: null, key: undefined });
21
+ render() {
22
+ const { error } = this.state;
23
+ if (error === null)
24
+ return this.props.children;
25
+ return this.props.fallback({
26
+ error,
27
+ reset: this.reset,
28
+ isChunkError: this.props.isChunkError?.(error) ?? false,
29
+ });
30
+ }
31
+ }
32
+ //# sourceMappingURL=error-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.js","sourceRoot":"","sources":["../src/error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkC,MAAM,OAAO,CAAC;AAwDlE,MAAM,OAAO,aAAc,SAAQ,SAAoC;IAC5D,KAAK,GAAU,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;IAExD,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,MAAM,CAAC,wBAAwB,CAAC,KAAyB,EAAE,KAAY;QACrE,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;QACtE,uFAAuF;QACvF,oFAAoF;QACpF,kBAAkB;QAClB,OAAO,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IACpF,CAAC;IAEQ,iBAAiB,CAAC,KAAY,EAAE,IAAe;QACtD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/F,CAAC;IAEgB,KAAK,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAE3E,MAAM;QACb,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;YACzB,KAAK;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK;SACxD,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,65 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Route guards.
4
+ *
5
+ * Two donors wrote these four, gave two of them the same names, and explained them in nearly
6
+ * the same words. What differed was one line — and that line is invariant 4, so it is the
7
+ * reason this file exists rather than being copied a ninth time.
8
+ *
9
+ * These are factories because the destinations are the app's: `paths.signIn` is not something a
10
+ * package can know, and one donor hardcoded `/login` in four places for exactly that reason.
11
+ *
12
+ * A guard is a COURTESY, never the boundary. The API refuses what it refuses whatever any of
13
+ * these decide. What they buy is that somebody who types a URL lands on one screen that
14
+ * explains itself instead of four panels each failing separately.
15
+ */
16
+ /** What the guard needs off the session store. */
17
+ export interface SessionState {
18
+ isAuthenticated: boolean;
19
+ isLoading: boolean;
20
+ }
21
+ /**
22
+ * The answer to a "who is this?" query, in the three states it actually has.
23
+ *
24
+ * This shape is invariant 4. A guard reading `!me?.isStaff` collapses "loading", "failed" and
25
+ * "no" into one branch — so an operator arriving while `/auth/me` is 500ing is told the page
26
+ * does not exist. Wrong cause, no retry, and no request id to quote to support. One donor hit
27
+ * that and fixed it; the other still has the collapsed version.
28
+ */
29
+ export type MeQuery<TMe> = {
30
+ status: "loading";
31
+ } | {
32
+ status: "error";
33
+ error: unknown;
34
+ } | {
35
+ status: "success";
36
+ data: TMe;
37
+ };
38
+ export interface GuardOptions {
39
+ /** Drawn while the session or the profile is still resolving. */
40
+ loading: ReactNode;
41
+ }
42
+ /**
43
+ * Signed in, or off to sign in — remembering where they were headed, so the redirect afterwards
44
+ * lands on the page they actually wanted rather than the home screen.
45
+ */
46
+ export declare function createRequireAuth(useSession: () => SessionState, signInPath: string, { loading }: GuardOptions): ({ children }: {
47
+ children: ReactNode;
48
+ }) => import("react").JSX.Element;
49
+ /** The mirror: somebody already signed in has no business on the sign-in screen. */
50
+ export declare function createRequireAnonymous(useSession: () => SessionState, homePath: string, { loading }: GuardOptions): ({ children }: {
51
+ children: ReactNode;
52
+ }) => import("react").JSX.Element;
53
+ /**
54
+ * A gate on something the profile says — suspended, staff, on a given plan.
55
+ *
56
+ * `allow` gets the profile and answers yes or no. `onDenied` draws the refusal; `onError` draws
57
+ * the failure, and they are SEPARATE arguments on purpose. That separation is the whole point
58
+ * of this file: "you may not see this" and "we could not find out" are different sentences, and
59
+ * showing the first when the second is true is how an outage becomes a support ticket about
60
+ * permissions.
61
+ */
62
+ export declare function createRequireProfile<TMe>(useMe: () => MeQuery<TMe>, { loading }: GuardOptions): (allow: (me: TMe) => boolean, onDenied: ReactNode | (() => ReactNode), onError: (error: unknown) => ReactNode) => ({ children }: {
63
+ children: ReactNode;
64
+ }) => import("react").JSX.Element;
65
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC;;;;;;;;;;;;;GAaG;AAEH,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,CAAC,GAAG,IACrB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjG,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,OAAO,EAAE,SAAS,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,YAAY,EAC9B,UAAU,EAAE,MAAM,EAClB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEG,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAUlE;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,YAAY,EAC9B,QAAQ,EAAE,MAAM,EAChB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEQ,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAMvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,YAAY,IAE1F,OAAO,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,EAC3B,UAAU,SAAS,GAAG,CAAC,MAAM,SAAS,CAAC,EACvC,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAEP,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAQvE"}
package/dist/guards.js ADDED
@@ -0,0 +1,52 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Navigate, useLocation } from "react-router-dom";
3
+ /**
4
+ * Signed in, or off to sign in — remembering where they were headed, so the redirect afterwards
5
+ * lands on the page they actually wanted rather than the home screen.
6
+ */
7
+ export function createRequireAuth(useSession, signInPath, { loading }) {
8
+ return function RequireAuth({ children }) {
9
+ const { isAuthenticated, isLoading } = useSession();
10
+ const location = useLocation();
11
+ if (isLoading)
12
+ return _jsx(_Fragment, { children: loading });
13
+ if (!isAuthenticated)
14
+ return (_jsx(Navigate, { to: signInPath, replace: true, state: { from: location.pathname + location.search } }));
15
+ return _jsx(_Fragment, { children: children });
16
+ };
17
+ }
18
+ /** The mirror: somebody already signed in has no business on the sign-in screen. */
19
+ export function createRequireAnonymous(useSession, homePath, { loading }) {
20
+ return function RequireAnonymous({ children }) {
21
+ const { isAuthenticated, isLoading } = useSession();
22
+ if (isLoading)
23
+ return _jsx(_Fragment, { children: loading });
24
+ if (isAuthenticated)
25
+ return _jsx(Navigate, { to: homePath, replace: true });
26
+ return _jsx(_Fragment, { children: children });
27
+ };
28
+ }
29
+ /**
30
+ * A gate on something the profile says — suspended, staff, on a given plan.
31
+ *
32
+ * `allow` gets the profile and answers yes or no. `onDenied` draws the refusal; `onError` draws
33
+ * the failure, and they are SEPARATE arguments on purpose. That separation is the whole point
34
+ * of this file: "you may not see this" and "we could not find out" are different sentences, and
35
+ * showing the first when the second is true is how an outage becomes a support ticket about
36
+ * permissions.
37
+ */
38
+ export function createRequireProfile(useMe, { loading }) {
39
+ return function requireProfile(allow, onDenied, onError) {
40
+ return function RequireProfile({ children }) {
41
+ const query = useMe();
42
+ if (query.status === "loading")
43
+ return _jsx(_Fragment, { children: loading });
44
+ if (query.status === "error")
45
+ return _jsx(_Fragment, { children: onError(query.error) });
46
+ if (!allow(query.data))
47
+ return _jsx(_Fragment, { children: typeof onDenied === "function" ? onDenied() : onDenied });
48
+ return _jsx(_Fragment, { children: children });
49
+ };
50
+ };
51
+ }
52
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAuCzD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAA8B,EAC9B,UAAkB,EAClB,EAAE,OAAO,EAAgB;IAEzB,OAAO,SAAS,WAAW,CAAC,EAAE,QAAQ,EAA2B;QAC/D,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAAC;QACpD,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/B,IAAI,SAAS;YAAE,OAAO,4BAAG,OAAO,GAAI,CAAC;QACrC,IAAI,CAAC,eAAe;YAClB,OAAO,CACL,KAAC,QAAQ,IAAC,EAAE,EAAE,UAAU,EAAE,OAAO,QAAC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,GAAI,CAC3F,CAAC;QACJ,OAAO,4BAAG,QAAQ,GAAI,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,sBAAsB,CACpC,UAA8B,EAC9B,QAAgB,EAChB,EAAE,OAAO,EAAgB;IAEzB,OAAO,SAAS,gBAAgB,CAAC,EAAE,QAAQ,EAA2B;QACpE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAAC;QACpD,IAAI,SAAS;YAAE,OAAO,4BAAG,OAAO,GAAI,CAAC;QACrC,IAAI,eAAe;YAAE,OAAO,KAAC,QAAQ,IAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,SAAG,CAAC;QAC/D,OAAO,4BAAG,QAAQ,GAAI,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAM,KAAyB,EAAE,EAAE,OAAO,EAAgB;IAC5F,OAAO,SAAS,cAAc,CAC5B,KAA2B,EAC3B,QAAuC,EACvC,OAAsC;QAEtC,OAAO,SAAS,cAAc,CAAC,EAAE,QAAQ,EAA2B;YAClE,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;YACtB,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;gBAAE,OAAO,4BAAG,OAAO,GAAI,CAAC;YACtD,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO;gBAAE,OAAO,4BAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAI,CAAC;YACjE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,OAAO,4BAAG,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAI,CAAC;YAC7F,OAAO,4BAAG,QAAQ,GAAI,CAAC;QACzB,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}