@protobase/ui 0.21.3 → 0.21.4

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
@@ -21,6 +21,7 @@ import { useProjectUi } from "./pages/project-ui.js";
21
21
  import { AssistantButton, AssistantPanel } from "./assistant-shell.js";
22
22
  import { bottomBarFromMeta } from "./bottom-bar-from-meta.js";
23
23
  import { breadcrumbFor } from "./breadcrumb-for.js";
24
+ import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
24
25
  import { sourceStatusOf } from "../data/with-sources.js";
25
26
  import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
26
27
  import { useAppAddress } from "./use-app-address.js";
@@ -128,15 +129,26 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
128
129
  const outsideOrganizations = Boolean(organizationSettings) && current !== void 0 && !current.organization;
129
130
  const organizationPage = organizationSettings && user ? organizationPageTitles[path] : void 0;
130
131
  const accountPage = !user ? void 0 : path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : organizationPage;
132
+ const resourceLabel = view?.names?.plural ?? (resource && humanize(resource));
131
133
  const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
132
134
  basePath,
133
135
  address,
134
136
  group: group?.label,
135
137
  page,
136
- resourceLabel: view?.names?.plural ?? (resource && humanize(resource)),
138
+ resourceLabel,
137
139
  recordTitle: title,
138
140
  recordLoading: titleLoading
139
141
  });
142
+ const singular = view?.names?.singular ?? (resource && humanize(resource));
143
+ useDocumentTitle(documentTitle(titleParts({
144
+ address,
145
+ page,
146
+ accountPage,
147
+ resourceLabel,
148
+ singular,
149
+ recordTitle: title,
150
+ recordLoading: titleLoading
151
+ }), meta.appName));
140
152
  const content = user && path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : user && path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
141
153
  title: "Admins only",
142
154
  children: "Only an admin can change how people sign in."
@@ -0,0 +1,34 @@
1
+ import { humanize } from "../../live/naming.js";
2
+ import { createWhere, printFilter } from "@protobase/schema";
3
+ //#region src/app/delete/blocked-delete.ts
4
+ const where$1 = createWhere();
5
+ /** What a delete other records still point at says, such as "Orders still point at this company." */
6
+ const blockedDeleteText = ({ blockingPlural, noun }) => `${blockingPlural} still point at this ${noun.toLowerCase()}.`;
7
+ /**
8
+ * Why the delete was refused, naming the records that point at it by their view's plural, and, when the server names
9
+ * the field pointing at it, a link to their list filtered to those records. `undefined` for a resource the app does not show.
10
+ */
11
+ const blockedDelete = ({ blockedBy, resources, views, noun, key }) => {
12
+ const { resource, field } = blockedBy;
13
+ if (!resources[resource]) return void 0;
14
+ const plural = views[resource]?.names?.plural ?? humanize(resource);
15
+ const text = blockedDeleteText({
16
+ blockingPlural: plural,
17
+ noun
18
+ });
19
+ if (!field || key === void 0) return { text };
20
+ const list = {
21
+ kind: "list",
22
+ resource,
23
+ filter: printFilter(where$1.eq(field, key))
24
+ };
25
+ return {
26
+ text,
27
+ link: {
28
+ label: `Show ${plural.toLowerCase()}`,
29
+ to: list
30
+ }
31
+ };
32
+ };
33
+ //#endregion
34
+ export { blockedDelete, blockedDeleteText };
@@ -1,6 +1,9 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { humanize } from "../../live/naming.js";
3
3
  import { useToast } from "../../toasts/toast-provider.js";
4
+ import { useAdminMeta } from "../meta-gate.js";
5
+ import { useAppAddress } from "../use-app-address.js";
6
+ import { blockedDelete } from "./blocked-delete.js";
4
7
  import { BulkDeleteDialog, ConfirmDeleteDialog, ConflictDialog } from "./dialogs.js";
5
8
  import { diffRecords } from "./record-diff.js";
6
9
  import { useState } from "react";
@@ -21,6 +24,15 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
21
24
  const [busy, setBusy] = useState(false);
22
25
  const noun = view?.names?.singular ?? humanize(model.name);
23
26
  const plural = view?.names?.plural ?? humanize(model.name);
27
+ const { resources, views } = useAdminMeta();
28
+ const { href, open } = useAppAddress();
29
+ const blocked = (error, target) => error.blockedBy && blockedDelete({
30
+ blockedBy: error.blockedBy,
31
+ resources,
32
+ views,
33
+ noun,
34
+ ...model.primaryKey.length === 1 && { key: target.key }
35
+ });
24
36
  const refresh = () => {
25
37
  for (const prefix of [
26
38
  "page",
@@ -87,10 +99,16 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
87
99
  } catch (error) {
88
100
  if (!isApiError(error)) throw error;
89
101
  setFlow(void 0);
102
+ const why = blocked(error, target);
90
103
  toast.show({
91
104
  state: "error",
92
105
  title: `Could not delete ${target.title}`,
93
- description: error.message
106
+ description: why?.text ?? error.message,
107
+ ...why?.link && { action: {
108
+ label: why.link.label,
109
+ href: href(why.link.to),
110
+ onNavigate: open
111
+ } }
94
112
  });
95
113
  } finally {
96
114
  setBusy(false);
@@ -106,7 +124,7 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
106
124
  if (!isApiError(error)) throw error;
107
125
  return {
108
126
  title: target.title,
109
- message: error.message
127
+ message: blocked(error, target)?.text ?? error.message
110
128
  };
111
129
  }
112
130
  };
@@ -13,7 +13,7 @@ const plainHeight = 44;
13
13
  * The list page's cards display: a card per record (see `RecordCard`), scrolled like the table. An embedded list,
14
14
  * which shows a few cards whole, draws them with `CardsDisplay`.
15
15
  */
16
- const ListCards = ({ model, view, display, list }) => {
16
+ const ListCards = ({ model, view, display, list, empty }) => {
17
17
  const plural = view?.names?.plural ?? humanize(model.name);
18
18
  const pictured = display.card === void 0 && (display.image !== void 0 || display.photos !== void 0);
19
19
  if (list.isLoading) return /* @__PURE__ */ jsx("div", {
@@ -24,7 +24,7 @@ const ListCards = ({ model, view, display, list }) => {
24
24
  title: "Could not load rows",
25
25
  error: list.error
26
26
  });
27
- if (!list.source || list.source.total === 0) return /* @__PURE__ */ jsxs("p", {
27
+ if (!list.source || list.source.total === 0) return empty ?? /* @__PURE__ */ jsxs("p", {
28
28
  className: "py-8 text-center text-muted-foreground",
29
29
  children: [
30
30
  "No ",
@@ -0,0 +1,27 @@
1
+ import { useEffect } from "react";
2
+ //#region src/app/document-title.ts
3
+ /** What a tab names, most specific first: the record and its list, the new record and its list, the list, or the page. */
4
+ const titleParts = ({ address, page, accountPage, resourceLabel, singular, recordTitle, recordLoading }) => {
5
+ if (accountPage) return [accountPage];
6
+ if (page) return [page.title];
7
+ if (!address || address.kind === "page") return [];
8
+ const list = resourceLabel ? [resourceLabel] : [];
9
+ if (address.kind === "list") return list;
10
+ if (address.kind === "create") return [...singular ? [`New ${singular.toLowerCase()}`] : [], ...list];
11
+ const record = recordTitle ?? (recordLoading ? void 0 : address.key);
12
+ return [...record ? [record] : [], ...list];
13
+ };
14
+ /** A tab's title: its parts, then the app's name, as in "Nordlicht GmbH · Companies · Protobase". */
15
+ const documentTitle = (parts, appName) => [...parts.filter(Boolean), appName].join(" · ");
16
+ /** Shows `text` as the tab's title while mounted, then puts back the title it found. */
17
+ const useDocumentTitle = (text) => {
18
+ useEffect(() => {
19
+ const before = document.title;
20
+ document.title = text;
21
+ return () => {
22
+ document.title = before;
23
+ };
24
+ }, [text]);
25
+ };
26
+ //#endregion
27
+ export { documentTitle, titleParts, useDocumentTitle };
@@ -40,7 +40,7 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
40
40
  const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true }) => {
41
41
  const { resources, views } = useAdminMeta();
42
42
  const { fields: fieldTypes } = useProjectUi();
43
- const { go } = useAppAddress();
43
+ const { go, href } = useAppAddress();
44
44
  const xs = useIsXs();
45
45
  const [orderBy, setOrderBy] = useState(sort || defaultOrderBy(model, view));
46
46
  const names = columns ?? listColumnNames(model, view);
@@ -125,6 +125,14 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
125
125
  resource: model.name,
126
126
  key: recordKey(model, row)
127
127
  });
128
+ const recordLink = (row) => ({
129
+ href: href({
130
+ kind: "record",
131
+ resource: model.name,
132
+ key: recordKey(model, row)
133
+ }),
134
+ onFollow: () => openRecord(row)
135
+ });
128
136
  const singular = view?.names?.singular ?? humanize(model.name);
129
137
  return /* @__PURE__ */ jsxs("div", {
130
138
  className: "flex flex-col gap-3",
@@ -136,6 +144,7 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
136
144
  sort: parseSort(orderBy),
137
145
  onSortChange: (next) => setOrderBy(sortToOrderBy(next)),
138
146
  onOpenRow: openRecord,
147
+ rowLink: recordLink,
139
148
  rowActions: [{
140
149
  label: `Open ${singular.toLowerCase()}`,
141
150
  onSelect: openRecord
@@ -16,15 +16,11 @@ import { useShownWidget } from "./field-types/shown-widget.js";
16
16
  import { WidgetPart } from "./field-types/widget-part.js";
17
17
  import { Markdown } from "../markdown/markdown.js";
18
18
  import { MarkdownEditor } from "../markdown/markdown-editor.js";
19
+ import { numeric, numericInputProps } from "./numeric-input.js";
19
20
  import { ArrowUpRight } from "lucide-react";
20
21
  import { jsx, jsxs } from "react/jsx-runtime";
21
22
  import { enumLabel } from "@protobase/schema";
22
23
  //#region src/app/field-editor.tsx
23
- const numeric = /* @__PURE__ */ new Set([
24
- "integer",
25
- "bigint",
26
- "decimal"
27
- ]);
28
24
  const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo, relationPicker, invalid, locked, placeholder, reveal, resource, onChange }) => {
29
25
  const control = useFieldControl();
30
26
  if (field.type === "file") return /* @__PURE__ */ jsx(FileField, {
@@ -136,6 +132,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
136
132
  trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", { children: hints.suffix.trim() }) : void 0,
137
133
  placeholder,
138
134
  className: cn(numeric.has(field.type) && "text-right tabular-nums"),
135
+ ...numericInputProps(field.type),
139
136
  value: String(value),
140
137
  onChange: (event) => onChange(event.target.value)
141
138
  });
@@ -20,7 +20,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
20
20
  const HandSortedList = ({ model, view, field, filter, columns }) => {
21
21
  const { resources, views } = useAdminMeta();
22
22
  const { fields: fieldTypes } = useProjectUi();
23
- const { go } = useAppAddress();
23
+ const { go, href } = useAppAddress();
24
24
  const specs = useMemo(() => embeddedColumns(model, view, columns, {
25
25
  resources,
26
26
  views,
@@ -80,6 +80,14 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
80
80
  resource: model.name,
81
81
  key: recordKey(model, row)
82
82
  });
83
+ const recordLink = (row) => ({
84
+ href: href({
85
+ kind: "record",
86
+ resource: model.name,
87
+ key: recordKey(model, row)
88
+ }),
89
+ onFollow: () => openRecord(row)
90
+ });
83
91
  return /* @__PURE__ */ jsx(DataGrid, {
84
92
  fit: true,
85
93
  scroll: "parent",
@@ -87,6 +95,7 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
87
95
  source,
88
96
  latencyMs: 0,
89
97
  onOpenRow: openRecord,
98
+ rowLink: recordLink,
90
99
  rowNumbers: false,
91
100
  reorder: {
92
101
  label: (row) => recordTitle(model, view, row, row.id),
@@ -0,0 +1,31 @@
1
+ //#region src/app/list-empty-state.ts
2
+ /** Why a list has no rows, and the buttons that help: clear the search, clear the filters, or create the first record. */
3
+ const listEmptyState = ({ plural, search, filtered, scoped, canCreate }) => {
4
+ const noun = plural.toLowerCase();
5
+ if (search) return {
6
+ message: `No ${noun} match “${search}”.`,
7
+ clearSearch: true,
8
+ clearFilters: filtered,
9
+ create: false
10
+ };
11
+ if (filtered) return {
12
+ message: `No ${noun} match these filters.`,
13
+ clearSearch: false,
14
+ clearFilters: true,
15
+ create: false
16
+ };
17
+ if (scoped) return {
18
+ message: `No ${noun} match.`,
19
+ clearSearch: false,
20
+ clearFilters: false,
21
+ create: false
22
+ };
23
+ return {
24
+ message: `No ${noun} yet.`,
25
+ clearSearch: false,
26
+ clearFilters: false,
27
+ create: canCreate
28
+ };
29
+ };
30
+ //#endregion
31
+ export { listEmptyState };
@@ -0,0 +1,23 @@
1
+ import { Button } from "../primitives/button.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/app/list-empty.tsx
4
+ /** A list without rows: why (see `listEmptyState`), with buttons to clear its search or filters, or its New button. */
5
+ const ListEmpty = ({ state, onClearSearch, onClearFilters, create }) => /* @__PURE__ */ jsxs("div", {
6
+ className: "flex flex-col items-center gap-3 py-8 text-center text-muted-foreground",
7
+ children: [/* @__PURE__ */ jsx("p", { children: state.message }), (state.clearSearch || state.clearFilters || state.create && create) && /* @__PURE__ */ jsxs("div", {
8
+ className: "flex flex-wrap justify-center gap-2",
9
+ children: [
10
+ state.clearSearch && /* @__PURE__ */ jsx(Button, {
11
+ onClick: onClearSearch,
12
+ children: "Clear search"
13
+ }),
14
+ state.clearFilters && /* @__PURE__ */ jsx(Button, {
15
+ onClick: onClearFilters,
16
+ children: "Clear filters"
17
+ }),
18
+ state.create && create
19
+ ]
20
+ })]
21
+ });
22
+ //#endregion
23
+ export { ListEmpty };
@@ -42,6 +42,8 @@ import { createOffered } from "./create/create-offered.js";
42
42
  import { ImportButton } from "./import/import-button.js";
43
43
  import { exportOffered, exportsSelection } from "./export/export-options.js";
44
44
  import { useExportFlow } from "./export/use-export-flow.js";
45
+ import { ListEmpty } from "./list-empty.js";
46
+ import { listEmptyState } from "./list-empty-state.js";
45
47
  import { ListCards } from "./displays/list-cards.js";
46
48
  import { useListScroll } from "./list-scroll.js";
47
49
  import { ListChart } from "./list-chart.js";
@@ -113,7 +115,7 @@ const ListPage = ({ address }) => {
113
115
  };
114
116
  const LoadedList = ({ model, address, scope, heading }) => {
115
117
  const meta = useAdminMeta();
116
- const { set, go } = useAppAddress();
118
+ const { set, go, href } = useAppAddress();
117
119
  const resource = model.name;
118
120
  const view = meta.views[resource];
119
121
  const fields = useMemo(() => widgetFields(view, model), [view, model]);
@@ -336,12 +338,21 @@ const LoadedList = ({ model, address, scope, heading }) => {
336
338
  key: recordKey(model, row)
337
339
  });
338
340
  };
341
+ const recordLink = (row) => ({
342
+ href: href({
343
+ kind: "record",
344
+ resource,
345
+ key: recordKey(model, row)
346
+ }),
347
+ onFollow: () => openRecord(row)
348
+ });
339
349
  const table = rows.source ? /* @__PURE__ */ jsx(DataGrid, {
340
350
  columns,
341
351
  source: rows.source,
342
352
  sort: parseSort(orderBy),
343
353
  onSortChange: (sort) => set({ orderBy: sortToOrderBy(sort) }),
344
354
  onOpenRow: openRecord,
355
+ rowLink: recordLink,
345
356
  cellEditor: cellEditing.editor,
346
357
  rowPatches: cellEditing.patches,
347
358
  initialScrollIndex: scroll.initialIndex,
@@ -373,13 +384,27 @@ const LoadedList = ({ model, address, scope, heading }) => {
373
384
  initialIndex: scroll.initialIndex,
374
385
  onScrollIndex: scroll.remember
375
386
  };
387
+ const emptyState = listEmptyState({
388
+ plural,
389
+ search: search?.kind === "search" ? search.text : "",
390
+ filtered: chips.length > 0,
391
+ scoped: Boolean(scope) || extras.some((clause) => clause.kind !== "search"),
392
+ canCreate: Boolean(createButton)
393
+ });
394
+ const empty = /* @__PURE__ */ jsx(ListEmpty, {
395
+ state: emptyState,
396
+ onClearSearch: () => commitSearch(""),
397
+ onClearFilters: () => setFilter(emptyFilterState),
398
+ create: createButton
399
+ });
376
400
  const drawDisplay = () => {
377
- if (!display) return table;
401
+ if (!display) return rows.source?.total === 0 && !rows.error ? empty : table;
378
402
  if (display.kind === "cards") return /* @__PURE__ */ jsx(ListCards, {
379
403
  model,
380
404
  view,
381
405
  display,
382
- list: cardsList
406
+ list: cardsList,
407
+ empty
383
408
  }, rows.key.join(" "));
384
409
  if (display.kind === "map") return /* @__PURE__ */ jsx(MapDisplay, {
385
410
  model,
@@ -0,0 +1,21 @@
1
+ //#region src/app/numeric-input.ts
2
+ /** Field types whose values are numbers: right-aligned, with a number keyboard on phones. */
3
+ const numeric = /* @__PURE__ */ new Set([
4
+ "integer",
5
+ "bigint",
6
+ "decimal"
7
+ ]);
8
+ /** The keyboard a phone shows for a field: a number pad for whole numbers, one with a decimal key for decimals. Enter saves or does nothing, hence `done`. */
9
+ const numericInputProps = (type) => {
10
+ if (type === "integer" || type === "bigint") return {
11
+ inputMode: "numeric",
12
+ enterKeyHint: "done"
13
+ };
14
+ if (type === "decimal") return {
15
+ inputMode: "decimal",
16
+ enterKeyHint: "done"
17
+ };
18
+ return {};
19
+ };
20
+ //#endregion
21
+ export { numeric, numericInputProps };
@@ -4,6 +4,8 @@ import { PageModel } from "@protobase/layout";
4
4
  //#region src/data/meta-data.d.ts
5
5
  export type MetaData = {
6
6
  etag: string;
7
+ /** The app's name, which ends the tab titles. */
8
+ appName: string;
7
9
  resources: Record<string, ResourceModel>;
8
10
  views: Record<string, ViewModel>;
9
11
  /** Composed pages by name. */
@@ -26,6 +26,7 @@ const useMeta = () => {
26
26
  }
27
27
  return {
28
28
  etag: result.etag,
29
+ appName: result.meta.appName,
29
30
  resources: Object.fromEntries(result.meta.resources.map((model) => [model.name, model])),
30
31
  views: Object.fromEntries(result.meta.views.map((model) => [model.resource, model])),
31
32
  pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
@@ -1,5 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { useIsDesktop } from "../lib/use-media-query.js";
3
+ import { RouteLink } from "../primitives/route-link.js";
3
4
  import { buildColumns, features } from "./columns.js";
4
5
  import { GridFooter } from "./grid-footer.js";
5
6
  import { RowMenu, availableActions } from "./row-menu.js";
@@ -19,7 +20,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
19
20
  import { useTable } from "@tanstack/react-table";
20
21
  //#region src/data-grid/data-grid.tsx
21
22
  const headerHeight = 32;
22
- const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable: selecting = true, reorder, fit = false, scroll = "own", className }) => {
23
+ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowLink, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable: selecting = true, reorder, fit = false, scroll = "own", className }) => {
23
24
  const selectable = selecting && !reorder;
24
25
  const rowHeight = compact ? 28 : 36;
25
26
  const narrow = !useIsDesktop();
@@ -150,6 +151,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
150
151
  onOpenRow,
151
152
  container: gridRef
152
153
  });
154
+ const linkColumn = (columns.find((spec) => spec.title) ?? columns[0])?.id;
153
155
  const minWidth = gridMinWidth(sized, gutter, trailing, minScale);
154
156
  const own = scroll === "own";
155
157
  const header = /* @__PURE__ */ jsx("div", {
@@ -201,9 +203,15 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
201
203
  }) : void 0,
202
204
  cells: tableRow.getAllCells().map((cell) => {
203
205
  const content = /* @__PURE__ */ jsx(table.FlexRender, { cell });
206
+ const editable = Boolean(cellEditor?.editable(row, cell.column.id));
207
+ const linked = rowLink && cell.column.id === linkColumn && !editable ? /* @__PURE__ */ jsx(RouteLink, {
208
+ to: rowLink(row),
209
+ className: "flex min-w-0 items-center hover:underline",
210
+ children: content
211
+ }) : content;
204
212
  return {
205
213
  spec: columns.find((spec) => spec.id === cell.column.id),
206
- ...cellEditing ? cellEditing.cell(index, row, cell.column.id, content) : { content }
214
+ ...cellEditing ? cellEditing.cell(index, row, cell.column.id, linked) : { content: linked }
207
215
  };
208
216
  })
209
217
  };
@@ -40,10 +40,14 @@ const dragStyle = (frame, dragY, transition) => ({
40
40
  transform: dragY === 0 ? void 0 : `translateY(${dragY}px)`,
41
41
  transition
42
42
  });
43
+ const openUnlessLink = (onOpen) => (event) => {
44
+ if (event.target.closest("a")) return;
45
+ onOpen();
46
+ };
43
47
  const DataRow = ({ cells, selected, onToggle, onOpen, menu, handle, rowRef, dragY, transition, dragging, ...frame }) => /* @__PURE__ */ jsxs("div", {
44
48
  ref: rowRef,
45
49
  role: "row",
46
- onClick: onOpen,
50
+ onClick: onOpen && openUnlessLink(onOpen),
47
51
  className: cn("absolute inset-x-0 top-0 grid items-center border-b", selected ? "bg-primary-soft" : "bg-background hover:bg-surface", onOpen && "cursor-pointer", dragging && "z-30 bg-surface shadow-pop"),
48
52
  style: dragY === void 0 ? frameStyle(frame) : dragStyle(frame, dragY, transition),
49
53
  children: [
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.21.3";
2
+ var version = "0.21.4";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };