@rebasepro/ui 0.10.0 → 0.10.1-canary.14e53ae

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import type { CollectionPropertyConfig } from "./CollectionViewTypes";
2
+ /**
3
+ * Pick the property that acts as a row's title.
4
+ *
5
+ * Headless mirror of the ranking in `@rebasepro/common`: identifiers are kept
6
+ * out of the title slot structurally (the id property, images, hidden fields),
7
+ * and among what is left a plain text field beats a description, an enum or a
8
+ * relation. Falls back to the id property only when a collection has nothing
9
+ * else to show.
10
+ */
11
+ export declare function resolveTitlePropertyKey(order: string[], properties: Record<string, CollectionPropertyConfig>, idProperty: string, titleProperty?: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/ui",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.10.1-canary.14e53ae",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { cls } from "../util";
3
+ import { AlertCircleIcon, AlertTriangleIcon, CheckCircleIcon, InfoIcon, XIcon } from "../icons";
3
4
 
4
5
  export interface AlertProps {
5
6
  children: React.ReactNode;
@@ -15,27 +16,61 @@ export interface AlertProps {
15
16
  const getSizeClasses = (size: "small" | "medium" | "large") => {
16
17
  switch (size) {
17
18
  case "small":
18
- return "px-4 py-1";
19
+ return "px-3 py-1.5 text-xs gap-2 rounded-md";
19
20
  case "large":
20
- return "px-4 py-4";
21
+ return "px-4 py-3.5 text-sm gap-3 rounded-lg";
21
22
  case "medium":
22
23
  default:
23
- return "px-4 py-2";
24
+ return "px-3.5 py-2.5 text-sm gap-2.5 rounded-lg";
24
25
  }
25
26
  }
27
+
28
+ const getIconSize = (size: "small" | "medium" | "large") => size === "small" ? 16 : 18;
29
+
26
30
  const getColorClasses = (severity: string) => {
27
31
  switch (severity) {
28
32
  case "error":
29
- return "bg-red-50 dark:bg-red-800 dark:text-red-100 text-red-900 border-l-[3px] border-l-red-500";
33
+ return "bg-red-500/8 dark:bg-red-500/12 text-red-800 dark:text-red-200 border border-red-500/20 dark:border-red-500/25";
34
+ case "warning":
35
+ return "bg-amber-500/8 dark:bg-amber-500/12 text-amber-800 dark:text-amber-200 border border-amber-500/20 dark:border-amber-500/25";
36
+ case "info":
37
+ return "bg-blue-500/8 dark:bg-blue-500/12 text-blue-800 dark:text-blue-200 border border-blue-500/20 dark:border-blue-500/25";
38
+ case "success":
39
+ return "bg-emerald-500/8 dark:bg-emerald-500/12 text-emerald-800 dark:text-emerald-200 border border-emerald-500/20 dark:border-emerald-500/25";
40
+ case "base":
41
+ default:
42
+ return "bg-surface-accent-500/8 dark:bg-surface-accent-400/10 text-surface-accent-800 dark:text-surface-accent-100 border border-surface-accent-500/15 dark:border-surface-accent-400/20";
43
+ }
44
+ };
45
+
46
+ const getIconColor = (severity: string) => {
47
+ switch (severity) {
48
+ case "error":
49
+ return "text-red-500 dark:text-red-400";
30
50
  case "warning":
31
- return "bg-amber-50 dark:bg-amber-800 dark:text-amber-100 text-amber-900 border-l-[3px] border-l-amber-500";
51
+ return "text-amber-500 dark:text-amber-400";
32
52
  case "info":
33
- return "bg-blue-100 dark:bg-blue-800 dark:text-blue-100 text-blue-900 border-l-[3px] border-l-blue-500";
53
+ return "text-blue-500 dark:text-blue-400";
34
54
  case "success":
35
- return "bg-emerald-50 dark:bg-emerald-800 dark:text-emerald-100 text-emerald-900 border-l-[3px] border-l-emerald-500";
55
+ return "text-emerald-500 dark:text-emerald-400";
36
56
  case "base":
37
57
  default:
38
- return "bg-surface-accent-50 dark:bg-surface-accent-800 dark:text-white text-surface-accent-900 border-l-[3px] border-l-surface-accent-400";
58
+ return "text-surface-accent-500 dark:text-surface-accent-400";
59
+ }
60
+ };
61
+
62
+ const getIcon = (severity: string, size: number, className: string) => {
63
+ switch (severity) {
64
+ case "error":
65
+ return <AlertCircleIcon size={size} className={className}/>;
66
+ case "warning":
67
+ return <AlertTriangleIcon size={size} className={className}/>;
68
+ case "success":
69
+ return <CheckCircleIcon size={size} className={className}/>;
70
+ case "info":
71
+ case "base":
72
+ default:
73
+ return <InfoIcon size={size} className={className}/>;
39
74
  }
40
75
  };
41
76
 
@@ -50,6 +85,7 @@ export const Alert: React.FC<AlertProps> = ({
50
85
  style
51
86
  }) => {
52
87
  const classes = getColorClasses(color);
88
+ const iconSize = getIconSize(size);
53
89
 
54
90
  return (
55
91
  <div
@@ -57,19 +93,26 @@ export const Alert: React.FC<AlertProps> = ({
57
93
  className={cls(
58
94
  getSizeClasses(size),
59
95
  "w-full",
60
- "font-medium",
61
- "rounded-lg flex items-center gap-2",
96
+ "font-medium leading-snug",
97
+ "flex items-start",
62
98
  classes,
63
99
  outerClassName)}>
64
- <div className={cls("grow", className)}>{children}</div>
100
+ <span className={cls("shrink-0 mt-px", getIconColor(color))}>
101
+ {getIcon(color, iconSize, "")}
102
+ </span>
103
+ <div className={cls("grow min-w-0 self-center", className)}>{children}</div>
104
+ {action}
65
105
  {onDismiss && (
66
106
  <button
67
- className="text-surface-accent-400 hover:text-surface-accent-600 dark:text-surface-accent-500 dark:hover:text-surface-accent-400"
107
+ type="button"
108
+ aria-label="Dismiss"
109
+ className={cls(
110
+ "shrink-0 rounded p-0.5 opacity-60 transition-opacity hover:opacity-100",
111
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-current/40")}
68
112
  onClick={onDismiss}>
69
- &times;
113
+ <XIcon size={iconSize - 2}/>
70
114
  </button>
71
115
  )}
72
- {action}
73
116
  </div>
74
117
  );
75
118
  };
@@ -14,6 +14,7 @@ import type {
14
14
  CollectionViewSize,
15
15
  CollectionSelectionController,
16
16
  } from "./CollectionViewTypes";
17
+ import { resolveTitlePropertyKey } from "./resolveTitleProperty";
17
18
 
18
19
  import { getValueInPath } from "./utils";
19
20
 
@@ -66,16 +67,10 @@ export function CollectionCardView<T extends Record<string, unknown> = Record<st
66
67
  );
67
68
 
68
69
  // ── Resolve title property ───────────────────────────────────────
69
- const titlePropertyKey = useMemo(() => {
70
- if (titleProperty) return titleProperty;
71
- for (const key of order) {
72
- const prop = properties[key];
73
- if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
74
- return key;
75
- }
76
- }
77
- return idProperty;
78
- }, [titleProperty, order, properties, idProperty]);
70
+ const titlePropertyKey = useMemo(
71
+ () => resolveTitlePropertyKey(order, properties, idProperty, titleProperty),
72
+ [titleProperty, order, properties, idProperty]
73
+ );
79
74
 
80
75
  // ── Resolve image property ───────────────────────────────────────
81
76
  const imageProperty = useMemo(() => {
@@ -14,6 +14,7 @@ import type {
14
14
  CollectionSelectionController,
15
15
  CollectionEnumValueConfig,
16
16
  } from "./CollectionViewTypes";
17
+ import { resolveTitlePropertyKey } from "./resolveTitleProperty";
17
18
 
18
19
  import { getValueInPath } from "./utils";
19
20
 
@@ -107,16 +108,10 @@ export function CollectionKanbanView<T extends Record<string, unknown> = Record<
107
108
  }, [properties, kanbanProperty, columns]);
108
109
 
109
110
  // ── Resolve title property ───────────────────────────────────────
110
- const titlePropertyKey = useMemo(() => {
111
- if (titleProperty) return titleProperty;
112
- for (const key of order) {
113
- const prop = properties[key];
114
- if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
115
- return key;
116
- }
117
- }
118
- return idProperty;
119
- }, [titleProperty, order, properties, idProperty]);
111
+ const titlePropertyKey = useMemo(
112
+ () => resolveTitlePropertyKey(order, properties, idProperty, titleProperty),
113
+ [titleProperty, order, properties, idProperty]
114
+ );
120
115
 
121
116
  // ── Preview properties (up to 2) ─────────────────────────────────
122
117
  const previewProperties = useMemo(() => {
@@ -12,6 +12,7 @@ import type {
12
12
  CollectionViewSize,
13
13
  CollectionSelectionController,
14
14
  } from "./CollectionViewTypes";
15
+ import { resolveTitlePropertyKey } from "./resolveTitleProperty";
15
16
 
16
17
  import { getValueInPath } from "./utils";
17
18
 
@@ -64,16 +65,10 @@ export function CollectionListView<T extends Record<string, unknown> = Record<st
64
65
  );
65
66
 
66
67
  // ── Resolve title property ───────────────────────────────────────
67
- const titlePropertyKey = useMemo(() => {
68
- if (titleProperty) return titleProperty;
69
- for (const key of order) {
70
- const prop = properties[key];
71
- if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
72
- return key;
73
- }
74
- }
75
- return idProperty;
76
- }, [titleProperty, order, properties, idProperty]);
68
+ const titlePropertyKey = useMemo(
69
+ () => resolveTitlePropertyKey(order, properties, idProperty, titleProperty),
70
+ [titleProperty, order, properties, idProperty]
71
+ );
77
72
 
78
73
  // ── Subtitle properties (up to 2) ────────────────────────────────
79
74
  const subtitleProperties = useMemo(() => {
@@ -0,0 +1,52 @@
1
+ import type { CollectionPropertyConfig } from "./CollectionViewTypes";
2
+
3
+ /**
4
+ * Pick the property that acts as a row's title.
5
+ *
6
+ * Headless mirror of the ranking in `@rebasepro/common`: identifiers are kept
7
+ * out of the title slot structurally (the id property, images, hidden fields),
8
+ * and among what is left a plain text field beats a description, an enum or a
9
+ * relation. Falls back to the id property only when a collection has nothing
10
+ * else to show.
11
+ */
12
+ export function resolveTitlePropertyKey(
13
+ order: string[],
14
+ properties: Record<string, CollectionPropertyConfig>,
15
+ idProperty: string,
16
+ titleProperty?: string
17
+ ): string {
18
+ if (titleProperty) return titleProperty;
19
+
20
+ const SCORE = {
21
+ RELATION: 10,
22
+ LONG_TEXT: 20,
23
+ ENUM: 30,
24
+ EMAIL: 45,
25
+ PLAIN_TEXT: 60,
26
+ NAMED: 100
27
+ };
28
+ const titleLikeKeys = new Set(["name", "fullname", "displayname", "title", "label", "heading", "subject", "username", "nickname"]);
29
+
30
+ let best: { key: string; score: number } | undefined;
31
+
32
+ order.forEach(key => {
33
+ const property = properties[key];
34
+ if (!property || property.hideFromCollection) return;
35
+ if (key === idProperty) return;
36
+ if (property.storage || property.url === "image") return;
37
+
38
+ let score: number;
39
+ if (property.type === "relation" || property.type === "reference") score = SCORE.RELATION;
40
+ else if (property.type !== "string") return;
41
+ else if (property.enum) score = SCORE.ENUM;
42
+ else if (property.multiline || property.markdown) score = SCORE.LONG_TEXT;
43
+ else if (property.email) score = SCORE.EMAIL;
44
+ else if (titleLikeKeys.has(key.toLowerCase().replace(/[^a-z0-9]/g, ""))) score = SCORE.NAMED;
45
+ else score = SCORE.PLAIN_TEXT;
46
+
47
+ if (!best || score > best.score) best = { key,
48
+ score };
49
+ });
50
+
51
+ return best?.key ?? idProperty;
52
+ }
@@ -240,7 +240,7 @@ export function ListView<T>({
240
240
  {renderRow({
241
241
  item,
242
242
  index: actualIndex,
243
- style: { height: estimatedRowHeight },
243
+ style: { height: estimatedRowHeight, overflow: "hidden" },
244
244
  className: cls(
245
245
  !isLast && "border-b",
246
246
  !isLast && borderMixinClass