@protobase/ui 0.23.3 → 0.23.5
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/app.d.ts +4 -1
- package/dist/app/app.js +25 -19
- package/dist/app/charts/chart-groups.js +3 -2
- package/dist/app/charts/period-chart.js +3 -2
- package/dist/app/create/create-values.js +1 -1
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/board-column.js +2 -1
- package/dist/app/displays/custom-display.js +1 -1
- package/dist/app/displays/display-frame.js +3 -2
- package/dist/app/displays/map-display.js +4 -3
- package/dist/app/embedded-list/cut-off-note.js +2 -1
- package/dist/app/hand-order/hand-sorted-list.js +2 -1
- package/dist/app/hand-order/use-hand-order.js +1 -1
- package/dist/app/import/import-check-step.js +3 -2
- package/dist/app/import/import-counts.js +2 -1
- package/dist/app/import/import-dialog.js +2 -1
- package/dist/app/import/import-result-step.js +2 -1
- package/dist/app/incoming-relations.js +1 -1
- package/dist/app/list-edit/use-list-edits.js +1 -1
- package/dist/app/list-page.js +1 -1
- package/dist/app/main.js +3 -1
- package/dist/app/map/records-map.js +4 -3
- package/dist/app/meta-gate-messages.js +16 -0
- package/dist/app/meta-gate.js +14 -5
- package/dist/app/nav-recent.js +1 -1
- package/dist/app/pages/blocks/link.js +1 -1
- package/dist/app/pages/blocks/progress.js +3 -2
- package/dist/app/pages/blocks/stat.js +2 -1
- package/dist/app/pages/render-node.js +1 -1
- package/dist/app/pages/use-layout-data.js +1 -1
- package/dist/app/record-preview.js +1 -1
- package/dist/app/record-row/record-row-body.js +2 -1
- package/dist/app/saved/saved-records.js +1 -1
- package/dist/app/search/messages.js +18 -0
- package/dist/app/search/search-model.js +6 -5
- package/dist/app/searches/saved-search-rows.js +1 -1
- package/dist/app/searches/saved-searches.js +1 -1
- package/dist/app/searches/update-search-dialog.js +1 -1
- package/dist/app/searches/use-save-search.js +1 -1
- package/dist/app-shell/bottom-bar.js +3 -2
- package/dist/app-shell/breadcrumb.js +2 -1
- package/dist/app-shell/drawer.js +2 -1
- package/dist/app-shell/global-search.js +5 -4
- package/dist/app-shell/messages.js +64 -0
- package/dist/app-shell/profile-menu.js +5 -4
- package/dist/app-shell/sidebar-group.js +6 -5
- package/dist/app-shell/sidebar.js +4 -3
- package/dist/app-shell/theme-switch.js +8 -7
- package/dist/app-shell/top-bar.js +5 -4
- package/dist/data/api-provider.d.ts +2 -0
- package/dist/data/focus-refresh.js +2 -0
- package/dist/data/live-refresh.js +29 -0
- package/dist/data/meta-data.d.ts +3 -0
- package/dist/data/use-live-updates.js +36 -0
- package/dist/data/use-meta.js +1 -0
- package/dist/data/use-record.js +1 -1
- package/dist/data/use-update-record.js +1 -1
- package/dist/format/date.js +42 -50
- package/dist/format/intl-cache.js +12 -0
- package/dist/format/messages.js +24 -0
- package/dist/format/money.js +9 -16
- package/dist/format/number-text.js +3 -2
- package/dist/format/number.js +7 -5
- package/dist/format/zone.js +3 -5
- package/dist/invitations/pending-invitation-row.js +2 -1
- package/dist/live/display-value.js +2 -1
- package/dist/locale/active-locale.js +24 -0
- package/dist/locale/app-locale.js +18 -0
- package/dist/locale/messages.js +12 -0
- package/dist/organizations/organizations-page.js +2 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-Di4_EOe7.js +69 -0
- package/dist/shell/assets/export-worker-O1HF9R4j.js +2 -0
- package/dist/shell/assets/{index-BShZnsl_.js → index-DIcL_2pR.js} +2 -2
- package/dist/shell/assets/{maplibre-Z-0y-JM5.js → maplibre-B8VhXCMb.js} +2 -2
- package/dist/shell/assets/{markdown-content-C2__fEXf.js → markdown-content-CDcQeWQm.js} +1 -1
- package/dist/shell/assets/{record-map-sG110J_j.js → record-map-Bd0yjDXL.js} +1 -1
- package/dist/shell/assets/records-map-QR_MUceu.js +1 -0
- package/dist/shell/assets/{shared-protobase-ui-QAWUDrTS.js → shared-protobase-ui-CWBBcLfz.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-DDou93IP.js +0 -69
- package/dist/shell/assets/export-worker-C1djGgjD.js +0 -2
- package/dist/shell/assets/records-map-mv11CA2m.js +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
2
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
3
|
-
import {
|
|
3
|
+
import { text } from "./messages.js";
|
|
4
4
|
import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
|
|
5
|
+
import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
|
|
5
6
|
//#region src/app/search/search-model.ts
|
|
6
7
|
const where$1 = createWhere();
|
|
7
8
|
const searchTargets = (meta) => Object.values(meta.resources).flatMap((model) => {
|
|
@@ -16,10 +17,10 @@ const searchTargets = (meta) => Object.values(meta.resources).flatMap((model) =>
|
|
|
16
17
|
/** "Search organizations and services", or "Search organizations, services and more" when it looks in more. */
|
|
17
18
|
const searchPlaceholder = (targets) => {
|
|
18
19
|
const names = targets.map((target) => target.label.toLowerCase());
|
|
19
|
-
if (names.length === 0) return "
|
|
20
|
-
if (names.length === 1) return
|
|
21
|
-
if (names.length === 2) return
|
|
22
|
-
return
|
|
20
|
+
if (names.length === 0) return text("search");
|
|
21
|
+
if (names.length === 1) return text("searchOne", names[0]);
|
|
22
|
+
if (names.length === 2) return text("searchTwo", names[0], names[1]);
|
|
23
|
+
return text("searchMore", names[0], names[1]);
|
|
23
24
|
};
|
|
24
25
|
/** The list filter for the text: the server's partial-word `search(...)` over the resource's search fields. */
|
|
25
26
|
const searchFilter = (text) => printFilter(where$1.search(text.trim()));
|
|
@@ -8,8 +8,8 @@ import { InlineFilterChips } from "./filter-chip-list.js";
|
|
|
8
8
|
import { RecordRowBody } from "../record-row/record-row-body.js";
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
10
10
|
import { useSearchRow } from "./use-search-row.js";
|
|
11
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
11
|
import { encodeKey } from "@protobase/schema/internal";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
//#region src/app/searches/saved-search-rows.tsx
|
|
14
14
|
const SearchRowItem = ({ model, view, target, targetView, row }) => {
|
|
15
15
|
const key = encodeKey(recordKey(model, row));
|
|
@@ -9,8 +9,8 @@ import { can } from "../permissions.js";
|
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
10
10
|
import { useDeleteFlow } from "../delete/use-delete-flow.js";
|
|
11
11
|
import { SavedSearchSection } from "./saved-search-section.js";
|
|
12
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
12
|
import { encodeKey } from "@protobase/schema/internal";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
//#region src/app/searches/saved-searches.tsx
|
|
15
15
|
/**
|
|
16
16
|
* The list page of a view with `searchesOf`: each of the user's saved searches, with what it finds (`SavedSearchSection`),
|
|
@@ -9,9 +9,9 @@ import { useFilterChips } from "./use-filter-chips.js";
|
|
|
9
9
|
import { EmbeddedList } from "../embedded-list/embedded-list.js";
|
|
10
10
|
import { addedFilter, filterChanges, netChange, removedFilter } from "./search-diff.js";
|
|
11
11
|
import { useMemo } from "react";
|
|
12
|
+
import { tableDisplay } from "@protobase/schema/internal";
|
|
12
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
14
|
import { useQuery } from "@tanstack/react-query";
|
|
14
|
-
import { tableDisplay } from "@protobase/schema/internal";
|
|
15
15
|
//#region src/app/searches/update-search-dialog.tsx
|
|
16
16
|
/** How many records each side of the diff shows; its count says how many there are in all. */
|
|
17
17
|
const shownLimit = 12;
|
|
@@ -8,8 +8,8 @@ import { can } from "../permissions.js";
|
|
|
8
8
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
9
9
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
10
10
|
import { useOpenedSearch } from "./use-opened-search.js";
|
|
11
|
-
import { useMutation } from "@tanstack/react-query";
|
|
12
11
|
import { encodeKey } from "@protobase/schema/internal";
|
|
12
|
+
import { useMutation } from "@tanstack/react-query";
|
|
13
13
|
//#region src/app/searches/use-save-search.ts
|
|
14
14
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
15
15
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { Ellipsis } from "lucide-react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/app-shell/bottom-bar.tsx
|
|
@@ -13,7 +14,7 @@ const BottomBar = ({ tabs, onNavigate, onMore, moreOpen, moreControls, sticky, r
|
|
|
13
14
|
const moreCurrent = !tabs.some((item) => item.active);
|
|
14
15
|
return /* @__PURE__ */ jsxs("nav", {
|
|
15
16
|
ref,
|
|
16
|
-
"aria-label": "
|
|
17
|
+
"aria-label": text("tabs"),
|
|
17
18
|
className: cn("flex shrink-0 border-t bg-background pb-[env(safe-area-inset-bottom)] md:hidden", sticky && "sticky bottom-0 z-30"),
|
|
18
19
|
children: [tabs.map((item) => {
|
|
19
20
|
const Icon = item.icon;
|
|
@@ -50,7 +51,7 @@ const BottomBar = ({ tabs, onNavigate, onMore, moreOpen, moreControls, sticky, r
|
|
|
50
51
|
className: "size-6",
|
|
51
52
|
strokeWidth: moreCurrent ? 2.2 : 1.9,
|
|
52
53
|
"aria-hidden": true
|
|
53
|
-
}), "
|
|
54
|
+
}), text("more")]
|
|
54
55
|
})]
|
|
55
56
|
});
|
|
56
57
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { Skeleton } from "../primitives/skeleton.js";
|
|
3
4
|
import { ChevronRight } from "lucide-react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -7,7 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
8
|
const inApp = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
8
9
|
/** The trail of where you are; the last crumb, the current page, shows on phones too. */
|
|
9
10
|
const Breadcrumb = ({ parts, onNavigate }) => /* @__PURE__ */ jsx("nav", {
|
|
10
|
-
"aria-label": "
|
|
11
|
+
"aria-label": text("breadcrumb"),
|
|
11
12
|
className: "flex min-w-0 items-center gap-1.5",
|
|
12
13
|
children: parts.map((part, index) => {
|
|
13
14
|
const last = index === parts.length - 1;
|
package/dist/app-shell/drawer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { text } from "./messages.js";
|
|
1
2
|
import { useFocusTrap } from "../lib/use-focus-trap.js";
|
|
2
3
|
import { useRef } from "react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -18,7 +19,7 @@ const Drawer = ({ id, open, onClose, children }) => {
|
|
|
18
19
|
id,
|
|
19
20
|
role: "dialog",
|
|
20
21
|
"aria-modal": "true",
|
|
21
|
-
"aria-label": "
|
|
22
|
+
"aria-label": text("navigation"),
|
|
22
23
|
tabIndex: -1,
|
|
23
24
|
className: "absolute inset-y-0 left-0 flex w-60 max-w-[85vw] bg-sidebar shadow-pop outline-none",
|
|
24
25
|
onClick: (event) => event.target.closest("a") && onClose(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { Input } from "../primitives/input.js";
|
|
3
4
|
import { Kbd } from "../primitives/kbd.js";
|
|
4
5
|
import { Spinner } from "../primitives/spinner.js";
|
|
@@ -65,7 +66,7 @@ const GlobalSearch = ({ search, className = "w-96", autoFocus, showShortcut = tr
|
|
|
65
66
|
leading: search.loading ? /* @__PURE__ */ jsx(Spinner, { className: "size-4" }) : /* @__PURE__ */ jsx(Search, { className: "size-4" }),
|
|
66
67
|
trailing: search.text ? /* @__PURE__ */ jsx("button", {
|
|
67
68
|
type: "button",
|
|
68
|
-
"aria-label": "
|
|
69
|
+
"aria-label": text("clearSearch"),
|
|
69
70
|
className: "-mr-1 inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
70
71
|
onMouseDown: (event) => event.preventDefault(),
|
|
71
72
|
onClick: clear,
|
|
@@ -75,7 +76,7 @@ const GlobalSearch = ({ search, className = "w-96", autoFocus, showShortcut = tr
|
|
|
75
76
|
children: /* @__PURE__ */ jsx(Kbd, { children: "⌘K" })
|
|
76
77
|
}) : void 0,
|
|
77
78
|
placeholder: search.placeholder,
|
|
78
|
-
"aria-label": "
|
|
79
|
+
"aria-label": text("globalSearch"),
|
|
79
80
|
role: "combobox",
|
|
80
81
|
"aria-autocomplete": "list",
|
|
81
82
|
"aria-expanded": open,
|
|
@@ -93,11 +94,11 @@ const GlobalSearch = ({ search, className = "w-96", autoFocus, showShortcut = tr
|
|
|
93
94
|
}), open && /* @__PURE__ */ jsxs("div", {
|
|
94
95
|
id: listId,
|
|
95
96
|
role: "listbox",
|
|
96
|
-
"aria-label": "
|
|
97
|
+
"aria-label": text("searchResults"),
|
|
97
98
|
className: "absolute right-0 top-full z-40 mt-1 max-h-96 w-full min-w-72 overflow-y-auto rounded-lg border bg-background py-1 shadow-pop",
|
|
98
99
|
children: [search.groups.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
99
100
|
className: "px-3 py-2 text-muted-foreground",
|
|
100
|
-
children: search.loading ? "
|
|
101
|
+
children: search.loading ? text("searching") : text("noMatches", search.query)
|
|
101
102
|
}), search.groups.map((group) => /* @__PURE__ */ jsxs("div", {
|
|
102
103
|
role: "group",
|
|
103
104
|
"aria-label": group.label,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineMessages } from "../locale/messages.js";
|
|
2
|
+
//#region src/app-shell/messages.ts
|
|
3
|
+
const text = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
navigation: "Navigation",
|
|
6
|
+
resources: "Resources",
|
|
7
|
+
breadcrumb: "Breadcrumb",
|
|
8
|
+
tabs: "Tabs",
|
|
9
|
+
more: "More",
|
|
10
|
+
openNavigation: "Open navigation",
|
|
11
|
+
backTo: (label) => `Back to ${label}`,
|
|
12
|
+
search: "Search",
|
|
13
|
+
closeSearch: "Close search",
|
|
14
|
+
clearSearch: "Clear search",
|
|
15
|
+
globalSearch: "Global search",
|
|
16
|
+
searchResults: "Search results",
|
|
17
|
+
searching: "Searching",
|
|
18
|
+
noMatches: (query) => `No matches for “${query}”`,
|
|
19
|
+
profileMenu: "Profile menu",
|
|
20
|
+
organizations: "Organizations",
|
|
21
|
+
roles: "Roles",
|
|
22
|
+
signIn: "Sign in",
|
|
23
|
+
signOut: "Sign out",
|
|
24
|
+
theme: "Theme",
|
|
25
|
+
light: "Light",
|
|
26
|
+
dark: "Dark",
|
|
27
|
+
system: "System",
|
|
28
|
+
recent: (plural) => `Recent ${plural}`,
|
|
29
|
+
loading: "Loading...",
|
|
30
|
+
couldNotLoad: "Could not load",
|
|
31
|
+
viewAll: "View all"
|
|
32
|
+
},
|
|
33
|
+
nl: {
|
|
34
|
+
navigation: "Navigatie",
|
|
35
|
+
resources: "Onderdelen",
|
|
36
|
+
breadcrumb: "Kruimelpad",
|
|
37
|
+
tabs: "Tabbladen",
|
|
38
|
+
more: "Meer",
|
|
39
|
+
openNavigation: "Navigatie openen",
|
|
40
|
+
backTo: (label) => `Terug naar ${label}`,
|
|
41
|
+
search: "Zoeken",
|
|
42
|
+
closeSearch: "Zoeken sluiten",
|
|
43
|
+
clearSearch: "Zoekopdracht wissen",
|
|
44
|
+
globalSearch: "Overal zoeken",
|
|
45
|
+
searchResults: "Zoekresultaten",
|
|
46
|
+
searching: "Zoeken",
|
|
47
|
+
noMatches: (query) => `Niets gevonden voor “${query}”`,
|
|
48
|
+
profileMenu: "Profielmenu",
|
|
49
|
+
organizations: "Organisaties",
|
|
50
|
+
roles: "Rollen",
|
|
51
|
+
signIn: "Inloggen",
|
|
52
|
+
signOut: "Uitloggen",
|
|
53
|
+
theme: "Thema",
|
|
54
|
+
light: "Licht",
|
|
55
|
+
dark: "Donker",
|
|
56
|
+
system: "Systeem",
|
|
57
|
+
recent: (plural) => `Recente ${plural}`,
|
|
58
|
+
loading: "Laden...",
|
|
59
|
+
couldNotLoad: "Kon niet laden",
|
|
60
|
+
viewAll: "Alles bekijken"
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export { text };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { Avatar } from "../primitives/avatar.js";
|
|
3
4
|
import { Badge } from "../primitives/badge.js";
|
|
4
5
|
import { SwitchGroup } from "./switch-group.js";
|
|
@@ -41,7 +42,7 @@ const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate,
|
|
|
41
42
|
children: [/* @__PURE__ */ jsxs("button", {
|
|
42
43
|
type: "button",
|
|
43
44
|
className: "flex min-h-11 w-full items-center gap-2.5 rounded-md p-1 text-left hover:bg-muted md:min-h-0",
|
|
44
|
-
"aria-label": "
|
|
45
|
+
"aria-label": text("profileMenu"),
|
|
45
46
|
"aria-haspopup": "menu",
|
|
46
47
|
"aria-expanded": open,
|
|
47
48
|
onClick: toggle,
|
|
@@ -81,7 +82,7 @@ const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate,
|
|
|
81
82
|
})]
|
|
82
83
|
}),
|
|
83
84
|
organizations && (organizations.current || organizations.others.length > 0) && /* @__PURE__ */ jsx(SwitchGroup, {
|
|
84
|
-
label: "
|
|
85
|
+
label: text("organizations"),
|
|
85
86
|
...organizations.current && { current: {
|
|
86
87
|
id: organizations.current.id,
|
|
87
88
|
label: organizations.current.name
|
|
@@ -97,7 +98,7 @@ const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate,
|
|
|
97
98
|
}
|
|
98
99
|
}),
|
|
99
100
|
roles && roles.roles.length > 1 && /* @__PURE__ */ jsx(SwitchGroup, {
|
|
100
|
-
label: "
|
|
101
|
+
label: text("roles"),
|
|
101
102
|
...activeRole && { current: {
|
|
102
103
|
id: activeRole.name,
|
|
103
104
|
label: activeRole.label
|
|
@@ -147,7 +148,7 @@ const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate,
|
|
|
147
148
|
role: "menuitem",
|
|
148
149
|
onClick: onSignOut,
|
|
149
150
|
className: menuRow,
|
|
150
|
-
children: [/* @__PURE__ */ jsx(LogOut, { className: "size-4" }), "
|
|
151
|
+
children: [/* @__PURE__ */ jsx(LogOut, { className: "size-4" }), text("signOut")]
|
|
151
152
|
})
|
|
152
153
|
]
|
|
153
154
|
})]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { navOpenStorageKey, readNavOpen, writeNavOpen } from "./nav-open-storage.js";
|
|
3
4
|
import { SidebarItem } from "./sidebar-item.js";
|
|
4
5
|
import { StatusDot } from "./status-dot.js";
|
|
@@ -49,23 +50,23 @@ const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
|
|
|
49
50
|
type: "button",
|
|
50
51
|
"aria-expanded": open,
|
|
51
52
|
"aria-controls": listId,
|
|
52
|
-
"aria-label":
|
|
53
|
+
"aria-label": text("recent", item.label.toLowerCase()),
|
|
53
54
|
onClick: toggle,
|
|
54
55
|
className: "absolute right-1 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded text-faint-foreground hover:bg-muted hover:text-foreground",
|
|
55
56
|
children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") })
|
|
56
57
|
})]
|
|
57
58
|
}), open && /* @__PURE__ */ jsxs("ul", {
|
|
58
59
|
id: listId,
|
|
59
|
-
"aria-label":
|
|
60
|
+
"aria-label": text("recent", item.label.toLowerCase()),
|
|
60
61
|
className: cn("flex flex-col gap-0.5 border-l pl-1.5", indent[mode]),
|
|
61
62
|
children: [
|
|
62
63
|
recent.state === "loading" && /* @__PURE__ */ jsx("li", {
|
|
63
64
|
className: cn("flex items-center text-faint-foreground", row[mode]),
|
|
64
|
-
children: "
|
|
65
|
+
children: text("loading")
|
|
65
66
|
}),
|
|
66
67
|
recent.state === "error" && /* @__PURE__ */ jsx("li", {
|
|
67
68
|
className: cn("flex items-center text-faint-foreground", row[mode]),
|
|
68
|
-
children: "
|
|
69
|
+
children: text("couldNotLoad")
|
|
69
70
|
}),
|
|
70
71
|
recent.state === "ready" && recent.records.map((record) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
|
|
71
72
|
...link(record.href),
|
|
@@ -84,7 +85,7 @@ const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
|
|
|
84
85
|
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
|
|
85
86
|
...link(href),
|
|
86
87
|
className: cn("flex items-center rounded-md text-faint-foreground transition-colors hover:bg-muted hover:text-foreground", row[mode]),
|
|
87
|
-
children: "
|
|
88
|
+
children: text("viewAll")
|
|
88
89
|
}) })
|
|
89
90
|
]
|
|
90
91
|
})]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { RouteLink } from "../primitives/route-link.js";
|
|
3
4
|
import { Logo } from "./logo.js";
|
|
4
5
|
import { navGroups } from "./nav.js";
|
|
@@ -15,7 +16,7 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, forceExpanded, pr
|
|
|
15
16
|
const expanded = expanding && forceExpanded;
|
|
16
17
|
const inline = isTextMode(mode) || expanding;
|
|
17
18
|
return /* @__PURE__ */ jsx("nav", {
|
|
18
|
-
"aria-label": "
|
|
19
|
+
"aria-label": text("resources"),
|
|
19
20
|
className: cn(sticky ? "sticky top-0 h-dvh self-start" : "relative", "z-30 shrink-0", sidebarWidth[mode]),
|
|
20
21
|
children: /* @__PURE__ */ jsxs("div", {
|
|
21
22
|
className: cn("group/side absolute inset-y-0 left-0 flex flex-col overflow-hidden border-r bg-sidebar", expanding ? "w-14 transition-[width,box-shadow] duration-150 hover:w-60 hover:shadow-pop focus-within:w-60 focus-within:shadow-pop" : "w-full", expanded && "w-60 shadow-pop"),
|
|
@@ -66,11 +67,11 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, forceExpanded, pr
|
|
|
66
67
|
className: "flex min-h-11 w-full items-center gap-2.5 whitespace-nowrap p-1.5 text-muted-foreground md:min-h-0"
|
|
67
68
|
}), signIn && /* @__PURE__ */ jsxs(RouteLink, {
|
|
68
69
|
to: signIn,
|
|
69
|
-
"aria-label": "
|
|
70
|
+
"aria-label": text("signIn"),
|
|
70
71
|
className: "flex min-h-11 w-full items-center gap-2.5 rounded-md p-1.5 text-left font-medium text-muted-foreground hover:bg-muted hover:text-foreground md:min-h-0",
|
|
71
72
|
children: [/* @__PURE__ */ jsx(LogIn, { className: "size-4 shrink-0" }), inline && /* @__PURE__ */ jsx("span", {
|
|
72
73
|
className: "whitespace-nowrap",
|
|
73
|
-
children: "
|
|
74
|
+
children: text("signIn")
|
|
74
75
|
})]
|
|
75
76
|
})] })
|
|
76
77
|
})
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
import { text } from "./messages.js";
|
|
1
2
|
import { Segmented } from "../primitives/segmented.js";
|
|
2
3
|
import { Monitor, Moon, Sun, SunMoon } from "lucide-react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/app-shell/theme-switch.tsx
|
|
5
|
-
const options = [
|
|
6
|
+
const options = () => [
|
|
6
7
|
{
|
|
7
8
|
value: "light",
|
|
8
|
-
label: "
|
|
9
|
+
label: text("light"),
|
|
9
10
|
icon: Sun
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
value: "dark",
|
|
13
|
-
label: "
|
|
14
|
+
label: text("dark"),
|
|
14
15
|
icon: Moon
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
value: "system",
|
|
18
|
-
label: "
|
|
19
|
+
label: text("system"),
|
|
19
20
|
icon: Monitor
|
|
20
21
|
}
|
|
21
22
|
];
|
|
@@ -26,13 +27,13 @@ const ThemeSwitch = ({ theme, className }) => /* @__PURE__ */ jsxs("div", {
|
|
|
26
27
|
/* @__PURE__ */ jsx(SunMoon, { className: "size-4" }),
|
|
27
28
|
/* @__PURE__ */ jsx("span", {
|
|
28
29
|
className: "flex-1",
|
|
29
|
-
children: "
|
|
30
|
+
children: text("theme")
|
|
30
31
|
}),
|
|
31
32
|
/* @__PURE__ */ jsx(Segmented, {
|
|
32
|
-
label: "
|
|
33
|
+
label: text("theme"),
|
|
33
34
|
iconOnly: true,
|
|
34
35
|
value: theme.preference,
|
|
35
|
-
options,
|
|
36
|
+
options: options(),
|
|
36
37
|
onChange: theme.onChange
|
|
37
38
|
})
|
|
38
39
|
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
2
3
|
import { Breadcrumb, inApp } from "./breadcrumb.js";
|
|
3
4
|
import { backTarget } from "./back-target.js";
|
|
4
5
|
import { GlobalSearch } from "./global-search.js";
|
|
@@ -30,7 +31,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
30
31
|
children: [
|
|
31
32
|
onMenu && /* @__PURE__ */ jsx("button", {
|
|
32
33
|
type: "button",
|
|
33
|
-
"aria-label": "
|
|
34
|
+
"aria-label": text("openNavigation"),
|
|
34
35
|
"aria-expanded": Boolean(menuOpen),
|
|
35
36
|
"aria-controls": menuControls,
|
|
36
37
|
onClick: onMenu,
|
|
@@ -39,7 +40,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
39
40
|
}),
|
|
40
41
|
back && /* @__PURE__ */ jsx("a", {
|
|
41
42
|
href: back.href,
|
|
42
|
-
"aria-label":
|
|
43
|
+
"aria-label": text("backTo", back.label),
|
|
43
44
|
onClick: (event) => {
|
|
44
45
|
if (!onNavigate || !inApp(event)) return;
|
|
45
46
|
event.preventDefault();
|
|
@@ -63,7 +64,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
63
64
|
})
|
|
64
65
|
}), /* @__PURE__ */ jsx("button", {
|
|
65
66
|
type: "button",
|
|
66
|
-
"aria-label": "
|
|
67
|
+
"aria-label": text("search"),
|
|
67
68
|
"aria-expanded": searchOpen,
|
|
68
69
|
onClick: () => setSearchOpen(true),
|
|
69
70
|
className: "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden",
|
|
@@ -91,7 +92,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
91
92
|
}
|
|
92
93
|
}), /* @__PURE__ */ jsx("button", {
|
|
93
94
|
type: "button",
|
|
94
|
-
"aria-label": "
|
|
95
|
+
"aria-label": text("closeSearch"),
|
|
95
96
|
onClick: () => setSearchOpen(false),
|
|
96
97
|
className: "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden",
|
|
97
98
|
children: /* @__PURE__ */ jsx(X, { className: "size-5" })
|
|
@@ -56,6 +56,7 @@ export declare const useClient: () => {
|
|
|
56
56
|
form: (name: string) => Promise<import("@protobase/client").PublicForm>;
|
|
57
57
|
formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
|
|
58
58
|
submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
|
|
59
|
+
live: (handlers: import("@protobase/client").LiveHandlers) => () => void;
|
|
59
60
|
};
|
|
60
61
|
/**
|
|
61
62
|
* The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
|
|
@@ -114,6 +115,7 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
114
115
|
form: (name: string) => Promise<import("@protobase/client").PublicForm>;
|
|
115
116
|
formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
|
|
116
117
|
submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
|
|
118
|
+
live: (handlers: import("@protobase/client").LiveHandlers) => () => void;
|
|
117
119
|
};
|
|
118
120
|
export type ApiProviderProps = {
|
|
119
121
|
/** Defaults to `/api/v1` on the current origin. */
|
|
@@ -9,6 +9,8 @@ const createFocusRefresh = () => {
|
|
|
9
9
|
let paused = 0;
|
|
10
10
|
return {
|
|
11
11
|
refetchOnWindowFocus: () => paused > 0 ? false : "always",
|
|
12
|
+
/** Whether a form on screen holds unsaved changes, so its record must not be read again under it. */
|
|
13
|
+
paused: () => paused > 0,
|
|
12
14
|
/** Holds the refetches until the returned function is called. */
|
|
13
15
|
pause: () => {
|
|
14
16
|
paused += 1;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads again what the page shows of the resources whose records changed: every query keyed by the resource, as after
|
|
3
|
+
* a write of the page's own. While a form holds unsaved changes (`paused`), the records stay as they are, only marked
|
|
4
|
+
* stale, so the form's Save still finds a conflict.
|
|
5
|
+
*/
|
|
6
|
+
const liveRefresh = (queryClient, paused) => {
|
|
7
|
+
const pending = /* @__PURE__ */ new Set();
|
|
8
|
+
let timer;
|
|
9
|
+
const flush = () => {
|
|
10
|
+
timer = void 0;
|
|
11
|
+
const resources = new Set(pending);
|
|
12
|
+
pending.clear();
|
|
13
|
+
const shown = (key) => resources.has(key[1]);
|
|
14
|
+
queryClient.invalidateQueries({ predicate: (query) => shown(query.queryKey) && query.queryKey[0] !== "record" });
|
|
15
|
+
queryClient.invalidateQueries({
|
|
16
|
+
predicate: (query) => shown(query.queryKey) && query.queryKey[0] === "record",
|
|
17
|
+
refetchType: paused() ? "none" : "active"
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
changed: (resources) => {
|
|
22
|
+
for (const resource of resources) pending.add(resource);
|
|
23
|
+
if (pending.size > 0 && !timer) timer = setTimeout(flush, 300);
|
|
24
|
+
},
|
|
25
|
+
stop: () => clearTimeout(timer)
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { liveRefresh };
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MapLayerModel, PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
|
|
2
|
+
import { Locale } from "@protobase/schema/internal";
|
|
2
3
|
import { BottomBarModel, ResourceModel, SignInViewModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
3
4
|
import { PageModel } from "@protobase/layout";
|
|
4
5
|
//#region src/data/meta-data.d.ts
|
|
@@ -34,6 +35,8 @@ export type MetaData = {
|
|
|
34
35
|
style: string;
|
|
35
36
|
layers?: MapLayerModel[];
|
|
36
37
|
};
|
|
38
|
+
/** The language the app's screens are in for everyone, when it sets one (see `AppLocale`). */
|
|
39
|
+
locale?: Locale;
|
|
37
40
|
/** The hosts besides the app's own whose images are drawn with the page's referrer (see `FileImage`). */
|
|
38
41
|
files?: {
|
|
39
42
|
trustedHosts: string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FocusRefreshContext } from "./focus-refresh.js";
|
|
2
|
+
import { useClient } from "./api-provider.js";
|
|
3
|
+
import { liveRefresh } from "./live-refresh.js";
|
|
4
|
+
import { sourceStatusOf } from "./with-sources.js";
|
|
5
|
+
import { useContext, useEffect, useMemo } from "react";
|
|
6
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
//#region src/data/use-live-updates.ts
|
|
8
|
+
/**
|
|
9
|
+
* Keeps what the page shows of the app's `.live()` resources current: hears which records change (`client.live`) and
|
|
10
|
+
* reads again the lists, records and counts of their resources. Connects only when the caller reads a live resource.
|
|
11
|
+
*/
|
|
12
|
+
const useLiveUpdates = (meta) => {
|
|
13
|
+
const client = useClient();
|
|
14
|
+
const queryClient = useQueryClient();
|
|
15
|
+
const focusRefresh = useContext(FocusRefreshContext);
|
|
16
|
+
const live = useMemo(() => Object.values(meta.resources).filter((model) => model.live && !sourceStatusOf(meta, model.name)).map((model) => model.name).sort().join(","), [meta]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!live) return;
|
|
19
|
+
const refresh = liveRefresh(queryClient, () => focusRefresh?.paused() ?? false);
|
|
20
|
+
const stop = client.live({
|
|
21
|
+
onChanges: (changes) => refresh.changed(changes.map((change) => change.resource)),
|
|
22
|
+
onResync: () => refresh.changed(live.split(","))
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
stop();
|
|
26
|
+
refresh.stop();
|
|
27
|
+
};
|
|
28
|
+
}, [
|
|
29
|
+
client,
|
|
30
|
+
queryClient,
|
|
31
|
+
focusRefresh,
|
|
32
|
+
live
|
|
33
|
+
]);
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { useLiveUpdates };
|
package/dist/data/use-meta.js
CHANGED
|
@@ -38,6 +38,7 @@ const useMeta = () => {
|
|
|
38
38
|
...result.meta.runtime && { runtime: result.meta.runtime },
|
|
39
39
|
...result.meta.errorReporting && { errorReporting: result.meta.errorReporting },
|
|
40
40
|
...result.meta.maps && { maps: result.meta.maps },
|
|
41
|
+
...result.meta.locale && { locale: result.meta.locale },
|
|
41
42
|
...result.meta.files && { files: result.meta.files },
|
|
42
43
|
...result.meta.plugins && { plugins: result.meta.plugins },
|
|
43
44
|
...result.meta.sources && { sources: result.meta.sources.map((source) => ({
|
package/dist/data/use-record.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
-
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
3
|
import { encodeKey } from "@protobase/schema/internal";
|
|
4
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
//#region src/data/use-record.ts
|
|
6
6
|
const useRecord = (resource, key, enabled = true) => {
|
|
7
7
|
const client = useClient();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
3
|
import { PreconditionFailedError } from "@protobase/client";
|
|
4
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
4
|
import { encodeKey } from "@protobase/schema/internal";
|
|
5
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
6
6
|
//#region src/data/use-update-record.ts
|
|
7
7
|
/**
|
|
8
8
|
* PATCH with `If-Match`. On success the cached record gets the new ETag and lists refetch;
|