@protobase/ui 0.21.6 → 0.21.8

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 (96) hide show
  1. package/dist/app/app.d.ts +3 -3
  2. package/dist/app/app.js +32 -13
  3. package/dist/app/comments/comment-body.js +13 -0
  4. package/dist/app/comments/comment-composer.js +149 -0
  5. package/dist/app/comments/comments-section.js +78 -0
  6. package/dist/app/comments/mention-text.js +45 -0
  7. package/dist/app/comments/use-mentionable.js +18 -0
  8. package/dist/app/comments/use-post-comment.js +15 -0
  9. package/dist/app/comments/use-record-comments.js +24 -0
  10. package/dist/app/create/create-page.js +4 -4
  11. package/dist/app/current-address.d.ts +4 -3
  12. package/dist/app/current-address.js +1 -0
  13. package/dist/app/custom-section.js +1 -1
  14. package/dist/app/displays/board-columns.js +1 -1
  15. package/dist/app/displays/board-display.js +1 -1
  16. package/dist/app/displays/calendar-controls.js +69 -0
  17. package/dist/app/displays/calendar-day.js +33 -0
  18. package/dist/app/displays/calendar-display.js +174 -0
  19. package/dist/app/displays/calendar-dnd.js +27 -0
  20. package/dist/app/displays/calendar-event.js +72 -0
  21. package/dist/app/displays/calendar-range.js +125 -0
  22. package/dist/app/displays/list-displays.js +8 -2
  23. package/dist/app/displays/map-display.js +1 -1
  24. package/dist/app/displays/record-card.js +2 -2
  25. package/dist/app/displays/use-board-column.js +1 -1
  26. package/dist/app/displays/use-board-move.js +1 -1
  27. package/dist/app/displays/use-calendar-move.js +68 -0
  28. package/dist/app/displays/use-calendar-rows.js +35 -0
  29. package/dist/app/displays/use-map-records.js +1 -1
  30. package/dist/app/embedded-list/embedded-list.js +26 -22
  31. package/dist/app/embedded-list/embedded-row-targets.js +65 -0
  32. package/dist/app/embedded-list/use-embedded-rows.js +1 -1
  33. package/dist/app/gallery/gallery-section.js +1 -1
  34. package/dist/app/gallery/lightbox.js +1 -1
  35. package/dist/app/hand-order/hand-sorted-list.js +3 -3
  36. package/dist/app/hand-order/use-hand-order.js +2 -2
  37. package/dist/app/import/import-dialog.js +1 -1
  38. package/dist/app/list-edit/use-list-edits.js +1 -1
  39. package/dist/app/list-page.js +20 -9
  40. package/dist/app/map/map-section.js +1 -1
  41. package/dist/app/pages/action-inputs/action-inputs-dialog.js +3 -3
  42. package/dist/app/pages/blocks/card-row.js +1 -1
  43. package/dist/app/pages/blocks/modal-form.js +3 -3
  44. package/dist/app/pages/blocks/record-card.js +1 -1
  45. package/dist/app/pages/field-value.js +1 -1
  46. package/dist/app/pages/use-run-action.js +9 -7
  47. package/dist/app/record-lines.js +1 -1
  48. package/dist/app/record-page.js +29 -15
  49. package/dist/app/record-row/record-row-body.js +1 -1
  50. package/dist/app/record-row/saved-record-row.js +1 -1
  51. package/dist/app/related-dots/related-dots.js +1 -1
  52. package/dist/app/related-opens.js +22 -0
  53. package/dist/app/related-section.js +5 -2
  54. package/dist/app/relation-picker.js +1 -1
  55. package/dist/app/search/search-model.js +1 -1
  56. package/dist/app/searches/saved-search-rows.js +2 -2
  57. package/dist/app/searches/saved-searches.js +2 -2
  58. package/dist/app/searches/update-search-dialog.js +1 -1
  59. package/dist/app/searches/use-save-search.js +1 -1
  60. package/dist/app/searches/use-saved-searches.js +1 -1
  61. package/dist/app/use-app-address.d.ts +3 -1
  62. package/dist/app/use-app-address.js +9 -10
  63. package/dist/assistant-dock/widget-view.js +1 -1
  64. package/dist/auth/auth-layout.d.ts +4 -2
  65. package/dist/auth/auth-layout.js +4 -3
  66. package/dist/auth/user-to-shell.js +3 -1
  67. package/dist/data/api-provider.d.ts +26 -0
  68. package/dist/data/query-keys.js +18 -1
  69. package/dist/data-grid/row-menu.js +8 -1
  70. package/dist/format/date.js +27 -1
  71. package/dist/forms/prove-human.js +24 -0
  72. package/dist/forms/public-form-page.js +142 -0
  73. package/dist/forms/use-form-address.js +11 -0
  74. package/dist/index.js +7 -7
  75. package/dist/live/columns-from-model.js +1 -1
  76. package/dist/live/use-filter-config.js +1 -1
  77. package/dist/package.js +1 -1
  78. package/dist/plugins/plugin-gate.js +1 -1
  79. package/dist/primitives/segmented.js +8 -3
  80. package/dist/primitives/textarea.d.ts +4 -3
  81. package/dist/primitives/textarea.js +9 -4
  82. package/dist/shell/assets/app-C01lazBK.js +69 -0
  83. package/dist/shell/assets/{export-worker-CVd5saLq.js → export-worker-BnOpS_4Q.js} +1 -1
  84. package/dist/shell/assets/index-CBGigFxb.css +2 -0
  85. package/dist/shell/assets/{index-BztYGbYc.js → index-xN6_oZU2.js} +1 -1
  86. package/dist/shell/assets/{maplibre-CVOEAJ62.js → maplibre-Ba9RapjY.js} +1 -1
  87. package/dist/shell/assets/{markdown-content-Bg8FC_aK.js → markdown-content-BUxrbovf.js} +1 -1
  88. package/dist/shell/assets/{record-map-ChUVZLR6.js → record-map-14zotZ2s.js} +1 -1
  89. package/dist/shell/assets/{records-map-BAI2D9VS.js → records-map-wi-0pXqF.js} +1 -1
  90. package/dist/shell/assets/shared-protobase-ui-BVwhKZk9.js +1 -0
  91. package/dist/shell/index.html +4 -4
  92. package/dist/shell/protobase.shell.json +1 -1
  93. package/package.json +5 -4
  94. package/dist/shell/assets/app-Dfqcc3td.js +0 -69
  95. package/dist/shell/assets/index-C1fzTv_X.css +0 -2
  96. package/dist/shell/assets/shared-protobase-ui-CzT3bJ35.js +0 -1
@@ -1,11 +1,12 @@
1
1
  import { Skeleton } from "../primitives/skeleton.js";
2
2
  import { keys } from "../data/query-keys.js";
3
3
  import { useClient } from "../data/api-provider.js";
4
- import { useAdminMeta, usePermissions } from "./meta-gate.js";
5
4
  import { Section } from "../record-view/section.js";
6
5
  import { ErrorBanner } from "./error-banner.js";
7
6
  import { listColumnNames } from "../live/columns-from-model.js";
7
+ import { useAdminMeta, usePermissions } from "./meta-gate.js";
8
8
  import { searchAll } from "../live/search-all.js";
9
+ import { relatedOpens } from "./related-opens.js";
9
10
  import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
10
11
  import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
11
12
  import { relatedHandOrder } from "./hand-order/hand-order.js";
@@ -82,6 +83,7 @@ const RelatedSection = ({ item, recordKey, owner }) => {
82
83
  const permissions = usePermissions(item.resource);
83
84
  const [sorting, setSorting] = useState(false);
84
85
  if (!model) return null;
86
+ const opens = relatedOpens(item, model, resources);
85
87
  const columns = item.columns ?? listColumnNames(model, view).filter((name) => name !== item.field);
86
88
  const handOrder = relatedHandOrder(item, model, permissions);
87
89
  const displaySwitch = /* @__PURE__ */ jsx(EmbeddedDisplaySwitch, {
@@ -124,7 +126,8 @@ const RelatedSection = ({ item, recordKey, owner }) => {
124
126
  columns,
125
127
  limit: item.limit ?? 10,
126
128
  display,
127
- empty: item.empty
129
+ empty: item.empty,
130
+ opens
128
131
  }, match.filter)
129
132
  });
130
133
  };
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
- import { labelFieldOf } from "../live/model-helpers.js";
4
3
  import { humanize } from "../live/naming.js";
4
+ import { labelFieldOf } from "../live/model-helpers.js";
5
5
  import { useList } from "../data/use-list.js";
6
6
  import { relationOptionsQuery, titleMatches } from "./relation-options.js";
7
7
  import { useCallback, useEffect, useId, useRef, useState } from "react";
@@ -1,5 +1,5 @@
1
- import { recordKey, recordTitleField } from "../../live/model-helpers.js";
2
1
  import { humanize } from "../../live/naming.js";
2
+ import { recordKey, recordTitleField } from "../../live/model-helpers.js";
3
3
  import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
4
4
  import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
5
5
  //#region src/app/search/search-model.ts
@@ -1,8 +1,8 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
- import { recordKey } from "../../live/model-helpers.js";
3
2
  import { humanize } from "../../live/naming.js";
4
- import { useAdminMeta } from "../meta-gate.js";
5
3
  import { ErrorBanner } from "../error-banner.js";
4
+ import { recordKey } from "../../live/model-helpers.js";
5
+ import { useAdminMeta } from "../meta-gate.js";
6
6
  import { AppLink } from "../use-app-address.js";
7
7
  import { InlineFilterChips } from "./filter-chip-list.js";
8
8
  import { RecordRowBody } from "../record-row/record-row-body.js";
@@ -1,9 +1,9 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { PageHeader } from "../../app-shell/page-header.js";
3
- import { recordKey } from "../../live/model-helpers.js";
4
3
  import { humanize } from "../../live/naming.js";
5
- import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
6
4
  import { ErrorBanner } from "../error-banner.js";
5
+ import { recordKey } from "../../live/model-helpers.js";
6
+ import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
7
7
  import { AppLink } from "../use-app-address.js";
8
8
  import { can } from "../permissions.js";
9
9
  import { useSavedSearches } from "./use-saved-searches.js";
@@ -2,8 +2,8 @@ import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { Button } from "../../primitives/button.js";
3
3
  import { useClient } from "../../data/api-provider.js";
4
4
  import { humanize } from "../../live/naming.js";
5
- import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
6
5
  import { defaultOrderBy } from "../list-order.js";
6
+ import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
7
7
  import { countQuery } from "../pages/use-layout-data.js";
8
8
  import { useFilterChips } from "./use-filter-chips.js";
9
9
  import { EmbeddedList } from "../embedded-list/embedded-list.js";
@@ -1,7 +1,7 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
- import { recordKey } from "../../live/model-helpers.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { useToast } from "../../toasts/toast-provider.js";
4
+ import { recordKey } from "../../live/model-helpers.js";
5
5
  import { useAdminMeta } from "../meta-gate.js";
6
6
  import { useAppAddress } from "../use-app-address.js";
7
7
  import { can } from "../permissions.js";
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
- import { useAdminMeta } from "../meta-gate.js";
4
3
  import { defaultOrderBy } from "../list-order.js";
4
+ import { useAdminMeta } from "../meta-gate.js";
5
5
  import { useQuery } from "@tanstack/react-query";
6
6
  //#region src/app/searches/use-saved-searches.ts
7
7
  /** Where the user's saved searches of `resource` are cached; a list key, so a write to the resource refreshes it. */
@@ -3,13 +3,15 @@ import { AnchorHTMLAttributes } from "react";
3
3
  import { AppAddress } from "@protobase/schema";
4
4
  //#region src/app/use-app-address.d.ts
5
5
  /**
6
- * The app's address as logical values, and the ways to change it: `set` changes the list in place, `go` moves to
6
+ * The app's address as logical values, and the ways to change it: `set` changes the list in place, `listTo` is the
7
+ * list after a change for a link to it, `go` moves to
7
8
  * another address, `href` is an address for an `<a href>` and `open` follows such an href. Every component reads and
8
9
  * writes the address through this hook, never the query string or a path of its own.
9
10
  */
10
11
  export declare const useAppAddress: () => {
11
12
  address: CurrentAddress | undefined;
12
13
  set: (change: ListChange) => void;
14
+ listTo: (change: ListChange) => AppAddress;
13
15
  go: (to: AppAddress, options?: {
14
16
  replace?: boolean;
15
17
  }) => void;
@@ -1,13 +1,14 @@
1
1
  import { useIsXs } from "../lib/use-media-query.js";
2
2
  import { Link, useRouter } from "./router.js";
3
- import { useAdminMeta } from "./meta-gate.js";
4
3
  import { changedList, currentAddress, listDefaults, listedResource } from "./current-address.js";
4
+ import { useAdminMeta } from "./meta-gate.js";
5
5
  import { useCallback, useMemo } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import { appPath, readAppPath } from "@protobase/schema";
8
8
  //#region src/app/use-app-address.tsx
9
9
  /**
10
- * The app's address as logical values, and the ways to change it: `set` changes the list in place, `go` moves to
10
+ * The app's address as logical values, and the ways to change it: `set` changes the list in place, `listTo` is the
11
+ * list after a change for a link to it, `go` moves to
11
12
  * another address, `href` is an address for an `<a href>` and `open` follows such an href. Every component reads and
12
13
  * writes the address through this hook, never the query string or a path of its own.
13
14
  */
@@ -32,16 +33,14 @@ const useAppAddress = () => {
32
33
  ]);
33
34
  const address = useMemo(() => currentAddress(read, defaults), [read, defaults]);
34
35
  const go = useCallback((to, options) => navigate(appPath(to), options), [navigate]);
36
+ const listTo = useCallback((change) => {
37
+ if (read?.kind !== "list") throw new Error("listTo and set change the list the app shows; use go to move to another address");
38
+ return changedList(read, change, defaults(read.resource));
39
+ }, [read, defaults]);
35
40
  return {
36
41
  address,
37
- set: useCallback((change) => {
38
- if (read?.kind !== "list") throw new Error("set changes the list the app shows; use go to move to another address");
39
- go(changedList(read, change, defaults(read.resource)), { replace: true });
40
- }, [
41
- read,
42
- defaults,
43
- go
44
- ]),
42
+ set: useCallback((change) => go(listTo(change), { replace: true }), [listTo, go]),
43
+ listTo,
45
44
  go,
46
45
  href: useCallback((to) => appPath(to, basePath), [basePath]),
47
46
  open: useCallback((target) => navigate(target.startsWith(`${basePath}/`) ? target.slice(basePath.length) : target), [basePath, navigate])
@@ -1,6 +1,6 @@
1
1
  import { reportRenderError } from "../app/error-reporting.js";
2
- import { ActionCard } from "../action-card/action-card.js";
3
2
  import { useProjectUi } from "../app/pages/project-ui.js";
3
+ import { ActionCard } from "../action-card/action-card.js";
4
4
  import { Component } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/assistant-dock/widget-view.tsx
@@ -3,8 +3,10 @@ import { ReactNode } from "react";
3
3
  export type AuthLayoutProps = {
4
4
  title: string;
5
5
  description?: string;
6
+ /** How wide the card is: `sm`, the default, for a few short inputs; `md` for a form with longer ones. */
7
+ size?: 'sm' | 'md';
6
8
  children: ReactNode;
7
9
  };
8
- /** The frame of the sign-in and first-run pages: logo, title and a card. */
9
- export declare const AuthLayout: ({ title, description, children }: AuthLayoutProps) => import("react").JSX.Element;
10
+ /** The frame of the sign-in and first-run pages and of public forms: logo, title and a card. */
11
+ export declare const AuthLayout: ({ title, description, size, children }: AuthLayoutProps) => import("react").JSX.Element;
10
12
  //#endregion
@@ -1,11 +1,12 @@
1
+ import { cn } from "../lib/cn.js";
1
2
  import { LogoMark } from "../app-shell/logo.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/auth/auth-layout.tsx
4
- /** The frame of the sign-in and first-run pages: logo, title and a card. */
5
- const AuthLayout = ({ title, description, children }) => /* @__PURE__ */ jsx("div", {
5
+ /** The frame of the sign-in and first-run pages and of public forms: logo, title and a card. */
6
+ const AuthLayout = ({ title, description, size = "sm", children }) => /* @__PURE__ */ jsx("div", {
6
7
  className: "flex min-h-full w-full items-center justify-center bg-surface px-4 py-10",
7
8
  children: /* @__PURE__ */ jsxs("div", {
8
- className: "w-full max-w-sm",
9
+ className: cn("w-full", size === "sm" ? "max-w-sm" : "max-w-lg"),
9
10
  children: [/* @__PURE__ */ jsxs("div", {
10
11
  className: "mb-6 flex flex-col items-center text-center",
11
12
  children: [
@@ -1,5 +1,7 @@
1
1
  //#region src/auth/user-to-shell.ts
2
+ /** The hue of someone's avatar, the same for the same id. */
2
3
  const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
4
+ /** The initials of someone's avatar: the first letters of the first two words of their name. */
3
5
  const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
4
6
  const rolesOf = (user) => (user.role ?? "user").split(",").map((role) => role.trim());
5
7
  /** The user holds the `admin` role, among others perhaps. */
@@ -18,4 +20,4 @@ const userToShell = (user) => {
18
20
  };
19
21
  };
20
22
  //#endregion
21
- export { isAdmin, userToShell };
23
+ export { hueOf, initialsOf, isAdmin, userToShell };
@@ -15,11 +15,21 @@ export declare const useClient: () => {
15
15
  etag?: string;
16
16
  }) => Promise<void>;
17
17
  undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
18
+ generate: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], action: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
18
19
  reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
19
20
  history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
20
21
  pageSize?: number;
21
22
  pageToken?: string;
22
23
  }) => Promise<import("@protobase/client").HistoryPage>;
24
+ comments: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
25
+ pageSize?: number;
26
+ pageToken?: string;
27
+ }) => Promise<import("@protobase/client").CommentPage>;
28
+ comment: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { body, mentions }: {
29
+ body: string;
30
+ mentions?: string[];
31
+ }) => Promise<import("@protobase/client").RecordComment>;
32
+ mentionable: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], query: string) => Promise<import("@protobase/client").MentionableUser[]>;
23
33
  batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
24
34
  facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
25
35
  filter?: import("@protobase/client").FilterInput;
@@ -43,6 +53,9 @@ export declare const useClient: () => {
43
53
  }) => Promise<import("@protobase/client").MetaResult>;
44
54
  invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: import("@protobase/client").InvokeOptions) => Promise<T>;
45
55
  upload: (resource: import("@protobase/client").ResourceRef, field: string, file: Blob, { onProgress, signal }?: import("@protobase/client").UploadOptions) => Promise<import("@protobase/client").UploadResult>;
56
+ form: (name: string) => Promise<import("@protobase/client").PublicForm>;
57
+ formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
58
+ submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
46
59
  };
47
60
  /**
48
61
  * The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
@@ -60,11 +73,21 @@ export declare const useResourceClient: (resource: string) => {
60
73
  etag?: string;
61
74
  }) => Promise<void>;
62
75
  undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
76
+ generate: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], action: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
63
77
  reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
64
78
  history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
65
79
  pageSize?: number;
66
80
  pageToken?: string;
67
81
  }) => Promise<import("@protobase/client").HistoryPage>;
82
+ comments: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
83
+ pageSize?: number;
84
+ pageToken?: string;
85
+ }) => Promise<import("@protobase/client").CommentPage>;
86
+ comment: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { body, mentions }: {
87
+ body: string;
88
+ mentions?: string[];
89
+ }) => Promise<import("@protobase/client").RecordComment>;
90
+ mentionable: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], query: string) => Promise<import("@protobase/client").MentionableUser[]>;
68
91
  batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
69
92
  facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
70
93
  filter?: import("@protobase/client").FilterInput;
@@ -88,6 +111,9 @@ export declare const useResourceClient: (resource: string) => {
88
111
  }) => Promise<import("@protobase/client").MetaResult>;
89
112
  invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: import("@protobase/client").InvokeOptions) => Promise<T>;
90
113
  upload: (resource: import("@protobase/client").ResourceRef, field: string, file: Blob, { onProgress, signal }?: import("@protobase/client").UploadOptions) => Promise<import("@protobase/client").UploadResult>;
114
+ form: (name: string) => Promise<import("@protobase/client").PublicForm>;
115
+ formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
116
+ submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
91
117
  };
92
118
  export type ApiProviderProps = {
93
119
  /** Defaults to `/api/v1` on the current origin. */
@@ -66,6 +66,21 @@ const keys = {
66
66
  "history",
67
67
  pageSize
68
68
  ],
69
+ /** A record's comments; starts with `record`, so refreshing the record refreshes them too. */
70
+ comments: (resource, key) => [
71
+ "record",
72
+ resource,
73
+ key,
74
+ "comments"
75
+ ],
76
+ /** The people a comment on a record may mention whose name contains `query`. */
77
+ mentionable: (resource, key, query) => [
78
+ "record",
79
+ resource,
80
+ key,
81
+ "mentionable",
82
+ query
83
+ ],
69
84
  facets: (resource, field, filter) => [
70
85
  "facets",
71
86
  resource,
@@ -134,7 +149,9 @@ const keys = {
134
149
  resource,
135
150
  key,
136
151
  path
137
- ]
152
+ ],
153
+ /** A public form, as `/-/forms/<name>` draws it. */
154
+ publicForm: (name) => ["public-form", name]
138
155
  };
139
156
  //#endregion
140
157
  export { keys };
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { RouteLink } from "../primitives/route-link.js";
2
3
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
4
  import { MoreHorizontal } from "lucide-react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -70,7 +71,13 @@ const RowMenu = ({ row, actions }) => {
70
71
  className: "fixed z-50 min-w-36 rounded-md border bg-background py-1 shadow-pop",
71
72
  children: actions.map((action) => /* @__PURE__ */ jsx("li", {
72
73
  role: "none",
73
- children: /* @__PURE__ */ jsx("button", {
74
+ children: action.link ? /* @__PURE__ */ jsx(RouteLink, {
75
+ role: "menuitem",
76
+ to: action.link(row),
77
+ onClick: () => setOpen(false),
78
+ className: cn("block w-full px-3 py-1.5 text-left hover:bg-muted", action.destructive && "text-danger-text"),
79
+ children: action.label
80
+ }) : /* @__PURE__ */ jsx("button", {
74
81
  type: "button",
75
82
  role: "menuitem",
76
83
  className: cn("block w-full px-3 py-1.5 text-left hover:bg-muted", action.destructive && "text-danger-text"),
@@ -16,9 +16,35 @@ const time = new Intl.DateTimeFormat("en-GB", {
16
16
  hour12: false,
17
17
  timeZone: "UTC"
18
18
  });
19
+ const month = new Intl.DateTimeFormat("en-GB", {
20
+ month: "long",
21
+ year: "numeric",
22
+ timeZone: "UTC"
23
+ });
24
+ const weekday = new Intl.DateTimeFormat("en-GB", {
25
+ weekday: "short",
26
+ timeZone: "UTC"
27
+ });
28
+ const weekdayDate = new Intl.DateTimeFormat("en-GB", {
29
+ weekday: "long",
30
+ day: "numeric",
31
+ month: "short",
32
+ year: "numeric",
33
+ timeZone: "UTC"
34
+ });
19
35
  const formatDate = (ms) => day.format(ms);
20
36
  const formatDayShort = (ms) => dayShort.format(ms);
21
37
  const formatDateTime = (ms) => `${day.format(ms)}, ${time.format(ms)}`;
38
+ /** "October 2026". */
39
+ const formatMonth = (ms) => month.format(ms);
40
+ /** "Mon". */
41
+ const formatWeekday = (ms) => weekday.format(ms);
42
+ /** "Monday, 12 Oct 2026". */
43
+ const formatWeekdayDate = (ms) => weekdayDate.format(ms);
44
+ /** "12–18 Oct 2026", "28 Dec 2026 – 3 Jan 2027": the days from `from` to `to`, both included. */
45
+ const formatDayRange = (from, to) => day.formatRange(from, to);
46
+ /** "09:30". */
47
+ const formatTime = (ms) => time.format(ms);
22
48
  const formatAgo = (seconds) => {
23
49
  if (seconds < 60) return `${seconds} s ago`;
24
50
  if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
@@ -40,4 +66,4 @@ const formatRelativeTime = (seconds) => {
40
66
  return relative.format(-Math.floor(seconds / size), unit);
41
67
  };
42
68
  //#endregion
43
- export { formatAgo, formatDate, formatDateTime, formatDayShort, formatRelativeTime };
69
+ export { formatAgo, formatDate, formatDateTime, formatDayRange, formatDayShort, formatMonth, formatRelativeTime, formatTime, formatWeekday, formatWeekdayDate };
@@ -0,0 +1,24 @@
1
+ import { solveChallenge } from "altcha-lib";
2
+ import { deriveKey } from "altcha-lib/algorithms/web/sha";
3
+ //#region src/forms/prove-human.ts
4
+ /**
5
+ * The bot check of a public form, done: fetches a fresh challenge and solves it in this browser, about a second of its
6
+ * work, so a check never expires while someone types.
7
+ */
8
+ const proveHuman = async (client, name) => {
9
+ const challenge = await client.formChallenge(name);
10
+ const solution = await solveChallenge({
11
+ challenge,
12
+ deriveKey
13
+ });
14
+ if (!solution) throw new Error("The check that you are not a bot took too long; send the form again");
15
+ return {
16
+ challenge,
17
+ solution: {
18
+ counter: solution.counter,
19
+ derivedKey: solution.derivedKey
20
+ }
21
+ };
22
+ };
23
+ //#endregion
24
+ export { proveHuman };
@@ -0,0 +1,142 @@
1
+ import { Spinner } from "../primitives/spinner.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { keys } from "../data/query-keys.js";
4
+ import { useClient } from "../data/api-provider.js";
5
+ import { AuthLayout } from "../auth/auth-layout.js";
6
+ import { humanize } from "../live/naming.js";
7
+ import { Field } from "../record-view/field.js";
8
+ import { createBody, hasDatabaseDefault, initialDraft, isRequired } from "../app/create/create-values.js";
9
+ import { useDocumentTitle } from "../app/document-title.js";
10
+ import { ErrorBanner } from "../app/error-banner.js";
11
+ import { Notice } from "../app/meta-gate.js";
12
+ import { FieldEditor } from "../app/field-editor.js";
13
+ import { fieldErrors, fieldMessage } from "../app/field-errors.js";
14
+ import { useFocusFirstInvalid } from "../app/use-focus-first-invalid.js";
15
+ import { proveHuman } from "./prove-human.js";
16
+ import { useState } from "react";
17
+ import { isApiError } from "@protobase/client";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { useQuery } from "@tanstack/react-query";
20
+ //#region src/forms/public-form-page.tsx
21
+ /**
22
+ * `/-/forms/<name>`: a public form, for anyone, signed in or not. It sends with a bot check its browser solves, and
23
+ * then shows the form's thanks in its place.
24
+ */
25
+ const PublicFormPage = ({ name }) => {
26
+ const client = useClient();
27
+ const form = useQuery({
28
+ queryKey: keys.publicForm(name),
29
+ queryFn: () => client.form(name)
30
+ });
31
+ if (form.isPending) return /* @__PURE__ */ jsxs("div", {
32
+ className: "flex h-full min-h-48 items-center justify-center gap-2 text-muted-foreground",
33
+ children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
34
+ });
35
+ if (form.error) {
36
+ if (isApiError(form.error) && form.error.status === 404) return /* @__PURE__ */ jsx(Notice, {
37
+ title: "Not found",
38
+ children: "There is no form at this address."
39
+ });
40
+ return /* @__PURE__ */ jsx(Notice, {
41
+ title: "Cannot reach the API",
42
+ children: isApiError(form.error) ? form.error.message : "The server did not answer. Is it running?"
43
+ });
44
+ }
45
+ return /* @__PURE__ */ jsx(LoadedForm, { form: form.data });
46
+ };
47
+ const LoadedForm = ({ form }) => {
48
+ const client = useClient();
49
+ const { resource: model, view } = form;
50
+ const title = form.title ?? view.names?.singular ?? humanize(view.resource);
51
+ useDocumentTitle(title);
52
+ const [draft, setDraft] = useState(() => initialDraft(model, [{
53
+ title,
54
+ fields: form.fields
55
+ }]));
56
+ const [errors, setErrors] = useState({});
57
+ const [failure, setFailure] = useState();
58
+ const [sending, setSending] = useState(false);
59
+ const [sent, setSent] = useState(false);
60
+ const [rejections, setRejections] = useState(0);
61
+ const formRef = useFocusFirstInvalid(rejections);
62
+ if (sent) return /* @__PURE__ */ jsx(AuthLayout, {
63
+ title,
64
+ size: "md",
65
+ children: /* @__PURE__ */ jsx("p", {
66
+ role: "status",
67
+ className: "rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
68
+ children: form.thanks
69
+ })
70
+ });
71
+ const change = (name, value) => {
72
+ setErrors((current) => Object.fromEntries(Object.entries(current).filter(([field]) => field !== name)));
73
+ setDraft((current) => ({
74
+ ...current,
75
+ [name]: value
76
+ }));
77
+ };
78
+ const send = async () => {
79
+ setSending(true);
80
+ setFailure(void 0);
81
+ try {
82
+ await client.submitForm(form.name, createBody(model, {}, draft), await proveHuman(client, form.name));
83
+ setSent(true);
84
+ } catch (error) {
85
+ const found = fieldErrors(error);
86
+ setErrors(found);
87
+ if (Object.keys(found).length > 0) setRejections((count) => count + 1);
88
+ else setFailure(error);
89
+ } finally {
90
+ setSending(false);
91
+ }
92
+ };
93
+ return /* @__PURE__ */ jsx(AuthLayout, {
94
+ title,
95
+ size: "md",
96
+ children: /* @__PURE__ */ jsxs("form", {
97
+ ref: formRef,
98
+ className: "flex flex-col gap-4",
99
+ onSubmit: (event) => {
100
+ event.preventDefault();
101
+ send();
102
+ },
103
+ children: [
104
+ form.fields.map((name) => {
105
+ const field = model.fields[name];
106
+ const hints = view.fields[name];
107
+ const label = hints?.label ?? humanize(name);
108
+ const problem = errors[name];
109
+ return /* @__PURE__ */ jsx(Field, {
110
+ name,
111
+ label,
112
+ help: hints?.help,
113
+ required: isRequired(field),
114
+ error: problem && fieldMessage(field, label, problem),
115
+ children: /* @__PURE__ */ jsx(FieldEditor, {
116
+ field,
117
+ hints,
118
+ value: draft[name] ?? (field.type === "boolean" ? false : ""),
119
+ stored: void 0,
120
+ placeholder: hasDatabaseDefault(field) ? "Set by the database" : void 0,
121
+ invalid: Boolean(problem),
122
+ onChange: (next) => change(name, next)
123
+ })
124
+ }, name);
125
+ }),
126
+ failure !== void 0 && /* @__PURE__ */ jsx(ErrorBanner, {
127
+ title: "Not sent",
128
+ error: failure
129
+ }),
130
+ /* @__PURE__ */ jsx(Button, {
131
+ variant: "primary",
132
+ type: "submit",
133
+ loading: sending,
134
+ className: "min-h-10",
135
+ children: "Send"
136
+ })
137
+ ]
138
+ })
139
+ });
140
+ };
141
+ //#endregion
142
+ export { PublicFormPage };
@@ -0,0 +1,11 @@
1
+ import { useRouter } from "../app/router.js";
2
+ import { useMemo } from "react";
3
+ import { readFormAddress } from "@protobase/schema";
4
+ //#region src/forms/use-form-address.ts
5
+ /** The public form the address shows, `/-/forms/<name>`, as logical values; `undefined` on any other page. */
6
+ const useFormAddress = () => {
7
+ const { path } = useRouter();
8
+ return useMemo(() => readFormAddress(path), [path]);
9
+ };
10
+ //#endregion
11
+ export { useFormAddress };
package/dist/index.js CHANGED
@@ -16,6 +16,13 @@ import { usePlugin } from "./plugins/plugin-context.js";
16
16
  import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
17
17
  import { useApiToken } from "./auth/use-api-token.js";
18
18
  import { useRecord } from "./data/use-record.js";
19
+ import { Field } from "./record-view/field.js";
20
+ import { Section } from "./record-view/section.js";
21
+ import { Textarea } from "./primitives/textarea.js";
22
+ import { useMeta } from "./data/use-meta.js";
23
+ import { AppLink, useAppAddress } from "./app/use-app-address.js";
24
+ import { useList } from "./data/use-list.js";
25
+ import { definePlugin, defineUi } from "./app/pages/project-ui.js";
19
26
  import { ChatMessage } from "./chat/chat-message.js";
20
27
  import { ChatThread } from "./chat/chat-thread.js";
21
28
  import { Composer } from "./chat/composer.js";
@@ -26,19 +33,12 @@ import { CompactTable } from "./compact-table/compact-table.js";
26
33
  import { diffStats, parseDiff } from "./diff-view/diff.js";
27
34
  import { DiffView } from "./diff-view/diff-view.js";
28
35
  import { StepList } from "./step-list/step-list.js";
29
- import { definePlugin, defineUi } from "./app/pages/project-ui.js";
30
36
  import { PartView } from "./assistant-dock/part-view.js";
31
37
  import { useBackendData } from "./assistant-dock/use-backend-data.js";
32
38
  import { AssistantDock } from "./assistant-dock/assistant-dock.js";
33
39
  import { useAssistant } from "./assistant-dock/use-assistant.js";
34
40
  import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
35
- import { useMeta } from "./data/use-meta.js";
36
41
  import { ResponsiveDialog } from "./primitives/responsive-dialog.js";
37
- import { Field } from "./record-view/field.js";
38
- import { Section } from "./record-view/section.js";
39
- import { Textarea } from "./primitives/textarea.js";
40
- import { AppLink, useAppAddress } from "./app/use-app-address.js";
41
- import { useList } from "./data/use-list.js";
42
42
  import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
43
43
  import { useLayoutRecord } from "./app/pages/record-scope.js";
44
44
  import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
@@ -1,8 +1,8 @@
1
- import { recordTitleField } from "./model-helpers.js";
2
1
  import { humanize } from "./naming.js";
3
2
  import { badgeWidth, headerWidth } from "../data-grid/content-widths.js";
4
3
  import { markdownExcerpt } from "../markdown/markdown-excerpt.js";
5
4
  import { isMarkdown } from "../markdown/markdown-hint.js";
5
+ import { recordTitleField } from "./model-helpers.js";
6
6
  import { enumLabel } from "@protobase/schema";
7
7
  //#region src/live/columns-from-model.ts
8
8
  const tones = {
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../data/query-keys.js";
2
2
  import { useClient } from "../data/api-provider.js";
3
- import { labelFieldOf } from "./model-helpers.js";
4
3
  import { humanize } from "./naming.js";
4
+ import { labelFieldOf } from "./model-helpers.js";
5
5
  import { fetchLabels } from "./relation-labels.js";
6
6
  import { setRange } from "../filter-panel/filter-state.js";
7
7
  import { stateToText } from "./filter-string.js";
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.21.6";
2
+ var version = "0.21.8";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -2,8 +2,8 @@ import { Spinner } from "../primitives/spinner.js";
2
2
  import { mergePlugins } from "./merge-plugins.js";
3
3
  import { usePluginSource } from "./plugin-source.js";
4
4
  import { usePluginLoads } from "./use-plugin-loads.js";
5
- import { ProjectUiContext } from "../app/pages/project-ui.js";
6
5
  import { useAdminMeta } from "../app/meta-gate.js";
6
+ import { ProjectUiContext } from "../app/pages/project-ui.js";
7
7
  import { createContext, useContext, useMemo } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/plugins/plugin-gate.tsx