@gogitcms/design-system 0.16.0-next.5 → 0.16.0-next.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gogitcms/design-system",
3
- "version": "0.16.0-next.5",
3
+ "version": "0.16.0-next.6",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "// exports": "The root entry is the react-native source the SPAs, desktop app and mobile app consume. ./web is the plain-DOM build for server-rendered surfaces (the Astro docs site) that don't run react-native-web, and ./css ships the tokens as custom properties. The trailing wildcard keeps deep paths resolvable — plugin bundling and the Vite aliases reach into src/ directly.",
@@ -4,10 +4,11 @@ import { useTheme, useThemeMode, ThemeScope } from "../ThemeProvider";
4
4
  import { lightTheme, darkTheme } from "../theme";
5
5
  import { Text } from "./Text";
6
6
  import { Icon } from "./Icon";
7
+ import type { IconName } from "../icons";
7
8
  import { Spinner } from "./Spinner";
8
9
 
9
10
  // One notification, shaped for display. Transport-agnostic: the frontend/mobile
10
- // apps map GraphQL rows and the desktop maps REST rows into this.
11
+ // apps map GraphQL rows and the desktop and dashboard map REST rows into this.
11
12
  export type NotificationItem = {
12
13
  id: string;
13
14
  title: string;
@@ -16,8 +17,39 @@ export type NotificationItem = {
16
17
  read: boolean;
17
18
  createdAt: string; // ISO 8601
18
19
  author?: string; // display name/email of who caused it, when known
20
+ // What produced it — picks the icon. Unknown kinds fall back to the bell.
21
+ kind?: NotificationKind | string;
22
+ // Where it happened, for a list that spans several repositories/projects.
23
+ // Omit whichever the list is already scoped to.
24
+ repository?: string;
25
+ project?: string;
19
26
  };
20
27
 
28
+ export type NotificationKind = "import" | "export" | "merge" | "change_request" | "member" | "branch" | "project";
29
+
30
+ // The icon for a notification kind. Exported so hosts that draw their own rows
31
+ // (a scope picker's section headers, say) agree with the list.
32
+ export function notificationKindIcon(kind?: string): IconName {
33
+ switch (kind) {
34
+ case "import":
35
+ return "download";
36
+ case "export":
37
+ return "upload";
38
+ case "merge":
39
+ return "gitMerge";
40
+ case "change_request":
41
+ return "gitPullRequest";
42
+ case "member":
43
+ return "user";
44
+ case "branch":
45
+ return "gitBranch";
46
+ case "project":
47
+ return "folder";
48
+ default:
49
+ return "bell";
50
+ }
51
+ }
52
+
21
53
  // relativeTime renders a compact age ("just now", "5m", "3h", "2d").
22
54
  function relativeTime(iso: string): string {
23
55
  const then = Date.parse(iso);
@@ -32,7 +64,65 @@ function relativeTime(iso: string): string {
32
64
  return `${days}d`;
33
65
  }
34
66
 
35
- // A single notification row: an unread dot + level-tinted title, body, and age.
67
+ // InlineCode renders a sentence whose backticked spans (`main`, `#12`'s
68
+ // branch) are set in the mono face, so a title like "Created `zyx` branch"
69
+ // reads as prose around a name rather than as punctuation. An unmatched
70
+ // backtick is left as typed.
71
+ export function InlineCode({
72
+ text,
73
+ variant = "sm",
74
+ weight,
75
+ color,
76
+ numberOfLines,
77
+ testID,
78
+ }: {
79
+ text: string;
80
+ variant?: "sm" | "body" | "monoSm" | "xs";
81
+ weight?: "regular" | "medium" | "semibold";
82
+ color?: string;
83
+ numberOfLines?: number;
84
+ testID?: string;
85
+ }) {
86
+ const t = useTheme();
87
+ const parts = text.split("`");
88
+ // An even number of parts means an odd number of backticks: not a pair, so
89
+ // render the text untouched.
90
+ if (parts.length % 2 === 0) {
91
+ return (
92
+ <Text variant={variant} weight={weight} color={color} numberOfLines={numberOfLines} testID={testID}>
93
+ {text}
94
+ </Text>
95
+ );
96
+ }
97
+ return (
98
+ <Text variant={variant} weight={weight} color={color} numberOfLines={numberOfLines} testID={testID}>
99
+ {parts.map((part, i) =>
100
+ i % 2 === 1 ? (
101
+ <Text
102
+ key={i}
103
+ variant={variant}
104
+ weight={weight}
105
+ color={color}
106
+ mono
107
+ style={{
108
+ backgroundColor: t.color.surfaceSunken,
109
+ borderRadius: t.radius.sm,
110
+ paddingHorizontal: 3,
111
+ }}
112
+ >
113
+ {part}
114
+ </Text>
115
+ ) : (
116
+ part
117
+ ),
118
+ )}
119
+ </Text>
120
+ );
121
+ }
122
+
123
+ // A single notification row: a kind icon, the title (unread rows in bold, error
124
+ // rows tinted), the body, and a context line — where it happened and who did
125
+ // it — with the age on the right.
36
126
  function NotificationRow({
37
127
  item,
38
128
  divided,
@@ -44,12 +134,16 @@ function NotificationRow({
44
134
  }) {
45
135
  const t = useTheme();
46
136
  const titleColor = item.level === "error" ? t.color.diffDelFg : t.color.textPrimary;
137
+ const context = [item.repository, item.project].filter(Boolean).join(" · ");
138
+ const meta = [context, item.author ? `by ${item.author}` : ""].filter(Boolean).join(" · ");
47
139
  return (
48
140
  <Pressable
49
141
  onPress={onPress}
142
+ disabled={!onPress}
143
+ testID={`notification-${item.id}`}
50
144
  style={({ pressed, hovered }: { pressed: boolean; hovered?: boolean }) => ({
51
145
  flexDirection: "row",
52
- gap: t.space(2.5),
146
+ gap: t.space(3),
53
147
  paddingHorizontal: t.space(4),
54
148
  paddingVertical: t.space(3),
55
149
  borderBottomWidth: divided ? 1 : 0,
@@ -57,33 +151,57 @@ function NotificationRow({
57
151
  backgroundColor: hovered || pressed ? t.color.surfaceHover : "transparent",
58
152
  })}
59
153
  >
60
- <View
61
- style={{
62
- width: 8,
63
- height: 8,
64
- marginTop: 5,
65
- borderRadius: t.radius.pill,
66
- backgroundColor: item.read ? "transparent" : t.color.diffDelFg,
67
- }}
68
- />
69
- <View style={{ flex: 1, gap: 2 }}>
70
- <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
71
- <Text
72
- variant="sm"
73
- weight={item.read ? "medium" : "semibold"}
74
- color={titleColor}
75
- numberOfLines={2}
76
- style={{ flex: 1 }}
77
- >
78
- {item.title}
79
- </Text>
154
+ <View style={{ width: 28, height: 28, alignItems: "center", justifyContent: "center", position: "relative" }}>
155
+ <View
156
+ style={{
157
+ width: 28,
158
+ height: 28,
159
+ borderRadius: t.radius.pill,
160
+ alignItems: "center",
161
+ justifyContent: "center",
162
+ backgroundColor: item.level === "error" ? t.color.diffDelBg : t.color.surfaceSunken,
163
+ }}
164
+ >
165
+ <Icon
166
+ name={notificationKindIcon(item.kind)}
167
+ size={14}
168
+ color={item.level === "error" ? t.color.diffDelFg : t.color.textSecondary}
169
+ />
170
+ </View>
171
+ {/* Unread dot, riding on the icon's corner. */}
172
+ {item.read ? null : (
173
+ <View
174
+ testID="notification-unread"
175
+ style={{
176
+ position: "absolute",
177
+ top: -1,
178
+ right: -1,
179
+ width: 8,
180
+ height: 8,
181
+ borderRadius: t.radius.pill,
182
+ backgroundColor: t.color.diffDelFg,
183
+ borderWidth: 1,
184
+ borderColor: t.color.surfaceRaised,
185
+ }}
186
+ />
187
+ )}
188
+ </View>
189
+ <View style={{ flex: 1, gap: 2, minWidth: 0 }}>
190
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: t.space(2) }}>
191
+ <View style={{ flex: 1, minWidth: 0 }}>
192
+ <InlineCode
193
+ text={item.title}
194
+ variant="sm"
195
+ weight={item.read ? "medium" : "semibold"}
196
+ color={titleColor}
197
+ numberOfLines={2}
198
+ />
199
+ </View>
80
200
  <Text variant="monoSm" color="tertiary">{relativeTime(item.createdAt)}</Text>
81
201
  </View>
82
- {item.body ? (
83
- <Text variant="monoSm" color="secondary" numberOfLines={2}>{item.body}</Text>
84
- ) : null}
85
- {item.author ? (
86
- <Text variant="monoSm" color="tertiary" numberOfLines={1}>{`by ${item.author}`}</Text>
202
+ {item.body ? <InlineCode text={item.body} variant="monoSm" color={t.color.textSecondary} numberOfLines={2} /> : null}
203
+ {meta ? (
204
+ <Text variant="monoSm" color="tertiary" numberOfLines={1}>{meta}</Text>
87
205
  ) : null}
88
206
  </View>
89
207
  </Pressable>
package/src/icons.ts CHANGED
@@ -28,6 +28,7 @@ export const icons = {
28
28
  chevronDown: ["m6 9 6 6 6-6"],
29
29
  chevronUp: ["m18 15-6-6-6 6"],
30
30
  folder: ["M3 6a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"],
31
+ user: ["M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", "M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"],
31
32
  building: ["M3 21h18", "M6 21V7l7-4v18", "M18 21V11l-5-3"],
32
33
  menu: ["M3 6h18", "M3 12h18", "M3 18h18"],
33
34
  plus: ["M5 12h14", "M12 5v14"],
package/src/index.ts CHANGED
@@ -115,9 +115,10 @@ export { Spinner } from "./components/Spinner";
115
115
  export type { SpinnerProps } from "./components/Spinner";
116
116
 
117
117
  // Notifications
118
- export { NotificationBell, NotificationList } from "./components/Notifications";
118
+ export { NotificationBell, NotificationList, InlineCode, notificationKindIcon } from "./components/Notifications";
119
119
  export type {
120
120
  NotificationItem,
121
+ NotificationKind,
121
122
  NotificationBellProps,
122
123
  NotificationListProps,
123
124
  } from "./components/Notifications";