@protobase/ui 0.12.9 → 0.12.11

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 (38) hide show
  1. package/dist/app/action-icon.js +3 -7
  2. package/dist/app/app.d.ts +4 -1
  3. package/dist/app/app.js +26 -18
  4. package/dist/app/create/create-page.js +32 -6
  5. package/dist/app/create/create-values.js +16 -3
  6. package/dist/app/current-address.d.ts +1 -0
  7. package/dist/app/displays/cards-display.js +58 -10
  8. package/dist/app/displays/list-displays.js +7 -3
  9. package/dist/app/field-editor.js +4 -0
  10. package/dist/app/named-icon.js +13 -0
  11. package/dist/app/pages/field-value.js +5 -5
  12. package/dist/app/pages/use-relation-labels.js +1 -1
  13. package/dist/app/record-page.js +4 -3
  14. package/dist/app/value-badge.js +14 -0
  15. package/dist/app-shell/app-shell.js +2 -1
  16. package/dist/app-shell/sidebar.js +12 -2
  17. package/dist/auth/auth-gate.js +17 -11
  18. package/dist/auth/auth-provider.js +10 -3
  19. package/dist/auth/sign-in-form.js +7 -1
  20. package/dist/auth/sign-in-page.js +2 -1
  21. package/dist/data-grid/cells.js +9 -6
  22. package/dist/lib/readable-text.js +23 -0
  23. package/dist/live/columns-from-model.js +3 -0
  24. package/dist/live/display-value.js +6 -3
  25. package/dist/live/use-filter-config.js +5 -3
  26. package/dist/live/value-color.js +8 -0
  27. package/dist/package.js +1 -1
  28. package/dist/primitives/badge.js +12 -3
  29. package/dist/roles/use-active-role.js +8 -3
  30. package/dist/shell/assets/app-B2rZN97s.js +65 -0
  31. package/dist/shell/assets/{index-CGPHR8-j.js → index-B7XSEKuk.js} +1 -1
  32. package/dist/shell/assets/index-otHceRXj.css +2 -0
  33. package/dist/shell/assets/{shared-protobase-ui-oolw3GIu.js → shared-protobase-ui-DhQ8wFhe.js} +1 -1
  34. package/dist/shell/index.html +4 -4
  35. package/dist/shell/protobase.shell.json +1 -1
  36. package/package.json +4 -4
  37. package/dist/shell/assets/app-Ds2CYFLS.js +0 -65
  38. package/dist/shell/assets/index-BMIEmNDF.css +0 -2
@@ -19,6 +19,8 @@ const AuthProvider = ({ session: given, children }) => {
19
19
  const [platformSignIn, setPlatformSignIn] = useState();
20
20
  const [staffSignIn, setStaffSignIn] = useState();
21
21
  const [organizations, setOrganizations] = useState();
22
+ const [anonymous, setAnonymous] = useState(false);
23
+ const signedOutState = useCallback(() => anonymous ? { kind: "visitor" } : { kind: "signed-out" }, [anonymous]);
22
24
  const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
23
25
  const signedOut = useRef(markSignedOut);
24
26
  signedOut.current = markSignedOut;
@@ -49,9 +51,10 @@ const AuthProvider = ({ session: given, children }) => {
49
51
  setPlatformSignIn(status.platformSignIn);
50
52
  setStaffSignIn(status.staffSignIn);
51
53
  setOrganizations(status.organizations);
54
+ setAnonymous(Boolean(status.anonymous));
52
55
  if (status.needsAdmin) return setState({ kind: "needs-admin" });
53
56
  const user = await session.session();
54
- return user ? enter(user) : setState({ kind: "signed-out" });
57
+ return user ? enter(user) : setState({ kind: status.anonymous ? "visitor" : "signed-out" });
55
58
  }).catch((error) => setState({
56
59
  kind: "error",
57
60
  message: error instanceof Error ? error.message : "The server did not answer"
@@ -73,6 +76,8 @@ const AuthProvider = ({ session: given, children }) => {
73
76
  ...organizations && { organizations },
74
77
  markSignedOut,
75
78
  recheck,
79
+ showSignIn: () => setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current),
80
+ ...anonymous && { browse: () => setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current) },
76
81
  signIn: async (email, password) => finish(await session.signIn(email, password)),
77
82
  sendSignInCode: (email) => session.sendSignInCode(email),
78
83
  signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
@@ -86,11 +91,11 @@ const AuthProvider = ({ session: given, children }) => {
86
91
  },
87
92
  stopStaffSession: async () => {
88
93
  await session.staff.stop();
89
- setState({ kind: "signed-out" });
94
+ setState(signedOutState());
90
95
  },
91
96
  signOut: async () => {
92
97
  await session.signOut();
93
- setState({ kind: "signed-out" });
98
+ setState(signedOutState());
94
99
  }
95
100
  }), [
96
101
  state,
@@ -102,6 +107,8 @@ const AuthProvider = ({ session: given, children }) => {
102
107
  platformSignIn,
103
108
  staffSignIn,
104
109
  organizations,
110
+ anonymous,
111
+ signedOutState,
105
112
  markSignedOut,
106
113
  recheck,
107
114
  finish,
@@ -13,7 +13,7 @@ const Divider = () => /* @__PURE__ */ jsxs("div", {
13
13
  /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
14
14
  ]
15
15
  });
16
- const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith }) => {
16
+ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith, onBrowse }) => {
17
17
  const [email, setEmail] = useState("");
18
18
  const [password, setPassword] = useState("");
19
19
  const emailInput = useRef(null);
@@ -127,6 +127,12 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["pass
127
127
  children: "Forgot password?"
128
128
  })
129
129
  ]
130
+ }),
131
+ onBrowse && /* @__PURE__ */ jsx("button", {
132
+ type: "button",
133
+ onClick: onBrowse,
134
+ className: "mt-4 w-full text-center text-[13px] font-medium text-muted-foreground hover:text-foreground hover:underline",
135
+ children: "Continue without signing in"
130
136
  })
131
137
  ]
132
138
  });
@@ -5,7 +5,7 @@ import { useState } from "react";
5
5
  import { AuthError } from "@protobase/client";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  //#region src/auth/sign-in-page.tsx
8
- const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: initialError }) => {
8
+ const SignInPage = ({ workspace, notice, onForgotPassword, onBrowse, onCodeSent, error: initialError }) => {
9
9
  const { signIn, signInSocial, signInProviders, signInMethods, passkeyDomains, sendSignInCode, signInWithPasskey } = useAuth();
10
10
  const [busy, setBusy] = useState(false);
11
11
  const [error, setError] = useState(initialError);
@@ -27,6 +27,7 @@ const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: in
27
27
  notice,
28
28
  methods: signInMethods,
29
29
  onForgotPassword,
30
+ onBrowse,
30
31
  providers: signInProviders,
31
32
  onContinueWith: (provider) => void attempt(() => signInSocial(provider), signInMessage),
32
33
  passkeyDomains,
@@ -6,6 +6,7 @@ import { formatInt, formatPercent, formatSigned } from "../format/number.js";
6
6
  import { formatDate, formatDateTime } from "../format/date.js";
7
7
  import { fileShown, isPreviewable } from "../live/file-values.js";
8
8
  import { userById } from "../mocks/users.js";
9
+ import { valueColor } from "../live/value-color.js";
9
10
  import { Check, Paperclip } from "lucide-react";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  import { enumLabel } from "@protobase/schema";
@@ -26,9 +27,9 @@ const renderCell = (spec, value) => {
26
27
  className: "truncate font-mono text-xs text-muted-foreground",
27
28
  children: String(value)
28
29
  });
29
- case "money": return /* @__PURE__ */ jsx("span", {
30
+ case "money": return /* @__PURE__ */ jsxs("span", {
30
31
  className: "tabular-nums",
31
- children: formatMoney(Number(value), spec.currency)
32
+ children: [formatMoney(Number(value), spec.currency), spec.suffix]
32
33
  });
33
34
  case "date": return /* @__PURE__ */ jsx("span", {
34
35
  className: "tabular-nums text-muted-foreground",
@@ -38,13 +39,13 @@ const renderCell = (spec, value) => {
38
39
  className: "tabular-nums text-muted-foreground",
39
40
  children: formatDateTime(Number(value))
40
41
  });
41
- case "number": return /* @__PURE__ */ jsx("span", {
42
+ case "number": return /* @__PURE__ */ jsxs("span", {
42
43
  className: "tabular-nums",
43
- children: formatInt(Number(value))
44
+ children: [formatInt(Number(value)), spec.suffix]
44
45
  });
45
- case "signed": return /* @__PURE__ */ jsx("span", {
46
+ case "signed": return /* @__PURE__ */ jsxs("span", {
46
47
  className: cn("tabular-nums", Number(value) < 0 ? "text-danger-text" : "text-success-text"),
47
- children: formatSigned(Number(value))
48
+ children: [formatSigned(Number(value)), spec.suffix]
48
49
  });
49
50
  case "percent": return Number(value) === 0 ? muted("—") : /* @__PURE__ */ jsx("span", {
50
51
  className: "tabular-nums",
@@ -52,6 +53,8 @@ const renderCell = (spec, value) => {
52
53
  });
53
54
  case "status": return /* @__PURE__ */ jsx(Badge, {
54
55
  tone: spec.tones?.[String(value)] ?? "neutral",
56
+ color: valueColor(spec.colors, value),
57
+ shape: spec.shape,
55
58
  children: enumLabel(String(value), spec.labels)
56
59
  });
57
60
  case "relation": return /* @__PURE__ */ jsx("span", {
@@ -0,0 +1,23 @@
1
+ //#region src/lib/readable-text.ts
2
+ const luminance = (hex) => {
3
+ const digits = hex.slice(1);
4
+ const full = digits.length === 3 ? [...digits].map((digit) => digit + digit).join("") : digits;
5
+ const [r, g, b] = [
6
+ 0,
7
+ 2,
8
+ 4
9
+ ].map((at) => {
10
+ const channel = Number.parseInt(full.slice(at, at + 2), 16) / 255;
11
+ return channel <= .03928 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4;
12
+ });
13
+ return .2126 * r + .7152 * g + .0722 * b;
14
+ };
15
+ const light = "#ffffff";
16
+ const dark = "#111827";
17
+ /** The text colour, white or near black, that reads best on a hex background. */
18
+ const readableOn = (background) => {
19
+ const shade = luminance(background);
20
+ return 1.05 / (shade + .05) >= (shade + .05) / (luminance(dark) + .05) ? light : dark;
21
+ };
22
+ //#endregion
23
+ export { readableOn };
@@ -82,6 +82,9 @@ const columnsFromModel = (resource, view, highlight = []) => {
82
82
  tones: Object.fromEntries((field.enumValues ?? []).map((value) => [value, toneFor(value)])),
83
83
  labels
84
84
  },
85
+ ...field.type === "enum" && viewField?.valueColors && { colors: viewField.valueColors },
86
+ ...field.type === "enum" && viewField?.badgeShape && { shape: viewField.badgeShape },
87
+ ...viewField?.suffix !== void 0 && { suffix: viewField.suffix },
85
88
  ...kind === "money" && {
86
89
  currency: "EUR",
87
90
  value: (row) => row[name] == null ? null : currencyCents(row[name])
@@ -2,7 +2,8 @@ import { formatDate, formatDateTime } from "../format/date.js";
2
2
  import { fileShown } from "./file-values.js";
3
3
  import { enumLabel } from "@protobase/schema";
4
4
  //#region src/live/display-value.ts
5
- /** A read-only rendering of a stored value, following the view's prefix, decimals, format and value label hints. */
5
+ const affixed = (text, hints) => `${hints.prefix ?? ""}${text}${hints.suffix ?? ""}`;
6
+ /** A read-only rendering of a stored value, following the view's prefix, suffix, decimals, format and value label hints. */
6
7
  const displayValue = (field, value, hints = {}) => {
7
8
  if (value === null || value === void 0 || value === "") return "—";
8
9
  switch (field.type) {
@@ -14,11 +15,13 @@ const displayValue = (field, value, hints = {}) => {
14
15
  case "decimal": {
15
16
  const text = hints.decimals === void 0 ? String(value) : Number(value).toFixed(hints.decimals);
16
17
  if (hints.format === "percent") return `${Number(value)}%`;
17
- return `${hints.prefix ?? ""}${Number(text).toLocaleString("en-IE", {
18
+ return affixed(Number(text).toLocaleString("en-IE", {
18
19
  minimumFractionDigits: hints.decimals ?? 0,
19
20
  maximumFractionDigits: hints.decimals ?? 20
20
- })}`;
21
+ }), hints);
21
22
  }
23
+ case "integer":
24
+ case "bigint": return affixed(String(value), hints);
22
25
  default: return typeof value === "object" ? JSON.stringify(value) : String(value);
23
26
  }
24
27
  };
@@ -2,9 +2,9 @@ import { formatMoney } from "../format/money.js";
2
2
  import { keys } from "../data/query-keys.js";
3
3
  import { useClient } from "../data/api-provider.js";
4
4
  import { humanize } from "./naming.js";
5
+ import { fetchLabels } from "./relation-labels.js";
5
6
  import { stateToText } from "./filter-string.js";
6
7
  import { useHistogram } from "../data/use-aggregates.js";
7
- import { fetchLabels } from "./relation-labels.js";
8
8
  import { widgetFields } from "./view-fields.js";
9
9
  import { useQueries, useQueryClient } from "@tanstack/react-query";
10
10
  import { enumLabel } from "@protobase/schema";
@@ -49,7 +49,9 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
49
49
  range: void 0
50
50
  }, state, extras);
51
51
  const histogram = useHistogram(model.name, skeleton.range?.id ?? "", rangeFilter, 24, Boolean(skeleton.range));
52
- const prefix = skeleton.range ? view?.fields[skeleton.range.id]?.prefix : void 0;
52
+ const rangeHints = skeleton.range ? view?.fields[skeleton.range.id] : void 0;
53
+ const prefix = rangeHints?.prefix;
54
+ const suffix = rangeHints?.suffix ?? "";
53
55
  return {
54
56
  facets: skeleton.facets.map((group, index) => {
55
57
  const field = model.fields[group.id];
@@ -71,7 +73,7 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
71
73
  max: Math.ceil(histogram.data.max),
72
74
  step: Math.max(1, Math.round((histogram.data.max - histogram.data.min) / 100)),
73
75
  histogram: histogram.data.buckets.map((bucket) => bucket.count),
74
- format: (value) => prefix === "€" ? formatMoney(value * 100).replace(".00", "") : String(value)
76
+ format: (value) => `${prefix === "€" ? formatMoney(value * 100).replace(".00", "") : String(value)}${suffix}`
75
77
  } },
76
78
  ...skeleton.dateField && {
77
79
  datePresets: skeleton.datePresets ?? true,
@@ -0,0 +1,8 @@
1
+ //#region src/live/value-color.ts
2
+ /** The colour a view gives a value's badge, if any. */
3
+ const valueColor = (colors, value) => {
4
+ const key = String(value);
5
+ return colors && Object.hasOwn(colors, key) ? colors[key] : void 0;
6
+ };
7
+ //#endregion
8
+ export { valueColor };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.9";
2
+ var version = "0.12.11";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { readableOn } from "../lib/readable-text.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/primitives/badge.tsx
4
5
  const tones = {
@@ -17,9 +18,17 @@ const dots = {
17
18
  red: "bg-danger",
18
19
  violet: "bg-violet"
19
20
  };
20
- const Badge = ({ tone = "neutral", dot = true, children, className }) => /* @__PURE__ */ jsxs("span", {
21
- className: cn("inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap", tones[tone], className),
22
- children: [dot && /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dots[tone]) }), children]
21
+ const arrow = "polygon(0 0, calc(100% - 0.625rem) 0, 100% 50%, calc(100% - 0.625rem) 100%, 0 100%)";
22
+ const Badge = ({ tone = "neutral", dot = true, color, shape, children, className }) => /* @__PURE__ */ jsxs("span", {
23
+ style: {
24
+ ...color !== void 0 && {
25
+ backgroundColor: color,
26
+ color: readableOn(color)
27
+ },
28
+ ...shape === "arrow" && { clipPath: arrow }
29
+ },
30
+ className: cn(shape === "arrow" ? "inline-flex h-5 items-center gap-1.5 rounded-l-sm pr-4 pl-2 text-xs font-semibold whitespace-nowrap" : "inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap", color === void 0 && tones[tone], className),
31
+ children: [dot && color === void 0 && shape === void 0 && /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dots[tone]) }), children]
23
32
  });
24
33
  //#endregion
25
34
  export { Badge };
@@ -5,17 +5,22 @@ import { useCallback, useEffect, useState } from "react";
5
5
  import { AuthError } from "@protobase/client";
6
6
  //#region src/roles/use-active-role.ts
7
7
  const failureMessage = (failure, fallback) => failure instanceof AuthError ? authMessage(failure) : fallback;
8
- /** The role the session acts with; a failed read or switch shows as a toast. */
9
- const useActiveRole = (reopen) => {
8
+ /** The role the session acts with, read while `enabled` (someone is signed in); a failed read or switch shows as a toast. */
9
+ const useActiveRole = (reopen, enabled = true) => {
10
10
  const { session } = useAuth();
11
11
  const toast = useToast();
12
12
  const [current, setCurrent] = useState();
13
13
  useEffect(() => {
14
+ if (!enabled) return;
14
15
  session.roles.current().then(setCurrent, (failure) => toast.show({
15
16
  state: "error",
16
17
  title: failureMessage(failure, "Could not read your roles")
17
18
  }));
18
- }, [session, toast]);
19
+ }, [
20
+ session,
21
+ toast,
22
+ enabled
23
+ ]);
19
24
  const switchTo = useCallback((name) => void session.roles.switchTo(name).then(reopen, (failure) => toast.show({
20
25
  state: "error",
21
26
  title: failureMessage(failure, "Could not switch the role")