@kabmo-labs/kabmo-ui 0.0.0-stage → 0.0.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.
package/src/sidebar.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The sidebar hooks: what lets a plugin read the session list and act on a
8
+ * session.
9
+ *
10
+ * bb calls this "threads"; Kabmo calls the same thing sessions. One
11
+ * concept under two names, and these hooks use Kabmo's word because that
12
+ * is what the host's own routes and types say.
13
+ *
14
+ * **EXPERIMENTAL.** The `experimental_` prefix is the marker bb uses for the
15
+ * same two hooks, and it means the shape may move between minor versions — a
16
+ * plugin that uses them opts into that. The stable participants (`useRpc`,
17
+ * `useRealtime`, `useComposer`) carry no prefix.
18
+ *
19
+ * Both go through `requireServices()`, so a plugin outside the app throws naming
20
+ * the real problem, and a host built before this seam answers an honest empty
21
+ * state rather than a broken one — the same rule `useRealtime` follows for an
22
+ * older host that cannot publish a topic.
23
+ */
24
+
25
+ import { useCallback, useEffect, useState } from 'preact/hooks';
26
+ import { requireServices } from './services';
27
+ import type { SidebarThread, SidebarThreadActions, SidebarThreadsState } from '@kabmo-labs/kabmo-sdk';
28
+
29
+ /**
30
+ * The sessions the sidebar is showing.
31
+ *
32
+ * `stale` is surfaced rather than hidden, for the reason `useRealtime` gives: a
33
+ * list shown as current when the socket is down is worse than one that says so.
34
+ */
35
+ export function experimental_useSidebarThreads(): SidebarThreadsState {
36
+ const [state, setState] = useState<{ value: SidebarThread[]; stale: boolean }>(() => ({
37
+ value: [],
38
+ stale: false,
39
+ }));
40
+ /** True until the host has answered once, so a view can hold its placeholder. */
41
+ const [isLoading, setIsLoading] = useState(true);
42
+
43
+ useEffect(() => {
44
+ const services = requireServices();
45
+ if (!services.subscribeSidebarThreads) {
46
+ // An older host cannot supply this. Reporting "loaded, empty" is the
47
+ // honest answer: there is nothing to wait for.
48
+ setIsLoading(false);
49
+ return;
50
+ }
51
+ // Seed from the host's current value BEFORE subscribing, so a component
52
+ // that mounted between two pushes does not render empty until the next one.
53
+ const seeded = services.sidebarThreads?.();
54
+ if (seeded) {
55
+ setState(seeded);
56
+ setIsLoading(false);
57
+ }
58
+ return services.subscribeSidebarThreads((next) => {
59
+ setState(next);
60
+ setIsLoading(false);
61
+ });
62
+ }, []);
63
+
64
+ const refresh = useCallback(() => {
65
+ requireServices().refreshSidebarThreads?.();
66
+ }, []);
67
+
68
+ return { threads: state.value, isLoading, stale: state.stale, refresh };
69
+ }
70
+
71
+ /**
72
+ * The session actions.
73
+ *
74
+ * The host owns the writes, so this is a thin pass-through — but the pass-through
75
+ * is the point: a plugin gets the SAME routes the sidebar's own controls call,
76
+ * with the server's own refusal wording, rather than a second implementation
77
+ * that would drift.
78
+ */
79
+ export function experimental_useSidebarThreadActions(): SidebarThreadActions {
80
+ const [error, setError] = useState<string | null>(null);
81
+
82
+ const actions = requireServices().sidebarActions?.();
83
+
84
+ /**
85
+ * Run one mutation, capturing a refusal's reason.
86
+ *
87
+ * TWO sources, because the host reports a refusal two ways and both are real:
88
+ * a `false` answer is the host saying no (its routes answer the refusal as a
89
+ * value), and the reason for it is on the actions object the host injected —
90
+ * while a THROW is a transport or programming failure with no host wording.
91
+ * Reading only the throw is what made a refused rename look like a silent
92
+ * no-op; reading only the false would lose the reason.
93
+ *
94
+ * The error clears on the next attempt, so a stale reason does not sit over a
95
+ * success.
96
+ */
97
+ const run = useCallback(
98
+ async (operation: () => Promise<boolean>): Promise<boolean> => {
99
+ setError(null);
100
+ try {
101
+ const ok = await operation();
102
+ // A refusal carries its reason on the host's actions object, which is
103
+ // where the route's own wording lands.
104
+ if (!ok) setError(actions?.error ?? null);
105
+ return ok;
106
+ } catch (cause) {
107
+ setError(cause instanceof Error ? cause.message : String(cause));
108
+ return false;
109
+ }
110
+ },
111
+ [actions],
112
+ );
113
+
114
+ return {
115
+ open: useCallback((sessionId: string) => actions?.open(sessionId), [actions]),
116
+ rename: useCallback(
117
+ (sessionId: string, title: string) => run(() => actions?.rename(sessionId, title) ?? Promise.resolve(false)),
118
+ [actions, run],
119
+ ),
120
+ setArchived: useCallback(
121
+ (sessionId: string, archived: boolean) =>
122
+ run(() => actions?.setArchived(sessionId, archived) ?? Promise.resolve(false)),
123
+ [actions, run],
124
+ ),
125
+ markSeen: useCallback(
126
+ (sessionId: string) => run(() => actions?.markSeen(sessionId) ?? Promise.resolve(false)),
127
+ [actions, run],
128
+ ),
129
+ remove: useCallback(
130
+ (sessionId: string) => run(() => actions?.remove(sessionId) ?? Promise.resolve(false)),
131
+ [actions, run],
132
+ ),
133
+ openNew: useCallback((options?: { folderId?: string }) => actions?.openNew(options), [actions]),
134
+ error,
135
+ };
136
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.