@protobase/ui 0.55.3 → 0.55.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/dist/app/create/create-page.js +11 -1
- package/dist/app/marks/marks-hook.d.ts +10 -0
- package/dist/app/nav-recent.js +10 -1
- package/dist/app/pages/merge-ui.d.ts +1 -1
- package/dist/app/pages/merge-ui.js +3 -1
- package/dist/app/pages/project-ui.d.ts +4 -1
- package/dist/app/use-live-save.js +11 -4
- package/dist/app/use-nav-recent.js +10 -6
- package/dist/app-shell/index.d.ts +1 -1
- package/dist/app-shell/nav.d.ts +13 -1
- package/dist/app-shell/record-marks.js +18 -0
- package/dist/app-shell/sidebar-group.js +10 -17
- package/dist/app-shell/sidebar-records.js +54 -0
- package/dist/auth/auth-provider.d.ts +4 -0
- package/dist/auth/use-signed-in-user.d.ts +13 -0
- package/dist/auth/use-signed-in-user.js +22 -0
- package/dist/chat/chat-thread.d.ts +10 -4
- package/dist/chat/chat-thread.js +20 -6
- package/dist/chat/thread-scroll.js +4 -1
- package/dist/chat/use-read-to-end.js +32 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -1
- package/dist/notifications/bell/notification-bell.js +5 -8
- package/dist/notifications/bell/unread-label.js +1 -3
- package/dist/notifications/push/push-notifications.d.ts +1 -0
- package/dist/package.js +1 -1
- package/dist/plugins/plugin-ui.js +1 -0
- package/dist/primitives/count-badge.js +20 -0
- package/dist/shell/assets/app-NiljOxYe.js +9 -0
- package/dist/shell/assets/{index-CEhwvXIf.js → index-BIUTidjX.js} +2 -2
- package/dist/shell/assets/{records-map-Cp4sUeC-.js → records-map-BwX2z-di.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-BD_5D_Ot.js +2 -0
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-NiljOxYe.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-CEhwvXIf.js.map → index-BIUTidjX.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-Cp4sUeC-.js.map → records-map-BwX2z-di.js.map} +1 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-BD_5D_Ot.js.map +1 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-CAj-S5YI.js +0 -9
- package/dist/shell/assets/shared-protobase-ui-Dd4pdw8C.js +0 -2
- package/dist/shell-source-maps/assets/app-CAj-S5YI.js.map +0 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-Dd4pdw8C.js.map +0 -1
|
@@ -12,9 +12,10 @@ import { can } from "../permissions.js";
|
|
|
12
12
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
13
13
|
import { fieldErrors, fieldMessage } from "../field-errors.js";
|
|
14
14
|
import { useFocusFirstInvalid } from "../use-focus-first-invalid.js";
|
|
15
|
-
import { useLiveSave } from "../use-live-save.js";
|
|
15
|
+
import { nothingSaved, useLiveSave } from "../use-live-save.js";
|
|
16
16
|
import { CreateRecordForm } from "./create-record-form.js";
|
|
17
17
|
import { useEffect, useState } from "react";
|
|
18
|
+
import { isApiError } from "@protobase/client";
|
|
18
19
|
import { jsx } from "react/jsx-runtime";
|
|
19
20
|
import { useQueryClient } from "@tanstack/react-query";
|
|
20
21
|
//#region src/app/create/create-page.tsx
|
|
@@ -92,6 +93,15 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
92
93
|
try {
|
|
93
94
|
created = await client.create(resource, createBody(model, meta.resources, draft));
|
|
94
95
|
} catch (error) {
|
|
96
|
+
const existing = isApiError(error) ? error.existing : void 0;
|
|
97
|
+
if (existing) {
|
|
98
|
+
setNewRecord({
|
|
99
|
+
kind: "record",
|
|
100
|
+
resource,
|
|
101
|
+
key: existing.key
|
|
102
|
+
});
|
|
103
|
+
return nothingSaved;
|
|
104
|
+
}
|
|
95
105
|
const found = fieldErrors(error);
|
|
96
106
|
setErrors(found);
|
|
97
107
|
if (Object.keys(found).length > 0) setRejections((count) => count + 1);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RecordMark } from "../../app-shell/nav.js";
|
|
2
|
+
//#region src/app/marks/marks-hook.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A hook a view's `.nav({ recent: { marks: name } })` names: how each record of `resource` stands out in the sidebar
|
|
5
|
+
* for the person reading, by the record's key as its address carries it, such as an unread conversation drawn bold with a
|
|
6
|
+
* count of its mentions. The sidebar group calls it while it shows the records, so it may use the API client, live
|
|
7
|
+
* updates and queries; returning undefined, as while it loads, marks none.
|
|
8
|
+
*/
|
|
9
|
+
export type MarksHook = (resource: string) => Record<string, RecordMark> | undefined;
|
|
10
|
+
//#endregion
|
package/dist/app/nav-recent.js
CHANGED
|
@@ -48,5 +48,14 @@ const recentRowDiffers = (model, fields, rows, key, record) => {
|
|
|
48
48
|
const row = rows.find((entry) => encodeKey(recordKey(model, entry)) === key);
|
|
49
49
|
return row !== void 0 && fields.some((field) => !Object.is(row[field], record[field]));
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The hook that marks a group's records for the reader: the project UI's `marks` hook the group names, called with its
|
|
53
|
+
* resource. None when the group names no marks, or a name no UI config registers, such as a plugin that is turned off.
|
|
54
|
+
*/
|
|
55
|
+
const recentMarks = (resource, recent, hooks) => {
|
|
56
|
+
if (recent.marks === void 0 || !hooks || !Object.hasOwn(hooks, recent.marks)) return;
|
|
57
|
+
const hook = hooks[recent.marks];
|
|
58
|
+
return () => hook(resource);
|
|
59
|
+
};
|
|
51
60
|
//#endregion
|
|
52
|
-
export { recentRecords, recentRequest, recentRowDiffers };
|
|
61
|
+
export { recentMarks, recentRecords, recentRequest, recentRowDiffers };
|
|
@@ -2,7 +2,7 @@ import { ProjectUi } from "./project-ui.js";
|
|
|
2
2
|
//#region src/app/pages/merge-ui.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
5
|
-
* handlers, field types, drop rules, board drops
|
|
5
|
+
* handlers, field types, drop rules, board drops, title rules and marks hooks are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
6
6
|
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one.
|
|
7
7
|
*/
|
|
8
8
|
export declare const mergeUi: (...uis: ProjectUi[]) => ProjectUi;
|
|
@@ -35,7 +35,7 @@ const around = (wrappers) => {
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
38
|
-
* handlers, field types, drop rules, board drops
|
|
38
|
+
* handlers, field types, drop rules, board drops, title rules and marks hooks are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
39
39
|
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one.
|
|
40
40
|
*/
|
|
41
41
|
const mergeUi = (...uis) => {
|
|
@@ -46,6 +46,7 @@ const mergeUi = (...uis) => {
|
|
|
46
46
|
const dropRules = union("The drop rule", named, (ui) => ui.dropRules);
|
|
47
47
|
const boardDrops = union("The board drop", named, (ui) => ui.boardDrops);
|
|
48
48
|
const titles = union("The title rule", named, (ui) => ui.titles);
|
|
49
|
+
const marks = union("The marks hook", named, (ui) => ui.marks);
|
|
49
50
|
const actionSlot = slot(named.flatMap(({ ui }) => ui.shell?.actions ? [ui.shell.actions] : []));
|
|
50
51
|
const rightPanel = slot(named.flatMap(({ ui }) => ui.shell?.rightPanel ? [ui.shell.rightPanel] : []));
|
|
51
52
|
const logo = named.findLast(({ ui }) => ui.shell?.logo)?.ui.shell?.logo;
|
|
@@ -60,6 +61,7 @@ const mergeUi = (...uis) => {
|
|
|
60
61
|
...Object.keys(dropRules).length > 0 && { dropRules },
|
|
61
62
|
...Object.keys(boardDrops).length > 0 && { boardDrops },
|
|
62
63
|
...Object.keys(titles).length > 0 && { titles },
|
|
64
|
+
...Object.keys(marks).length > 0 && { marks },
|
|
63
65
|
...(actionSlot || rightPanel || logo || bell) && { shell: {
|
|
64
66
|
...actionSlot && { actions: actionSlot },
|
|
65
67
|
...rightPanel && { rightPanel },
|
|
@@ -3,6 +3,7 @@ import { FieldWidget } from "../field-types/field-widget.js";
|
|
|
3
3
|
import { BoardDrop } from "../displays/board-drop.js";
|
|
4
4
|
import { DropRule } from "../hand-order/drop-rule.js";
|
|
5
5
|
import { TitleRule } from "../titles/title-rule.js";
|
|
6
|
+
import { MarksHook } from "../marks/marks-hook.js";
|
|
6
7
|
import { ComponentType, ReactNode } from "react";
|
|
7
8
|
import { Client } from "@protobase/client";
|
|
8
9
|
import { AppAddress } from "@protobase/schema";
|
|
@@ -47,7 +48,7 @@ export type BeforeSignInProps = {
|
|
|
47
48
|
* The project's own React code for composed pages: custom components by the name their layout declares
|
|
48
49
|
* (`component('UsageChart')`) or a widget part names, handlers for named actions that have no built-in behaviour,
|
|
49
50
|
* field types by the name a view's `.widget(name)` chooses, drop rules by the name a list's `drag.rule` gives, board
|
|
50
|
-
* drops by the name a list's `drag.board` gives, title rules by the name a view's `.titleFrom` gives, and the shell's slots. `extends` lists UI configs merged in before it (see `mergeUi`), or whole `protobase.ui.tsx` modules
|
|
51
|
+
* drops by the name a list's `drag.board` gives, title rules by the name a view's `.titleFrom` gives, marks hooks by the name a view's `nav.recent.marks` gives, and the shell's slots. `extends` lists UI configs merged in before it (see `mergeUi`), or whole `protobase.ui.tsx` modules
|
|
51
52
|
* (`import * as tasksUi from '...'`), their exported components included; `name` names it in their errors.
|
|
52
53
|
*/
|
|
53
54
|
export type ProjectUi = {
|
|
@@ -63,6 +64,8 @@ export type ProjectUi = {
|
|
|
63
64
|
boardDrops?: Record<string, BoardDrop>;
|
|
64
65
|
/** A record's title for the person reading it, by the name a view's `.titleFrom` gives (see `TitleRule`). */
|
|
65
66
|
titles?: Record<string, TitleRule>;
|
|
67
|
+
/** How the sidebar marks a resource's records for the person reading, by the name a view's `nav.recent.marks` gives (see `MarksHook`). */
|
|
68
|
+
marks?: Record<string, MarksHook>;
|
|
66
69
|
/**
|
|
67
70
|
* Rendered in the shell on every page: `actions` at the end of the top bar, `rightPanel` beside the page, `logo`
|
|
68
71
|
* at the top of the sidebar in place of Protobase's (see `ShellLogoProps`), and `bell` in the top bar in place of the
|
|
@@ -5,10 +5,13 @@ import { useEffect, useRef, useState } from "react";
|
|
|
5
5
|
import { isApiError } from "@protobase/client";
|
|
6
6
|
//#region src/app/use-live-save.ts
|
|
7
7
|
const savedForMs = 2e3;
|
|
8
|
+
/** What a save's action resolves to when it wrote nothing and needs no word, such as a create that opens the record it repeats. */
|
|
9
|
+
const nothingSaved = Symbol("nothing saved");
|
|
8
10
|
/**
|
|
9
11
|
* Shows the outcome of a save the way the view asks: a toast (spinner, then check) or the state of
|
|
10
|
-
* the Save button. API errors are reported and not rethrown; anything else propagates.
|
|
11
|
-
* save went through
|
|
12
|
+
* the Save button. API errors are reported and not rethrown; anything else propagates. An action that resolves to
|
|
13
|
+
* `nothingSaved` wrote nothing and says nothing: its toast goes away. `run` resolves to whether the save went through
|
|
14
|
+
* or wrote nothing.
|
|
12
15
|
*/
|
|
13
16
|
const useLiveSave = (mode, subject) => {
|
|
14
17
|
const toast = useToast();
|
|
@@ -24,7 +27,11 @@ const useLiveSave = (mode, subject) => {
|
|
|
24
27
|
setMessage(void 0);
|
|
25
28
|
if (mode === "button") setPhase("saving");
|
|
26
29
|
try {
|
|
27
|
-
await action()
|
|
30
|
+
if (await action() === nothingSaved) {
|
|
31
|
+
if (id !== void 0) toast.dismiss(id);
|
|
32
|
+
if (mode === "button") setPhase("idle");
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
28
35
|
if (id !== void 0) toast.update(id, {
|
|
29
36
|
state: "success",
|
|
30
37
|
title: text("subjectSaved", subject)
|
|
@@ -57,4 +64,4 @@ const useLiveSave = (mode, subject) => {
|
|
|
57
64
|
};
|
|
58
65
|
};
|
|
59
66
|
//#endregion
|
|
60
|
-
export { useLiveSave };
|
|
67
|
+
export { nothingSaved, useLiveSave };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { keys } from "../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../data/api-provider.js";
|
|
3
|
+
import { useProjectUi } from "./pages/project-ui.js";
|
|
3
4
|
import { useRecordTitle } from "./titles/record-titles.js";
|
|
4
5
|
import { userMenuResources } from "./user-menu-from-meta.js";
|
|
5
|
-
import { recentRecords, recentRequest, recentRowDiffers } from "./nav-recent.js";
|
|
6
|
+
import { recentMarks, recentRecords, recentRequest, recentRowDiffers } from "./nav-recent.js";
|
|
6
7
|
import { useEffect, useMemo } from "react";
|
|
7
8
|
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
8
9
|
//#region src/app/use-nav-recent.ts
|
|
@@ -31,11 +32,12 @@ const useFollowRecords = (groups) => {
|
|
|
31
32
|
/**
|
|
32
33
|
* The records of every sidebar group (`nav.recent`), one list request each, under the `list` query key so that any
|
|
33
34
|
* write to the resource refreshes them, as does reading one of their records anew. The server applies the caller's
|
|
34
|
-
* access, as on the list page.
|
|
35
|
+
* access, as on the list page. A group that names `marks` gets the project UI's hook, which the sidebar calls.
|
|
35
36
|
*/
|
|
36
37
|
const useNavRecent = (meta, basePath, active) => {
|
|
37
38
|
const client = useClient();
|
|
38
39
|
const titleOf = useRecordTitle();
|
|
40
|
+
const { marks } = useProjectUi();
|
|
39
41
|
const groups = useMemo(() => {
|
|
40
42
|
const inMenu = userMenuResources(meta);
|
|
41
43
|
return Object.values(meta.views).flatMap((view) => {
|
|
@@ -45,23 +47,25 @@ const useNavRecent = (meta, basePath, active) => {
|
|
|
45
47
|
model,
|
|
46
48
|
view,
|
|
47
49
|
recent,
|
|
48
|
-
request: recentRequest(model, view, recent)
|
|
50
|
+
request: recentRequest(model, view, recent),
|
|
51
|
+
useMarks: recentMarks(model.name, recent, marks)
|
|
49
52
|
}] : [];
|
|
50
53
|
});
|
|
51
|
-
}, [meta]);
|
|
54
|
+
}, [meta, marks]);
|
|
52
55
|
useFollowRecords(groups);
|
|
53
56
|
const results = useQueries({ queries: groups.map((group) => ({
|
|
54
57
|
queryKey: groupKey(group),
|
|
55
58
|
queryFn: () => client.list(group.model.name, group.request)
|
|
56
59
|
})) });
|
|
57
|
-
return Object.fromEntries(groups.map(({ model, view, recent }, index) => {
|
|
60
|
+
return Object.fromEntries(groups.map(({ model, view, recent, useMarks }, index) => {
|
|
58
61
|
const result = results[index];
|
|
59
62
|
if (result.isPending) return [model.name, { state: "loading" }];
|
|
60
63
|
if (result.isError) return [model.name, { state: "error" }];
|
|
61
64
|
const activeKey = active.resource === model.name ? active.key : void 0;
|
|
62
65
|
return [model.name, {
|
|
63
66
|
state: "ready",
|
|
64
|
-
records: recentRecords(model, view, recent, result.data.items, basePath, activeKey, titleOf)
|
|
67
|
+
records: recentRecords(model, view, recent, result.data.items, basePath, activeKey, titleOf),
|
|
68
|
+
...useMarks && { useMarks }
|
|
65
69
|
}];
|
|
66
70
|
}));
|
|
67
71
|
};
|
package/dist/app-shell/nav.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import "lucide-react";
|
|
2
|
-
import "@protobase/schema";
|
|
2
|
+
import "@protobase/schema";
|
|
3
|
+
//#region src/app-shell/nav.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* How a record under a sidebar entry stands out for the person reading, such as an unread conversation: `strong` draws
|
|
6
|
+
* its title bold, `count` a badge at the end of its row (none at 0), and `label`, such as "3 mentions", is added to its
|
|
7
|
+
* name for screen readers and its tooltip.
|
|
8
|
+
*/
|
|
9
|
+
export type RecordMark = {
|
|
10
|
+
strong?: boolean;
|
|
11
|
+
count?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/app-shell/record-marks.ts
|
|
2
|
+
const markRecords = (records, marks) => records.map((record) => {
|
|
3
|
+
const mark = marks && Object.hasOwn(marks, record.id) ? marks[record.id] : void 0;
|
|
4
|
+
return mark ? {
|
|
5
|
+
...record,
|
|
6
|
+
mark
|
|
7
|
+
} : record;
|
|
8
|
+
});
|
|
9
|
+
/** What a sidebar record is called for screen readers and in its tooltip: its title, with its status and its mark's label. */
|
|
10
|
+
const recordNames = (record) => {
|
|
11
|
+
const extra = [record.status?.label, record.mark?.label].filter((part) => part !== void 0 && part !== "");
|
|
12
|
+
return {
|
|
13
|
+
ariaLabel: [record.label, ...extra].join(", "),
|
|
14
|
+
title: extra.length === 0 ? record.label : `${record.label} (${extra.join(", ")})`
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { markRecords, recordNames };
|
|
@@ -3,14 +3,15 @@ import { text } from "./messages.js";
|
|
|
3
3
|
import { subRow, subRowIndent } from "./sidebar-rows.js";
|
|
4
4
|
import { navOpenStorageKey, readNavOpen, writeNavOpen } from "./nav-open-storage.js";
|
|
5
5
|
import { SidebarItem } from "./sidebar-item.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SidebarRecords } from "./sidebar-records.js";
|
|
7
7
|
import { useId, useState } from "react";
|
|
8
8
|
import { ChevronRight } from "lucide-react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/app-shell/sidebar-group.tsx
|
|
11
11
|
/**
|
|
12
|
-
* A sidebar entry with a few of its records under it
|
|
13
|
-
* records (remembered in this browser), and "View all" at the end opens
|
|
12
|
+
* A sidebar entry with a few of its records under it, each marked for the reader when the group has marks. The label
|
|
13
|
+
* opens the list, the chevron shows or hides the records (remembered in this browser), and "View all" at the end opens
|
|
14
|
+
* the list too.
|
|
14
15
|
*/
|
|
15
16
|
const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
|
|
16
17
|
const storageKey = navOpenStorageKey(item.id);
|
|
@@ -60,20 +61,12 @@ const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
|
|
|
60
61
|
className: cn("flex items-center text-faint-foreground", subRow[mode]),
|
|
61
62
|
children: text("couldNotLoad")
|
|
62
63
|
}),
|
|
63
|
-
recent.state === "ready" &&
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
children: [record.status && /* @__PURE__ */ jsx(StatusDot, {
|
|
70
|
-
tone: record.status.tone,
|
|
71
|
-
pulse: record.status.pulse
|
|
72
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
73
|
-
className: "truncate",
|
|
74
|
-
children: record.label
|
|
75
|
-
})]
|
|
76
|
-
}) }, record.id)),
|
|
64
|
+
recent.state === "ready" && /* @__PURE__ */ jsx(SidebarRecords, {
|
|
65
|
+
records: recent.records,
|
|
66
|
+
useMarks: recent.useMarks,
|
|
67
|
+
mode,
|
|
68
|
+
onNavigate
|
|
69
|
+
}),
|
|
77
70
|
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
|
|
78
71
|
...link(href),
|
|
79
72
|
className: cn("flex items-center rounded-md text-faint-foreground transition-colors hover:bg-muted hover:text-foreground", subRow[mode]),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { subRow } from "./sidebar-rows.js";
|
|
3
|
+
import { CountBadge } from "../primitives/count-badge.js";
|
|
4
|
+
import { markRecords, recordNames } from "./record-marks.js";
|
|
5
|
+
import { StatusDot } from "./status-dot.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/app-shell/sidebar-records.tsx
|
|
8
|
+
const RecordRows = ({ records, mode, onNavigate }) => records.map((record) => {
|
|
9
|
+
const { ariaLabel, title } = recordNames(record);
|
|
10
|
+
const open = onNavigate && ((event) => {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
onNavigate(record.href);
|
|
13
|
+
});
|
|
14
|
+
const strong = record.mark?.strong === true;
|
|
15
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
|
|
16
|
+
href: record.href,
|
|
17
|
+
onClick: open,
|
|
18
|
+
"aria-label": ariaLabel,
|
|
19
|
+
title,
|
|
20
|
+
"aria-current": record.active ? "page" : void 0,
|
|
21
|
+
className: cn("flex min-w-0 items-center rounded-md transition-colors", subRow[mode], strong ? "font-bold" : "font-medium", record.active || strong ? "text-foreground" : "text-muted-foreground", record.active ? "bg-muted" : "hover:bg-muted hover:text-foreground"),
|
|
22
|
+
children: [
|
|
23
|
+
record.status && /* @__PURE__ */ jsx(StatusDot, {
|
|
24
|
+
tone: record.status.tone,
|
|
25
|
+
pulse: record.status.pulse
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ jsx("span", {
|
|
28
|
+
className: "truncate",
|
|
29
|
+
children: record.label
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx(CountBadge, {
|
|
32
|
+
count: record.mark?.count,
|
|
33
|
+
className: "ml-auto shrink-0"
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
}) }, record.id);
|
|
37
|
+
});
|
|
38
|
+
const MarkedRows = ({ records, useMarks, ...rest }) => /* @__PURE__ */ jsx(RecordRows, {
|
|
39
|
+
records: markRecords(records, useMarks()),
|
|
40
|
+
...rest
|
|
41
|
+
});
|
|
42
|
+
/** The rows of a sidebar group's records, marked by the group's `useMarks` hook when it has one. */
|
|
43
|
+
const SidebarRecords = ({ records, useMarks, mode, onNavigate }) => useMarks ? /* @__PURE__ */ jsx(MarkedRows, {
|
|
44
|
+
records,
|
|
45
|
+
useMarks,
|
|
46
|
+
mode,
|
|
47
|
+
onNavigate
|
|
48
|
+
}) : /* @__PURE__ */ jsx(RecordRows, {
|
|
49
|
+
records,
|
|
50
|
+
mode,
|
|
51
|
+
onNavigate
|
|
52
|
+
});
|
|
53
|
+
//#endregion
|
|
54
|
+
export { SidebarRecords };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import "./auth-provider.js";
|
|
2
|
+
//#region src/auth/use-signed-in-user.d.ts
|
|
3
|
+
/** Who is signed in: their user id, the id `.author()` stores and mentions hold, and their name. */
|
|
4
|
+
export type SignedInUser = {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The signed-in person, from the session the app already holds, such as for a filter on their own user id;
|
|
10
|
+
* `undefined` while the session loads, for a visitor and when nobody is signed in.
|
|
11
|
+
*/
|
|
12
|
+
export declare const useSignedInUser: () => SignedInUser | undefined;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useOptionalAuth } from "./auth-provider.js";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
//#region src/auth/use-signed-in-user.ts
|
|
4
|
+
/** The signed-in person of an auth state; none while it loads, for a visitor, and before the sign-in finished. */
|
|
5
|
+
const signedInUserOf = (state) => {
|
|
6
|
+
if (state?.kind !== "signed-in") return;
|
|
7
|
+
const { id, name } = state.user;
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
...name && { name }
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The signed-in person, from the session the app already holds, such as for a filter on their own user id;
|
|
15
|
+
* `undefined` while the session loads, for a visitor and when nobody is signed in.
|
|
16
|
+
*/
|
|
17
|
+
const useSignedInUser = () => {
|
|
18
|
+
const state = useOptionalAuth()?.state;
|
|
19
|
+
return useMemo(() => signedInUserOf(state), [state]);
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { signedInUserOf, useSignedInUser };
|
|
@@ -12,11 +12,17 @@ export type ChatThreadProps = {
|
|
|
12
12
|
loadingEarlier?: boolean;
|
|
13
13
|
/** Changes when the reader sends a message, such as a count of their sends: each change goes to the end wherever they were, and the message that arrives next follows. */
|
|
14
14
|
followKey?: string | number;
|
|
15
|
+
/**
|
|
16
|
+
* Called while the page shows and the reader is at the end of the thread: when it opens or a message arrives there,
|
|
17
|
+
* when they scroll to the end, and when the page shows again with them there. Moves a read position, for one.
|
|
18
|
+
*/
|
|
19
|
+
onReadToEnd?: () => void;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
|
-
* The scrolling list of a chat's messages.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* The scrolling list of a chat's messages. It opens on its `ChatDivider tone="new"` line when it has one, else at the
|
|
23
|
+
* end. A reader at the end follows new messages; one who scrolled back stays where they are until `followKey` changes,
|
|
24
|
+
* and earlier messages loaded above keep them on the message they were reading. It scrolls inside itself, not with the
|
|
25
|
+
* page, since a chat keeps its composer in view beneath it.
|
|
20
26
|
*/
|
|
21
|
-
export declare const ChatThread: ({ children, empty, length, onLoadEarlier, loadingEarlier, followKey }: ChatThreadProps) => import("react").JSX.Element;
|
|
27
|
+
export declare const ChatThread: ({ children, empty, length, onLoadEarlier, loadingEarlier, followKey, onReadToEnd }: ChatThreadProps) => import("react").JSX.Element;
|
|
22
28
|
//#endregion
|
package/dist/chat/chat-thread.js
CHANGED
|
@@ -3,15 +3,18 @@ import { Button } from "../primitives/button.js";
|
|
|
3
3
|
import { text } from "./messages.js";
|
|
4
4
|
import { scrollAfterChange } from "./thread-scroll.js";
|
|
5
5
|
import { readPlace } from "./read-place.js";
|
|
6
|
+
import { useReadToEnd } from "./use-read-to-end.js";
|
|
6
7
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
//#region src/chat/chat-thread.tsx
|
|
10
|
+
const newLineOf = (list) => list.querySelector("[data-chat-divider][data-tone=\"new\"]");
|
|
9
11
|
/**
|
|
10
|
-
* The scrolling list of a chat's messages.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The scrolling list of a chat's messages. It opens on its `ChatDivider tone="new"` line when it has one, else at the
|
|
13
|
+
* end. A reader at the end follows new messages; one who scrolled back stays where they are until `followKey` changes,
|
|
14
|
+
* and earlier messages loaded above keep them on the message they were reading. It scrolls inside itself, not with the
|
|
15
|
+
* page, since a chat keeps its composer in view beneath it.
|
|
13
16
|
*/
|
|
14
|
-
const ChatThread = ({ children, empty, length, onLoadEarlier, loadingEarlier, followKey }) => {
|
|
17
|
+
const ChatThread = ({ children, empty, length, onLoadEarlier, loadingEarlier, followKey, onReadToEnd }) => {
|
|
15
18
|
const box = useRef(null);
|
|
16
19
|
const list = useRef(null);
|
|
17
20
|
const place = useRef({
|
|
@@ -22,20 +25,31 @@ const ChatThread = ({ children, empty, length, onLoadEarlier, loadingEarlier, fo
|
|
|
22
25
|
const remember = () => {
|
|
23
26
|
if (box.current && list.current) place.current = readPlace(box.current, list.current, followKey);
|
|
24
27
|
};
|
|
28
|
+
const readToEnd = () => place.current.atEnd && place.current.first !== void 0;
|
|
29
|
+
const tellRead = useReadToEnd(onReadToEnd, readToEnd);
|
|
30
|
+
const scrolled = () => {
|
|
31
|
+
const wasAtEnd = place.current.atEnd;
|
|
32
|
+
remember();
|
|
33
|
+
if (!wasAtEnd && readToEnd()) tellRead();
|
|
34
|
+
};
|
|
25
35
|
useLayoutEffect(() => {
|
|
26
36
|
const element = box.current;
|
|
27
37
|
if (!element || !list.current) return;
|
|
28
38
|
const before = place.current;
|
|
29
39
|
const anchorTop = before.anchor && before.anchor.element.isConnected ? before.anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top : void 0;
|
|
40
|
+
const newLine = newLineOf(list.current);
|
|
41
|
+
const newLineTop = newLine ? newLine.getBoundingClientRect().top - element.getBoundingClientRect().top : void 0;
|
|
30
42
|
const next = scrollAfterChange(before, {
|
|
31
43
|
first: list.current.firstElementChild ?? void 0,
|
|
32
44
|
anchorTop,
|
|
33
45
|
scrollTop: element.scrollTop,
|
|
34
46
|
scrollHeight: element.scrollHeight,
|
|
35
|
-
followKey
|
|
47
|
+
followKey,
|
|
48
|
+
newLineTop
|
|
36
49
|
});
|
|
37
50
|
if (next !== void 0) element.scrollTop = next;
|
|
38
51
|
remember();
|
|
52
|
+
if (readToEnd()) tellRead();
|
|
39
53
|
}, [length, followKey]);
|
|
40
54
|
useEffect(() => {
|
|
41
55
|
const element = box.current;
|
|
@@ -50,7 +64,7 @@ const ChatThread = ({ children, empty, length, onLoadEarlier, loadingEarlier, fo
|
|
|
50
64
|
const showsEmpty = length === 0 && Boolean(empty);
|
|
51
65
|
return /* @__PURE__ */ jsxs("div", {
|
|
52
66
|
ref: box,
|
|
53
|
-
onScroll:
|
|
67
|
+
onScroll: scrolled,
|
|
54
68
|
className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 [overflow-anchor:none]",
|
|
55
69
|
children: [
|
|
56
70
|
onLoadEarlier && /* @__PURE__ */ jsx(Button, {
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
const isAtEnd = (box) => box.scrollHeight - box.scrollTop - box.clientHeight <= 24;
|
|
3
3
|
/**
|
|
4
4
|
* The scroll position a chat takes once its messages or follow key changed, or `undefined` to stay put. A changed follow
|
|
5
|
-
* key, such as the reader sending a message, goes to the end wherever the reader was.
|
|
5
|
+
* key, such as the reader sending a message, goes to the end wherever the reader was. A chat that gets its first
|
|
6
|
+
* messages opens with its "New messages" line near the top, when it has one at `newLineTop` below the top of the box,
|
|
7
|
+
* and at the end otherwise. Messages loaded before the first
|
|
6
8
|
* keep the reader on the message they were reading, moved by as much as it moved; otherwise a reader at the end follows
|
|
7
9
|
* the newest message, and one who scrolled back stays where they are.
|
|
8
10
|
*/
|
|
9
11
|
const scrollAfterChange = (before, now) => {
|
|
10
12
|
if (now.followKey !== before.followKey) return now.scrollHeight;
|
|
13
|
+
if (before.first === void 0 && now.first !== void 0 && now.newLineTop !== void 0) return Math.max(0, now.scrollTop + now.newLineTop - 16);
|
|
11
14
|
if (before.first !== void 0 && now.first !== before.first && before.anchor && now.anchorTop !== void 0) return now.scrollTop + now.anchorTop - before.anchor.top;
|
|
12
15
|
if (before.atEnd) return now.scrollHeight;
|
|
13
16
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
2
|
+
//#region src/chat/use-read-to-end.ts
|
|
3
|
+
/**
|
|
4
|
+
* Tells `onReadToEnd` that the reader is at the end of a chat, but only while the page shows: the returned function
|
|
5
|
+
* tells it now, and the page becoming visible again tells it when `atEnd()` holds then. Call it before the layout
|
|
6
|
+
* effects that tell, so they tell the latest `onReadToEnd`.
|
|
7
|
+
*/
|
|
8
|
+
const useReadToEnd = (onReadToEnd, atEnd) => {
|
|
9
|
+
const latest = useRef({
|
|
10
|
+
onReadToEnd,
|
|
11
|
+
atEnd
|
|
12
|
+
});
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
latest.current = {
|
|
15
|
+
onReadToEnd,
|
|
16
|
+
atEnd
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
const tell = useCallback(() => {
|
|
20
|
+
if (document.visibilityState === "visible") latest.current.onReadToEnd?.();
|
|
21
|
+
}, []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const shown = () => {
|
|
24
|
+
if (latest.current.atEnd()) tell();
|
|
25
|
+
};
|
|
26
|
+
document.addEventListener("visibilitychange", shown);
|
|
27
|
+
return () => document.removeEventListener("visibilitychange", shown);
|
|
28
|
+
}, [tell]);
|
|
29
|
+
return tell;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { useReadToEnd };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { RecordMark } from "./app-shell/nav.js";
|
|
1
2
|
import { LogoMark, ShellLogoProps } from "./app-shell/logo.js";
|
|
2
3
|
import { FieldWidget, FieldWidgetEditorProps, FieldWidgetProps } from "./app/field-types/field-widget.js";
|
|
3
4
|
import { BoardDrop, BoardDropContext, BoardDropUndo, BoardDropWrite } from "./app/displays/board-drop.js";
|
|
4
5
|
import { Drop, DropRule } from "./app/hand-order/drop-rule.js";
|
|
5
6
|
import { TitleReader, TitleRule } from "./app/titles/title-rule.js";
|
|
7
|
+
import { MarksHook } from "./app/marks/marks-hook.js";
|
|
6
8
|
import { ActionContext, ActionHandler, BeforeSignInProps, ProjectUi, definePlugin, defineUi } from "./app/pages/project-ui.js";
|
|
7
9
|
import { PluginInfo, usePlugin } from "./plugins/plugin-context.js";
|
|
8
10
|
import { App, AppProps } from "./app/app.js";
|
|
@@ -26,8 +28,9 @@ import { ListDisplayRow, RecordCardProps } from "./app/displays/list-displays.js
|
|
|
26
28
|
import { ListDisplay, useListDisplay } from "./app/displays/display-context.js";
|
|
27
29
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
28
30
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
29
|
-
import { MembersResult, useMembers } from "./people/use-members.js";
|
|
30
31
|
import { PushState } from "./notifications/push/web-push.js";
|
|
32
|
+
import { SignedInUser, useSignedInUser } from "./auth/use-signed-in-user.js";
|
|
33
|
+
import { MembersResult, useMembers } from "./people/use-members.js";
|
|
31
34
|
import { usePushNotifications } from "./notifications/push/use-push-notifications.js";
|
|
32
35
|
import { useInbox, useMarkNotificationsRead, useUnreadNotifications } from "./notifications/bell/use-inbox.js";
|
|
33
36
|
import { SignInPrompt, SignInPromptProps } from "./auth/sign-in-prompt.js";
|
|
@@ -81,4 +84,4 @@ import { Section } from "./record-view/section.js";
|
|
|
81
84
|
import { Form, FormProps } from "./app/create/form.js";
|
|
82
85
|
import { RecordPicker, RecordPickerProps } from "./app/record-picker.js";
|
|
83
86
|
import { Spinner } from "./primitives/spinner.js";
|
|
84
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, type MembersResult, MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PeopleSearch, type PickedUser, type PluginInfo, type ProjectUi, type PushState, type RecordCardProps, RecordLink, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, type TitleReader, type TitleRule, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
|
87
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, type MarksHook, type MembersResult, MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PeopleSearch, type PickedUser, type PluginInfo, type ProjectUi, type PushState, type RecordCardProps, RecordLink, type RecordMark, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type SignedInUser, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, type TitleReader, type TitleRule, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useUnreadNotifications, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,8 @@ import { useInbox, useMarkNotificationsRead, useUnreadNotifications } from "./no
|
|
|
59
59
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
60
60
|
import { App } from "./app/app.js";
|
|
61
61
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
62
|
+
import { useSignedInUser } from "./auth/use-signed-in-user.js";
|
|
62
63
|
import { Carousel } from "./carousel/carousel.js";
|
|
63
64
|
import { Choices } from "./primitives/choices.js";
|
|
64
65
|
import { RecordPicker } from "./app/record-picker.js";
|
|
65
|
-
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, Carousel, ChatDivider, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MentionBox, MentionedText, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
|
66
|
+
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, Carousel, ChatDivider, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MentionBox, MentionedText, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useUnreadNotifications, useUpdateRecord };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Button } from "../../primitives/button.js";
|
|
2
|
-
import { Badge } from "../../primitives/badge.js";
|
|
3
2
|
import { useRouter } from "../../app/router.js";
|
|
4
3
|
import { usePopoverMenu } from "../../primitives/popover-menu.js";
|
|
4
|
+
import { CountBadge } from "../../primitives/count-badge.js";
|
|
5
5
|
import { usePushNotifications } from "../push/use-push-notifications.js";
|
|
6
6
|
import { NotificationPanel } from "./notification-panel.js";
|
|
7
7
|
import { notificationTarget } from "./notification-target.js";
|
|
8
8
|
import { PushControl } from "./push-control.js";
|
|
9
9
|
import { historyRefused, useInbox, useMarkNotificationsRead, useUnreadNotifications } from "./use-inbox.js";
|
|
10
|
-
import { bellLabel
|
|
10
|
+
import { bellLabel } from "./unread-label.js";
|
|
11
11
|
import { useState } from "react";
|
|
12
12
|
import { Bell } from "lucide-react";
|
|
13
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -25,7 +25,6 @@ const NotificationBell = () => {
|
|
|
25
25
|
const push = usePushNotifications();
|
|
26
26
|
const { navigate, basePath } = useRouter();
|
|
27
27
|
const [now, setNow] = useState(Date.now);
|
|
28
|
-
const badge = unreadBadge(unread.data);
|
|
29
28
|
const label = bellLabel(unread.data);
|
|
30
29
|
const origin = window.location.origin;
|
|
31
30
|
if (historyRefused(unread.error)) return null;
|
|
@@ -47,11 +46,9 @@ const NotificationBell = () => {
|
|
|
47
46
|
"aria-expanded": open,
|
|
48
47
|
onClick: toggle,
|
|
49
48
|
className: "relative max-md:size-9",
|
|
50
|
-
children: [/* @__PURE__ */ jsx(Bell, { className: "size-4 max-md:size-5" }),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
className: "pointer-events-none absolute -top-1 -right-1.5 h-4 min-w-4 justify-center px-1 text-[10px] font-semibold max-md:top-0 max-md:right-0",
|
|
54
|
-
children: badge
|
|
49
|
+
children: [/* @__PURE__ */ jsx(Bell, { className: "size-4 max-md:size-5" }), /* @__PURE__ */ jsx(CountBadge, {
|
|
50
|
+
count: unread.data,
|
|
51
|
+
className: "pointer-events-none absolute -top-1 -right-1.5 max-md:top-0 max-md:right-0"
|
|
55
52
|
})]
|
|
56
53
|
}), open && createPortal(/* @__PURE__ */ jsx("div", {
|
|
57
54
|
ref: menu,
|