@protobase/ui 0.12.14 → 0.12.16

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.
@@ -0,0 +1,77 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { keys } from "../../data/query-keys.js";
3
+ import { useClient } from "../../data/api-provider.js";
4
+ import { cardPhotoList, cardPhotosRequest, shownPhoto } from "./card-photos.js";
5
+ import { useState } from "react";
6
+ import { ChevronLeft, ChevronRight } from "lucide-react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useQuery } from "@tanstack/react-query";
9
+ //#region src/app/displays/card-carousel.tsx
10
+ const arrow = "pointer-events-auto absolute top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full bg-background/85 text-foreground shadow-sm opacity-0 transition-opacity group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 [@media(hover:none)]:opacity-100";
11
+ /**
12
+ * A card whose picture steps through its photos with arrows, the card's own image first. The photos load when an arrow
13
+ * is first pressed, so the list loads as it does without them. The arrows sit over the card's link, beside it in the
14
+ * document, so pressing one does not open the record; the card's list item must be `relative group/card`.
15
+ */
16
+ const CardCarousel = ({ photos, recordKey, cover, children }) => {
17
+ const client = useClient();
18
+ const [opened, setOpened] = useState(false);
19
+ const [step, setStep] = useState(0);
20
+ const request = cardPhotosRequest(photos, recordKey);
21
+ const loaded = useQuery({
22
+ queryKey: keys.list(photos.resource, request.filter, request.orderBy ?? "", request.pageSize, `card-photos:${photos.image}`),
23
+ enabled: opened,
24
+ queryFn: async () => (await client.list(photos.resource, request)).items.map((item) => item[photos.image])
25
+ });
26
+ const list = cardPhotoList(cover, loaded.data ?? []);
27
+ const index = shownPhoto(step, list.length);
28
+ const press = (by) => {
29
+ setOpened(true);
30
+ setStep((current) => current + by);
31
+ };
32
+ const single = loaded.data !== void 0 && list.length < 2;
33
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(list[index]), !single && /* @__PURE__ */ jsxs("div", {
34
+ className: "pointer-events-none absolute inset-x-px top-px aspect-[4/3]",
35
+ "aria-busy": loaded.isFetching,
36
+ children: [
37
+ /* @__PURE__ */ jsx("button", {
38
+ type: "button",
39
+ "aria-label": "Previous photo",
40
+ className: cn(arrow, "left-2"),
41
+ onClick: () => press(-1),
42
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
43
+ className: "size-4",
44
+ "aria-hidden": true
45
+ })
46
+ }),
47
+ /* @__PURE__ */ jsx("button", {
48
+ type: "button",
49
+ "aria-label": "Next photo",
50
+ className: cn(arrow, "right-2"),
51
+ onClick: () => press(1),
52
+ children: /* @__PURE__ */ jsx(ChevronRight, {
53
+ className: "size-4",
54
+ "aria-hidden": true
55
+ })
56
+ }),
57
+ loaded.error && /* @__PURE__ */ jsx("span", {
58
+ className: "absolute inset-x-2 top-2 rounded bg-background/85 px-1.5 py-0.5 text-center text-xs text-danger-text",
59
+ children: "Could not load the photos"
60
+ }),
61
+ loaded.data !== void 0 && /* @__PURE__ */ jsxs("span", {
62
+ className: "absolute inset-x-0 bottom-2 flex justify-center",
63
+ "aria-live": "polite",
64
+ children: [/* @__PURE__ */ jsx("span", {
65
+ className: "sr-only",
66
+ children: `Photo ${index + 1} of ${list.length}`
67
+ }), /* @__PURE__ */ jsx("span", {
68
+ className: "flex items-center gap-1 rounded-full bg-background/70 px-1.5 py-1 text-[11px] tabular-nums leading-none",
69
+ "aria-hidden": true,
70
+ children: list.length > 10 ? `${index + 1} / ${list.length}` : list.map((_, dot) => /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dot === index ? "bg-foreground" : "bg-foreground/35") }, dot))
71
+ })]
72
+ })
73
+ ]
74
+ })] });
75
+ };
76
+ //#endregion
77
+ export { CardCarousel };
@@ -0,0 +1,21 @@
1
+ import { isStoredFile } from "../../live/file-values.js";
2
+ import { createWhere, printFilter } from "@protobase/schema";
3
+ //#region src/app/displays/card-photos.ts
4
+ const where = createWhere();
5
+ /** The list request for a card's photos: the records pointing at the card's, in the display's order, with only their image. */
6
+ const cardPhotosRequest = (photos, recordKey) => ({
7
+ filter: printFilter(where.eq(photos.field, recordKey)),
8
+ ...photos.sort !== void 0 && { orderBy: photos.sort },
9
+ fields: [photos.image],
10
+ pageSize: 100
11
+ });
12
+ /** The files a card steps through: its own image first, then the loaded photos less that same file; empty values are left out. */
13
+ const cardPhotoList = (cover, loaded) => {
14
+ const coverUri = isStoredFile(cover) ? cover.uri : void 0;
15
+ const rest = loaded.filter((photo) => isStoredFile(photo) && photo.uri !== coverUri);
16
+ return coverUri === void 0 ? rest : [cover, ...rest];
17
+ };
18
+ /** The position shown after `step` presses forward, or back when negative, wrapping around both ends. */
19
+ const shownPhoto = (step, count) => count === 0 ? 0 : (step % count + count) % count;
20
+ //#endregion
21
+ export { cardPhotoList, cardPhotosRequest, shownPhoto };
@@ -11,6 +11,7 @@ import { CustomBoundary } from "../pages/custom-boundary.js";
11
11
  import { WidgetPart } from "../field-types/widget-part.js";
12
12
  import { namedIcon } from "../named-icon.js";
13
13
  import { ValueBadge } from "../value-badge.js";
14
+ import { CardCarousel } from "./card-carousel.js";
14
15
  import { DisplayFooter, DisplayState } from "./display-frame.js";
15
16
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
17
  //#region src/app/displays/cards-display.tsx
@@ -39,9 +40,10 @@ const MetaValue = ({ icon, children }) => {
39
40
  });
40
41
  };
41
42
  /**
42
- * A card per record, opening its page: its image across the top, the title over the subtitle, the badge beside it,
43
- * small meta fields below, those with a value, and the labels of its flags that are true; or, with a `card` component,
44
- * what it draws of the row, the built-in content again when it is missing or throws.
43
+ * A card per record, opening its page: its image across the top, with arrows through its photos when the display names
44
+ * them, the title over the subtitle, the badge beside it, small meta fields below, those with a value, and the labels of
45
+ * its flags that are true; or, with a `card` component, what it draws of the row, the built-in content again when it is
46
+ * missing or throws.
45
47
  */
46
48
  const CardsDisplay = ({ model, view, display, list }) => {
47
49
  const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
@@ -76,6 +78,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
76
78
  });
77
79
  };
78
80
  const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
81
+ const photos = Card ? void 0 : display.photos;
79
82
  return /* @__PURE__ */ jsxs(DisplayState, {
80
83
  list,
81
84
  empty: `No ${plural} match.`,
@@ -89,8 +92,9 @@ const CardsDisplay = ({ model, view, display, list }) => {
89
92
  children: list.rows.map((row) => {
90
93
  const status = badge === void 0 ? void 0 : row.record[badge];
91
94
  const raised = flags.filter((name) => row.record[name] === true);
92
- const builtIn = /* @__PURE__ */ jsxs(Fragment, { children: [
93
- image !== void 0 && /* @__PURE__ */ jsx(CardImage, { value: row.record[image] }),
95
+ const cover = image === void 0 ? void 0 : row.record[image];
96
+ const builtIn = (photo) => /* @__PURE__ */ jsxs(Fragment, { children: [
97
+ (image !== void 0 || photos !== void 0) && /* @__PURE__ */ jsx(CardImage, { value: photo }),
94
98
  /* @__PURE__ */ jsxs("span", {
95
99
  className: "flex items-start justify-between gap-2",
96
100
  children: [/* @__PURE__ */ jsx("span", {
@@ -128,7 +132,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
128
132
  }) }, name))
129
133
  })
130
134
  ] });
131
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AppLink, {
135
+ const card = (photo) => /* @__PURE__ */ jsx(AppLink, {
132
136
  to: {
133
137
  kind: "record",
134
138
  resource: model.name,
@@ -137,10 +141,19 @@ const CardsDisplay = ({ model, view, display, list }) => {
137
141
  className: "flex h-full flex-col gap-1.5 rounded-lg border border-border bg-background p-3 text-[13px] hover:border-border-strong hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
138
142
  children: Card ? /* @__PURE__ */ jsx(CustomBoundary, {
139
143
  name: display.card,
140
- fallback: builtIn,
144
+ fallback: builtIn(photo),
141
145
  children: /* @__PURE__ */ jsx(Card, { row })
142
- }) : builtIn
143
- }) }, row.key);
146
+ }) : builtIn(photo)
147
+ });
148
+ return /* @__PURE__ */ jsx("li", {
149
+ className: photos ? "group/card relative" : void 0,
150
+ children: photos ? /* @__PURE__ */ jsx(CardCarousel, {
151
+ photos,
152
+ recordKey: row.key,
153
+ cover,
154
+ children: card
155
+ }) : card(cover)
156
+ }, row.key);
144
157
  })
145
158
  }), /* @__PURE__ */ jsx(DisplayFooter, { list })]
146
159
  });
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.14";
2
+ var version = "0.12.16";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };