@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.
Files changed (85) hide show
  1. package/dist/app/app.d.ts +4 -1
  2. package/dist/app/app.js +25 -19
  3. package/dist/app/charts/chart-groups.js +3 -2
  4. package/dist/app/charts/period-chart.js +3 -2
  5. package/dist/app/create/create-values.js +1 -1
  6. package/dist/app/custom-section.js +1 -1
  7. package/dist/app/displays/board-column.js +2 -1
  8. package/dist/app/displays/custom-display.js +1 -1
  9. package/dist/app/displays/display-frame.js +3 -2
  10. package/dist/app/displays/map-display.js +4 -3
  11. package/dist/app/embedded-list/cut-off-note.js +2 -1
  12. package/dist/app/hand-order/hand-sorted-list.js +2 -1
  13. package/dist/app/hand-order/use-hand-order.js +1 -1
  14. package/dist/app/import/import-check-step.js +3 -2
  15. package/dist/app/import/import-counts.js +2 -1
  16. package/dist/app/import/import-dialog.js +2 -1
  17. package/dist/app/import/import-result-step.js +2 -1
  18. package/dist/app/incoming-relations.js +1 -1
  19. package/dist/app/list-edit/use-list-edits.js +1 -1
  20. package/dist/app/list-page.js +1 -1
  21. package/dist/app/main.js +3 -1
  22. package/dist/app/map/records-map.js +4 -3
  23. package/dist/app/meta-gate-messages.js +16 -0
  24. package/dist/app/meta-gate.js +14 -5
  25. package/dist/app/nav-recent.js +1 -1
  26. package/dist/app/pages/blocks/link.js +1 -1
  27. package/dist/app/pages/blocks/progress.js +3 -2
  28. package/dist/app/pages/blocks/stat.js +2 -1
  29. package/dist/app/pages/render-node.js +1 -1
  30. package/dist/app/pages/use-layout-data.js +1 -1
  31. package/dist/app/record-preview.js +1 -1
  32. package/dist/app/record-row/record-row-body.js +2 -1
  33. package/dist/app/saved/saved-records.js +1 -1
  34. package/dist/app/search/messages.js +18 -0
  35. package/dist/app/search/search-model.js +6 -5
  36. package/dist/app/searches/saved-search-rows.js +1 -1
  37. package/dist/app/searches/saved-searches.js +1 -1
  38. package/dist/app/searches/update-search-dialog.js +1 -1
  39. package/dist/app/searches/use-save-search.js +1 -1
  40. package/dist/app-shell/bottom-bar.js +3 -2
  41. package/dist/app-shell/breadcrumb.js +2 -1
  42. package/dist/app-shell/drawer.js +2 -1
  43. package/dist/app-shell/global-search.js +5 -4
  44. package/dist/app-shell/messages.js +64 -0
  45. package/dist/app-shell/profile-menu.js +5 -4
  46. package/dist/app-shell/sidebar-group.js +6 -5
  47. package/dist/app-shell/sidebar.js +4 -3
  48. package/dist/app-shell/theme-switch.js +8 -7
  49. package/dist/app-shell/top-bar.js +5 -4
  50. package/dist/data/api-provider.d.ts +2 -0
  51. package/dist/data/focus-refresh.js +2 -0
  52. package/dist/data/live-refresh.js +29 -0
  53. package/dist/data/meta-data.d.ts +3 -0
  54. package/dist/data/use-live-updates.js +36 -0
  55. package/dist/data/use-meta.js +1 -0
  56. package/dist/data/use-record.js +1 -1
  57. package/dist/data/use-update-record.js +1 -1
  58. package/dist/format/date.js +42 -50
  59. package/dist/format/intl-cache.js +12 -0
  60. package/dist/format/messages.js +24 -0
  61. package/dist/format/money.js +9 -16
  62. package/dist/format/number-text.js +3 -2
  63. package/dist/format/number.js +7 -5
  64. package/dist/format/zone.js +3 -5
  65. package/dist/invitations/pending-invitation-row.js +2 -1
  66. package/dist/live/display-value.js +2 -1
  67. package/dist/locale/active-locale.js +24 -0
  68. package/dist/locale/app-locale.js +18 -0
  69. package/dist/locale/messages.js +12 -0
  70. package/dist/organizations/organizations-page.js +2 -1
  71. package/dist/package.js +1 -1
  72. package/dist/shell/assets/app-Di4_EOe7.js +69 -0
  73. package/dist/shell/assets/export-worker-O1HF9R4j.js +2 -0
  74. package/dist/shell/assets/{index-BShZnsl_.js → index-DIcL_2pR.js} +2 -2
  75. package/dist/shell/assets/{maplibre-Z-0y-JM5.js → maplibre-B8VhXCMb.js} +2 -2
  76. package/dist/shell/assets/{markdown-content-C2__fEXf.js → markdown-content-CDcQeWQm.js} +1 -1
  77. package/dist/shell/assets/{record-map-sG110J_j.js → record-map-Bd0yjDXL.js} +1 -1
  78. package/dist/shell/assets/records-map-QR_MUceu.js +1 -0
  79. package/dist/shell/assets/{shared-protobase-ui-QAWUDrTS.js → shared-protobase-ui-CWBBcLfz.js} +1 -1
  80. package/dist/shell/index.html +3 -3
  81. package/dist/shell/protobase.shell.json +1 -1
  82. package/package.json +4 -4
  83. package/dist/shell/assets/app-DDou93IP.js +0 -69
  84. package/dist/shell/assets/export-worker-C1djGgjD.js +0 -2
  85. 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 { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
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 "Search";
20
- if (names.length === 1) return `Search ${names[0]}`;
21
- if (names.length === 2) return `Search ${names[0]} and ${names[1]}`;
22
- return `Search ${names[0]}, ${names[1]} and more`;
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": "Tabs",
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
- }), "More"]
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": "Breadcrumb",
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;
@@ -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": "Navigation",
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": "Clear search",
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": "Global search",
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": "Search results",
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 ? "Searching" : `No matches for “${search.query}”`
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": "Profile menu",
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: "Organizations",
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: "Roles",
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" }), "Sign out"]
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": `Recent ${item.label.toLowerCase()}`,
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": `Recent ${item.label.toLowerCase()}`,
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: "Loading..."
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: "Could not load"
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: "View all"
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": "Resources",
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": "Sign in",
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: "Sign in"
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: "Light",
9
+ label: text("light"),
9
10
  icon: Sun
10
11
  },
11
12
  {
12
13
  value: "dark",
13
- label: "Dark",
14
+ label: text("dark"),
14
15
  icon: Moon
15
16
  },
16
17
  {
17
18
  value: "system",
18
- label: "System",
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: "Theme"
30
+ children: text("theme")
30
31
  }),
31
32
  /* @__PURE__ */ jsx(Segmented, {
32
- label: "Theme",
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": "Open navigation",
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": `Back to ${back.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": "Search",
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": "Close search",
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 };
@@ -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 };
@@ -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) => ({
@@ -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;