elbe-ui 0.2.11 → 0.2.19

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,30 @@
1
+ export interface PostArgs {
2
+ path?: {
3
+ [key: string]: string | number | boolean | undefined;
4
+ };
5
+ query?: {
6
+ [key: string]: string | number | boolean | undefined;
7
+ };
8
+ body?: any;
9
+ }
10
+ /**
11
+ * ApiService is a simple wrapper around fetch that handles JSON serialization and error handling.
12
+ * to use it, you must first call `ApiService.init(apiURL)` with the base URL of your API.
13
+ */
14
+ export declare class ApiService {
15
+ private apiURL;
16
+ private static _i;
17
+ static get i(): ApiService;
18
+ private constructor();
19
+ static init(apiURL: string): void;
20
+ private _fetch;
21
+ get(path: string, args?: PostArgs): Promise<any>;
22
+ post(path: string, args: PostArgs): Promise<any>;
23
+ delete(path: string, args: PostArgs): Promise<any>;
24
+ }
25
+ export interface ApiError {
26
+ code: number;
27
+ message: string;
28
+ data?: any;
29
+ }
30
+ export declare function ifApiError(e: any): ApiError | null;
@@ -12,6 +12,13 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
12
12
  [x: string]: string | number | null | undefined;
13
13
  [x: number]: string | number | null | undefined;
14
14
  cssText?: string | null;
15
+ clipPath?: string | number | null | undefined;
16
+ filter?: string | number | null | undefined;
17
+ marker?: string | number | null | undefined;
18
+ mask?: string | number | null | undefined;
19
+ x?: string | number | null | undefined;
20
+ y?: string | number | null | undefined;
21
+ padding?: string | number | null | undefined;
15
22
  accentColor?: string | number | null | undefined;
16
23
  alignContent?: string | number | null | undefined;
17
24
  alignItems?: string | number | null | undefined;
@@ -118,7 +125,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
118
125
  caretColor?: string | number | null | undefined;
119
126
  clear?: string | number | null | undefined;
120
127
  clip?: string | number | null | undefined;
121
- clipPath?: string | number | null | undefined;
122
128
  clipRule?: string | number | null | undefined;
123
129
  color?: string | number | null | undefined;
124
130
  colorInterpolation?: string | number | null | undefined;
@@ -160,7 +166,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
160
166
  fill?: string | number | null | undefined;
161
167
  fillOpacity?: string | number | null | undefined;
162
168
  fillRule?: string | number | null | undefined;
163
- filter?: string | number | null | undefined;
164
169
  flex?: string | number | null | undefined;
165
170
  flexBasis?: string | number | null | undefined;
166
171
  flexDirection?: string | number | null | undefined;
@@ -252,11 +257,9 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
252
257
  marginLeft?: string | number | null | undefined;
253
258
  marginRight?: string | number | null | undefined;
254
259
  marginTop?: string | number | null | undefined;
255
- marker?: string | number | null | undefined;
256
260
  markerEnd?: string | number | null | undefined;
257
261
  markerMid?: string | number | null | undefined;
258
262
  markerStart?: string | number | null | undefined;
259
- mask?: string | number | null | undefined;
260
263
  maskClip?: string | number | null | undefined;
261
264
  maskComposite?: string | number | null | undefined;
262
265
  maskImage?: string | number | null | undefined;
@@ -304,7 +307,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
304
307
  overscrollBehaviorInline?: string | number | null | undefined;
305
308
  overscrollBehaviorX?: string | number | null | undefined;
306
309
  overscrollBehaviorY?: string | number | null | undefined;
307
- padding?: string | number | null | undefined;
308
310
  paddingBlock?: string | number | null | undefined;
309
311
  paddingBlockEnd?: string | number | null | undefined;
310
312
  paddingBlockStart?: string | number | null | undefined;
@@ -506,8 +508,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
506
508
  wordSpacing?: string | number | null | undefined;
507
509
  wordWrap?: string | number | null | undefined;
508
510
  writingMode?: string | number | null | undefined;
509
- x?: string | number | null | undefined;
510
- y?: string | number | null | undefined;
511
511
  zIndex?: string | number | null | undefined;
512
512
  zoom?: string | number | null | undefined;
513
513
  };
@@ -525,6 +525,13 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
525
525
  [x: string]: string | number | null | undefined;
526
526
  [x: number]: string | number | null | undefined;
527
527
  cssText?: string | null;
528
+ clipPath?: string | number | null | undefined;
529
+ filter?: string | number | null | undefined;
530
+ marker?: string | number | null | undefined;
531
+ mask?: string | number | null | undefined;
532
+ x?: string | number | null | undefined;
533
+ y?: string | number | null | undefined;
534
+ padding?: string | number | null | undefined;
528
535
  accentColor?: string | number | null | undefined;
529
536
  alignContent?: string | number | null | undefined;
530
537
  alignItems?: string | number | null | undefined;
@@ -631,7 +638,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
631
638
  caretColor?: string | number | null | undefined;
632
639
  clear?: string | number | null | undefined;
633
640
  clip?: string | number | null | undefined;
634
- clipPath?: string | number | null | undefined;
635
641
  clipRule?: string | number | null | undefined;
636
642
  color?: string | number | null | undefined;
637
643
  colorInterpolation?: string | number | null | undefined;
@@ -673,7 +679,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
673
679
  fill?: string | number | null | undefined;
674
680
  fillOpacity?: string | number | null | undefined;
675
681
  fillRule?: string | number | null | undefined;
676
- filter?: string | number | null | undefined;
677
682
  flex?: string | number | null | undefined;
678
683
  flexBasis?: string | number | null | undefined;
679
684
  flexDirection?: string | number | null | undefined;
@@ -765,11 +770,9 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
765
770
  marginLeft?: string | number | null | undefined;
766
771
  marginRight?: string | number | null | undefined;
767
772
  marginTop?: string | number | null | undefined;
768
- marker?: string | number | null | undefined;
769
773
  markerEnd?: string | number | null | undefined;
770
774
  markerMid?: string | number | null | undefined;
771
775
  markerStart?: string | number | null | undefined;
772
- mask?: string | number | null | undefined;
773
776
  maskClip?: string | number | null | undefined;
774
777
  maskComposite?: string | number | null | undefined;
775
778
  maskImage?: string | number | null | undefined;
@@ -817,7 +820,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
817
820
  overscrollBehaviorInline?: string | number | null | undefined;
818
821
  overscrollBehaviorX?: string | number | null | undefined;
819
822
  overscrollBehaviorY?: string | number | null | undefined;
820
- padding?: string | number | null | undefined;
821
823
  paddingBlock?: string | number | null | undefined;
822
824
  paddingBlockEnd?: string | number | null | undefined;
823
825
  paddingBlockStart?: string | number | null | undefined;
@@ -1019,8 +1021,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
1019
1021
  wordSpacing?: string | number | null | undefined;
1020
1022
  wordWrap?: string | number | null | undefined;
1021
1023
  writingMode?: string | number | null | undefined;
1022
- x?: string | number | null | undefined;
1023
- y?: string | number | null | undefined;
1024
1024
  zIndex?: string | number | null | undefined;
1025
1025
  zoom?: string | number | null | undefined;
1026
1026
  };
@@ -0,0 +1,15 @@
1
+ import { type ApiError } from "../../service/s_api";
2
+ export declare function ErrorView({ error, retry, debug, }: {
3
+ error: any;
4
+ retry?: () => any;
5
+ debug?: boolean;
6
+ }): import("preact").JSX.Element;
7
+ export declare function PrettyErrorView({ apiError, retry, labels, }: {
8
+ apiError: ApiError;
9
+ retry?: () => any;
10
+ labels?: {
11
+ retry?: string;
12
+ home?: string;
13
+ details?: string;
14
+ };
15
+ }): import("preact").JSX.Element;
@@ -0,0 +1,3 @@
1
+ export declare function Spinner({ padding }: {
2
+ padding?: number;
3
+ }): import("preact").JSX.Element;
@@ -1,5 +1,5 @@
1
- import { type ElbeProps } from "../..";
2
1
  import type { ElbeChild } from "../util/util";
2
+ import type { ElbeProps } from "./box";
3
3
  export type ToggleButtonItem<T> = {
4
4
  icon?: (_: any) => ElbeChild;
5
5
  label: string;
@@ -1,3 +1,19 @@
1
1
  import type React from "preact/compat";
2
2
  export type ElbeChild = React.ReactNode;
3
3
  export type ElbeChildren = ElbeChild[] | ElbeChild;
4
+ /**
5
+ * use the web share api if available, otherwise copy the data to the clipboard
6
+ * @param data the data you want to share
7
+ * @param toastMsg the message to show in the toast if the share api is not available
8
+ */
9
+ export declare function share(data: {
10
+ title: string;
11
+ text?: string;
12
+ url: string;
13
+ }, toastMsg?: string): void;
14
+ /**
15
+ * copy the text to the clipboard
16
+ * @param text the text to copy to the clipboard
17
+ * @param toastMsg the message to show in the toast
18
+ */
19
+ export declare function copyToClipboard(text: string, toastMsg?: string): void;
package/elbe.scss CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  @import url("https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
7
- @import url("https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10..0,100..900&display=swap");
7
+ @import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
8
8
  @import url("https://fonts.googleapis.com/css2?family=Calistoga&display=swap");
9
9
  @import url("https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap");
10
10
 
@@ -30,7 +30,7 @@ $c-modes: (
30
30
  border: #ffffff14,
31
31
  ),
32
32
  secondary: (
33
- back: inter($c-accent, #ffffff, 5%),
33
+ back: inter($c-accent, #ffffff, 7%),
34
34
  front: #222222,
35
35
  border: #22222214,
36
36
  ),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elbe-ui",
3
3
  "type": "module",
4
- "version": "0.2.11",
4
+ "version": "0.2.19",
5
5
  "author": "Robin Naumann",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "build": "rm -r ./dist ; bun build --target=node src/index.tsx --outdir=dist && bun run build:declaration",
28
28
  "build:declaration": "tsc --emitDeclarationOnly --project tsconfig.types.json"
29
29
  },
30
- "module": "src/index.ts",
30
+ "module": "src/index.tsx",
31
31
  "devDependencies": {
32
32
  "@types/bun": "latest"
33
33
  },
@@ -35,7 +35,9 @@
35
35
  "typescript": "^5.0.0"
36
36
  },
37
37
  "dependencies": {
38
+ "@preact/signals": "^1.3.0",
38
39
  "lucide-react": "^0.438.0",
39
- "preact": "^10.23.2"
40
+ "preact": "^10.23.2",
41
+ "preact-router": "^4.1.2"
40
42
  }
41
43
  }
@@ -0,0 +1,126 @@
1
+ import { Signal, useSignal } from "@preact/signals";
2
+ import { type PreactContext, createContext } from "preact";
3
+ import { useContext } from "preact/hooks";
4
+ import { ErrorView } from "../ui/components/error_view";
5
+ import { Spinner } from "../ui/components/spinner";
6
+
7
+ export interface BitUseInterface<C, T> {
8
+ signal: Signal<BitState<T>>;
9
+ ctrl: C;
10
+ map: <D>(m: TriMap<T, D>) => D | preact.JSX.Element;
11
+ onData: (f: (d: T) => any) => any;
12
+ }
13
+
14
+ interface BitData<C, T> {
15
+ ctrl: C;
16
+ state: Signal<BitState<T>>;
17
+ }
18
+
19
+ export interface BitState<T> {
20
+ loading?: boolean;
21
+ error?: any;
22
+ data?: T;
23
+ }
24
+
25
+ export type BitContext<T, C> = PreactContext<BitData<T, C> | null>;
26
+
27
+ export interface TriMap<T, D> {
28
+ onLoading?: () => D;
29
+ onError?: (e: string) => D;
30
+ onData?: (value: T) => D;
31
+ }
32
+
33
+ export interface TWParams<T> {
34
+ emit: (t: T) => void;
35
+ emitLoading: () => void;
36
+ emitError: (e: any) => void;
37
+ map: <D>(m: TriMap<T, D>) => D;
38
+ signal: Signal<BitState<T>>;
39
+ }
40
+
41
+ export function makeBit<C, T>(name: string): BitContext<C, T> {
42
+ const c = createContext<BitData<C, T> | null>(null);
43
+ c.displayName = name;
44
+ return c;
45
+ }
46
+
47
+ export function ProvideBit<I, C, T>(
48
+ context: BitContext<C, T>,
49
+ parameters: I,
50
+ worker: (p: I, d: TWParams<T>, ctrl: C) => void,
51
+ ctrl: (p: I, d: TWParams<T>) => C,
52
+ children: any
53
+ ) {
54
+ const s = useSignal<BitState<T>>({ loading: true });
55
+
56
+ const _set = (n: BitState<T>) => {
57
+ if (JSON.stringify(n) === JSON.stringify(s.peek())) return;
58
+ s.value = n;
59
+ };
60
+
61
+ const emit = (data: T) => _set({ data });
62
+ const emitLoading = () => _set({ loading: true });
63
+ const emitError = (error: any) => {
64
+ console.warn(`BIT: ${context.displayName} emitted ERROR`, error);
65
+ return _set({ error });
66
+ };
67
+
68
+ function map<D>(m: TriMap<T, D>) {
69
+ const st = s.value;
70
+ if (st.loading) return m.onLoading!();
71
+ if (st.error) return m.onError!(st.error);
72
+ return m.onData!(st.data ?? (null as any));
73
+ }
74
+
75
+ const c = ctrl(parameters, { emit, emitLoading, emitError, map, signal: s });
76
+ worker(parameters, { emit, emitLoading, emitError, map, signal: s }, c);
77
+
78
+ return (
79
+ <context.Provider value={{ ctrl: c, state: s }}>
80
+ {children}
81
+ </context.Provider>
82
+ );
83
+ }
84
+
85
+ export function useBit<C, T>(context: BitContext<C, T>): BitUseInterface<C, T> {
86
+ try {
87
+ const { ctrl, state } = useContext(context)!;
88
+ const v = state.value;
89
+
90
+ function map<D>(m: TriMap<T, D>) {
91
+ if (v.loading) return (m.onLoading || (() => <Spinner />))();
92
+ if (v.error)
93
+ return (
94
+ m.onError ||
95
+ ((e) => <ErrorView error={e} retry={(ctrl as any).reload ?? null} />)
96
+ )(v.error);
97
+ return m.onData!(v.data ?? (null as any));
98
+ }
99
+
100
+ return {
101
+ signal: state,
102
+ ctrl,
103
+ map,
104
+ /**
105
+ * this is a quality of life function that allows
106
+ * you to chain the map function with the onData function
107
+ * @param f the builder function
108
+ * @returns the built component
109
+ */
110
+ onData: (f: (d: T) => void) => map({ onData: f }),
111
+ };
112
+ } catch (e) {
113
+ const err = `BIT ERROR: NO ${context.displayName} PROVIDED`;
114
+ console.error(err, e);
115
+ return {
116
+ map: (_: any) => <div>{err}</div>,
117
+ ctrl: null as any,
118
+ signal: null as any,
119
+ onData: () => <div>{err}</div>,
120
+ };
121
+ }
122
+ }
123
+
124
+ function BitSpinner({ name }: { name: string }) {
125
+ return <Spinner />;
126
+ }
@@ -0,0 +1,112 @@
1
+ import { useEffect } from "preact/hooks";
2
+ import type { JSX } from "preact/jsx-runtime";
3
+ import type { BitContext, BitUseInterface, TWParams } from "./bit";
4
+ import { makeBit as mb, ProvideBit, useBit } from "./bit";
5
+
6
+ abstract class BitControl<I, DT> {
7
+ p: I;
8
+ bit: TWParams<DT>;
9
+
10
+ constructor(p: I, bit: TWParams<DT>) {
11
+ this.bit = bit;
12
+ this.p = p;
13
+ }
14
+
15
+ act(fn: (b: DT) => Promise<void>) {
16
+ this.bit.map({
17
+ onData: async (d) => {
18
+ try {
19
+ await fn(d);
20
+ } catch (e: any) {
21
+ if (e && e.code && e.message)
22
+ console.error(`[BitERROR] act: ${e.code} (${e.message})`);
23
+ else console.error("[BitERROR] act: ", e);
24
+ }
25
+ },
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Clean up resources. This is called once
31
+ * the element is removed from the DOM.
32
+ */
33
+ dispose() {}
34
+ }
35
+
36
+ export abstract class WorkerControl<I, DT> extends BitControl<I, DT> {
37
+ reload: (() => Promise<void>) | null = null;
38
+
39
+ abstract worker(): Promise<DT>;
40
+ }
41
+
42
+ export abstract class StreamControl<I, DT, Stream> extends BitControl<I, DT> {
43
+ protected stream: Stream | null = null;
44
+ abstract listen(): Stream;
45
+
46
+ dispose(): void {
47
+ if (this.stream) this.disposeStream(this.stream);
48
+ }
49
+ abstract disposeStream(stream: Stream): void;
50
+ }
51
+
52
+ function make<I, DT, C extends BitControl<I, DT>>(
53
+ name: string
54
+ ): BitContext<C, DT> {
55
+ return mb<C, DT>(name);
56
+ }
57
+
58
+ function use<I, DT, C extends BitControl<I, DT>>(
59
+ b: BitContext<C, DT>
60
+ ): BitUseInterface<C, DT> {
61
+ return useBit<C, DT>(b);
62
+ }
63
+
64
+ export function CtrlBit<I, DT, C extends BitControl<I, DT>>(
65
+ ctrl: (p: I, d: TWParams<DT>) => C,
66
+ name?: string
67
+ ): {
68
+ Provide: (props: I & { children: React.ReactNode }) => JSX.Element;
69
+ use: () => BitUseInterface<C, DT>;
70
+ } {
71
+ const context = make<I, DT, C>((name || "Unknown") + "Bit");
72
+
73
+ function Provide({ children, ...p }: { children: React.ReactNode } & I) {
74
+ return ProvideBit(
75
+ context,
76
+ p,
77
+ async (p, b, c) => {
78
+ b.emitLoading();
79
+
80
+ try {
81
+ if (c instanceof WorkerControl) {
82
+ if (c.reload) await c.reload();
83
+ }
84
+ if (c instanceof StreamControl) {
85
+ (c as any).stream = c.listen();
86
+ }
87
+ } catch (e) {
88
+ b.emitError(e);
89
+ }
90
+ },
91
+
92
+ (p, b) => {
93
+ const c = ctrl(p as I, b);
94
+ // clean up on unmount
95
+ useEffect(() => () => c.dispose(), []);
96
+ if (c instanceof WorkerControl) {
97
+ c.reload = async () => {
98
+ b.emitLoading();
99
+ try {
100
+ b.emit(await c.worker());
101
+ } catch (e) {
102
+ b.emitError(e);
103
+ }
104
+ };
105
+ }
106
+ return c;
107
+ },
108
+ children
109
+ );
110
+ }
111
+ return { Provide: Provide, use: () => use<I, DT, C>(context) };
112
+ }
@@ -1,6 +1,9 @@
1
1
  import * as Lucide from "lucide-react";
2
2
 
3
3
  // exports
4
+ export * from "./bit/bit";
5
+ export * from "./bit/ctrl_bit";
6
+ export * from "./service/s_api";
4
7
  export * from "./ui/color_theme";
5
8
  export * from "./ui/components/badge";
6
9
  export * from "./ui/components/box";
@@ -14,6 +17,7 @@ export * from "./ui/components/input/input_field";
14
17
  export * from "./ui/components/input/range";
15
18
  export * from "./ui/components/input/select";
16
19
  export * from "./ui/components/padded";
20
+ export * from "./ui/components/spinner";
17
21
  export * from "./ui/components/text";
18
22
  export * from "./ui/components/toggle_button";
19
23
  export * from "./ui/components/util";
@@ -0,0 +1,102 @@
1
+ export interface PostArgs {
2
+ path?: { [key: string]: string | number | boolean | undefined };
3
+ query?: { [key: string]: string | number | boolean | undefined };
4
+ body?: any;
5
+ }
6
+
7
+ const _noArgs: PostArgs = {};
8
+
9
+ /**
10
+ * ApiService is a simple wrapper around fetch that handles JSON serialization and error handling.
11
+ * to use it, you must first call `ApiService.init(apiURL)` with the base URL of your API.
12
+ */
13
+ export class ApiService {
14
+ private static _i: ApiService | null = null;
15
+ public static get i(): ApiService {
16
+ if (!ApiService._i) throw "ApiService not initialized. Call ApiService.init(apiURL)";
17
+ return ApiService._i;
18
+ }
19
+
20
+ private constructor(private apiURL: string) {}
21
+
22
+ static init(apiURL: string) {
23
+ if (ApiService._i) throw "ApiService already initialized";
24
+ ApiService._i = new ApiService(apiURL);
25
+ }
26
+
27
+ private async _fetch(
28
+ p: string,
29
+ method: "GET" | "POST" | "DELETE",
30
+ { path, query, body }: PostArgs
31
+ ): Promise<any> {
32
+ try {
33
+ p = path
34
+ ? p.replace(/:([a-zA-Z0-9_]+)/g, (m, p1) => {
35
+ const v = path[p1];
36
+ if (v === undefined)
37
+ throw { code: 400, message: `missing parameter ${p1}` };
38
+ return v?.toString() ?? "";
39
+ })
40
+ : p;
41
+
42
+ const queryStr =
43
+ query != null ? "?" + new URLSearchParams(query as any).toString() : "";
44
+ const response = await fetch(this.apiURL + p + queryStr, {
45
+ method,
46
+ credentials: "include",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: body ? JSON.stringify(body) : undefined,
49
+ });
50
+ if (response.ok) {
51
+ try {
52
+ return await response.json();
53
+ } catch (e) {
54
+ return null;
55
+ }
56
+ }
57
+ let data = null;
58
+ try {
59
+ data = await response.clone().json();
60
+ } catch (e) {
61
+ data = await response.text();
62
+ }
63
+
64
+ throw {
65
+ code: response.status,
66
+ message: data.message ?? "undefined error",
67
+ data,
68
+ } as ApiError;
69
+ } catch (e) {
70
+ rethrow(e, 0, "unknown error");
71
+ }
72
+ }
73
+
74
+ async get(path: string, args?: PostArgs): Promise<any> {
75
+ return this._fetch(path, "GET", args || _noArgs);
76
+ }
77
+
78
+ async post(path: string, args: PostArgs): Promise<any> {
79
+ return this._fetch(path, "POST", args || _noArgs);
80
+ }
81
+
82
+ async delete(path: string, args: PostArgs): Promise<any> {
83
+ return this._fetch(path, "DELETE", args || _noArgs);
84
+ }
85
+ }
86
+
87
+ function rethrow(e: any, code: number, message: string): ApiError {
88
+ // if e implements the apiError interface, rethrow it:
89
+ if (e && e.code !== null && e.message !== null) throw e;
90
+ throw { code, message, data: e };
91
+ }
92
+
93
+ export interface ApiError {
94
+ code: number;
95
+ message: string;
96
+ data?: any;
97
+ }
98
+
99
+ export function ifApiError(e: any): ApiError | null {
100
+ if (e && e.code !== null && e.message !== null) return e;
101
+ return null;
102
+ }
@@ -0,0 +1,72 @@
1
+ import { useSignal } from "@preact/signals";
2
+ import { route } from "preact-router";
3
+ import { ElbeDialog, Icons } from "../..";
4
+ import { ifApiError, type ApiError } from "../../service/s_api";
5
+
6
+ export function ErrorView({
7
+ error,
8
+ retry,
9
+ debug,
10
+ }: {
11
+ error: any;
12
+ retry?: () => any;
13
+ debug?: boolean;
14
+ }) {
15
+ const apiError: ApiError = ifApiError(error) ?? {
16
+ code: 0,
17
+ message: "unknown error",
18
+ data: error,
19
+ };
20
+ return !debug ? (
21
+ <PrettyErrorView apiError={apiError} retry={retry} />
22
+ ) : (
23
+ <div class="column padded card inverse cross-stretch">
24
+ <h3 style="margin: 0">ERROR: {apiError.code}</h3>
25
+ <p>{apiError.message}</p>
26
+ <pre>{JSON.stringify(apiError.data, null, 2)}</pre>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export function PrettyErrorView({
32
+ apiError,
33
+ retry,
34
+ labels = {
35
+ retry: "retry",
36
+ home: "go home",
37
+ details: "error details",
38
+ },
39
+ }: {
40
+ apiError: ApiError;
41
+ retry?: () => any;
42
+ labels?: { retry?: string; home?: string; details?: string };
43
+ }) {
44
+ const openSig = useSignal(false);
45
+ return (
46
+ <div class="column padded cross-center" style="margin: 1rem 0">
47
+ <Icons.OctagonAlert />
48
+ <h3 style="margin: 0">{apiError.code}</h3>
49
+ <span class="pointer" onClick={() => (openSig.value = true)}>
50
+ {apiError.message}
51
+ </span>
52
+ {retry && (
53
+ <button class="action" onClick={() => retry()}>
54
+ <Icons.RotateCcw /> {labels.retry ?? "retry"}
55
+ </button>
56
+ )}
57
+ {apiError.code === 404 && (
58
+ <button class="action" onClick={() => route("/")}>
59
+ <Icons.House />
60
+ {labels.home ?? "go home"}
61
+ </button>
62
+ )}
63
+ <ElbeDialog
64
+ title={labels.details ?? "error details"}
65
+ open={openSig.value}
66
+ onClose={() => (openSig.value = false)}
67
+ >
68
+ <pre class="card inverse">{JSON.stringify(apiError.data, null, 2)}</pre>
69
+ </ElbeDialog>
70
+ </div>
71
+ );
72
+ }
@@ -29,7 +29,6 @@ function _btn(
29
29
  { icon, onTap, ...elbe }: IconButtonProps,
30
30
  colorManner: ElbeColorManners = "major"
31
31
  ) {
32
- console.log("icon", icon);
33
32
  return (
34
33
  <button
35
34
  {...applyProps(