@marktiderman/genesis-studio 0.2.5 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @marktiderman/genesis-studio
2
2
 
3
- **Genesis Data Studio**, the workspace UI: a sidebar of the tables the signed-in user may see, and a
3
+ **Genesis Data Studio**, the editor UI: a sidebar of the tables the signed-in user may see, and a
4
4
  sheet of the selected one where a cell is edited in place. Every save is checked against the
5
5
  version of the row it was read at and committed with its audit entry, and the outcome is on screen:
6
6
  "Saved", with the audit entry, or "Not saved" and why.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/DataStudio.tsx
4
4
  import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo8, useRef, useState as useState10 } from "react";
5
5
  import { keepPreviousData as keepPreviousData2, useQuery as useQuery5, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
6
- import { ChevronLeft as ChevronLeft2, ChevronRight as ChevronRight2, Database, History as History2, Lock as Lock2, PencilLine, Plus as Plus3, Table2 as Table22, Trash2 as Trash22, Undo2, X as X4 } from "lucide-react";
6
+ import { ChevronLeft, ChevronRight, Database, History as History2, Lock as Lock2, PencilLine, Plus as Plus3, Table2 as Table22, Trash2 as Trash22, Undo2, X as X4 } from "lucide-react";
7
7
  import { GenesisProvider } from "@marktiderman/genesis-core/provider";
8
8
  import {
9
9
  encodeRecordKey as encodeRecordKey5,
@@ -11,10 +11,10 @@ import {
11
11
  keyFields as keyFields3
12
12
  } from "@marktiderman/genesis-core/schema";
13
13
  import { useSavedViews } from "@marktiderman/genesis-ui/data";
14
- import { Button as Button9 } from "@marktiderman/genesis-ui/button";
14
+ import { Button as Button8 } from "@marktiderman/genesis-ui/button";
15
15
  import { Dialog as Dialog3, DialogContent as DialogContent3, DialogDescription as DialogDescription3, DialogHeader as DialogHeader3, DialogTitle as DialogTitle3 } from "@marktiderman/genesis-ui/dialog";
16
16
  import { Alert as Alert2, AlertDescription as AlertDescription2, AlertTitle as AlertTitle2 } from "@marktiderman/genesis-ui/alert";
17
- import { cn as cn4 } from "@marktiderman/genesis-ui/utils";
17
+ import { cn as cn3 } from "@marktiderman/genesis-ui/utils";
18
18
 
19
19
  // src/router.ts
20
20
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -43,7 +43,7 @@ function locationFromSearchParams(params) {
43
43
  ...params.get(PARAM.record) ? { record: params.get(PARAM.record) } : {},
44
44
  ...VIEWS.includes(params.get(PARAM.view) ?? "") ? { view: params.get(PARAM.view) } : {},
45
45
  ...params.get(PARAM.by) ? { by: params.get(PARAM.by) } : {},
46
- .../^\d{4}-\d{2}$/.test(params.get(PARAM.month) ?? "") ? { month: params.get(PARAM.month) } : {}
46
+ .../^\d{4}-(0[1-9]|1[0-2])$/.test(params.get(PARAM.month) ?? "") ? { month: params.get(PARAM.month) } : {}
47
47
  };
48
48
  }
49
49
  function searchParamsFromLocation(location, base) {
@@ -1731,12 +1731,12 @@ function KanbanView({
1731
1731
  // src/views/CalendarView.tsx
1732
1732
  import { useEffect as useEffect4, useMemo as useMemo5 } from "react";
1733
1733
  import { keepPreviousData, useQuery as useQuery4 } from "@tanstack/react-query";
1734
- import { ChevronLeft, ChevronRight } from "lucide-react";
1735
1734
  import { encodeRecordKey as encodeRecordKey3, isDataWriteError as isDataWriteError3 } from "@marktiderman/genesis-core/schema";
1736
- import { Button as Button6 } from "@marktiderman/genesis-ui/button";
1737
- import { cn as cn3 } from "@marktiderman/genesis-ui/utils";
1735
+ import { EventCalendar, readEventTime as readEventTime2 } from "@marktiderman/genesis-ui/data";
1738
1736
 
1739
1737
  // src/views/fields.ts
1738
+ import { deviceTimeZone } from "@marktiderman/genesis-core/dates";
1739
+ import { calendarWindow, readEventTime, timeStorage, windowBounds } from "@marktiderman/genesis-ui/data";
1740
1740
  function boardFields(schema) {
1741
1741
  return schema.fields.filter((f) => !f.array && Boolean(choicesFor(f)));
1742
1742
  }
@@ -1746,58 +1746,20 @@ function dateFields(schema) {
1746
1746
  function isDateOnly(field) {
1747
1747
  return field.dbType === "date" || field.widget === "date";
1748
1748
  }
1749
- var DB_DATE = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?)(\.\d+)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/;
1750
- function localDay(value, dateOnly) {
1751
- if (typeof value !== "string") return null;
1752
- const m = DB_DATE.exec(value.trim());
1753
- if (!m) return null;
1754
- if (dateOnly || !m[2]) return m[1];
1755
- let zone = m[4] ?? "";
1756
- if (/^[+-]\d{2}$/.test(zone)) zone += ":00";
1757
- else if (/^[+-]\d{4}$/.test(zone)) zone = `${zone.slice(0, 3)}:${zone.slice(3)}`;
1758
- const time = m[2].length === 5 ? `${m[2]}:00` : m[2];
1759
- const ms = m[3] ? m[3].slice(0, 4).padEnd(4, "0") : "";
1760
- const date = /* @__PURE__ */ new Date(`${m[1]}T${time}${ms}${zone}`);
1761
- if (Number.isNaN(date.getTime())) return null;
1762
- return dayKey(date);
1763
- }
1764
1749
  function dayKey(date) {
1765
1750
  const pad = (n) => String(n).padStart(2, "0");
1766
1751
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1767
1752
  }
1768
- function isZoned(field) {
1769
- return !isDateOnly(field) && (field.dbType === void 0 || /with time zone|timestamptz/.test(field.dbType));
1770
- }
1771
1753
  function monthBounds(month, field) {
1772
- const [y, m] = month.split("-").map(Number);
1773
- const start = new Date(y, m - 1, 1);
1774
- const next = new Date(y, m, 1);
1775
- return isZoned(field) ? { from: start.toISOString(), to: next.toISOString() } : { from: dayKey(start), to: dayKey(next) };
1754
+ return windowBounds(calendarWindow("month", `${month}-01`, { weekStartsOn: 1 }), timeStorage(field), deviceTimeZone());
1776
1755
  }
1777
1756
  function currentMonth(now = /* @__PURE__ */ new Date()) {
1778
1757
  return dayKey(now).slice(0, 7);
1779
1758
  }
1780
- function shiftMonth(month, by) {
1781
- const [y, m] = month.split("-").map(Number);
1782
- return currentMonth(new Date(y, m - 1 + by, 1));
1783
- }
1784
1759
 
1785
1760
  // src/views/CalendarView.tsx
1786
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1761
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1787
1762
  var CALENDAR_CAP = 200;
1788
- var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1789
- function weeksOf(month) {
1790
- const [y, m] = month.split("-").map(Number);
1791
- const first = new Date(y, m - 1, 1);
1792
- const days = new Date(y, m, 0).getDate();
1793
- const lead = (first.getDay() + 6) % 7;
1794
- const cells = [
1795
- ...Array.from({ length: lead }, () => null),
1796
- ...Array.from({ length: days }, (_, i) => dayKey(new Date(y, m - 1, i + 1)))
1797
- ];
1798
- while (cells.length % 7) cells.push(null);
1799
- return Array.from({ length: cells.length / 7 }, (_, w) => cells.slice(w * 7, w * 7 + 7));
1800
- }
1801
1763
  function CalendarView({
1802
1764
  provider,
1803
1765
  schema,
@@ -1835,83 +1797,55 @@ function CalendarView({
1835
1797
  if (signedOut) onSignedOut();
1836
1798
  }, [signedOut, onSignedOut]);
1837
1799
  const rows = query.data?.data ?? [];
1838
- const byDay = useMemo5(() => {
1839
- const map = /* @__PURE__ */ new Map();
1840
- for (const row of rows) {
1841
- const day = localDay(row[field.name], dateOnly);
1842
- if (!day) continue;
1843
- map.set(day, [...map.get(day) ?? [], row]);
1844
- }
1845
- return map;
1846
- }, [rows, field.name, dateOnly]);
1847
- const total = query.data?.total ?? 0;
1848
- const today = dayKey(/* @__PURE__ */ new Date());
1849
- const title = (/* @__PURE__ */ new Date(`${month}-01T00:00:00`)).toLocaleDateString(void 0, { month: "long", year: "numeric" });
1850
1800
  const labelField = schema.labelField;
1851
- return /* @__PURE__ */ jsxs7("div", { className: "space-y-2", "data-testid": "studio-calendar", children: [
1852
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1853
- /* @__PURE__ */ jsx8(Button6, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", "aria-label": "Previous month", onClick: () => onMonthChange(shiftMonth(month, -1)), children: /* @__PURE__ */ jsx8(ChevronLeft, { className: "h-4 w-4", "aria-hidden": true }) }),
1854
- /* @__PURE__ */ jsx8(Button6, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", "aria-label": "Next month", onClick: () => onMonthChange(shiftMonth(month, 1)), children: /* @__PURE__ */ jsx8(ChevronRight, { className: "h-4 w-4", "aria-hidden": true }) }),
1855
- /* @__PURE__ */ jsx8(Button6, { type: "button", variant: "ghost", size: "sm", onClick: () => onMonthChange(currentMonth()), children: "Today" }),
1856
- /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold", "aria-live": "polite", children: title }),
1857
- /* @__PURE__ */ jsxs7("span", { className: "text-xs text-muted-foreground", children: [
1801
+ const events = useMemo5(
1802
+ () => rows.flatMap((row) => {
1803
+ const start = readEventTime2(row[field.name], { dateOnly });
1804
+ if (start === null) return [];
1805
+ const key = String(encodeRecordKey3(schema, row));
1806
+ return [{ key, title: displayText(labelField ? row[labelField] : void 0) ?? key, start, row }];
1807
+ }),
1808
+ [rows, field.name, dateOnly, schema, labelField]
1809
+ );
1810
+ const total = query.data?.total ?? 0;
1811
+ return /* @__PURE__ */ jsx8(
1812
+ EventCalendar,
1813
+ {
1814
+ events,
1815
+ layout: "month",
1816
+ weekStartsOn: 1,
1817
+ anchor: `${month}-01`,
1818
+ onAnchorChange: (day) => onMonthChange(day.slice(0, 7)),
1819
+ label: `${schema.label ?? schema.name} by ${fieldLabel(field)}`,
1820
+ toolbar: /* @__PURE__ */ jsxs7("span", { className: "text-xs text-muted-foreground", children: [
1858
1821
  "by ",
1859
1822
  fieldLabel(field)
1860
- ] })
1861
- ] }),
1862
- total > rows.length && /* @__PURE__ */ jsxs7("p", { role: "status", className: "text-xs text-muted-foreground", "data-testid": "studio-calendar-capped", children: [
1863
- "Showing the first ",
1864
- rows.length.toLocaleString(),
1865
- " of ",
1866
- total.toLocaleString(),
1867
- " rows this month. Filter the table to see the rest."
1868
- ] }),
1869
- query.isError && !signedOut && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-sm text-destructive", children: query.error instanceof Error ? query.error.message : String(query.error) }),
1870
- /* @__PURE__ */ jsx8("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs7("table", { className: "w-full min-w-[42rem] table-fixed border-collapse text-sm", children: [
1871
- /* @__PURE__ */ jsxs7("caption", { className: "sr-only", children: [
1872
- schema.label ?? schema.name,
1873
- " by ",
1874
- fieldLabel(field),
1875
- ", ",
1876
- title
1877
1823
  ] }),
1878
- /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { children: WEEKDAYS.map((d) => /* @__PURE__ */ jsx8("th", { scope: "col", className: "py-1 text-left text-xs font-medium text-muted-foreground", children: d }, d)) }) }),
1879
- /* @__PURE__ */ jsx8("tbody", { children: weeksOf(month).map((week, w) => /* @__PURE__ */ jsx8("tr", { children: week.map((day, d) => /* @__PURE__ */ jsx8(
1880
- "td",
1881
- {
1882
- className: cn3("h-28 border align-top", !day && "bg-muted/30"),
1883
- "data-day": day ?? void 0,
1884
- children: day && /* @__PURE__ */ jsxs7("div", { className: "flex h-full flex-col gap-0.5 p-1", children: [
1885
- /* @__PURE__ */ jsx8("span", { className: cn3("text-xs", day === today ? "font-bold text-primary" : "text-muted-foreground"), children: Number(day.slice(8)) }),
1886
- /* @__PURE__ */ jsx8("ul", { className: "max-h-24 space-y-0.5 overflow-y-auto", children: (byDay.get(day) ?? []).map((row) => {
1887
- const id = String(encodeRecordKey3(schema, row));
1888
- return /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(
1889
- "button",
1890
- {
1891
- type: "button",
1892
- className: "w-full truncate rounded bg-primary/10 px-1.5 py-0.5 text-left text-xs hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1893
- onClick: () => onOpenRecord(id),
1894
- "data-testid": "studio-calendar-item",
1895
- children: displayText(labelField ? row[labelField] : void 0) ?? id
1896
- }
1897
- ) }, id);
1898
- }) })
1899
- ] })
1900
- },
1901
- day ?? `pad-${w}-${d}`
1902
- )) }, w)) })
1903
- ] }) })
1904
- ] });
1824
+ notice: /* @__PURE__ */ jsxs7(Fragment3, { children: [
1825
+ total > rows.length && /* @__PURE__ */ jsxs7("p", { role: "status", className: "text-xs text-muted-foreground", "data-testid": "studio-calendar-capped", children: [
1826
+ "Showing the first ",
1827
+ rows.length.toLocaleString(),
1828
+ " of ",
1829
+ total.toLocaleString(),
1830
+ " rows in this view. Filter the table to see the rest."
1831
+ ] }),
1832
+ query.isError && !signedOut && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-sm text-destructive", children: query.error instanceof Error ? query.error.message : String(query.error) })
1833
+ ] }),
1834
+ loading: query.isPending,
1835
+ onSelect: (event) => onOpenRecord(event.key),
1836
+ testID: "studio-calendar"
1837
+ }
1838
+ );
1905
1839
  }
1906
1840
 
1907
1841
  // src/toolbar/ImportDialog.tsx
1908
1842
  import { useMemo as useMemo6, useState as useState8 } from "react";
1909
1843
  import { Upload } from "lucide-react";
1910
- import { Button as Button7 } from "@marktiderman/genesis-ui/button";
1844
+ import { Button as Button6 } from "@marktiderman/genesis-ui/button";
1911
1845
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@marktiderman/genesis-ui/dialog";
1912
1846
  import { Label as Label2 } from "@marktiderman/genesis-ui/label";
1913
1847
  import { Select as Select3, SelectContent as SelectContent3, SelectItem as SelectItem3, SelectTrigger as SelectTrigger3, SelectValue as SelectValue3 } from "@marktiderman/genesis-ui/select";
1914
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1848
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1915
1849
  var SKIP = "__genesis_skip__";
1916
1850
  function ImportDialog({ provider, schema, onImported }) {
1917
1851
  const [open, setOpen] = useState8(false);
@@ -1965,9 +1899,9 @@ function ImportDialog({ provider, schema, onImported }) {
1965
1899
  }
1966
1900
  }
1967
1901
  const mapped = Object.values(mapping).filter((f) => f && f !== SKIP).length;
1968
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1902
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1969
1903
  /* @__PURE__ */ jsx9(
1970
- Button7,
1904
+ Button6,
1971
1905
  {
1972
1906
  type: "button",
1973
1907
  variant: "ghost",
@@ -2003,7 +1937,7 @@ function ImportDialog({ provider, schema, onImported }) {
2003
1937
  }
2004
1938
  )
2005
1939
  ] }),
2006
- file && /* @__PURE__ */ jsxs8(Fragment3, { children: [
1940
+ file && /* @__PURE__ */ jsxs8(Fragment4, { children: [
2007
1941
  /* @__PURE__ */ jsxs8("p", { className: "text-xs text-muted-foreground", children: [
2008
1942
  file.name,
2009
1943
  ": ",
@@ -2029,7 +1963,7 @@ function ImportDialog({ provider, schema, onImported }) {
2029
1963
  "All ",
2030
1964
  check.count,
2031
1965
  " rows are ready to import."
2032
- ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
1966
+ ] }) : /* @__PURE__ */ jsxs8(Fragment4, { children: [
2033
1967
  /* @__PURE__ */ jsxs8("p", { className: "font-medium text-destructive", children: [
2034
1968
  (check.failures ?? []).length,
2035
1969
  " of ",
@@ -2045,8 +1979,8 @@ function ImportDialog({ provider, schema, onImported }) {
2045
1979
  ] }) }),
2046
1980
  error && /* @__PURE__ */ jsx9("p", { role: "alert", className: "text-sm text-destructive", children: error }),
2047
1981
  /* @__PURE__ */ jsxs8("div", { className: "flex justify-end gap-2", children: [
2048
- /* @__PURE__ */ jsx9(Button7, { type: "button", variant: "outline", disabled: busy || mapped === 0, onClick: () => void run(true), testID: "studio-import-check-button", children: busy ? "Checking\u2026" : "Check rows" }),
2049
- /* @__PURE__ */ jsxs8(Button7, { type: "button", disabled: busy || !check?.ok, onClick: () => void run(false), testID: "studio-import-commit", children: [
1982
+ /* @__PURE__ */ jsx9(Button6, { type: "button", variant: "outline", disabled: busy || mapped === 0, onClick: () => void run(true), testID: "studio-import-check-button", children: busy ? "Checking\u2026" : "Check rows" }),
1983
+ /* @__PURE__ */ jsxs8(Button6, { type: "button", disabled: busy || !check?.ok, onClick: () => void run(false), testID: "studio-import-commit", children: [
2050
1984
  "Import ",
2051
1985
  check?.ok ? check.count : "",
2052
1986
  " rows"
@@ -2060,7 +1994,7 @@ function ImportDialog({ provider, schema, onImported }) {
2060
1994
 
2061
1995
  // src/toolbar/BulkEdit.tsx
2062
1996
  import { useState as useState9 } from "react";
2063
- import { Button as Button8 } from "@marktiderman/genesis-ui/button";
1997
+ import { Button as Button7 } from "@marktiderman/genesis-ui/button";
2064
1998
  import { Dialog as Dialog2, DialogContent as DialogContent2, DialogDescription as DialogDescription2, DialogHeader as DialogHeader2, DialogTitle as DialogTitle2 } from "@marktiderman/genesis-ui/dialog";
2065
1999
  import { Input as Input3 } from "@marktiderman/genesis-ui/input";
2066
2000
  import { Label as Label3 } from "@marktiderman/genesis-ui/label";
@@ -2140,8 +2074,8 @@ function BulkEditDialog({ schema, count, open, onOpenChange, onApply }) {
2140
2074
  /* @__PURE__ */ jsx10("ul", { className: "mt-1 list-disc pl-5 text-xs", children: (result.failures ?? []).slice(0, 10).map((f) => /* @__PURE__ */ jsx10("li", { children: f.message }, f.index)) })
2141
2075
  ] }),
2142
2076
  /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
2143
- /* @__PURE__ */ jsx10(Button8, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
2144
- /* @__PURE__ */ jsx10(Button8, { type: "button", disabled: busy || !target, onClick: () => void apply(), testID: "studio-bulk-apply", children: busy ? "Saving\u2026" : `Set on ${count} row${count === 1 ? "" : "s"}` })
2077
+ /* @__PURE__ */ jsx10(Button7, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
2078
+ /* @__PURE__ */ jsx10(Button7, { type: "button", disabled: busy || !target, onClick: () => void apply(), testID: "studio-bulk-apply", children: busy ? "Saving\u2026" : `Set on ${count} row${count === 1 ? "" : "s"}` })
2145
2079
  ] })
2146
2080
  ] })
2147
2081
  ] })
@@ -2310,7 +2244,7 @@ function SheetView({
2310
2244
  }
2311
2245
 
2312
2246
  // src/DataStudio.tsx
2313
- import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2247
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2314
2248
  var PER_PAGE = 50;
2315
2249
  var SEARCH_DEBOUNCE_MS = 300;
2316
2250
  function isUnauthenticated(error) {
@@ -2459,14 +2393,14 @@ function StudioWorkspace({
2459
2393
  );
2460
2394
  }, []);
2461
2395
  if (signedOut) {
2462
- return /* @__PURE__ */ jsx12("div", { className: cn4("flex min-h-[320px] items-center justify-center p-6", className), "data-testid": "studio-sign-in", children: signIn ?? /* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Sign in to use the studio." }) });
2396
+ return /* @__PURE__ */ jsx12("div", { className: cn3("flex min-h-[320px] items-center justify-center p-6", className), "data-testid": "studio-sign-in", children: signIn ?? /* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Sign in to use the studio." }) });
2463
2397
  }
2464
2398
  const selected = router.location.resource;
2465
2399
  const list = resources.data ?? [];
2466
- return /* @__PURE__ */ jsxs11("div", { className: cn4("w-full space-y-3", className), children: [
2400
+ return /* @__PURE__ */ jsxs11("div", { className: cn3("w-full space-y-3", className), children: [
2467
2401
  /* @__PURE__ */ jsxs11("div", { className: variant === "card" ? "overflow-hidden rounded-lg border bg-background" : "min-w-0", "data-testid": "studio", "data-variant": variant, children: [
2468
- (title !== null || resources.isLoading || resources.isError) && /* @__PURE__ */ jsxs11("div", { className: cn4("flex items-center gap-2", variant === "card" ? "border-b px-3 py-2" : "pb-3"), children: [
2469
- title !== null && (variant === "card" ? /* @__PURE__ */ jsxs11(Fragment4, { children: [
2402
+ (title !== null || resources.isLoading || resources.isError) && /* @__PURE__ */ jsxs11("div", { className: cn3("flex items-center gap-2", variant === "card" ? "border-b px-3 py-2" : "pb-3"), children: [
2403
+ title !== null && (variant === "card" ? /* @__PURE__ */ jsxs11(Fragment5, { children: [
2470
2404
  /* @__PURE__ */ jsx12(Database, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }),
2471
2405
  /* @__PURE__ */ jsx12("span", { className: "text-sm font-semibold", children: title })
2472
2406
  ] }) : /* @__PURE__ */ jsx12("h1", { className: "text-2xl font-semibold tracking-tight", children: title })),
@@ -2861,11 +2795,11 @@ function Sheet2({
2861
2795
  selected.size > 0 && /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-1 rounded-md bg-muted px-2 text-sm", "data-testid": "studio-selection", children: [
2862
2796
  selected.size,
2863
2797
  " selected",
2864
- schema.access.update && provider.updateMany && /* @__PURE__ */ jsxs11(Button9, { type: "button", variant: "ghost", size: "sm", testID: "studio-bulk-set", onClick: () => setBulkIds([...selected]), children: [
2798
+ schema.access.update && provider.updateMany && /* @__PURE__ */ jsxs11(Button8, { type: "button", variant: "ghost", size: "sm", testID: "studio-bulk-set", onClick: () => setBulkIds([...selected]), children: [
2865
2799
  /* @__PURE__ */ jsx12(PencilLine, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
2866
2800
  "Set a field\u2026"
2867
2801
  ] }),
2868
- schema.access.delete && provider.deleteMany && /* @__PURE__ */ jsxs11(Button9, { type: "button", variant: "ghost", size: "sm", className: "text-destructive", testID: "studio-bulk-delete", onClick: () => void deleteSelected(), children: [
2802
+ schema.access.delete && provider.deleteMany && /* @__PURE__ */ jsxs11(Button8, { type: "button", variant: "ghost", size: "sm", className: "text-destructive", testID: "studio-bulk-delete", onClick: () => void deleteSelected(), children: [
2869
2803
  /* @__PURE__ */ jsx12(Trash22, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
2870
2804
  "Delete"
2871
2805
  ] }),
@@ -2968,12 +2902,12 @@ function Sheet2({
2968
2902
  total === 1 ? "" : "s"
2969
2903
  ] }),
2970
2904
  pageCount > 1 && /* @__PURE__ */ jsxs11("span", { className: "ml-auto flex items-center gap-1", children: [
2971
- /* @__PURE__ */ jsx12(Button9, { type: "button", variant: "ghost", size: "sm", disabled: page <= 1, onClick: () => navigate({ page: page - 1 }), "aria-label": "Previous page", children: /* @__PURE__ */ jsx12(ChevronLeft2, { className: "h-3.5 w-3.5", "aria-hidden": true }) }),
2905
+ /* @__PURE__ */ jsx12(Button8, { type: "button", variant: "ghost", size: "sm", disabled: page <= 1, onClick: () => navigate({ page: page - 1 }), "aria-label": "Previous page", children: /* @__PURE__ */ jsx12(ChevronLeft, { className: "h-3.5 w-3.5", "aria-hidden": true }) }),
2972
2906
  "Page ",
2973
2907
  page,
2974
2908
  " of ",
2975
2909
  pageCount,
2976
- /* @__PURE__ */ jsx12(Button9, { type: "button", variant: "ghost", size: "sm", disabled: page >= pageCount, onClick: () => navigate({ page: page + 1 }), "aria-label": "Next page", children: /* @__PURE__ */ jsx12(ChevronRight2, { className: "h-3.5 w-3.5", "aria-hidden": true }) })
2910
+ /* @__PURE__ */ jsx12(Button8, { type: "button", variant: "ghost", size: "sm", disabled: page >= pageCount, onClick: () => navigate({ page: page + 1 }), "aria-label": "Next page", children: /* @__PURE__ */ jsx12(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": true }) })
2977
2911
  ] })
2978
2912
  ] })
2979
2913
  ] })
@@ -3096,7 +3030,7 @@ function ActivityPanel({ entries, onUndo }) {
3096
3030
  /* @__PURE__ */ jsx12(History2, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }),
3097
3031
  "This session's edits",
3098
3032
  entries.length > 0 && /* @__PURE__ */ jsx12("span", { className: "text-xs font-normal text-muted-foreground", children: entries.length }),
3099
- entries[0] && /* @__PURE__ */ jsx12("span", { className: cn4("ml-2 truncate text-xs font-normal", entries[0].outcome === "saved" ? "text-muted-foreground" : "text-destructive"), children: entries[0].message })
3033
+ entries[0] && /* @__PURE__ */ jsx12("span", { className: cn3("ml-2 truncate text-xs font-normal", entries[0].outcome === "saved" ? "text-muted-foreground" : "text-destructive"), children: entries[0].message })
3100
3034
  ] }),
3101
3035
  entries.length === 0 ? /* @__PURE__ */ jsx12("p", { className: "px-3 py-3 text-xs text-muted-foreground", children: "Click a cell to edit it. Each save is checked against the latest version of the row, and audited." }) : /* @__PURE__ */ jsx12("ol", { className: "divide-y", "aria-live": "polite", children: entries.map((entry) => /* @__PURE__ */ jsxs11("li", { className: "px-3 py-2 text-xs", "data-testid": `studio-activity-${entry.outcome}`, children: [
3102
3036
  /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [