@gperezmz/bb-plugin-thread-glance 0.2.3 → 0.2.4
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/app.tsx +11 -2
- package/dist/app.css +1 -1
- package/dist/app.js +29 -29
- package/dist/app.meta.json +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.meta.json +1 -1
- package/features/thread-list/components/FoldRows.tsx +1 -1
- package/features/thread-list/components/GroupSection.tsx +2 -6
- package/features/thread-list/components/{Toolbar.tsx → SettingsPanel.tsx} +19 -36
- package/features/thread-list/components/ThreadList.tsx +1 -3
- package/features/thread-list/components/glyphs.tsx +13 -1
- package/features/thread-list/data/same-window.ts +34 -0
- package/features/thread-list/data/useClientPreferences.ts +15 -7
- package/features/thread-list/data/usePreferences.ts +16 -7
- package/features/thread-list/icons.ts +0 -1
- package/features/thread-list/index.ts +1 -0
- package/features/thread-list/model/layout.ts +5 -2
- package/features/thread-list/model/settings.ts +1 -1
- package/features/thread-list/model/state.ts +6 -3
- package/package.json +1 -1
package/dist/server.meta.json
CHANGED
|
@@ -22,7 +22,7 @@ import { ROW_ICON_BUTTON } from "./ThreadRowView";
|
|
|
22
22
|
export const OlderRowView = memo(function OlderRowView({ row, controller }: { row: OlderRow; controller: RowController }) {
|
|
23
23
|
const { label, ariaLabel } = olderRowText(row);
|
|
24
24
|
// A family's fold sits where its children do, with a dots glyph in the
|
|
25
|
-
//
|
|
25
|
+
// Status column; the group's fold keeps the chevron.
|
|
26
26
|
const inFamily = row.scope !== "group";
|
|
27
27
|
return (
|
|
28
28
|
<button
|
|
@@ -43,8 +43,6 @@ export interface GroupController {
|
|
|
43
43
|
|
|
44
44
|
export type DropStates = ReadonlyMap<string, "valid" | "blocked" | "unchanged" | "before" | "after">;
|
|
45
45
|
|
|
46
|
-
/** The sticky toolbar's height; headers stick below it. */
|
|
47
|
-
export const TOOLBAR_HEIGHT = 28;
|
|
48
46
|
const ROW_HEIGHT = { compact: 28, comfortable: 44 };
|
|
49
47
|
|
|
50
48
|
function canRename(group: GroupView): boolean {
|
|
@@ -96,9 +94,8 @@ const GroupHeader = memo(function GroupHeader({
|
|
|
96
94
|
role={undefined}
|
|
97
95
|
tabIndex={undefined}
|
|
98
96
|
data-sidebar="group-label"
|
|
99
|
-
style={{ top: inOverflow ? 0 : TOOLBAR_HEIGHT }}
|
|
100
97
|
className={cn(
|
|
101
|
-
"group/header sticky z-20 flex h-7 items-center gap-1 rounded-md bg-sidebar pl-2 pr-1 text-xs text-muted-foreground max-md:pointer-coarse:h-9",
|
|
98
|
+
"group/header sticky top-0 z-20 flex h-7 items-center gap-1 rounded-md bg-sidebar pl-2 pr-1 text-xs text-muted-foreground max-md:pointer-coarse:h-9",
|
|
102
99
|
dropActive && "bg-sidebar-accent",
|
|
103
100
|
draggable.isDragging && "opacity-50",
|
|
104
101
|
!controller.compact && !inOverflow && "select-none",
|
|
@@ -431,8 +428,7 @@ export const AttentionSection = memo(function AttentionSection({ view, ...rest }
|
|
|
431
428
|
return (
|
|
432
429
|
<section aria-label="Needs attention" className={cn("relative mb-1 flex w-full min-w-0 flex-col rounded-md pb-0.5", ATTENTION_BAND)}>
|
|
433
430
|
<h2
|
|
434
|
-
|
|
435
|
-
className="sticky z-20 flex h-7 items-center gap-1 rounded-t-md bg-[var(--tg-surface)] pl-2 pr-1 text-xs font-medium text-muted-foreground max-md:pointer-coarse:h-9"
|
|
431
|
+
className="sticky top-0 z-20 flex h-7 items-center gap-1 rounded-t-md bg-[var(--tg-surface)] pl-2 pr-1 text-xs font-medium text-muted-foreground max-md:pointer-coarse:h-9"
|
|
436
432
|
>
|
|
437
433
|
<span className="min-w-0 flex-1 truncate">Needs attention</span>
|
|
438
434
|
{view.familyCount > 0 ? (
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
|
|
3
|
-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
1
|
+
// Thread Glance's settings, shown by its item in bb's sidebar footer.
|
|
2
|
+
import { experimental_Icon as Icon, type ExperimentalSidebarFooterDisclosureProps } from "@get-bb/plugin-sdk/app";
|
|
4
3
|
import { cn } from "@/lib/utils";
|
|
5
4
|
import type { ClientPreferences, Preferences } from "@/shared/preferences";
|
|
6
5
|
import { childAttentionFor, countsEveryChild, lifecyclesFor, sortArrow, sortFieldPatch, threadsShown } from "../model/settings";
|
|
7
6
|
import { effectiveSortField } from "../model/sort";
|
|
7
|
+
import { useClientPreferences } from "../data/useClientPreferences";
|
|
8
|
+
import { usePreferences } from "../data/usePreferences";
|
|
8
9
|
import { ICONS } from "../icons";
|
|
9
|
-
import { TOOLBAR_HEIGHT } from "./GroupSection";
|
|
10
|
-
import { ROW_ICON_BUTTON } from "./ThreadRowView";
|
|
11
10
|
|
|
12
11
|
// bb's own segmented controls (the Reasoning picker, the diff view toggle)
|
|
13
12
|
// mark the chosen item with the state-active token and no shadow. The track
|
|
14
13
|
// is a translucent tint, so the chosen item reads as raised in both themes.
|
|
15
|
-
|
|
14
|
+
// An item never shrinks below its text: where a line cannot hold its label
|
|
15
|
+
// and its control, the control wraps onto a line of its own.
|
|
16
|
+
const SEGMENT_TRACK = "flex flex-1 gap-0.5 rounded-md bg-surface-recessed p-0.5";
|
|
16
17
|
const SEGMENT_ITEM =
|
|
17
|
-
"flex
|
|
18
|
+
"flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-1 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
18
19
|
|
|
19
20
|
function segmentClass(selected: boolean): string {
|
|
20
21
|
return cn(
|
|
@@ -25,12 +26,12 @@ function segmentClass(selected: boolean): string {
|
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/** A label, then its control
|
|
29
|
+
/** A label, then its control: on one line where the panel is wide enough, else the control below. */
|
|
29
30
|
function Line({ label, children }: { label: string; children: React.ReactNode }) {
|
|
30
31
|
return (
|
|
31
|
-
<div className="flex items-center gap-2 px-2 py-1">
|
|
32
|
-
<span className="w-[5.5rem] shrink-0 text-sm">{label}</span>
|
|
33
|
-
{children}
|
|
32
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-2 py-1">
|
|
33
|
+
<span className="min-w-[5.5rem] shrink-0 whitespace-nowrap text-sm">{label}</span>
|
|
34
|
+
<div className="flex flex-1 gap-2">{children}</div>
|
|
34
35
|
</div>
|
|
35
36
|
);
|
|
36
37
|
}
|
|
@@ -104,7 +105,7 @@ function Heading({ children }: { children: React.ReactNode }) {
|
|
|
104
105
|
return <h3 className="px-2 pb-0.5 pt-3 text-xs font-medium text-muted-foreground first:pt-0">{children}</h3>;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
function SettingsPanel({
|
|
108
109
|
prefs,
|
|
109
110
|
client,
|
|
110
111
|
onPrefs,
|
|
@@ -208,31 +209,13 @@ export function SettingsPanel({
|
|
|
208
209
|
);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
/** The
|
|
212
|
-
export function
|
|
213
|
-
prefs,
|
|
214
|
-
client,
|
|
215
|
-
onPrefs,
|
|
216
|
-
onClient,
|
|
217
|
-
}: {
|
|
218
|
-
prefs: Preferences;
|
|
219
|
-
client: ClientPreferences;
|
|
220
|
-
onPrefs(patch: Partial<Preferences>): void;
|
|
221
|
-
onClient(patch: Partial<ClientPreferences>): void;
|
|
222
|
-
}) {
|
|
212
|
+
/** The footer item's panel, reading and writing the preferences the list reads. */
|
|
213
|
+
export function SettingsDisclosure(_props: ExperimentalSidebarFooterDisclosureProps) {
|
|
214
|
+
const { prefs, update } = usePreferences();
|
|
215
|
+
const [client, updateClient] = useClientPreferences();
|
|
223
216
|
return (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<Popover>
|
|
227
|
-
<PopoverTrigger asChild>
|
|
228
|
-
<button type="button" aria-label="Thread Glance settings" title="List settings" className={ROW_ICON_BUTTON}>
|
|
229
|
-
<Icon name={ICONS.settings} aria-hidden className="size-4" />
|
|
230
|
-
</button>
|
|
231
|
-
</PopoverTrigger>
|
|
232
|
-
<PopoverContent align="end" className="w-80 p-2">
|
|
233
|
-
<SettingsPanel prefs={prefs} client={client} onPrefs={onPrefs} onClient={onClient} />
|
|
234
|
-
</PopoverContent>
|
|
235
|
-
</Popover>
|
|
217
|
+
<div className="p-1">
|
|
218
|
+
<SettingsPanel prefs={prefs} client={client} onPrefs={update} onClient={updateClient} />
|
|
236
219
|
</div>
|
|
237
220
|
);
|
|
238
221
|
}
|
|
@@ -54,7 +54,6 @@ import { cancelPendingCards } from "./row-card";
|
|
|
54
54
|
import { GroupSection, AttentionSection, type DropStates, type GroupController } from "./GroupSection";
|
|
55
55
|
import type { ProviderDisplay } from "./ProviderBadge";
|
|
56
56
|
import { ThreadDetails } from "./ThreadDetails";
|
|
57
|
-
import { Toolbar } from "./Toolbar";
|
|
58
57
|
|
|
59
58
|
const PLUGIN_ID = "thread-glance";
|
|
60
59
|
|
|
@@ -108,7 +107,7 @@ function ThreadListBody({
|
|
|
108
107
|
onRetry,
|
|
109
108
|
}: PluginThreadListProps & { attempt: number; onRetry(): void }) {
|
|
110
109
|
const { prefs, hydrated, update } = usePreferences();
|
|
111
|
-
const [client
|
|
110
|
+
const [client] = useClientPreferences();
|
|
112
111
|
const sidebar = useSidebarThreads({ experimental_lifecycles: prefs.threadLifecycles });
|
|
113
112
|
const actions = useThreadActions();
|
|
114
113
|
const sdk = useSdk();
|
|
@@ -679,7 +678,6 @@ function ThreadListBody({
|
|
|
679
678
|
return (
|
|
680
679
|
<ListLiveContext.Provider value={live}>
|
|
681
680
|
<div className="flex w-full min-w-0 flex-col px-1.5 pb-2">
|
|
682
|
-
<Toolbar prefs={prefs} client={client} onPrefs={update} onClient={updateClient} />
|
|
683
681
|
{threads.length === 0 ? (
|
|
684
682
|
// bb's own pinned New thread button covers the empty list.
|
|
685
683
|
<p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
|
|
@@ -47,6 +47,18 @@ export function UnreadDot({ label, className }: { label?: string; className?: st
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// The Idle state's ring: uncoloured, smaller than the column and thinned, so
|
|
51
|
+
// it reads as status yet stays fainter than the unread dot and the draft
|
|
52
|
+
// pencil. Screen readers skip it, so an Idle row announces what it did before.
|
|
53
|
+
export function IdleRing({ className }: { className?: string }) {
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
aria-hidden
|
|
57
|
+
className={cn("inline-block size-2.5 shrink-0 rounded-full border-[1.5px] border-current opacity-50", className)}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
export function GlyphIcon({
|
|
51
63
|
glyph,
|
|
52
64
|
label,
|
|
@@ -56,7 +68,7 @@ export function GlyphIcon({
|
|
|
56
68
|
label?: string;
|
|
57
69
|
className?: string;
|
|
58
70
|
}) {
|
|
59
|
-
if (glyph.icon ===
|
|
71
|
+
if (glyph.icon === "ring") return <IdleRing className={TONE_CLASS[glyph.tone]} />;
|
|
60
72
|
if (glyph.icon === "dot") return <UnreadDot label={label} />;
|
|
61
73
|
return (
|
|
62
74
|
<Icon
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The list and the settings panel mount apart, each with its own copy of the
|
|
2
|
+
// preferences. A change made in one reaches the other here, in the same
|
|
3
|
+
// window, at once; the server and localStorage stay what other windows read.
|
|
4
|
+
import { useCallback, useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
type Apply<Patch> = (patch: Patch) => void;
|
|
7
|
+
|
|
8
|
+
export function sameWindow<Patch>(): Set<Apply<Patch>> {
|
|
9
|
+
return new Set();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Joins this copy to `copies` for as long as it is mounted.
|
|
14
|
+
*
|
|
15
|
+
* Returns the call that hands a patch this copy made to every other copy.
|
|
16
|
+
*/
|
|
17
|
+
export function useSameWindow<State extends object>(
|
|
18
|
+
copies: Set<Apply<Partial<State>>>,
|
|
19
|
+
setState: (update: (current: State) => State) => void,
|
|
20
|
+
): Apply<Partial<State>> {
|
|
21
|
+
const apply = useCallback((patch: Partial<State>) => setState((current) => ({ ...current, ...patch })), [setState]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
copies.add(apply);
|
|
24
|
+
return () => {
|
|
25
|
+
copies.delete(apply);
|
|
26
|
+
};
|
|
27
|
+
}, [copies, apply]);
|
|
28
|
+
return useCallback(
|
|
29
|
+
(patch: Partial<State>) => {
|
|
30
|
+
for (const other of copies) if (other !== apply) other(patch);
|
|
31
|
+
},
|
|
32
|
+
[copies, apply],
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -5,16 +5,24 @@ import {
|
|
|
5
5
|
parseClientPreferences,
|
|
6
6
|
type ClientPreferences,
|
|
7
7
|
} from "@/shared/preferences";
|
|
8
|
+
import { sameWindow, useSameWindow } from "./same-window";
|
|
8
9
|
import { readJson, writeJson } from "./storage";
|
|
9
10
|
|
|
11
|
+
const copies = sameWindow<Partial<ClientPreferences>>();
|
|
12
|
+
|
|
10
13
|
export function useClientPreferences(): [ClientPreferences, (patch: Partial<ClientPreferences>) => void] {
|
|
11
14
|
const [value, setValue] = useState(() => parseClientPreferences(readJson(CLIENT_PREFERENCES_STORAGE_KEY)));
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const tellOthers = useSameWindow(copies, setValue);
|
|
16
|
+
const update = useCallback(
|
|
17
|
+
(patch: Partial<ClientPreferences>) => {
|
|
18
|
+
setValue((current) => {
|
|
19
|
+
const next = { ...current, ...patch };
|
|
20
|
+
writeJson(CLIENT_PREFERENCES_STORAGE_KEY, next);
|
|
21
|
+
return next;
|
|
22
|
+
});
|
|
23
|
+
tellOthers(patch);
|
|
24
|
+
},
|
|
25
|
+
[tellOthers],
|
|
26
|
+
);
|
|
19
27
|
return [value, update];
|
|
20
28
|
}
|
|
@@ -14,10 +14,16 @@ import {
|
|
|
14
14
|
type PreferenceKey,
|
|
15
15
|
type Preferences,
|
|
16
16
|
} from "@/shared/preferences";
|
|
17
|
+
import { sameWindow, useSameWindow } from "./same-window";
|
|
17
18
|
import { readJson, writeJson } from "./storage";
|
|
18
19
|
|
|
19
20
|
const WRITE_DEBOUNCE_MS = 150;
|
|
20
21
|
|
|
22
|
+
const copies = sameWindow<Partial<Preferences>>();
|
|
23
|
+
// Writes not yet sent, whichever copy made them: a reload or an echo in any
|
|
24
|
+
// copy must not put back the value one of them replaced.
|
|
25
|
+
const pending = new Map<PreferenceKey, unknown>();
|
|
26
|
+
|
|
21
27
|
export interface PreferencesState {
|
|
22
28
|
prefs: Preferences;
|
|
23
29
|
/** The server's values have arrived (or failed; the mirror stands in). */
|
|
@@ -31,7 +37,6 @@ export function usePreferences(): PreferencesState {
|
|
|
31
37
|
coercePreferences(readJson(PREFERENCES_MIRROR_STORAGE_KEY)),
|
|
32
38
|
);
|
|
33
39
|
const [hydrated, setHydrated] = useState(false);
|
|
34
|
-
const pending = useRef(new Map<PreferenceKey, unknown>());
|
|
35
40
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
36
41
|
const connection = useRealtimeConnectionState();
|
|
37
42
|
const wasConnected = useRef(false);
|
|
@@ -41,7 +46,7 @@ export function usePreferences(): PreferencesState {
|
|
|
41
46
|
const { preferences } = await rpc.call("listPreferences", null);
|
|
42
47
|
const next = coercePreferences(preferences);
|
|
43
48
|
// Keys with a write in flight keep the local value.
|
|
44
|
-
for (const [key, value] of pending
|
|
49
|
+
for (const [key, value] of pending) (next as Record<string, unknown>)[key] = value;
|
|
45
50
|
setPrefs(next);
|
|
46
51
|
writeJson(PREFERENCES_MIRROR_STORAGE_KEY, next);
|
|
47
52
|
} finally {
|
|
@@ -73,7 +78,7 @@ export function usePreferences(): PreferencesState {
|
|
|
73
78
|
|
|
74
79
|
useRealtime(CHANNELS.preferences, (payload) => {
|
|
75
80
|
const signal = payload as PreferenceSignal;
|
|
76
|
-
if (!isPreferenceKey(signal?.key) || pending.
|
|
81
|
+
if (!isPreferenceKey(signal?.key) || pending.has(signal.key)) return;
|
|
77
82
|
const parsed = parsePreference(signal.key, signal.value);
|
|
78
83
|
if (!parsed.success) return;
|
|
79
84
|
setPrefs((current) => {
|
|
@@ -84,8 +89,8 @@ export function usePreferences(): PreferencesState {
|
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
const flush = useCallback(() => {
|
|
87
|
-
const writes = [...pending
|
|
88
|
-
pending.
|
|
92
|
+
const writes = [...pending];
|
|
93
|
+
pending.clear();
|
|
89
94
|
for (const [key, value] of writes) {
|
|
90
95
|
rpc.call("setPreference", { key, value }).catch((error: unknown) => {
|
|
91
96
|
toast.error(`Couldn't save the ${key} setting`, {
|
|
@@ -103,6 +108,9 @@ export function usePreferences(): PreferencesState {
|
|
|
103
108
|
[flush],
|
|
104
109
|
);
|
|
105
110
|
|
|
111
|
+
// Another copy's change arrives already mirrored and on its way to the server.
|
|
112
|
+
const tellOthers = useSameWindow(copies, setPrefs);
|
|
113
|
+
|
|
106
114
|
const update = useCallback(
|
|
107
115
|
(patch: Partial<Preferences>) => {
|
|
108
116
|
setPrefs((current) => {
|
|
@@ -110,8 +118,9 @@ export function usePreferences(): PreferencesState {
|
|
|
110
118
|
writeJson(PREFERENCES_MIRROR_STORAGE_KEY, next);
|
|
111
119
|
return next;
|
|
112
120
|
});
|
|
121
|
+
tellOthers(patch);
|
|
113
122
|
for (const [key, value] of Object.entries(patch)) {
|
|
114
|
-
if (isPreferenceKey(key)) pending.
|
|
123
|
+
if (isPreferenceKey(key)) pending.set(key, value);
|
|
115
124
|
}
|
|
116
125
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
117
126
|
timer.current = setTimeout(() => {
|
|
@@ -119,7 +128,7 @@ export function usePreferences(): PreferencesState {
|
|
|
119
128
|
flush();
|
|
120
129
|
}, WRITE_DEBOUNCE_MS);
|
|
121
130
|
},
|
|
122
|
-
[flush],
|
|
131
|
+
[flush, tellOthers],
|
|
123
132
|
);
|
|
124
133
|
|
|
125
134
|
return { prefs, hydrated, update };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// How far a row sits from the list's left edge. Pure, so a test can
|
|
2
2
|
// call it without rendering.
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Left padding of a root row: its Status column starts where the group
|
|
5
|
+
* header's text does.
|
|
6
|
+
*/
|
|
4
7
|
export const ROOT_INDENT = 8;
|
|
5
8
|
/**
|
|
6
|
-
* A child's
|
|
9
|
+
* A child's Status column starts this far right of its parent's, so the
|
|
7
10
|
* child's title sits this far right of the parent's title.
|
|
8
11
|
*/
|
|
9
12
|
export const FOLDED_STEP = 12;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What the settings
|
|
1
|
+
// What the settings panel's controls show and write. Pure.
|
|
2
2
|
import type { ChildAttention, Lifecycle, Preferences, SortField } from "@/shared/preferences";
|
|
3
3
|
import { effectiveDirection, effectiveSortField, naturalDirection } from "./sort";
|
|
4
4
|
|
|
@@ -48,9 +48,12 @@ const NEEDS: Record<NeedsKind, { icon: string; label: string }> = {
|
|
|
48
48
|
input: { icon: "MessageQuestion", label: "Needs your input" },
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* How a glyph is drawn. `dot` is the unread dot, `ring` the Idle state's
|
|
53
|
+
* faint ring; everything else a host icon.
|
|
54
|
+
*/
|
|
52
55
|
export interface Glyph {
|
|
53
|
-
icon: string | "dot" |
|
|
56
|
+
icon: string | "dot" | "ring";
|
|
54
57
|
tone: Tone;
|
|
55
58
|
spin: boolean;
|
|
56
59
|
shine: boolean;
|
|
@@ -231,7 +234,7 @@ export function computeState(thread: PluginSidebarThread, inputs: StateInputs):
|
|
|
231
234
|
}
|
|
232
235
|
if (inputs.unread) return { kind: "unread", label: "Unread", glyph: glyph("dot", "none"), ...none };
|
|
233
236
|
if (inputs.hasDraft) return { kind: "draft", label: "Unsubmitted draft", glyph: glyph("Edit", "muted"), ...none };
|
|
234
|
-
return { kind: "idle", label: "Idle", glyph: glyph(
|
|
237
|
+
return { kind: "idle", label: "Idle", glyph: glyph("ring", "muted"), ...none };
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/**
|