@protobase/ui 0.19.5 → 0.19.7

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 (144) hide show
  1. package/dist/account/account-section.js +2 -2
  2. package/dist/account/account-security-page.js +2 -2
  3. package/dist/account/backup-codes.js +1 -1
  4. package/dist/account/passkeys-section.js +2 -2
  5. package/dist/account/password-dialog.js +1 -1
  6. package/dist/account/sign-in-policy-form.js +2 -2
  7. package/dist/account/sign-in-policy-page.js +2 -2
  8. package/dist/account/sign-in-providers-section.js +1 -1
  9. package/dist/account/staff-sign-in-log.js +3 -3
  10. package/dist/account/two-factor-setup.js +6 -6
  11. package/dist/action-card/action-card.js +1 -1
  12. package/dist/activity-chart/activity-chart.js +2 -2
  13. package/dist/app/delete/dialogs.js +3 -3
  14. package/dist/app/displays/display-controls.js +1 -1
  15. package/dist/app/displays/display-frame.js +1 -1
  16. package/dist/app/displays/list-cards.js +1 -1
  17. package/dist/app/displays/map-display.js +1 -1
  18. package/dist/app/displays/record-card.js +1 -1
  19. package/dist/app/embedded-list/embedded-list.js +2 -2
  20. package/dist/app/error-banner.js +1 -1
  21. package/dist/app/export/builder-port.js +54 -0
  22. package/dist/app/export/builder.js +61 -0
  23. package/dist/app/export/cells.js +93 -0
  24. package/dist/app/export/csv.js +17 -0
  25. package/dist/app/export/export-dialog.js +235 -0
  26. package/dist/app/export/export-options.js +49 -0
  27. package/dist/app/export/export-worker.d.ts +1 -0
  28. package/dist/app/export/export-worker.js +9 -0
  29. package/dist/app/export/run-export.js +62 -0
  30. package/dist/app/export/sink.js +91 -0
  31. package/dist/app/export/use-export-flow.js +139 -0
  32. package/dist/app/export/xlsx.js +91 -0
  33. package/dist/app/field-editor.js +6 -12
  34. package/dist/app/file-field.js +4 -4
  35. package/dist/app/gallery/lightbox.js +2 -2
  36. package/dist/app/gallery/photo-details.js +1 -1
  37. package/dist/app/import/import-check-step.js +2 -2
  38. package/dist/app/import/import-file-step.js +2 -2
  39. package/dist/app/import/import-mapping-step.js +1 -1
  40. package/dist/app/import/import-result-step.js +2 -2
  41. package/dist/app/import/import-select.js +2 -2
  42. package/dist/app/list-page.js +28 -3
  43. package/dist/app/meta-gate.js +3 -3
  44. package/dist/app/pages/blocks/card-frame.js +4 -4
  45. package/dist/app/pages/blocks/field.js +2 -5
  46. package/dist/app/pages/blocks/stat.js +2 -2
  47. package/dist/app/pages/problem.js +1 -1
  48. package/dist/app/pages/render-node.js +1 -1
  49. package/dist/app/record-page.js +2 -2
  50. package/dist/app/record-row/record-row-body.js +3 -3
  51. package/dist/app/record-row/saved-record-row.js +1 -1
  52. package/dist/app/related-section.js +1 -1
  53. package/dist/app/relation-picker.js +1 -1
  54. package/dist/app/runtime/runtime-update.js +3 -3
  55. package/dist/app/saved/saved-list.js +1 -1
  56. package/dist/app/searches/saved-search-rows.js +2 -2
  57. package/dist/app/searches/saved-search-section.js +1 -1
  58. package/dist/app/searches/saved-searches.js +1 -1
  59. package/dist/app/searches/search-controls.js +2 -2
  60. package/dist/app/searches/search-name-dialog.js +1 -1
  61. package/dist/app/searches/update-search-dialog.js +16 -19
  62. package/dist/app/sensitive-field.js +1 -1
  63. package/dist/app/source-notice.js +3 -3
  64. package/dist/app/staff-banner.js +1 -1
  65. package/dist/app-shell/breadcrumb.js +1 -1
  66. package/dist/app-shell/global-search.js +1 -1
  67. package/dist/app-shell/page-header.js +1 -1
  68. package/dist/app-shell/profile-menu.js +3 -3
  69. package/dist/app-shell/sidebar-group.js +1 -1
  70. package/dist/app-shell/sidebar-item.js +1 -1
  71. package/dist/app-shell/sidebar.js +2 -2
  72. package/dist/assistant-dock/part-view.js +1 -4
  73. package/dist/assistant-dock/widget-view.js +1 -4
  74. package/dist/auth/auth-gate.js +2 -2
  75. package/dist/auth/auth-layout.js +1 -1
  76. package/dist/auth/code-form.js +2 -2
  77. package/dist/auth/first-run-page.js +1 -1
  78. package/dist/auth/forgot-password-form.js +2 -2
  79. package/dist/auth/provider-sign-in-page.js +1 -1
  80. package/dist/auth/reset-password-form.js +3 -3
  81. package/dist/auth/setup-required-form.js +2 -2
  82. package/dist/auth/setup-required-page.js +1 -1
  83. package/dist/auth/sign-in-form.js +5 -5
  84. package/dist/auth/sign-in-prompt.js +2 -2
  85. package/dist/auth/sign-up-details-form.js +3 -3
  86. package/dist/auth/sign-up-email-form.js +4 -4
  87. package/dist/auth/staff-sign-in-form.js +3 -3
  88. package/dist/auth/two-factor-form.js +5 -5
  89. package/dist/chat/chat-message.js +1 -1
  90. package/dist/chat/chat-thread.js +1 -1
  91. package/dist/data/query-keys.js +7 -0
  92. package/dist/data-grid/grid-row.js +1 -1
  93. package/dist/data-grid/header-menu.js +1 -1
  94. package/dist/data-grid/row-menu.js +1 -1
  95. package/dist/filter-panel/facet-combobox.js +1 -1
  96. package/dist/filter-panel/filter-menu.js +1 -1
  97. package/dist/filter-panel/filter-panel.js +2 -2
  98. package/dist/inline-child-table/line-menu.js +1 -1
  99. package/dist/inline-child-table/line-row.js +4 -4
  100. package/dist/inline-child-table/number-cell.js +1 -1
  101. package/dist/inline-child-table/totals-footer.js +2 -2
  102. package/dist/organizations/app-roles-picker.js +1 -1
  103. package/dist/organizations/invitation-page.js +3 -3
  104. package/dist/organizations/invitations-page.js +2 -2
  105. package/dist/organizations/members-page.js +2 -2
  106. package/dist/organizations/organization-page.js +1 -1
  107. package/dist/organizations/organizations-page.js +2 -2
  108. package/dist/package.js +1 -1
  109. package/dist/plugins/before-sign-in.js +1 -1
  110. package/dist/plugins/plugin-gate.js +1 -1
  111. package/dist/plugins/plugin-notice.js +1 -1
  112. package/dist/primitives/button.js +1 -1
  113. package/dist/primitives/dialog.js +2 -2
  114. package/dist/primitives/input.js +1 -1
  115. package/dist/primitives/menu-select.js +2 -2
  116. package/dist/primitives/responsive-dialog.js +1 -1
  117. package/dist/primitives/segmented.js +1 -1
  118. package/dist/primitives/sheet.js +1 -1
  119. package/dist/primitives/textarea.js +1 -1
  120. package/dist/record-view/action-rail.js +1 -1
  121. package/dist/record-view/field.js +1 -1
  122. package/dist/record-view/key-facts.js +2 -2
  123. package/dist/record-view/record-sidebar.js +2 -2
  124. package/dist/record-view/section.js +2 -2
  125. package/dist/record-view/spec-table.js +1 -1
  126. package/dist/shell/assets/app-BRVJlz_E.js +68 -0
  127. package/dist/shell/assets/export-worker-CVd5saLq.js +2 -0
  128. package/dist/shell/assets/{index-BzCCIfII.js → index-CafsxSWx.js} +1 -1
  129. package/dist/shell/assets/index-Cuulz26c.css +2 -0
  130. package/dist/shell/assets/maplibre-Cb7ks8SG.js +814 -0
  131. package/dist/shell/assets/{record-map-BK_02Lmk.js → record-map-dXyPrmGg.js} +1 -1
  132. package/dist/shell/assets/{records-map-Dn72A_l-.js → records-map-bGH18FRn.js} +1 -1
  133. package/dist/shell/assets/{shared-protobase-ui-B6lrj7eA.js → shared-protobase-ui-e-G14mxb.js} +1 -1
  134. package/dist/shell/index.html +4 -4
  135. package/dist/shell/protobase.shell.json +1 -1
  136. package/dist/side-panel/side-panel.js +1 -1
  137. package/dist/step-list/step-list.js +1 -1
  138. package/dist/styles.css +1 -1
  139. package/dist/theme.css +2 -0
  140. package/dist/toasts/toast.js +2 -2
  141. package/package.json +5 -4
  142. package/dist/shell/assets/app-CZ4drn7G.js +0 -67
  143. package/dist/shell/assets/index-BoeKjvo8.css +0 -2
  144. package/dist/shell/assets/maplibre-BOsAtlAo.js +0 -814
@@ -0,0 +1,91 @@
1
+ import { Zip, ZipDeflate } from "fflate";
2
+ //#region src/app/export/xlsx.ts
3
+ /** The most data rows a sheet holds: Excel's 1,048,576 rows, less the header. */
4
+ const xlsxMaxRows = 1048575;
5
+ const maxCellText = 32767;
6
+ const invalidXml = /[\u0000-\u0008\u000b\u000c\u000e-\u001f￾￿]/g;
7
+ const escapeXml = (text) => text.replace(invalidXml, "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
8
+ /** A column's letters: 0 is A, 25 is Z, 26 is AA. */
9
+ const columnLetters = (index) => {
10
+ const letter = String.fromCharCode(65 + index % 26);
11
+ return index < 26 ? letter : columnLetters(Math.floor(index / 26) - 1) + letter;
12
+ };
13
+ const textCell = (ref, text, style = "") => `<c r="${ref}"${style} t="inlineStr"><is><t xml:space="preserve">${escapeXml(text.slice(0, maxCellText))}</t></is></c>`;
14
+ const xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
15
+ const mainNs = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
16
+ const relNs = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
17
+ const packageRelNs = "http://schemas.openxmlformats.org/package/2006/relationships";
18
+ const contentTypes = `${xml}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/></Types>`;
19
+ const rootRels = `${xml}<Relationships xmlns="${packageRelNs}"><Relationship Id="rId1" Type="${relNs}/officeDocument" Target="xl/workbook.xml"/></Relationships>`;
20
+ const workbookRels = `${xml}<Relationships xmlns="${packageRelNs}"><Relationship Id="rId1" Type="${relNs}/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="${relNs}/styles" Target="styles.xml"/></Relationships>`;
21
+ const sheetName = (name) => name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim() || "Sheet1";
22
+ const workbook = (name) => `${xml}<workbook xmlns="${mainNs}" xmlns:r="${relNs}"><sheets><sheet name="${escapeXml(sheetName(name))}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
23
+ const styles = (formats) => {
24
+ const numFmts = formats.map((code, index) => `<numFmt numFmtId="${164 + index}" formatCode="${escapeXml(code)}"/>`).join("");
25
+ const xfs = formats.map((_, index) => `<xf numFmtId="${164 + index}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>`).join("");
26
+ return `${xml}<styleSheet xmlns="${mainNs}">${formats.length > 0 ? `<numFmts count="${formats.length}">${numFmts}</numFmts>` : ""}<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${2 + formats.length}"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>${xfs}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
27
+ };
28
+ const encoder = new TextEncoder();
29
+ /**
30
+ * Writes a one-sheet .xlsx file as it goes, handing each compressed piece to `onChunk`: a bold, frozen header row, then
31
+ * `rows` as often as they come, and `finish` once. Text is written inline in its cell rather than in a table of shared
32
+ * strings, so the writer holds no more than the rows it is given.
33
+ */
34
+ const createXlsxWriter = (sheet, onChunk) => {
35
+ const zip = new Zip((error, chunk, final) => {
36
+ if (error) throw error;
37
+ onChunk(chunk, final);
38
+ });
39
+ const add = (path, text) => {
40
+ const file = new ZipDeflate(path, { level: 6 });
41
+ zip.add(file);
42
+ file.push(encoder.encode(text), true);
43
+ };
44
+ add("[Content_Types].xml", contentTypes);
45
+ add("_rels/.rels", rootRels);
46
+ add("xl/workbook.xml", workbook(sheet.name));
47
+ add("xl/_rels/workbook.xml.rels", workbookRels);
48
+ const worksheet = new ZipDeflate("xl/worksheets/sheet1.xml", { level: 6 });
49
+ zip.add(worksheet);
50
+ const cols = sheet.widths.map((width, index) => `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"/>`).join("");
51
+ const frozen = "<sheetViews><sheetView workbookViewId=\"0\"><pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozen\"/></sheetView></sheetViews>";
52
+ const header = `<row r="1">${sheet.headers.map((text, index) => textCell(`${columnLetters(index)}1`, text, " s=\"1\"")).join("")}</row>`;
53
+ worksheet.push(encoder.encode(`${xml}<worksheet xmlns="${mainNs}">${frozen}${cols ? `<cols>${cols}</cols>` : ""}<sheetData>${header}`));
54
+ const letters = sheet.headers.map((_, index) => columnLetters(index));
55
+ const formats = [];
56
+ const styleOf = (format) => {
57
+ const index = formats.indexOf(format);
58
+ if (index >= 0) return index + 2;
59
+ formats.push(format);
60
+ return formats.length + 1;
61
+ };
62
+ const cellXml = (cell, ref) => {
63
+ switch (cell.kind) {
64
+ case "empty": return "";
65
+ case "text": return textCell(ref, cell.value);
66
+ case "boolean": return `<c r="${ref}" t="b"><v>${cell.value ? 1 : 0}</v></c>`;
67
+ case "number": return `<c r="${ref}"${cell.format ? ` s="${styleOf(cell.format)}"` : ""}><v>${cell.value}</v></c>`;
68
+ }
69
+ };
70
+ let rowNumber = 1;
71
+ const rows = (cells) => {
72
+ let text = "";
73
+ for (const row of cells) {
74
+ rowNumber += 1;
75
+ if (rowNumber > 1048576) throw new Error(`A sheet holds at most ${xlsxMaxRows} rows`);
76
+ text += `<row r="${rowNumber}">${row.map((cell, index) => cellXml(cell, `${letters[index]}${rowNumber}`)).join("")}</row>`;
77
+ }
78
+ worksheet.push(encoder.encode(text));
79
+ };
80
+ const finish = () => {
81
+ worksheet.push(encoder.encode("</sheetData></worksheet>"), true);
82
+ add("xl/styles.xml", styles(formats));
83
+ zip.end();
84
+ };
85
+ return {
86
+ rows,
87
+ finish
88
+ };
89
+ };
90
+ //#endregion
91
+ export { columnLetters, createXlsxWriter, xlsxMaxRows };
@@ -50,7 +50,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
50
50
  })
51
51
  });
52
52
  if (field.type === "boolean" && locked) return /* @__PURE__ */ jsx("div", {
53
- className: "py-1.5 text-[13px] text-muted-foreground",
53
+ className: "py-1.5 text-muted-foreground",
54
54
  children: displayValue(field, stored, hints)
55
55
  });
56
56
  if (field.type === "boolean") return /* @__PURE__ */ jsx(Switch, {
@@ -61,7 +61,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
61
61
  if (field.type === "relation") {
62
62
  const label = relationLabel ?? String(stored ?? "—");
63
63
  return /* @__PURE__ */ jsx("div", {
64
- className: "py-1.5 text-[13px]",
64
+ className: "py-1.5",
65
65
  children: relationTo ? /* @__PURE__ */ jsx(AppLink, {
66
66
  to: relationTo,
67
67
  className: "font-medium text-primary-text hover:underline",
@@ -71,13 +71,13 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
71
71
  }
72
72
  const multiline = Boolean(hints?.multiline) && field.type === "text";
73
73
  if (!isEditable(field) || locked) return /* @__PURE__ */ jsx("div", {
74
- className: cn("py-1.5 text-[13px] tabular-nums", field.type === "decimal" || field.type === "integer" ? "font-medium" : "text-muted-foreground", multiline && "whitespace-pre-line break-words"),
74
+ className: cn("py-1.5 tabular-nums", field.type === "decimal" || field.type === "integer" ? "font-medium" : "text-muted-foreground", multiline && "whitespace-pre-line break-words"),
75
75
  children: displayValue(field, stored, hints)
76
76
  });
77
77
  if (field.type === "enum") return /* @__PURE__ */ jsxs("select", {
78
78
  value: String(value),
79
79
  onChange: (event) => onChange(event.target.value),
80
- className: "h-8 w-full rounded-md border border-border-strong bg-background px-2 text-[13px] shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20",
80
+ className: "h-8 w-full rounded-md border border-border-strong bg-background px-2 shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20",
81
81
  children: [value === "" && /* @__PURE__ */ jsx("option", {
82
82
  value: "",
83
83
  children: "Choose..."
@@ -94,14 +94,8 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
94
94
  });
95
95
  return /* @__PURE__ */ jsx(Input, {
96
96
  invalid,
97
- leading: hints?.prefix ? /* @__PURE__ */ jsx("span", {
98
- className: "text-[13px]",
99
- children: hints.prefix
100
- }) : void 0,
101
- trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", {
102
- className: "text-[13px]",
103
- children: hints.suffix.trim()
104
- }) : void 0,
97
+ leading: hints?.prefix ? /* @__PURE__ */ jsx("span", { children: hints.prefix }) : void 0,
98
+ trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", { children: hints.suffix.trim() }) : void 0,
105
99
  placeholder,
106
100
  className: cn(numeric.has(field.type) && "text-right tabular-nums"),
107
101
  value: String(value),
@@ -27,11 +27,11 @@ const Thumbnail = ({ value }) => {
27
27
  const FileSummary = ({ value }) => {
28
28
  const shown = fileShown(value);
29
29
  if (!shown) return /* @__PURE__ */ jsx("div", {
30
- className: "py-1.5 text-[13px] text-muted-foreground",
30
+ className: "py-1.5 text-muted-foreground",
31
31
  children: "—"
32
32
  });
33
33
  if ("missing" in shown) return /* @__PURE__ */ jsx("div", {
34
- className: "py-1.5 text-[13px] text-danger-text",
34
+ className: "py-1.5 text-danger-text",
35
35
  children: "The file is missing from storage"
36
36
  });
37
37
  const url = "url" in shown ? shown.url : void 0;
@@ -39,7 +39,7 @@ const FileSummary = ({ value }) => {
39
39
  return /* @__PURE__ */ jsxs("div", {
40
40
  className: "flex min-w-0 items-center gap-3 py-1",
41
41
  children: [/* @__PURE__ */ jsx(Thumbnail, { value }), /* @__PURE__ */ jsxs("div", {
42
- className: "min-w-0 text-[13px]",
42
+ className: "min-w-0",
43
43
  children: [url && !url.startsWith("blob:") ? /* @__PURE__ */ jsx("a", {
44
44
  href: url,
45
45
  target: "_blank",
@@ -158,7 +158,7 @@ const FileField = ({ field, hints, resource, value, locked, invalid, onChange })
158
158
  },
159
159
  onDragLeave: () => setDragging(false),
160
160
  onDrop: drop,
161
- className: cn("flex items-center gap-3 rounded-md border border-dashed px-3 py-2.5 text-[13px] text-muted-foreground", dragging ? "border-primary bg-primary/5" : invalid ? "border-danger" : "border-border-strong"),
161
+ className: cn("flex items-center gap-3 rounded-md border border-dashed px-3 py-2.5 text-muted-foreground", dragging ? "border-primary bg-primary/5" : invalid ? "border-danger" : "border-border-strong"),
162
162
  children: [
163
163
  /* @__PURE__ */ jsx(Upload, { className: "size-4 shrink-0" }),
164
164
  /* @__PURE__ */ jsx("span", {
@@ -77,7 +77,7 @@ const Lightbox = ({ title, photos, index, onIndex, onClose, details, detailsTitl
77
77
  children: [/* @__PURE__ */ jsxs("div", {
78
78
  className: "flex shrink-0 items-center justify-between gap-2 p-3",
79
79
  children: [/* @__PURE__ */ jsx("span", {
80
- className: "rounded-full bg-background/85 px-3 py-1 text-[13px] font-medium tabular-nums text-foreground",
80
+ className: "rounded-full bg-background/85 px-3 py-1 font-medium tabular-nums text-foreground",
81
81
  "aria-live": "polite",
82
82
  children: `${index + 1} / ${count}`
83
83
  }), /* @__PURE__ */ jsxs("div", {
@@ -141,7 +141,7 @@ const Lightbox = ({ title, photos, index, onIndex, onClose, details, detailsTitl
141
141
  "aria-label": detailsTitle,
142
142
  className: "min-h-dvh w-[360px] shrink-0 border-l border-border bg-background p-5 text-foreground",
143
143
  children: [/* @__PURE__ */ jsx("h2", {
144
- className: "mb-3 text-[15px] font-semibold",
144
+ className: "mb-3 text-[16px] font-semibold",
145
145
  children: detailsTitle
146
146
  }), shownDetails]
147
147
  }),
@@ -34,7 +34,7 @@ const PhotoDetails = ({ model, view, photoKey, fields }) => {
34
34
  resource: model.name,
35
35
  key: photoKey
36
36
  },
37
- className: "mt-3 inline-block text-[13px] font-medium text-primary-text hover:underline",
37
+ className: "mt-3 inline-block font-medium text-primary-text hover:underline",
38
38
  children: `Open ${singular}`
39
39
  })] });
40
40
  };
@@ -37,7 +37,7 @@ const ProblemList = ({ rows }) => /* @__PURE__ */ jsxs("ul", {
37
37
  })]
38
38
  });
39
39
  const ImportChecking = ({ rows }) => /* @__PURE__ */ jsxs("p", {
40
- className: "flex items-center gap-2 text-[13px] text-muted-foreground",
40
+ className: "flex items-center gap-2 text-muted-foreground",
41
41
  role: "status",
42
42
  children: [
43
43
  /* @__PURE__ */ jsx(Spinner, { className: "size-3.5" }),
@@ -54,7 +54,7 @@ const ImportCheckStep = ({ check, model, resources, views, columns, headers, fil
54
54
  const view = views[model.name];
55
55
  const shown = columns.map((column) => model.fields[column.field]);
56
56
  return /* @__PURE__ */ jsxs("div", {
57
- className: "flex flex-col gap-4 text-[13px]",
57
+ className: "flex flex-col gap-4",
58
58
  children: [
59
59
  /* @__PURE__ */ jsx(ImportCounts, {
60
60
  label: "Check",
@@ -8,7 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  const ImportFileStep = ({ file, sheets, sheet, reading, onChoose, onSheet }) => {
9
9
  const id = useId();
10
10
  return /* @__PURE__ */ jsxs("div", {
11
- className: "flex flex-col gap-4 text-[13px]",
11
+ className: "flex flex-col gap-4",
12
12
  children: [
13
13
  /* @__PURE__ */ jsxs("div", {
14
14
  className: "flex flex-col gap-1.5",
@@ -27,7 +27,7 @@ const ImportFileStep = ({ file, sheets, sheet, reading, onChoose, onSheet }) =>
27
27
  const chosen = event.target.files?.[0];
28
28
  if (chosen) onChoose(chosen);
29
29
  },
30
- className: "text-[13px] file:mr-3 file:h-8 file:rounded-md file:border file:border-border-strong file:bg-background file:px-3 file:font-medium file:text-foreground max-sm:file:min-h-11"
30
+ className: "file:mr-3 file:h-8 file:rounded-md file:border file:border-border-strong file:bg-background file:px-3 file:font-medium file:text-foreground max-sm:file:min-h-11"
31
31
  }),
32
32
  /* @__PURE__ */ jsx("p", {
33
33
  className: "text-xs text-muted-foreground",
@@ -48,7 +48,7 @@ const ImportMappingStep = ({ flow, model, resources, views, canUpdate }) => {
48
48
  }));
49
49
  const keys = matchFields(model);
50
50
  return /* @__PURE__ */ jsxs("div", {
51
- className: "flex flex-col gap-5 text-[13px]",
51
+ className: "flex flex-col gap-5",
52
52
  children: [
53
53
  canUpdate && /* @__PURE__ */ jsxs("div", {
54
54
  className: "flex flex-col gap-1.5",
@@ -10,7 +10,7 @@ const count = (value) => value.toLocaleString("en-IE");
10
10
  const ImportProgressStep = ({ progress, total, cancelling }) => {
11
11
  const done = progress.created + progress.updated + progress.failed;
12
12
  return /* @__PURE__ */ jsxs("div", {
13
- className: "flex flex-col gap-2 text-[13px]",
13
+ className: "flex flex-col gap-2",
14
14
  children: [/* @__PURE__ */ jsxs("div", {
15
15
  className: "flex items-baseline justify-between gap-2 text-xs",
16
16
  children: [/* @__PURE__ */ jsx("span", {
@@ -40,7 +40,7 @@ const ImportProgressStep = ({ progress, total, cancelling }) => {
40
40
  };
41
41
  /** What the import did: created, updated, skipped and failed rows, and the rows it did not write with the reasons, to download. */
42
42
  const ImportSummary = ({ result, stopped, headers, fileName }) => /* @__PURE__ */ jsxs("div", {
43
- className: "flex flex-col gap-4 text-[13px]",
43
+ className: "flex flex-col gap-4",
44
44
  children: [
45
45
  stopped !== void 0 && /* @__PURE__ */ jsx(ErrorBanner, {
46
46
  title: "The import stopped. The rows counted below were saved.",
@@ -10,7 +10,7 @@ const ImportSelect = ({ label, name, value, options, onChange, help, className }
10
10
  children: [
11
11
  /* @__PURE__ */ jsx("label", {
12
12
  htmlFor: id,
13
- className: "text-[13px] font-medium",
13
+ className: "font-medium",
14
14
  children: label
15
15
  }),
16
16
  /* @__PURE__ */ jsx("select", {
@@ -18,7 +18,7 @@ const ImportSelect = ({ label, name, value, options, onChange, help, className }
18
18
  "aria-label": name,
19
19
  value,
20
20
  onChange: (event) => onChange(event.target.value),
21
- className: "h-8 w-full min-w-0 rounded-md border border-border-strong bg-background px-2 text-[13px] shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 max-sm:min-h-11",
21
+ className: "h-8 w-full min-w-0 rounded-md border border-border-strong bg-background px-2 shadow-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 max-sm:min-h-11",
22
22
  children: options.map((option) => /* @__PURE__ */ jsx("option", {
23
23
  value: option.value,
24
24
  children: option.label
@@ -29,6 +29,8 @@ import { Sheet } from "../primitives/sheet.js";
29
29
  import { useDeleteFlow } from "./delete/use-delete-flow.js";
30
30
  import { useRestore } from "./delete/use-restore.js";
31
31
  import { ImportButton } from "./import/import-button.js";
32
+ import { exportOffered, exportsSelection } from "./export/export-options.js";
33
+ import { useExportFlow } from "./export/use-export-flow.js";
32
34
  import { SignInPrompt } from "../auth/sign-in-prompt.js";
33
35
  import { ListCards } from "./displays/list-cards.js";
34
36
  import { MapDisplay } from "./displays/map-display.js";
@@ -44,7 +46,7 @@ import { facetNames, filterChips } from "./searches/saved-search.js";
44
46
  import { SaveSearchButton } from "./searches/save-search-button.js";
45
47
  import { SavedSearches } from "./searches/saved-searches.js";
46
48
  import { useMemo, useState } from "react";
47
- import { Plus, SlidersHorizontal } from "lucide-react";
49
+ import { Download, Plus, SlidersHorizontal } from "lucide-react";
48
50
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
49
51
  import { createWhere } from "@protobase/schema";
50
52
  import { tableDisplay } from "@protobase/schema/internal";
@@ -272,6 +274,23 @@ const LoadedList = ({ model, address, scope, heading }) => {
272
274
  chips: filterChips(model, view, filterText, facetNames(config))
273
275
  });
274
276
  const importButton = !scope && /* @__PURE__ */ jsx(ImportButton, { model });
277
+ const exportFlow = useExportFlow({
278
+ model,
279
+ view,
280
+ resources: meta.resources,
281
+ views: meta.views,
282
+ plural,
283
+ filter: loadFilter,
284
+ orderBy
285
+ });
286
+ const exportButton = exportOffered(view) && /* @__PURE__ */ jsxs(Button, {
287
+ onClick: () => exportFlow.open(),
288
+ ...!desktop && {
289
+ "aria-label": "Export",
290
+ title: "Export"
291
+ },
292
+ children: [/* @__PURE__ */ jsx(Download, { className: "size-3.5" }), desktop && "Export"]
293
+ });
275
294
  const displaySwitch = (menu) => /* @__PURE__ */ jsx(DisplaySwitch, {
276
295
  view,
277
296
  value: display?.name ?? tableDisplay,
@@ -300,11 +319,14 @@ const LoadedList = ({ model, address, scope, heading }) => {
300
319
  available: (row) => rowAllows(row, "delete"),
301
320
  onSelect: (row) => void remove.start(target(row))
302
321
  }] : []],
303
- bulkActions: can(permissions, "delete") ? [{
322
+ bulkActions: [...exportOffered(view) && exportsSelection(model) ? [{
323
+ label: (count) => `Export ${count}`,
324
+ onRun: (rows) => exportFlow.open(rows)
325
+ }] : [], ...can(permissions, "delete") ? [{
304
326
  label: (count) => `Delete ${count}`,
305
327
  destructive: true,
306
328
  onRun: (rows) => remove.startBulk(rows.map(target))
307
- }] : [],
329
+ }] : []],
308
330
  rowNumbers: false,
309
331
  pinKey: auth.kind === "signed-in" ? pinnedStorageKey(auth.user.id, resource) : void 0,
310
332
  scroll: "parent",
@@ -344,6 +366,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
344
366
  className: "flex min-h-0 flex-1 flex-col gap-3 px-5 py-4",
345
367
  children: [
346
368
  remove.dialogs,
369
+ exportFlow.dialogs,
347
370
  /* @__PURE__ */ jsx(PageHeader, {
348
371
  title: heading?.title ?? plural,
349
372
  subtitle: heading ? heading.subtitle : view?.help,
@@ -352,10 +375,12 @@ const LoadedList = ({ model, address, scope, heading }) => {
352
375
  displaySwitch(false),
353
376
  saveSearch,
354
377
  importButton,
378
+ exportButton,
355
379
  createButton
356
380
  ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
357
381
  saveSearch,
358
382
  importButton,
383
+ exportButton,
359
384
  createButton
360
385
  ] })
361
386
  }),
@@ -19,10 +19,10 @@ const Notice = ({ title, children }) => /* @__PURE__ */ jsx("div", {
19
19
  children: /* @__PURE__ */ jsxs("div", {
20
20
  className: "max-w-md text-center",
21
21
  children: [/* @__PURE__ */ jsx("h1", {
22
- className: "text-[15px] font-semibold",
22
+ className: "text-[16px] font-semibold",
23
23
  children: title
24
24
  }), /* @__PURE__ */ jsx("p", {
25
- className: "mt-1 text-[13px] text-muted-foreground",
25
+ className: "mt-1 text-muted-foreground",
26
26
  children
27
27
  })]
28
28
  })
@@ -69,7 +69,7 @@ const WithSources = ({ meta, children }) => {
69
69
  const MetaGate = ({ children }) => {
70
70
  const meta = useMeta();
71
71
  if (meta.isPending) return /* @__PURE__ */ jsxs("div", {
72
- className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
72
+ className: "flex h-full min-h-48 items-center justify-center gap-2 text-muted-foreground",
73
73
  children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
74
74
  });
75
75
  if (meta.error) return /* @__PURE__ */ jsx(Notice, {
@@ -10,10 +10,10 @@ const CardFrame = ({ title, description, actions, className, bodyClassName, chil
10
10
  children: [/* @__PURE__ */ jsxs("div", {
11
11
  className: "min-w-0",
12
12
  children: [title && /* @__PURE__ */ jsx("h2", {
13
- className: "text-[15px] font-semibold tracking-tight",
13
+ className: "text-[16px] font-semibold tracking-tight",
14
14
  children: title
15
15
  }), description && /* @__PURE__ */ jsx("p", {
16
- className: "mt-0.5 text-[13px] text-muted-foreground",
16
+ className: "mt-0.5 text-muted-foreground",
17
17
  children: description
18
18
  })]
19
19
  }), actions && /* @__PURE__ */ jsx("div", {
@@ -21,13 +21,13 @@ const CardFrame = ({ title, description, actions, className, bodyClassName, chil
21
21
  children: actions
22
22
  })]
23
23
  }), /* @__PURE__ */ jsx("div", {
24
- className: cn("min-w-0 flex-1 p-4 text-[13px]", bodyClassName),
24
+ className: cn("min-w-0 flex-1 p-4", bodyClassName),
25
25
  children
26
26
  })]
27
27
  });
28
28
  /** A short line in place of content: nothing to show, or not loaded yet. */
29
29
  const Quiet = ({ children }) => /* @__PURE__ */ jsx("p", {
30
- className: "text-[13px] text-muted-foreground",
30
+ className: "text-muted-foreground",
31
31
  children
32
32
  });
33
33
  //#endregion
@@ -25,10 +25,7 @@ const FieldBlock = ({ node }) => {
25
25
  value: value.value,
26
26
  format
27
27
  });
28
- if (label === false) return /* @__PURE__ */ jsx("div", {
29
- className: "text-[13px]",
30
- children: shown
31
- });
28
+ if (label === false) return /* @__PURE__ */ jsx("div", { children: shown });
32
29
  const text = label ?? fieldLabel(name, value.state === "ready" ? views[value.model.name]?.fields[value.field.name]?.label : void 0);
33
30
  return /* @__PURE__ */ jsxs("div", {
34
31
  className: "min-w-0",
@@ -36,7 +33,7 @@ const FieldBlock = ({ node }) => {
36
33
  className: "text-xs text-muted-foreground",
37
34
  children: text
38
35
  }), /* @__PURE__ */ jsx("div", {
39
- className: "mt-0.5 truncate text-[13px]",
36
+ className: "mt-0.5 truncate",
40
37
  children: shown
41
38
  })]
42
39
  });
@@ -14,7 +14,7 @@ const Count = ({ model, filter }) => {
14
14
  const count = useCount(model, filter);
15
15
  if (count.isPending) return /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-16" });
16
16
  if (count.error) return /* @__PURE__ */ jsx("span", {
17
- className: "text-[13px] text-danger-text",
17
+ className: "text-danger-text",
18
18
  children: "Could not count"
19
19
  });
20
20
  return /* @__PURE__ */ jsx(AppLink, {
@@ -54,7 +54,7 @@ const StatBlock = ({ node }) => {
54
54
  bodyClassName: "flex flex-col",
55
55
  children: [
56
56
  /* @__PURE__ */ jsx("p", {
57
- className: "text-[13px] text-muted-foreground break-words",
57
+ className: "text-muted-foreground break-words",
58
58
  children: label
59
59
  }),
60
60
  /* @__PURE__ */ jsxs("div", {
@@ -3,7 +3,7 @@ import { jsx } from "react/jsx-runtime";
3
3
  /** A warning in place of something the app cannot draw, such as a component no one registered. */
4
4
  const Problem = ({ children }) => /* @__PURE__ */ jsx("div", {
5
5
  role: "alert",
6
- className: "rounded-lg border border-dashed border-warning bg-warning-soft p-3 text-[13px] text-warning-text",
6
+ className: "rounded-lg border border-dashed border-warning bg-warning-soft p-3 text-warning-text",
7
7
  children
8
8
  });
9
9
  //#endregion
@@ -49,7 +49,7 @@ const renderChildren = (children) => {
49
49
  const [first] = run;
50
50
  if (!isInline(first)) return /* @__PURE__ */ jsx(RenderNode, { node: first }, index);
51
51
  return /* @__PURE__ */ jsx("p", {
52
- className: "text-[13px] leading-relaxed",
52
+ className: "leading-6",
53
53
  children: run.map((child, position) => typeof child === "string" ? /* @__PURE__ */ jsx(Fragment, { children: child }, position) : /* @__PURE__ */ jsx(RenderNode, { node: child }, position))
54
54
  }, index);
55
55
  });
@@ -323,7 +323,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
323
323
  })
324
324
  }),
325
325
  shown !== "form" && offersEdit && editor.dirty && /* @__PURE__ */ jsxs("div", {
326
- className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
326
+ className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2",
327
327
  role: "status",
328
328
  children: [/* @__PURE__ */ jsx("span", { children: "You have unsaved changes to this record." }), /* @__PURE__ */ jsxs("span", {
329
329
  className: "flex gap-2",
@@ -339,7 +339,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
339
339
  })]
340
340
  }),
341
341
  shown === "form" && editor.restored && /* @__PURE__ */ jsxs("div", {
342
- className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
342
+ className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2",
343
343
  role: "status",
344
344
  children: [/* @__PURE__ */ jsx("span", { children: "Your unsaved changes were restored." }), /* @__PURE__ */ jsxs("span", {
345
345
  className: "flex gap-2",
@@ -68,7 +68,7 @@ const RecordRowBody = ({ model, view, filter, sort = "", display: named, limit,
68
68
  showChips && chips.length > 0 || total !== void 0 || note ? /* @__PURE__ */ jsxs("div", {
69
69
  className: "flex flex-wrap items-center justify-between gap-2",
70
70
  children: [showChips && chips.length > 0 ? /* @__PURE__ */ jsx(FilterChipList, { chips }) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("p", {
71
- className: "flex flex-wrap gap-x-3 text-[13px] text-muted-foreground",
71
+ className: "flex flex-wrap gap-x-3 text-muted-foreground",
72
72
  children: [note && /* @__PURE__ */ jsx("span", { children: note }), total !== void 0 && /* @__PURE__ */ jsx("span", {
73
73
  className: "tabular-nums",
74
74
  children: `${total.toLocaleString("en-GB")} ${total === 1 ? singular : plural.toLowerCase()}`
@@ -79,7 +79,7 @@ const RecordRowBody = ({ model, view, filter, sort = "", display: named, limit,
79
79
  title: "Could not load",
80
80
  error: rows.error
81
81
  }) : !source || source.total === 0 ? /* @__PURE__ */ jsx("p", {
82
- className: "text-[13px] text-muted-foreground",
82
+ className: "text-muted-foreground",
83
83
  children: empty ?? `No ${plural.toLowerCase()} match.`
84
84
  }) : /* @__PURE__ */ jsx(RecordRow, {
85
85
  model,
@@ -98,7 +98,7 @@ const RecordRowBody = ({ model, view, filter, sort = "", display: named, limit,
98
98
  resource: list
99
99
  }),
100
100
  onClick: viewAll?.onOpen,
101
- className: "flex items-center gap-1 self-end text-[13px] font-medium text-primary-text hover:underline",
101
+ className: "flex items-center gap-1 self-end font-medium text-primary-text hover:underline",
102
102
  children: [viewAll?.label ?? /* @__PURE__ */ jsxs(Fragment$1, { children: ["View all ", /* @__PURE__ */ jsx("span", {
103
103
  className: "sr-only",
104
104
  children: plural.toLowerCase()
@@ -25,7 +25,7 @@ const SavedRecordRow = ({ model, view, empty, ...props }) => {
25
25
  if (!saved.data) return /* @__PURE__ */ jsx(Skeleton, { className: "h-24" });
26
26
  const scope = savedScope(listed.model, saved.data);
27
27
  if (!scope) return /* @__PURE__ */ jsx("p", {
28
- className: "text-[13px] text-muted-foreground",
28
+ className: "text-muted-foreground",
29
29
  children: empty ?? `No ${plural} yet.`
30
30
  });
31
31
  return /* @__PURE__ */ jsx(RecordRowBody, {
@@ -62,7 +62,7 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
62
62
  children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
63
63
  });
64
64
  const Empty = ({ text }) => /* @__PURE__ */ jsx("p", {
65
- className: "text-[13px] text-muted-foreground",
65
+ className: "text-muted-foreground",
66
66
  children: text ?? "Nothing here yet."
67
67
  });
68
68
  /**
@@ -38,7 +38,7 @@ const RelationPicker = ({ target, view, value, label, invalid, onChange }) => {
38
38
  onChange: (event) => setText(event.target.value)
39
39
  }), open && /* @__PURE__ */ jsxs("ul", {
40
40
  role: "listbox",
41
- className: "absolute z-30 mt-1 max-h-60 w-full overflow-y-auto rounded-md border bg-background py-1 text-[13px] shadow-pop",
41
+ className: "absolute z-30 mt-1 max-h-60 w-full overflow-y-auto rounded-md border bg-background py-1 shadow-pop",
42
42
  children: [
43
43
  options.data?.items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
44
44
  type: "button",
@@ -7,7 +7,7 @@ import { ArrowUpCircle } from "lucide-react";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/app/runtime/runtime-update.tsx
9
9
  const Row = ({ label, value }) => /* @__PURE__ */ jsxs("div", {
10
- className: "flex justify-between gap-4 py-1 text-[13px]",
10
+ className: "flex justify-between gap-4 py-1",
11
11
  children: [/* @__PURE__ */ jsx("span", {
12
12
  className: "text-muted-foreground",
13
13
  children: label
@@ -60,11 +60,11 @@ const RuntimeUpdate = ({ client }) => {
60
60
  }),
61
61
  error !== void 0 && /* @__PURE__ */ jsx("p", {
62
62
  role: "alert",
63
- className: "mt-3 text-[13px] text-danger-text",
63
+ className: "mt-3 text-danger-text",
64
64
  children: updateErrorText(error)
65
65
  }),
66
66
  checkError !== void 0 && /* @__PURE__ */ jsx("p", {
67
- className: "mt-3 text-[13px] text-muted-foreground",
67
+ className: "mt-3 text-muted-foreground",
68
68
  children: "The status could not be checked again; this is the last one read."
69
69
  })
70
70
  ]
@@ -30,7 +30,7 @@ const SavedList = ({ model, view, target, children }) => {
30
30
  }));
31
31
  const scope = savedScope(target, saved.data);
32
32
  if (!scope) return frame(/* @__PURE__ */ jsxs("p", {
33
- className: "py-8 text-center text-[13px] text-muted-foreground",
33
+ className: "py-8 text-center text-muted-foreground",
34
34
  children: [
35
35
  "No ",
36
36
  plural.toLowerCase(),
@@ -25,7 +25,7 @@ const SearchRowItem = ({ model, view, target, targetView, row }) => {
25
25
  "aria-label": title,
26
26
  className: "flex flex-col gap-3 py-4 first:pt-0 last:pb-0",
27
27
  children: [/* @__PURE__ */ jsx("h3", {
28
- className: "text-[14px] font-semibold",
28
+ className: "text-[15px] font-semibold",
29
29
  children: /* @__PURE__ */ jsx(AppLink, {
30
30
  to: {
31
31
  kind: "record",
@@ -55,7 +55,7 @@ const SavedSearchRows = ({ model, view, empty }) => {
55
55
  });
56
56
  if (!saved.data) return /* @__PURE__ */ jsx(Skeleton, { className: "h-40" });
57
57
  if (saved.data.length === 0) return /* @__PURE__ */ jsx("p", {
58
- className: "text-[13px] text-muted-foreground",
58
+ className: "text-muted-foreground",
59
59
  children: empty ?? `No ${plural} yet.`
60
60
  });
61
61
  return /* @__PURE__ */ jsx("div", {
@@ -53,7 +53,7 @@ const SavedSearchSection = ({ model, view, of, target, targetView, row, recordKe
53
53
  children: [/* @__PURE__ */ jsx("div", {
54
54
  className: "flex min-w-0 flex-col gap-2",
55
55
  children: /* @__PURE__ */ jsx("h2", {
56
- className: "text-[15px] font-semibold",
56
+ className: "text-[16px] font-semibold",
57
57
  children: /* @__PURE__ */ jsx(AppLink, {
58
58
  to: {
59
59
  kind: "record",
@@ -58,7 +58,7 @@ const SavedSearches = ({ model, view }) => {
58
58
  if (saved.data.length === 0) {
59
59
  const searched = (targetView?.names?.plural ?? humanize(target.name)).toLowerCase();
60
60
  return frame(/* @__PURE__ */ jsxs("p", {
61
- className: "py-8 text-center text-[13px] text-muted-foreground",
61
+ className: "py-8 text-center text-muted-foreground",
62
62
  children: [
63
63
  "No ",
64
64
  plural.toLowerCase(),