@protobase/ui 0.12.6 → 0.12.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.
package/dist/app/app.js CHANGED
@@ -86,7 +86,8 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
86
86
  const group = groups.find((entry) => entry.items.some((item) => item.id === place));
87
87
  const model = resource ? meta.resources[resource] : void 0;
88
88
  const record = useRecord(shown?.resource ?? "", shown?.key ?? "", Boolean(model && shown));
89
- const title = model && shown && record.data ? recordTitle(model, view, record.data.record, shown.key) : shown?.key;
89
+ const title = model && shown && record.data ? recordTitle(model, view, record.data.record, shown.key) : void 0;
90
+ const titleLoading = Boolean(shown) && (record.isLoading || drawnFrom?.state === "loading");
90
91
  const { shell } = useProjectUi();
91
92
  const Actions = shell?.actions;
92
93
  const RightPanel = shell?.rightPanel;
@@ -126,7 +127,8 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
126
127
  group: group?.label,
127
128
  page,
128
129
  resourceLabel: view?.names?.plural ?? (resource && humanize(resource)),
129
- recordTitle: title
130
+ recordTitle: title,
131
+ recordLoading: titleLoading
130
132
  });
131
133
  const content = path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
132
134
  title: "Admins only",
@@ -1,7 +1,7 @@
1
1
  import { appPath } from "@protobase/schema";
2
2
  //#region src/app/breadcrumb-for.ts
3
3
  /** Group, then the page or resource list, then the record (or "New"): each with a page of its own links to it. */
4
- const breadcrumbFor = ({ basePath, address, group, page, resourceLabel, recordTitle }) => {
4
+ const breadcrumbFor = ({ basePath, address, group, page, resourceLabel, recordTitle, recordLoading }) => {
5
5
  const groupCrumb = group ? [group] : [];
6
6
  if (page) return [...groupCrumb, {
7
7
  label: page.title,
@@ -19,12 +19,17 @@ const breadcrumbFor = ({ basePath, address, group, page, resourceLabel, recordTi
19
19
  }, basePath)
20
20
  }] : [];
21
21
  if (address.kind === "list") return [...groupCrumb, ...resource];
22
+ const href = appPath(address, basePath);
22
23
  const record = address.kind === "create" ? {
23
24
  label: "New",
24
- href: appPath(address, basePath)
25
+ href
26
+ } : recordTitle === void 0 && recordLoading ? {
27
+ label: "Loading record",
28
+ href,
29
+ loading: true
25
30
  } : {
26
31
  label: recordTitle ?? address.key,
27
- href: appPath(address, basePath)
32
+ href
28
33
  };
29
34
  return [
30
35
  ...groupCrumb,
@@ -1,4 +1,5 @@
1
1
  import { Badge } from "../../primitives/badge.js";
2
+ import { Skeleton } from "../../primitives/skeleton.js";
2
3
  import { humanize } from "../../live/naming.js";
3
4
  import { useProjectUi } from "../pages/project-ui.js";
4
5
  import { cardFields } from "./list-displays.js";
@@ -7,7 +8,6 @@ import { displayValue } from "../../live/display-value.js";
7
8
  import { shownWidget } from "../field-types/shown-widget.js";
8
9
  import { CustomBoundary } from "../pages/custom-boundary.js";
9
10
  import { WidgetPart } from "../field-types/widget-part.js";
10
- import { Skeleton } from "../../primitives/skeleton.js";
11
11
  import { toneFor } from "../../live/columns-from-model.js";
12
12
  import { DisplayFooter, DisplayState } from "./display-frame.js";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -20,6 +20,7 @@ const DisplaySwitch = ({ view, value, onChange }) => {
20
20
  if (choices.length < 2) return null;
21
21
  return /* @__PURE__ */ jsx(Segmented, {
22
22
  label: "Display",
23
+ size: "md",
23
24
  value,
24
25
  onChange,
25
26
  options: choices.map((choice) => ({
@@ -40,7 +41,7 @@ const SortMenu = ({ model, view, display, orderBy, onChange }) => {
40
41
  "aria-label": "Sort",
41
42
  value: `${current.columnId} ${current.direction}`,
42
43
  onChange: (event) => onChange(event.target.value),
43
- className: "h-7 rounded-md border border-border-strong bg-background px-2 text-xs shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20",
44
+ className: "h-8 rounded-md border border-border-strong bg-background px-2.5 text-[13px] shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20",
44
45
  children: names.flatMap((name) => directions.map(([direction, word]) => /* @__PURE__ */ jsxs("option", {
45
46
  value: `${name} ${direction}`,
46
47
  children: [
@@ -15,6 +15,7 @@ import { pinnedStorageKey } from "../data-grid/pinned-storage.js";
15
15
  import { DataGrid } from "../data-grid/data-grid.js";
16
16
  import { ActiveFilterChips } from "../filter-panel/active-filter-chips.js";
17
17
  import { activeChips, emptyFilterState, removeChip } from "../filter-panel/filter-state.js";
18
+ import { ToolbarRow } from "../filter-panel/toolbar-row.js";
18
19
  import { FilterPanel } from "../filter-panel/filter-panel.js";
19
20
  import { columnsFromModel } from "../live/columns-from-model.js";
20
21
  import { stateToText, textToState } from "../live/filter-string.js";
@@ -124,11 +125,29 @@ const LoadedList = ({ model, address }) => {
124
125
  setFilter(state, text ? [...others, where.search(text)] : others);
125
126
  };
126
127
  const plural = view?.names?.plural ?? humanize(resource);
128
+ const sortMenu = display && /* @__PURE__ */ jsx(SortMenu, {
129
+ model,
130
+ view,
131
+ display,
132
+ orderBy,
133
+ onChange: (next) => set({ orderBy: next })
134
+ });
135
+ const searchBox = model.search?.length ? /* @__PURE__ */ jsx(ListSearch, {
136
+ value: search?.kind === "search" ? search.text : "",
137
+ onCommit: commitSearch,
138
+ placeholder: `Search ${plural.toLowerCase()}`
139
+ }) : null;
127
140
  const panel = /* @__PURE__ */ jsx(FilterPanel, {
128
141
  layout,
129
142
  config,
130
143
  value: state,
131
- onChange: setFilter
144
+ onChange: setFilter,
145
+ leading: sortMenu,
146
+ trailing: searchBox
147
+ });
148
+ const desktopToolbar = layout === "bar" ? panel : /* @__PURE__ */ jsx(ToolbarRow, {
149
+ groups: [sortMenu ? [sortMenu] : []],
150
+ trailing: searchBox
132
151
  });
133
152
  const chips = activeChips(config, state);
134
153
  const phoneFilters = /* @__PURE__ */ jsxs("div", {
@@ -217,18 +236,8 @@ const LoadedList = ({ model, address }) => {
217
236
  value: display?.name ?? tableDisplay,
218
237
  onChange: (name) => set({ display: name })
219
238
  }),
220
- display && /* @__PURE__ */ jsx(SortMenu, {
221
- model,
222
- view,
223
- display,
224
- orderBy,
225
- onChange: (next) => set({ orderBy: next })
226
- }),
227
- model.search?.length ? /* @__PURE__ */ jsx(ListSearch, {
228
- value: search?.kind === "search" ? search.text : "",
229
- onCommit: commitSearch,
230
- placeholder: `Search ${plural.toLowerCase()}`
231
- }) : null,
239
+ !desktop && sortMenu,
240
+ !desktop && searchBox,
232
241
  can(permissions, "create") && /* @__PURE__ */ jsxs(Button, {
233
242
  variant: "primary",
234
243
  onClick: () => go({
@@ -258,7 +267,7 @@ const LoadedList = ({ model, address }) => {
258
267
  noun: plural.toLowerCase(),
259
268
  filter: parsed.ok ? filterText : ""
260
269
  }),
261
- !desktop ? phoneFilters : layout === "bar" && panel,
270
+ desktop ? desktopToolbar : phoneFilters,
262
271
  desktop && layout === "panel" ? /* @__PURE__ */ jsxs("div", {
263
272
  className: "flex min-h-0 flex-1 gap-4",
264
273
  children: [panel, shown]
@@ -17,7 +17,7 @@ const ListSearch = ({ value, onCommit, placeholder }) => {
17
17
  onCommit
18
18
  ]);
19
19
  return /* @__PURE__ */ jsx(Input, {
20
- wrapperClassName: "min-w-0 flex-1 sm:w-72 sm:flex-none",
20
+ wrapperClassName: "min-w-0 flex-1 sm:w-72 sm:flex-initial",
21
21
  leading: /* @__PURE__ */ jsx(Search, { className: "size-4" }),
22
22
  placeholder,
23
23
  "aria-label": "Search",
@@ -1,6 +1,6 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
1
2
  import { useAdminMeta } from "../../meta-gate.js";
2
3
  import { ErrorBanner } from "../../error-banner.js";
3
- import { Skeleton } from "../../../primitives/skeleton.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { scopeOf, useListPage } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -1,6 +1,6 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
1
2
  import { humanize } from "../../../live/naming.js";
2
3
  import { useAdminMeta } from "../../meta-gate.js";
3
- import { Skeleton } from "../../../primitives/skeleton.js";
4
4
  import { useLayoutRecord } from "../record-scope.js";
5
5
  import { propsOf } from "./block-props.js";
6
6
  import { FieldValue } from "../field-value.js";
@@ -1,6 +1,6 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
1
2
  import { useAdminMeta } from "../../meta-gate.js";
2
3
  import { ErrorBanner } from "../../error-banner.js";
3
- import { Skeleton } from "../../../primitives/skeleton.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { useFirstRecord } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -1,6 +1,6 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
1
2
  import { useAdminMeta } from "../../meta-gate.js";
2
3
  import { AppLink } from "../../use-app-address.js";
3
- import { Skeleton } from "../../../primitives/skeleton.js";
4
4
  import { useLayoutRecord } from "../record-scope.js";
5
5
  import { useCount } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -1,6 +1,6 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
1
2
  import { useAdminMeta } from "../../meta-gate.js";
2
3
  import { ErrorBanner } from "../../error-banner.js";
3
- import { Skeleton } from "../../../primitives/skeleton.js";
4
4
  import { useListPage } from "../use-layout-data.js";
5
5
  import { propsOf } from "./block-props.js";
6
6
  import { CardFrame, Quiet } from "./card-frame.js";
@@ -1,9 +1,9 @@
1
+ import { Skeleton } from "../primitives/skeleton.js";
1
2
  import { keys } from "../data/query-keys.js";
2
3
  import { useClient } from "../data/api-provider.js";
3
4
  import { useAdminMeta } from "./meta-gate.js";
4
5
  import { ErrorBanner } from "./error-banner.js";
5
6
  import { Section } from "../record-view/section.js";
6
- import { Skeleton } from "../primitives/skeleton.js";
7
7
  import { RecordTable } from "./pages/record-table.js";
8
8
  import { defaultColumns } from "./pages/blocks/table.js";
9
9
  import { useState } from "react";
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { Skeleton } from "../primitives/skeleton.js";
2
3
  import { ChevronRight } from "lucide-react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/app-shell/breadcrumb.tsx
@@ -9,26 +10,32 @@ const Breadcrumb = ({ parts, onNavigate }) => /* @__PURE__ */ jsx("nav", {
9
10
  className: "flex min-w-0 items-center gap-1.5 text-[13px]",
10
11
  children: parts.map((part, index) => {
11
12
  const last = index === parts.length - 1;
12
- const { label, href } = typeof part === "string" ? {
13
+ const { label, href, loading } = typeof part === "string" ? {
13
14
  label: part,
14
- href: void 0
15
+ href: void 0,
16
+ loading: false
15
17
  } : part;
18
+ const content = loading ? /* @__PURE__ */ jsx(Skeleton, { className: "w-24" }) : label;
16
19
  const text = cn("truncate", last ? "font-semibold" : "text-muted-foreground");
17
20
  return /* @__PURE__ */ jsxs("span", {
18
21
  className: cn("min-w-0 items-center gap-1.5", last ? "flex" : "hidden md:flex"),
19
22
  children: [index > 0 && /* @__PURE__ */ jsx(ChevronRight, { className: "hidden size-3.5 shrink-0 text-faint-foreground md:block" }), href ? /* @__PURE__ */ jsx("a", {
20
23
  href,
21
24
  "aria-current": last ? "page" : void 0,
25
+ "aria-label": loading ? label : void 0,
26
+ "aria-busy": loading || void 0,
22
27
  onClick: (event) => {
23
28
  if (!onNavigate || !inApp(event)) return;
24
29
  event.preventDefault();
25
30
  onNavigate(href);
26
31
  },
27
32
  className: cn(text, "rounded-sm hover:text-foreground hover:underline"),
28
- children: label
33
+ children: content
29
34
  }) : /* @__PURE__ */ jsx("span", {
30
35
  className: text,
31
- children: label
36
+ "aria-label": loading ? label : void 0,
37
+ "aria-busy": loading || void 0,
38
+ children: content
32
39
  })]
33
40
  }, `${index}-${label}`);
34
41
  })
@@ -1,9 +1,9 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { Skeleton } from "../primitives/skeleton.js";
2
3
  import { formatInt } from "../format/number.js";
3
4
  import { Checkbox } from "../primitives/checkbox.js";
4
5
  import { gridTemplate } from "./grid-template.js";
5
6
  import { isRightAligned } from "./column-spec.js";
6
- import { Skeleton } from "../primitives/skeleton.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/data-grid/grid-row.tsx
9
9
  const frameStyle = (frame) => ({
@@ -6,10 +6,12 @@ import { FilterMenu } from "./filter-menu.js";
6
6
  import { FilterSection } from "./filter-section.js";
7
7
  import { RangeFilter } from "./range-filter.js";
8
8
  import { ToggleRow } from "./toggle-row.js";
9
+ import { ToolbarRow, toolbarShows } from "./toolbar-row.js";
9
10
  import { useFilterState } from "./use-filter-state.js";
11
+ import { Fragment } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
13
  //#region src/filter-panel/filter-panel.tsx
12
- const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabels, defaultOpenMenu, fullWidth }) => {
14
+ const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabels, defaultOpenMenu, fullWidth, leading, trailing }) => {
13
15
  const [state, setState] = useFilterState(value, defaultValue, onChange);
14
16
  const chips = activeChips(config, state);
15
17
  const chipsView = /* @__PURE__ */ jsx(ActiveFilterChips, {
@@ -76,35 +78,41 @@ const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabe
76
78
  })
77
79
  ]
78
80
  });
81
+ const menus = [
82
+ ...config.facets.map((group) => /* @__PURE__ */ jsx(FilterMenu, {
83
+ label: group.label,
84
+ activeCount: (state.facets[group.id] ?? []).length,
85
+ defaultOpen: defaultOpenMenu === group.id,
86
+ children: facetView(group)
87
+ }, group.id)),
88
+ ...config.range ? [/* @__PURE__ */ jsx(FilterMenu, {
89
+ label: config.range.label,
90
+ activeCount: state.range ? 1 : 0,
91
+ width: 280,
92
+ defaultOpen: defaultOpenMenu === config.range.id,
93
+ children: rangeView
94
+ }, "range")] : [],
95
+ ...dateView ? [/* @__PURE__ */ jsx(Fragment, { children: dateView }, "date")] : []
96
+ ];
97
+ const toggles = (config.toggles ?? []).map((toggle) => /* @__PURE__ */ jsx("div", {
98
+ className: "flex items-center gap-2 text-[13px] text-muted-foreground",
99
+ children: /* @__PURE__ */ jsx(ToggleRow, {
100
+ label: toggle.label,
101
+ checked: state.toggles.includes(toggle.id),
102
+ onChange: () => setState(toggleFlag(state, toggle.id))
103
+ })
104
+ }, toggle.id));
105
+ const groups = [
106
+ leading ? [/* @__PURE__ */ jsx(Fragment, { children: leading }, "leading")] : [],
107
+ menus,
108
+ toggles
109
+ ];
110
+ if (!toolbarShows(groups, trailing) && chips.length === 0 && !lockedLabels?.length) return null;
79
111
  return /* @__PURE__ */ jsxs("div", {
80
112
  className: "flex flex-col gap-2",
81
- children: [/* @__PURE__ */ jsxs("div", {
82
- className: "flex flex-wrap items-center gap-2",
83
- children: [
84
- config.facets.map((group) => /* @__PURE__ */ jsx(FilterMenu, {
85
- label: group.label,
86
- activeCount: (state.facets[group.id] ?? []).length,
87
- defaultOpen: defaultOpenMenu === group.id,
88
- children: facetView(group)
89
- }, group.id)),
90
- config.range && /* @__PURE__ */ jsx(FilterMenu, {
91
- label: config.range.label,
92
- activeCount: state.range ? 1 : 0,
93
- width: 280,
94
- defaultOpen: defaultOpenMenu === config.range.id,
95
- children: rangeView
96
- }),
97
- dateView,
98
- config.toggles && /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border" }),
99
- config.toggles?.map((toggle) => /* @__PURE__ */ jsx("div", {
100
- className: "flex items-center gap-2 text-[13px] text-muted-foreground",
101
- children: /* @__PURE__ */ jsx(ToggleRow, {
102
- label: toggle.label,
103
- checked: state.toggles.includes(toggle.id),
104
- onChange: () => setState(toggleFlag(state, toggle.id))
105
- })
106
- }, toggle.id))
107
- ]
113
+ children: [/* @__PURE__ */ jsx(ToolbarRow, {
114
+ groups,
115
+ trailing
108
116
  }), chipsView]
109
117
  });
110
118
  };
@@ -0,0 +1,22 @@
1
+ import { Fragment } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/filter-panel/toolbar-row.tsx
4
+ /** Whether a toolbar row with these groups and trailing control shows anything. */
5
+ const toolbarShows = (groups, trailing) => groups.some((group) => group.length > 0) || Boolean(trailing);
6
+ /**
7
+ * One row of list controls: the non-empty groups in order with a divider between each two, then the trailing control
8
+ * at the right end, narrowing before it wraps. Nothing at all when it would show nothing.
9
+ */
10
+ const ToolbarRow = ({ groups, trailing }) => {
11
+ if (!toolbarShows(groups, trailing)) return null;
12
+ const shown = groups.filter((group) => group.length > 0);
13
+ return /* @__PURE__ */ jsxs("div", {
14
+ className: "flex flex-wrap items-center gap-2",
15
+ children: [shown.map((group, index) => /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 && /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border" }), group] }, index)), trailing && /* @__PURE__ */ jsx("div", {
16
+ className: "flex min-w-32 flex-1 justify-end",
17
+ children: trailing
18
+ })]
19
+ });
20
+ };
21
+ //#endregion
22
+ export { ToolbarRow, toolbarShows };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.6";
2
+ var version = "0.12.8";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -1,15 +1,25 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/primitives/segmented.tsx
4
- const Segmented = ({ value, options, onChange, label }) => /* @__PURE__ */ jsx("span", {
4
+ const sizes = {
5
+ sm: {
6
+ group: "h-7",
7
+ option: "px-2.5 text-xs"
8
+ },
9
+ md: {
10
+ group: "h-8",
11
+ option: "px-3 text-[13px]"
12
+ }
13
+ };
14
+ const Segmented = ({ value, options, onChange, label, size = "sm" }) => /* @__PURE__ */ jsx("span", {
5
15
  role: "group",
6
16
  "aria-label": label,
7
- className: "inline-flex h-7 rounded-md bg-muted p-0.5",
17
+ className: cn("inline-flex", sizes[size].group, "rounded-md bg-muted p-0.5"),
8
18
  children: options.map((option) => /* @__PURE__ */ jsxs("button", {
9
19
  type: "button",
10
20
  "aria-pressed": option.value === value,
11
21
  onClick: () => onChange(option.value),
12
- className: cn("inline-flex items-center gap-1.5 rounded px-2.5 text-xs font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
22
+ className: cn("inline-flex items-center gap-1.5 rounded", sizes[size].option, "font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
13
23
  children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "size-3.5" }), option.label]
14
24
  }, option.value))
15
25
  });