@protobase/ui 0.19.3 → 0.19.5
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/account/backup-codes.js +2 -9
- package/dist/app/displays/list-displays.js +2 -1
- package/dist/app/displays/map-display.js +3 -1
- package/dist/app/filled-fields.js +5 -0
- package/dist/app/gallery/details-open-storage.js +10 -0
- package/dist/app/gallery/gallery-layout.js +3 -1
- package/dist/app/gallery/lightbox.js +107 -61
- package/dist/app/import/convert.js +151 -0
- package/dist/app/import/csv-parser.js +119 -0
- package/dist/app/import/failed-rows.js +21 -0
- package/dist/app/import/import-button.js +31 -0
- package/dist/app/import/import-check-step.js +123 -0
- package/dist/app/import/import-counts.js +19 -0
- package/dist/app/import/import-dialog.js +152 -0
- package/dist/app/import/import-file-step.js +65 -0
- package/dist/app/import/import-mapping-step.js +195 -0
- package/dist/app/import/import-result-step.js +86 -0
- package/dist/app/import/import-select.js +35 -0
- package/dist/app/import/locale-formats.js +15 -0
- package/dist/app/import/lookups.js +48 -0
- package/dist/app/import/mapping.js +78 -0
- package/dist/app/import/parse-worker.d.ts +1 -0
- package/dist/app/import/parse-worker.js +39 -0
- package/dist/app/import/prepare-rows.js +129 -0
- package/dist/app/import/run-import.js +130 -0
- package/dist/app/import/sheet-cell.js +17 -0
- package/dist/app/import/sheet-file.js +58 -0
- package/dist/app/import/sheet-source.js +72 -0
- package/dist/app/import/use-import-flow.js +225 -0
- package/dist/app/import/xlsx-reader.js +43 -0
- package/dist/app/import/xlsx-xml.js +169 -0
- package/dist/app/import/zip-reader.js +72 -0
- package/dist/app/list-page.js +8 -1
- package/dist/app/map/map-layer-choice.js +11 -0
- package/dist/app/map/map-layer-switch.js +55 -0
- package/dist/app/map/map-section.js +3 -1
- package/dist/app/map/position-areas.js +4 -2
- package/dist/app/map/record-map.js +16 -11
- package/dist/app/map/records-map.js +7 -2
- package/dist/app/named-icon.js +9 -1
- package/dist/app/record-page.js +11 -2
- package/dist/auth/sign-in-prompt.js +1 -1
- package/dist/data/api-provider.js +8 -2
- package/dist/data/meta-data.d.ts +3 -2
- package/dist/lib/save-file.js +12 -0
- package/dist/lib/use-focus-trap.js +7 -1
- package/dist/package.js +1 -1
- package/dist/primitives/sheet-swipe.js +5 -0
- package/dist/primitives/sheet.js +40 -12
- package/dist/record-view/key-facts.js +29 -0
- package/dist/shell/assets/app-CZ4drn7G.js +67 -0
- package/dist/shell/assets/index-BoeKjvo8.css +2 -0
- package/dist/shell/assets/{index-5frCs7fQ.js → index-BzCCIfII.js} +1 -1
- package/dist/shell/assets/maplibre-BOsAtlAo.js +814 -0
- package/dist/shell/assets/parse-worker-BtcY0dQw.js +5 -0
- package/dist/shell/assets/record-map-BK_02Lmk.js +1 -0
- package/dist/shell/assets/records-map-Dn72A_l-.js +1 -0
- package/dist/shell/assets/{shared-protobase-ui-Ceamz_7l.js → shared-protobase-ui-B6lrj7eA.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-ChEqQ9ca.js +0 -66
- package/dist/shell/assets/index-D-4IHhqX.css +0 -2
- package/dist/shell/assets/maplibre-D8plsjP2.js +0 -814
- package/dist/shell/assets/record-map-BDJTOwZo.js +0 -1
- package/dist/shell/assets/records-map-C-XKw6m1.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MapLayerSwitch } from "./map-layer-switch.js";
|
|
1
2
|
import { mapLib } from "./maplibre.js";
|
|
2
3
|
import { MapPin } from "lucide-react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -5,9 +6,9 @@ import { Map as Map$1, Marker, NavigationControl } from "react-map-gl/maplibre";
|
|
|
5
6
|
//#region src/app/map/record-map.tsx
|
|
6
7
|
/**
|
|
7
8
|
* A map of `style` centred on `position` at `zoom`, with a pin on it. It pans and zooms with two fingers on a phone and
|
|
8
|
-
* with Ctrl or Cmd and the wheel, so scrolling the page past it does not move it.
|
|
9
|
+
* with Ctrl or Cmd and the wheel, so scrolling the page past it does not move it. With `layers`, it switches to them.
|
|
9
10
|
*/
|
|
10
|
-
const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
|
|
11
|
+
const RecordMap = ({ style, layers, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
|
|
11
12
|
mapLib,
|
|
12
13
|
mapStyle: style,
|
|
13
14
|
initialViewState: {
|
|
@@ -19,16 +20,20 @@ const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
|
|
|
19
20
|
width: "100%",
|
|
20
21
|
height: "100%"
|
|
21
22
|
},
|
|
22
|
-
children: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx(NavigationControl, { showCompass: false }),
|
|
25
|
+
/* @__PURE__ */ jsx(MapLayerSwitch, { layers }),
|
|
26
|
+
/* @__PURE__ */ jsx(Marker, {
|
|
27
|
+
latitude: position.latitude,
|
|
28
|
+
longitude: position.longitude,
|
|
29
|
+
anchor: "bottom",
|
|
30
|
+
children: /* @__PURE__ */ jsx(MapPin, {
|
|
31
|
+
"aria-hidden": true,
|
|
32
|
+
className: "size-8 fill-primary text-primary-foreground drop-shadow",
|
|
33
|
+
strokeWidth: 1.5
|
|
34
|
+
})
|
|
30
35
|
})
|
|
31
|
-
|
|
36
|
+
]
|
|
32
37
|
});
|
|
33
38
|
//#endregion
|
|
34
39
|
export { RecordMap };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { groupPoints } from "../displays/map-groups.js";
|
|
3
|
+
import { MapLayerSwitch } from "./map-layer-switch.js";
|
|
3
4
|
import { mapLib } from "./maplibre.js";
|
|
4
|
-
import { PositionAreas } from "./position-areas.js";
|
|
5
|
+
import { PositionAreas, areaLayerId } from "./position-areas.js";
|
|
5
6
|
import { themeColor } from "./theme-color.js";
|
|
6
7
|
import { useEffect, useRef, useState } from "react";
|
|
7
8
|
import { MapPin } from "lucide-react";
|
|
@@ -32,7 +33,7 @@ const corners = (bounds) => [[bounds.west, bounds.south], [bounds.east, bounds.n
|
|
|
32
33
|
* marker selects its records; choosing the map elsewhere clears the selection. A chosen marker the covered bottom would
|
|
33
34
|
* hide moves up into the open part of the map.
|
|
34
35
|
*/
|
|
35
|
-
const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, selected, covered, onSelect, onClear, markerLabel, bucketLabel }) => {
|
|
36
|
+
const RecordsMap = ({ style, layers, initial, points, buckets, fit, onFitted, onMove, selected, covered, onSelect, onClear, markerLabel, bucketLabel }) => {
|
|
36
37
|
const ref = useRef(null);
|
|
37
38
|
const [groups, setGroups] = useState([]);
|
|
38
39
|
const [colors, setColors] = useState();
|
|
@@ -117,6 +118,10 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
117
118
|
onClick: onClear,
|
|
118
119
|
children: [
|
|
119
120
|
/* @__PURE__ */ jsx(NavigationControl, { showCompass: false }),
|
|
121
|
+
/* @__PURE__ */ jsx(MapLayerSwitch, {
|
|
122
|
+
layers,
|
|
123
|
+
below: areaLayerId
|
|
124
|
+
}),
|
|
120
125
|
colors && /* @__PURE__ */ jsx(PositionAreas, {
|
|
121
126
|
points,
|
|
122
127
|
selected,
|
package/dist/app/named-icon.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bed, Bell, Bookmark, ExternalLink, FileText, Heart, RulerDimensionLine, Send, Wallet } from "lucide-react";
|
|
1
|
+
import { Bath, Bed, Bell, Bookmark, Calendar, DoorOpen, ExternalLink, FileText, Heart, House, KeyRound, LandPlot, Leaf, RulerDimensionLine, Send, Tag, Wallet } from "lucide-react";
|
|
2
2
|
//#region src/app/named-icon.ts
|
|
3
3
|
/** The icons an action's, a field's or a sign-in prompt's `icon` may name. */
|
|
4
4
|
const byName = {
|
|
@@ -7,6 +7,14 @@ const byName = {
|
|
|
7
7
|
send: Send,
|
|
8
8
|
ruler: RulerDimensionLine,
|
|
9
9
|
bed: Bed,
|
|
10
|
+
bath: Bath,
|
|
11
|
+
house: House,
|
|
12
|
+
"door-open": DoorOpen,
|
|
13
|
+
"land-plot": LandPlot,
|
|
14
|
+
leaf: Leaf,
|
|
15
|
+
tag: Tag,
|
|
16
|
+
key: KeyRound,
|
|
17
|
+
calendar: Calendar,
|
|
10
18
|
heart: Heart,
|
|
11
19
|
bell: Bell,
|
|
12
20
|
bookmark: Bookmark,
|
package/dist/app/record-page.js
CHANGED
|
@@ -10,6 +10,7 @@ import { recordTitle } from "../live/model-helpers.js";
|
|
|
10
10
|
import { humanize } from "../live/naming.js";
|
|
11
11
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
12
12
|
import { displayValue } from "../live/display-value.js";
|
|
13
|
+
import { filledFields } from "./filled-fields.js";
|
|
13
14
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
14
15
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
15
16
|
import { ErrorBanner } from "./error-banner.js";
|
|
@@ -22,9 +23,11 @@ import { Section } from "../record-view/section.js";
|
|
|
22
23
|
import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record-sidebar.js";
|
|
23
24
|
import { RecordHeader } from "../record-view/record-header.js";
|
|
24
25
|
import { SpecTable } from "../record-view/spec-table.js";
|
|
26
|
+
import { KeyFacts } from "../record-view/key-facts.js";
|
|
25
27
|
import { toneFor } from "../live/columns-from-model.js";
|
|
26
28
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
27
29
|
import { useRestore } from "./delete/use-restore.js";
|
|
30
|
+
import { namedIcon } from "./named-icon.js";
|
|
28
31
|
import { ValueBadge } from "./value-badge.js";
|
|
29
32
|
import { FieldValue } from "./pages/field-value.js";
|
|
30
33
|
import { auditField } from "../live/audit-fields.js";
|
|
@@ -375,12 +378,18 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
375
378
|
recordKey
|
|
376
379
|
}, `gallery:${index}`)];
|
|
377
380
|
if (item.kind !== "section") return [];
|
|
378
|
-
const
|
|
381
|
+
const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
|
|
382
|
+
const facts = shown === "read" && item.presentation === "facts";
|
|
383
|
+
const fields = facts ? filledFields(stored.record, visibleFields) : visibleFields;
|
|
379
384
|
if (fields.length === 0) return [];
|
|
380
385
|
return [/* @__PURE__ */ jsx(Section, {
|
|
381
386
|
title: item.title,
|
|
382
387
|
help: helpShown(item.helpWhen, view, editing) ? item.help : void 0,
|
|
383
|
-
children:
|
|
388
|
+
children: facts ? /* @__PURE__ */ jsx(KeyFacts, { facts: fields.map((name) => ({
|
|
389
|
+
label: readRow(name).label,
|
|
390
|
+
value: readRow(name).value,
|
|
391
|
+
icon: namedIcon(hint(name)?.icon)
|
|
392
|
+
})) }) : shown === "read" ? /* @__PURE__ */ jsx(SpecTable, { rows: fields.map(readRow) }) : /* @__PURE__ */ jsx("div", {
|
|
384
393
|
className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
|
|
385
394
|
children: fields.map(fieldView)
|
|
386
395
|
})
|
|
@@ -2,8 +2,8 @@ import { RouteLink } from "../primitives/route-link.js";
|
|
|
2
2
|
import { useAuth } from "./auth-provider.js";
|
|
3
3
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
4
4
|
import { useSignInAddress } from "./use-sign-in-address.js";
|
|
5
|
-
import { namedIcon } from "../app/named-icon.js";
|
|
6
5
|
import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
|
|
6
|
+
import { namedIcon } from "../app/named-icon.js";
|
|
7
7
|
import { useId } from "react";
|
|
8
8
|
import { Check, LogIn } from "lucide-react";
|
|
9
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
|
-
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { createClient } from "@protobase/client";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
@@ -21,6 +21,12 @@ const useResourceClient = (resource) => {
|
|
|
21
21
|
const own = useClient();
|
|
22
22
|
return useContext(ResourceClientContext)?.(resource) ?? own;
|
|
23
23
|
};
|
|
24
|
+
/** `useResourceClient` for any resource: the client of the API that serves each, for code that reaches several. */
|
|
25
|
+
const useResourceClients = () => {
|
|
26
|
+
const own = useClient();
|
|
27
|
+
const resourceClient = useContext(ResourceClientContext);
|
|
28
|
+
return useCallback((resource) => resourceClient?.(resource) ?? own, [own, resourceClient]);
|
|
29
|
+
};
|
|
24
30
|
/** The client of a source's API, one per URL, with the signed-in user's token; for `MetaGate`. */
|
|
25
31
|
const useSourceClients = () => {
|
|
26
32
|
const clientOf = useContext(SourceClientsContext);
|
|
@@ -104,4 +110,4 @@ const ApiProvider = ({ baseUrl, client: given, sources, fetch, queryClient: give
|
|
|
104
110
|
});
|
|
105
111
|
};
|
|
106
112
|
//#endregion
|
|
107
|
-
export { ApiProvider, ClientProvider, useClient, useResourceClient, useSourceClients };
|
|
113
|
+
export { ApiProvider, ClientProvider, useClient, useResourceClient, useResourceClients, useSourceClients };
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
|
|
1
|
+
import { MapLayerModel, PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
|
|
2
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
|
|
@@ -27,9 +27,10 @@ export type MetaData = {
|
|
|
27
27
|
dsn: string;
|
|
28
28
|
environment?: string;
|
|
29
29
|
};
|
|
30
|
-
/** The map style the
|
|
30
|
+
/** The map style the app's maps are drawn with, and the layers they may switch to, when the app has one (see `MapSection`). */
|
|
31
31
|
maps?: {
|
|
32
32
|
style: string;
|
|
33
|
+
layers?: MapLayerModel[];
|
|
33
34
|
};
|
|
34
35
|
/** The hosts besides the app's own whose images are drawn with the page's referrer (see `FileImage`). */
|
|
35
36
|
files?: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/lib/save-file.ts
|
|
2
|
+
/** Hands the browser a file to save under `name`, made from `text`. */
|
|
3
|
+
const saveFile = (text, name, type = "text/plain") => {
|
|
4
|
+
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
5
|
+
Object.assign(document.createElement("a"), {
|
|
6
|
+
href: url,
|
|
7
|
+
download: name
|
|
8
|
+
}).click();
|
|
9
|
+
URL.revokeObjectURL(url);
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { saveFile };
|
|
@@ -2,9 +2,11 @@ import { lockPageScroll } from "./page-scroll-lock.js";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
//#region src/lib/use-focus-trap.ts
|
|
4
4
|
const focusable = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
5
|
+
const stack = [];
|
|
5
6
|
/**
|
|
6
7
|
* While `active`: moves focus into `container`, keeps Tab inside it, locks page scrolling (see `lockPageScroll`),
|
|
7
|
-
* calls `onEscape` on Escape, and returns focus to the previously focused element afterwards.
|
|
8
|
+
* calls `onEscape` on Escape, and returns focus to the previously focused element afterwards. A trap opened while
|
|
9
|
+
* another is active takes Tab and Escape until it ends.
|
|
8
10
|
*/
|
|
9
11
|
const useFocusTrap = (container, active, onEscape) => {
|
|
10
12
|
const escape = useRef(onEscape);
|
|
@@ -14,9 +16,12 @@ const useFocusTrap = (container, active, onEscape) => {
|
|
|
14
16
|
const root = container.current;
|
|
15
17
|
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
16
18
|
const unlock = lockPageScroll();
|
|
19
|
+
const token = {};
|
|
20
|
+
stack.push(token);
|
|
17
21
|
const items = () => [...root.querySelectorAll(focusable)];
|
|
18
22
|
(items()[0] ?? root).focus();
|
|
19
23
|
const onKey = (event) => {
|
|
24
|
+
if (stack.at(-1) !== token) return;
|
|
20
25
|
if (event.key === "Escape") return escape.current();
|
|
21
26
|
if (event.key !== "Tab") return;
|
|
22
27
|
const list = items();
|
|
@@ -37,6 +42,7 @@ const useFocusTrap = (container, active, onEscape) => {
|
|
|
37
42
|
document.addEventListener("keydown", onKey);
|
|
38
43
|
return () => {
|
|
39
44
|
document.removeEventListener("keydown", onKey);
|
|
45
|
+
stack.splice(stack.indexOf(token), 1);
|
|
40
46
|
unlock();
|
|
41
47
|
previous?.focus();
|
|
42
48
|
};
|
package/dist/package.js
CHANGED
package/dist/primitives/sheet.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { useFocusTrap } from "../lib/use-focus-trap.js";
|
|
2
|
+
import { closesSheet } from "./sheet-swipe.js";
|
|
2
3
|
import { useRef } from "react";
|
|
3
4
|
import { X } from "lucide-react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/primitives/sheet.tsx
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* A panel that rises from the bottom of the screen, for the filter panel on mobile screens. It closes on Escape, the
|
|
9
|
+
* close button, a press above it and a swipe down on its header. Its content scrolls inside it when taller than the
|
|
10
|
+
* panel, which stops short of the top so the screen behind stays in sight.
|
|
11
|
+
*/
|
|
7
12
|
const Sheet = ({ title, open, onClose, children }) => {
|
|
8
13
|
const panel = useRef(null);
|
|
14
|
+
const start = useRef(void 0);
|
|
9
15
|
useFocusTrap(panel, open, onClose);
|
|
10
16
|
if (!open) return null;
|
|
17
|
+
const down = (event) => {
|
|
18
|
+
if (event.pointerType === "touch") start.current = {
|
|
19
|
+
x: event.clientX,
|
|
20
|
+
y: event.clientY
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const up = (event) => {
|
|
24
|
+
if (!start.current) return;
|
|
25
|
+
const closes = closesSheet(event.clientX - start.current.x, event.clientY - start.current.y);
|
|
26
|
+
start.current = void 0;
|
|
27
|
+
if (closes) onClose();
|
|
28
|
+
};
|
|
11
29
|
return /* @__PURE__ */ jsxs("div", {
|
|
12
30
|
className: "fixed inset-0 z-[80]",
|
|
13
31
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -22,17 +40,27 @@ const Sheet = ({ title, open, onClose, children }) => {
|
|
|
22
40
|
tabIndex: -1,
|
|
23
41
|
className: "absolute inset-x-0 bottom-0 flex max-h-[85vh] flex-col rounded-t-xl border-t bg-background shadow-pop outline-none",
|
|
24
42
|
children: [/* @__PURE__ */ jsxs("header", {
|
|
25
|
-
className: "flex shrink-0 items-center justify-between border-b px-4",
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
className: "relative flex shrink-0 touch-none items-center justify-between border-b px-4",
|
|
44
|
+
onPointerDown: down,
|
|
45
|
+
onPointerUp: up,
|
|
46
|
+
onPointerCancel: () => start.current = void 0,
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsx("span", {
|
|
49
|
+
className: "absolute left-1/2 top-1.5 h-1 w-9 -translate-x-1/2 rounded-full bg-border-strong",
|
|
50
|
+
"aria-hidden": true
|
|
51
|
+
}),
|
|
52
|
+
/* @__PURE__ */ jsx("h2", {
|
|
53
|
+
className: "text-[15px] font-semibold",
|
|
54
|
+
children: title
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ jsx("button", {
|
|
57
|
+
type: "button",
|
|
58
|
+
"aria-label": "Close",
|
|
59
|
+
onClick: onClose,
|
|
60
|
+
className: "-mr-2 inline-flex size-11 items-center justify-center rounded-md text-muted-foreground hover:bg-muted",
|
|
61
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-5" })
|
|
62
|
+
})
|
|
63
|
+
]
|
|
36
64
|
}), /* @__PURE__ */ jsx("div", {
|
|
37
65
|
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-6",
|
|
38
66
|
children
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/record-view/key-facts.tsx
|
|
3
|
+
/**
|
|
4
|
+
* A record's key facts side by side, as the bedrooms, area and energy label of a listing on a real estate site: each
|
|
5
|
+
* value under its icon, or under its label without one, wrapping onto more rows as needed, two to a row on a mobile
|
|
6
|
+
* screen.
|
|
7
|
+
*/
|
|
8
|
+
const KeyFacts = ({ facts }) => /* @__PURE__ */ jsx("dl", {
|
|
9
|
+
className: "grid grid-cols-2 gap-x-4 gap-y-5 text-[13px] sm:flex sm:flex-wrap sm:gap-x-10",
|
|
10
|
+
children: facts.map(({ label, value, icon: Icon }) => /* @__PURE__ */ jsxs("div", {
|
|
11
|
+
title: label,
|
|
12
|
+
className: "flex min-w-0 flex-col gap-1.5",
|
|
13
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
14
|
+
className: "text-muted-foreground",
|
|
15
|
+
children: Icon ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
|
|
16
|
+
className: "size-5",
|
|
17
|
+
"aria-hidden": true
|
|
18
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: "sr-only",
|
|
20
|
+
children: label
|
|
21
|
+
})] }) : label
|
|
22
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
23
|
+
className: "min-w-0 break-words text-[15px] font-semibold text-foreground",
|
|
24
|
+
children: value
|
|
25
|
+
})]
|
|
26
|
+
}, label))
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { KeyFacts };
|