@protobase/ui 0.14.1 → 0.14.3

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 (61) hide show
  1. package/dist/app/current-address.js +19 -1
  2. package/dist/app/displays/card-carousel.js +47 -39
  3. package/dist/app/displays/card-image.js +25 -0
  4. package/dist/app/displays/card-photos.js +11 -15
  5. package/dist/app/displays/cards-display.js +2 -1
  6. package/dist/app/displays/list-displays.d.ts +2 -0
  7. package/dist/app/displays/list-displays.js +9 -3
  8. package/dist/app/displays/map-display.js +5 -4
  9. package/dist/app/displays/record-card.js +28 -34
  10. package/dist/app/displays/use-map-records.js +8 -6
  11. package/dist/app/embedded-list/embedded-columns.js +1 -1
  12. package/dist/app/embedded-list/use-embedded-rows.js +11 -6
  13. package/dist/app/list-page.js +44 -11
  14. package/dist/app/menu-icons.js +3 -1
  15. package/dist/app/named-icon.js +5 -3
  16. package/dist/app/nav-from-meta.js +21 -1
  17. package/dist/app/pages/blocks/card-row.js +1 -1
  18. package/dist/app/pages/blocks/modal-form.js +1 -1
  19. package/dist/app/pages/blocks/record-card.js +1 -1
  20. package/dist/app/pages/blocks/show.js +1 -1
  21. package/dist/app/pages/blocks/stat.js +1 -1
  22. package/dist/app/pages/use-run-action.js +1 -1
  23. package/dist/app/saved/favorite-button.js +49 -0
  24. package/dist/app/saved/saved-list.js +43 -0
  25. package/dist/app/saved/saved-records.js +38 -0
  26. package/dist/app/saved/use-favorite.js +62 -0
  27. package/dist/app/saved/use-saved-records.js +36 -0
  28. package/dist/app/use-app-address.js +5 -2
  29. package/dist/auth/provider-icon.js +15 -0
  30. package/dist/auth/sign-in-form.js +6 -1
  31. package/dist/auth/sign-in-prompt.d.ts +22 -0
  32. package/dist/auth/sign-in-prompt.js +84 -0
  33. package/dist/data/api-provider.d.ts +2 -0
  34. package/dist/data/meta-data.d.ts +3 -1
  35. package/dist/data/query-keys.js +0 -5
  36. package/dist/data/use-meta.js +1 -0
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +3 -1
  39. package/dist/live/grid-loader.js +8 -3
  40. package/dist/live/reference-options.js +2 -2
  41. package/dist/live/relation-labels.js +16 -18
  42. package/dist/live/row-photos.js +18 -0
  43. package/dist/live/use-filter-config.js +7 -2
  44. package/dist/package.js +1 -1
  45. package/dist/primitives/button.js +7 -2
  46. package/dist/primitives/dialog.js +1 -1
  47. package/dist/primitives/responsive-dialog.d.ts +17 -0
  48. package/dist/primitives/responsive-dialog.js +44 -0
  49. package/dist/shell/assets/app-BkySvmpb.js +66 -0
  50. package/dist/shell/assets/index-BXYHpFoe.css +2 -0
  51. package/dist/shell/assets/{index-BCLK6g9a.js → index-Ddh359yv.js} +1 -1
  52. package/dist/shell/assets/{maplibre-gl-DWx1yzYO.js → maplibre-gl-Du00jL_c.js} +1 -1
  53. package/dist/shell/assets/{record-map-C2M8dEjy.js → record-map-82NQGiUy.js} +2 -2
  54. package/dist/shell/assets/{records-map-huS0J9xV.js → records-map-BC0FGfLG.js} +1 -1
  55. package/dist/shell/assets/shared-protobase-ui-CVi8JFzf.js +1 -0
  56. package/dist/shell/index.html +4 -4
  57. package/dist/shell/protobase.shell.json +1 -1
  58. package/package.json +4 -4
  59. package/dist/shell/assets/app-Dkz6z_t_.js +0 -66
  60. package/dist/shell/assets/index-BQ4FIU4i.css +0 -2
  61. package/dist/shell/assets/shared-protobase-ui-CsJxPCx6.js +0 -1
@@ -0,0 +1,84 @@
1
+ import { useAuth } from "./auth-provider.js";
2
+ import { Button, buttonClass } from "../primitives/button.js";
3
+ import { namedIcon } from "../app/named-icon.js";
4
+ import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
5
+ import { useId } from "react";
6
+ import { Check, LogIn } from "lucide-react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/auth/sign-in-prompt.tsx
9
+ const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE__ */ jsx("ul", {
10
+ className: "flex flex-col gap-2 text-left text-[13px]",
11
+ children: benefits.map((benefit) => /* @__PURE__ */ jsxs("li", {
12
+ className: "flex items-start gap-2",
13
+ children: [/* @__PURE__ */ jsx(Check, {
14
+ className: "mt-0.5 size-4 shrink-0 text-primary",
15
+ "aria-hidden": true
16
+ }), benefit]
17
+ }, benefit))
18
+ }) : null;
19
+ const Actions = ({ prompt }) => {
20
+ const { showSignIn } = useAuth();
21
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp && /* @__PURE__ */ jsx("a", {
22
+ href: prompt.signUp.href,
23
+ target: "_blank",
24
+ rel: "noreferrer",
25
+ className: buttonClass(),
26
+ children: prompt.signUp.label
27
+ }), /* @__PURE__ */ jsx(Button, {
28
+ variant: "primary",
29
+ onClick: showSignIn,
30
+ children: "Sign in"
31
+ })] });
32
+ };
33
+ /**
34
+ * What a visitor who is not signed in is told about something they could use once signed in: the prompt's title, what
35
+ * signing in gives and its benefits, with "Sign in" and, when the prompt names one, a page to make an account. As a page
36
+ * it stands in for a list the visitor cannot see; as a dialog it answers a press on a button that needs an account.
37
+ */
38
+ const SignInPrompt = (props) => {
39
+ const { prompt } = props;
40
+ const heading = useId();
41
+ if (props.as === "dialog") return /* @__PURE__ */ jsx(ResponsiveDialog, {
42
+ title: prompt.title,
43
+ description: prompt.description,
44
+ open: props.open,
45
+ onClose: props.onClose,
46
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
47
+ onClick: props.onClose,
48
+ children: "Not now"
49
+ }), /* @__PURE__ */ jsx(Actions, { prompt })] }),
50
+ children: /* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits })
51
+ });
52
+ const Icon = namedIcon(prompt.icon) ?? LogIn;
53
+ return /* @__PURE__ */ jsxs("section", {
54
+ "aria-labelledby": heading,
55
+ className: "mx-auto flex w-full max-w-md flex-col items-center gap-4 px-5 py-10 text-center sm:py-16",
56
+ children: [
57
+ /* @__PURE__ */ jsx("span", {
58
+ className: "flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary",
59
+ children: /* @__PURE__ */ jsx(Icon, {
60
+ className: "size-6",
61
+ "aria-hidden": true
62
+ })
63
+ }),
64
+ /* @__PURE__ */ jsxs("div", {
65
+ className: "flex flex-col gap-1.5",
66
+ children: [/* @__PURE__ */ jsx("h1", {
67
+ id: heading,
68
+ className: "text-lg font-semibold",
69
+ children: prompt.title
70
+ }), /* @__PURE__ */ jsx("p", {
71
+ className: "text-[13px] text-muted-foreground",
72
+ children: prompt.description
73
+ })]
74
+ }),
75
+ /* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits }),
76
+ /* @__PURE__ */ jsx("div", {
77
+ className: "mt-2 flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row sm:justify-center [&>*]:min-h-11 sm:[&>*]:min-h-0",
78
+ children: /* @__PURE__ */ jsx(Actions, { prompt })
79
+ })
80
+ ]
81
+ });
82
+ };
83
+ //#endregion
84
+ export { SignInPrompt };
@@ -20,6 +20,7 @@ export declare const useClient: () => {
20
20
  facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
21
21
  filter?: import("@protobase/client").FilterInput;
22
22
  limit?: number;
23
+ label?: string;
23
24
  }) => Promise<import("@protobase/client").Facet[]>;
24
25
  series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
25
26
  filter?: import("@protobase/client").FilterInput;
@@ -59,6 +60,7 @@ export declare const useResourceClient: (resource: string) => {
59
60
  facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
60
61
  filter?: import("@protobase/client").FilterInput;
61
62
  limit?: number;
63
+ label?: string;
62
64
  }) => Promise<import("@protobase/client").Facet[]>;
63
65
  series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
64
66
  filter?: import("@protobase/client").FilterInput;
@@ -1,5 +1,5 @@
1
1
  import { PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
2
- import { BottomBarModel, ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
2
+ import { BottomBarModel, ResourceModel, SignInViewModel, UserMenuModel, ViewModel } from "@protobase/schema";
3
3
  import { PageModel } from "@protobase/layout";
4
4
  //#region src/data/meta-data.d.ts
5
5
  export type MetaData = {
@@ -10,6 +10,8 @@ export type MetaData = {
10
10
  pages: Record<string, PageModel>;
11
11
  permissions: Record<string, ResourcePermissions>;
12
12
  userMenu?: UserMenuModel;
13
+ /** For a visitor, the resources they could use once signed in, by name, with what to tell them (see `SignInPrompt`). */
14
+ signIn?: Record<string, SignInViewModel>;
13
15
  /** The tabs of the user's bottom bar on phones, when one names their active role. */
14
16
  bottomBar?: Omit<BottomBarModel, 'roles'>;
15
17
  /** The assistant backend, when the app has one and the user has the admin or ai role. */
@@ -101,11 +101,6 @@ const keys = {
101
101
  resource,
102
102
  key,
103
103
  path
104
- ],
105
- labelMap: (resource, ids) => [
106
- "label-map",
107
- resource,
108
- ids
109
104
  ]
110
105
  };
111
106
  //#endregion
@@ -31,6 +31,7 @@ const useMeta = () => {
31
31
  pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
32
32
  permissions: result.meta.permissions ?? {},
33
33
  ...result.meta.userMenu && { userMenu: result.meta.userMenu },
34
+ ...result.meta.signIn && { signIn: Object.fromEntries(result.meta.signIn.map((entry) => [entry.resource, entry])) },
34
35
  ...result.meta.bottomBar && { bottomBar: result.meta.bottomBar },
35
36
  ...result.meta.assistant && { assistant: result.meta.assistant },
36
37
  ...result.meta.runtime && { runtime: result.meta.runtime },
package/dist/index.d.ts CHANGED
@@ -20,6 +20,8 @@ import { ListDisplayRow, RecordCardProps } from "./app/displays/list-displays.js
20
20
  import { ListDisplay, useListDisplay } from "./app/displays/display-context.js";
21
21
  import { RecordLink } from "./app/displays/record-link.js";
22
22
  import { useApiToken } from "./auth/use-api-token.js";
23
+ import { SignInPrompt, SignInPromptProps } from "./auth/sign-in-prompt.js";
24
+ import { ResponsiveDialog, ResponsiveDialogProps } from "./primitives/responsive-dialog.js";
23
25
  import { AssistantDock, AssistantDockProps } from "./assistant-dock/assistant-dock.js";
24
26
  import { PartView } from "./assistant-dock/part-view.js";
25
27
  import { useAssistant } from "./assistant-dock/use-assistant.js";
@@ -39,4 +41,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
39
41
  import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
40
42
  import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
41
43
  import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
42
- export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
44
+ export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ import { useAssistant } from "./assistant-dock/use-assistant.js";
19
19
  import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
20
20
  import { useMeta } from "./data/use-meta.js";
21
21
  import { AppLink, useAppAddress } from "./app/use-app-address.js";
22
+ import { ResponsiveDialog } from "./primitives/responsive-dialog.js";
23
+ import { SignInPrompt } from "./auth/sign-in-prompt.js";
22
24
  import { useListDisplay } from "./app/displays/display-context.js";
23
25
  import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
24
26
  import { useLayoutRecord } from "./app/pages/record-scope.js";
@@ -30,4 +32,4 @@ import { useList } from "./data/use-list.js";
30
32
  import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
31
33
  import { App } from "./app/app.js";
32
34
  import { RecordLink } from "./app/displays/record-link.js";
33
- export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, SidePanel, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
35
+ export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, ResponsiveDialog, SidePanel, SignInPrompt, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
@@ -1,6 +1,7 @@
1
1
  import { keys } from "../data/query-keys.js";
2
2
  import { recordId } from "./model-helpers.js";
3
- import { withLabels } from "./relation-labels.js";
3
+ import { listLabels, withLabels } from "./relation-labels.js";
4
+ import { listPhotos, withPhotos } from "./row-photos.js";
4
5
  //#region src/live/grid-loader.ts
5
6
  const exactBelow = 5e3;
6
7
  /**
@@ -10,6 +11,8 @@ const exactBelow = 5e3;
10
11
  */
11
12
  const createGridLoader = ({ queryClient, client, resources, views, model, columns, filter, orderBy }) => {
12
13
  const pageKey = (number) => keys.page(model.name, filter, orderBy, 40, number);
14
+ const labels = listLabels(resources, views, model, columns);
15
+ const photos = listPhotos(views[model.name]);
13
16
  /** The server's total is a planner estimate, off by a lot for small results; settle those exactly. */
14
17
  const firstPageTotal = async (result) => {
15
18
  if (result.nextPageToken === "") return result.items.length;
@@ -40,7 +43,9 @@ const createGridLoader = ({ queryClient, client, resources, views, model, column
40
43
  const params = {
41
44
  filter,
42
45
  orderBy,
43
- pageSize: 40
46
+ pageSize: 40,
47
+ labels,
48
+ photos
44
49
  };
45
50
  const result = token !== void 0 ? await client.list(model.name, {
46
51
  ...params,
@@ -56,7 +61,7 @@ const createGridLoader = ({ queryClient, client, resources, views, model, column
56
61
  };
57
62
  const ids = result.items.map((record) => recordId(model, record));
58
63
  return {
59
- rows: (await withLabels(queryClient, client, resources, views, model, columns, result.items)).map((record, index) => ({
64
+ rows: withLabels(resources, model, columns, withPhotos(model, result.items, result.photos), result.labels).map((record, index) => ({
60
65
  ...record,
61
66
  id: ids[index]
62
67
  })),
@@ -8,7 +8,7 @@ const matchOptions = (matches, counted) => {
8
8
  ...counts.get(value) !== void 0 && { count: counts.get(value) }
9
9
  }));
10
10
  };
11
- /** The keys a facet needs names for: the values it counted and those chosen, which may lie outside the counted ones. */
12
- const labelKeys = (counted, chosen) => [.../* @__PURE__ */ new Set([...counted, ...chosen])];
11
+ /** The keys a facet still needs names for: the counted values come named, but a chosen one may lie outside them. */
12
+ const labelKeys = (counted, chosen) => [...new Set(chosen.filter((value) => !counted.includes(value)))];
13
13
  //#endregion
14
14
  export { labelKeys, matchOptions };
@@ -22,32 +22,30 @@ const fetchLabels = async (queryClient, client, target, ids, view) => {
22
22
  });
23
23
  return new Map(entries);
24
24
  };
25
- /** Replaces each relation value with `{ id, name }` so grid cells can show the name. */
26
- const withLabels = async (queryClient, client, resources, views, model, columns, rows) => {
27
- const relations = columns.flatMap((name) => {
28
- const field = model.fields[name];
29
- const target = field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
30
- return target ? [{
31
- name,
32
- target
33
- }] : [];
34
- });
35
- const resolved = await Promise.all(relations.map(async ({ name, target }) => {
36
- const ids = rows.flatMap((row) => row[name] == null ? [] : [String(row[name])]);
37
- return [name, await fetchLabels(queryClient, client, target, ids, views[target.name])];
38
- }));
39
- const labels = new Map(resolved);
25
+ const relationColumns = (resources, model, columns) => columns.flatMap((name) => {
26
+ const field = model.fields[name];
27
+ const target = field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
28
+ return target ? [{
29
+ name,
30
+ target
31
+ }] : [];
32
+ });
33
+ /** What a list call asks `labels` for: each relation among `columns` to a single-key resource, by the field that names its records. */
34
+ const listLabels = (resources, views, model, columns) => Object.fromEntries(relationColumns(resources, model, columns).filter(({ target }) => target.primaryKey.length === 1).map(({ name, target }) => [name, labelFieldOf(target, views[target.name])]));
35
+ /** Replaces each relation value among `columns` with `{ id, name }`, named by the list call's `labels`, else by its key. */
36
+ const withLabels = (resources, model, columns, rows, labels = {}) => {
37
+ const relations = relationColumns(resources, model, columns);
40
38
  return rows.map((row) => {
41
39
  const next = { ...row };
42
- for (const [name, names] of labels) {
40
+ for (const { name } of relations) {
43
41
  if (row[name] == null) continue;
44
42
  next[name] = {
45
43
  id: row[name],
46
- name: names.get(String(row[name])) ?? String(row[name])
44
+ name: labels[name]?.[String(row[name])] ?? String(row[name])
47
45
  };
48
46
  }
49
47
  return next;
50
48
  });
51
49
  };
52
50
  //#endregion
53
- export { fetchLabels, withLabels };
51
+ export { fetchLabels, listLabels, withLabels };
@@ -0,0 +1,18 @@
1
+ //#region src/live/row-photos.ts
2
+ /** Where a loaded row keeps its photos, beside its fields, as it keeps its `id`. */
3
+ const photosKey = "$photos";
4
+ /** The photos a list's rows load with: those its cards displays name, which the server checks they all share. */
5
+ const listPhotos = (view) => {
6
+ for (const display of view?.list?.displays ?? []) if (display.kind === "cards" && display.photos) return display.photos;
7
+ };
8
+ /** The rows, each with its record's photos from a list call's `photos`, when the call asked for them; none for a record it left out. */
9
+ const withPhotos = (model, rows, photos) => {
10
+ if (photos === void 0 || model.primaryKey.length !== 1) return rows;
11
+ const key = model.primaryKey[0];
12
+ return rows.map((row) => ({
13
+ ...row,
14
+ [photosKey]: photos[String(row[key])] ?? []
15
+ }));
16
+ };
17
+ //#endregion
18
+ export { listPhotos, photosKey, withPhotos };
@@ -1,6 +1,7 @@
1
1
  import { formatMoney } from "../format/money.js";
2
2
  import { keys } from "../data/query-keys.js";
3
3
  import { useClient } from "../data/api-provider.js";
4
+ import { labelFieldOf } from "./model-helpers.js";
4
5
  import { humanize } from "./naming.js";
5
6
  import { fetchLabels } from "./relation-labels.js";
6
7
  import { setRange } from "../filter-panel/filter-state.js";
@@ -37,11 +38,14 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
37
38
  [group]: []
38
39
  }
39
40
  }, extras);
41
+ const target = targetOf(field);
42
+ const label = target?.primaryKey.length === 1 ? labelFieldOf(target, views[target.name]) : void 0;
40
43
  return {
41
44
  queryKey: keys.facets(model.name, field, filter),
42
45
  queryFn: () => client.facets(model.name, field, {
43
46
  filter,
44
- limit: 50
47
+ limit: 50,
48
+ ...label && { label }
45
49
  })
46
50
  };
47
51
  }) });
@@ -84,7 +88,8 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
84
88
  };
85
89
  }
86
90
  const field = model.fields[group.id];
87
- const names = new Map(labelQueries[index]?.data ?? []);
91
+ const named = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.label === void 0 ? [] : [[String(facet.value), facet.label]]);
92
+ const names = new Map([...named, ...labelQueries[index]?.data ?? []]);
88
93
  const values = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [{
89
94
  value: String(facet.value),
90
95
  label: field?.type === "enum" ? enumLabel(String(facet.value), view?.fields[group.id]?.valueLabels) : names.get(String(facet.value)) ?? String(facet.value),
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.14.1";
2
+ var version = "0.14.3";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -12,12 +12,17 @@ const sizes = {
12
12
  sm: "h-7 px-2.5 text-xs gap-1.5",
13
13
  md: "h-8 px-3 text-[13px] gap-2"
14
14
  };
15
+ /** A button's classes, for a link that looks like one. */
16
+ const buttonClass = ({ variant = "secondary", size = "md" } = {}) => cn("inline-flex shrink-0 items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-60 disabled:cursor-default", variants[variant], sizes[size]);
15
17
  const Button = ({ variant = "secondary", size = "md", loading, className, children, disabled, ...rest }) => /* @__PURE__ */ jsxs("button", {
16
18
  type: "button",
17
19
  disabled: disabled || loading,
18
- className: cn("inline-flex shrink-0 items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-60 disabled:cursor-default", variants[variant], sizes[size], className),
20
+ className: cn(buttonClass({
21
+ variant,
22
+ size
23
+ }), className),
19
24
  ...rest,
20
25
  children: [loading && /* @__PURE__ */ jsx(Spinner, { className: "size-3.5" }), children]
21
26
  });
22
27
  //#endregion
23
- export { Button };
28
+ export { Button, buttonClass };
@@ -30,7 +30,7 @@ const Dialog = ({ title, description, onClose, actions, children }) => {
30
30
  children
31
31
  }),
32
32
  /* @__PURE__ */ jsx("div", {
33
- className: "mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end [&>button]:min-h-11 sm:[&>button]:min-h-0",
33
+ className: "mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end [&>*]:min-h-11 sm:[&>*]:min-h-0",
34
34
  children: actions
35
35
  })
36
36
  ]
@@ -0,0 +1,17 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/primitives/responsive-dialog.d.ts
3
+ export type ResponsiveDialogProps = {
4
+ title: string;
5
+ description?: ReactNode;
6
+ open: boolean;
7
+ onClose: () => void;
8
+ /** Buttons: right-aligned in the modal, stacked full width at the bottom of the sheet. */
9
+ actions: ReactNode;
10
+ children?: ReactNode;
11
+ };
12
+ /**
13
+ * A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
14
+ * the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
15
+ */
16
+ export declare const ResponsiveDialog: ({ title, description, open, onClose, actions, children }: ResponsiveDialogProps) => import("react").JSX.Element | null;
17
+ //#endregion
@@ -0,0 +1,44 @@
1
+ import { useIsDesktop } from "../lib/use-media-query.js";
2
+ import { Dialog } from "./dialog.js";
3
+ import { Sheet } from "./sheet.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/primitives/responsive-dialog.tsx
6
+ /**
7
+ * A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
8
+ * the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
9
+ */
10
+ const ResponsiveDialog = ({ title, description, open, onClose, actions, children }) => {
11
+ const desktop = useIsDesktop();
12
+ if (!open) return null;
13
+ if (desktop) return /* @__PURE__ */ jsx(Dialog, {
14
+ title,
15
+ description,
16
+ onClose,
17
+ actions,
18
+ children
19
+ });
20
+ return /* @__PURE__ */ jsx(Sheet, {
21
+ title,
22
+ open: true,
23
+ onClose,
24
+ children: /* @__PURE__ */ jsxs("div", {
25
+ className: "pt-3 text-[13px]",
26
+ children: [
27
+ description && /* @__PURE__ */ jsx("p", {
28
+ className: "text-muted-foreground",
29
+ children: description
30
+ }),
31
+ children && /* @__PURE__ */ jsx("div", {
32
+ className: "mt-4",
33
+ children
34
+ }),
35
+ /* @__PURE__ */ jsx("div", {
36
+ className: "mt-5 flex flex-col-reverse gap-2 [&>*]:min-h-11",
37
+ children: actions
38
+ })
39
+ ]
40
+ })
41
+ });
42
+ };
43
+ //#endregion
44
+ export { ResponsiveDialog };