@pablozaiden/webapp 0.2.0 → 0.2.1

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.
@@ -0,0 +1,64 @@
1
+ export class WebAppApiError extends Error {
2
+ status: number;
3
+ error?: string;
4
+ details?: unknown;
5
+
6
+ constructor(message: string, status: number, error?: string, details?: unknown) {
7
+ super(message);
8
+ this.name = "WebAppApiError";
9
+ this.status = status;
10
+ this.error = error;
11
+ this.details = details;
12
+ }
13
+ }
14
+
15
+ export type AuthRequiredListener = () => void;
16
+
17
+ const authRequiredListeners = new Set<AuthRequiredListener>();
18
+
19
+ export function onAuthRequired(listener: AuthRequiredListener): () => void {
20
+ authRequiredListeners.add(listener);
21
+ return () => authRequiredListeners.delete(listener);
22
+ }
23
+
24
+ function emitAuthRequired(): void {
25
+ for (const listener of authRequiredListeners) {
26
+ listener();
27
+ }
28
+ }
29
+
30
+ export function appPath(path: string): string {
31
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) return path;
32
+ const base = document.querySelector("base")?.getAttribute("href") ?? "/";
33
+ return new URL(path.replace(/^\/+/, ""), new URL(base, window.location.href)).toString();
34
+ }
35
+
36
+ export function appWebSocketUrl(path = "/api/ws"): string {
37
+ const url = new URL(appPath(path));
38
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
39
+ return url.toString();
40
+ }
41
+
42
+ export async function appFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
43
+ const response = await fetch(typeof input === "string" ? appPath(input) : input, {
44
+ ...init,
45
+ credentials: init?.credentials ?? "same-origin",
46
+ headers: {
47
+ accept: "application/json",
48
+ ...init?.headers,
49
+ },
50
+ });
51
+ if (response.headers.get("x-webapp-passkey-required") === "true" || response.headers.get("x-passkey-auth-required") === "true") {
52
+ emitAuthRequired();
53
+ }
54
+ if (!response.ok) {
55
+ let body: { error?: string; message?: string; details?: unknown } | undefined;
56
+ try {
57
+ body = await response.clone().json() as typeof body;
58
+ } catch {
59
+ body = undefined;
60
+ }
61
+ throw new WebAppApiError(body?.message ?? `Request failed with status ${response.status}`, response.status, body?.error, body?.details);
62
+ }
63
+ return response;
64
+ }
package/src/web/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./WebAppRoot";
2
2
  export * from "./components";
3
3
  export * from "./render";
4
+ export * from "./api-client";
4
5
  export * from "./sidebar/types";
5
6
  export * from "./realtime/useRealtime";
6
7
  export type { ThemePreference } from "../contracts";
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { appWebSocketUrl } from "../api-client";
2
3
 
3
4
  export type RealtimeStatus = "connecting" | "open" | "closed";
4
5
  export type RealtimeAction = "created" | "updated" | "changed" | "deleted";
@@ -71,8 +72,7 @@ export function useRealtime<TEvent>({
71
72
  for (const [key, value] of Object.entries(filters ?? {})) {
72
73
  if (value) params.set(key, value);
73
74
  }
74
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
75
- socket = new WebSocket(`${protocol}//${window.location.host}${path}${params.size ? `?${params.toString()}` : ""}`);
75
+ socket = new WebSocket(appWebSocketUrl(`${path}${params.size ? `?${params.toString()}` : ""}`));
76
76
  setStatus("connecting");
77
77
  socket.onopen = () => {
78
78
  retryRef.current = 0;
@@ -1064,6 +1064,45 @@ textarea::placeholder {
1064
1064
  color: var(--wapp-muted);
1065
1065
  }
1066
1066
 
1067
+ .wapp-shutdown-countdown {
1068
+ display: grid;
1069
+ gap: 0.5rem;
1070
+ margin-top: 0.75rem;
1071
+ font-weight: 600;
1072
+ }
1073
+
1074
+ .wapp-shutdown-message {
1075
+ color: #dc2626;
1076
+ font-size: 0.875rem;
1077
+ }
1078
+
1079
+ :root.dark .wapp-shutdown-message {
1080
+ color: #f87171;
1081
+ }
1082
+
1083
+ .wapp-shutdown-progress {
1084
+ width: 100%;
1085
+ height: 0.375rem;
1086
+ overflow: hidden;
1087
+ border-radius: 999px;
1088
+ background: #fecaca;
1089
+ }
1090
+
1091
+ :root.dark .wapp-shutdown-progress {
1092
+ background: #7f1d1d;
1093
+ }
1094
+
1095
+ .wapp-shutdown-progress-bar {
1096
+ height: 100%;
1097
+ border-radius: inherit;
1098
+ background: #ef4444;
1099
+ transition: width 1s linear;
1100
+ }
1101
+
1102
+ :root.dark .wapp-shutdown-progress-bar {
1103
+ background: #f87171;
1104
+ }
1105
+
1067
1106
  .wapp-segmented {
1068
1107
  display: flex;
1069
1108
  width: 100%;