create-feedbax 0.2.0 → 0.2.2

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 (143) hide show
  1. package/README.md +17 -5
  2. package/dist/cli.js +2610 -291
  3. package/dist/index.js +2375 -200
  4. package/package.json +7 -7
  5. package/template/.feedbax-template.sha256 +1 -1
  6. package/template/.gitignore.template +1 -0
  7. package/template/.npmignore.template +1 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/AGENTS.md +13 -0
  10. package/template/CONNECTED.md +418 -0
  11. package/template/README.md +97 -0
  12. package/template/components.json +1 -1
  13. package/template/eslint.config.mjs +23 -0
  14. package/template/feedbax-vite.ts +79 -0
  15. package/template/migrations/0001_connected.sql +6 -0
  16. package/template/migrations/0002_participation.sql +8 -0
  17. package/template/migrations/0003_images.sql +3 -0
  18. package/template/migrations/0004_followed_status.sql +9 -0
  19. package/template/migrations/0005_github.sql +4 -0
  20. package/template/migrations/0006_integration.sql +3 -0
  21. package/template/migrations/0007_ownership.sql +2 -0
  22. package/template/migrations/0008_integration_recovery.sql +5 -0
  23. package/template/migrations/0009_observed_history.sql +3 -0
  24. package/template/package.json +39 -34
  25. package/template/src/authorized-draft-post.tsx +1 -0
  26. package/template/src/changelog-page.tsx +63 -54
  27. package/template/src/changelog-runtime.ts +2 -1
  28. package/template/src/cloudflare-workers.d.ts +9 -0
  29. package/template/src/comment-creation.test.tsx +95 -7
  30. package/template/src/comment-thread-page.ts +11 -1
  31. package/template/src/components/command-palette.tsx +59 -30
  32. package/template/src/components/connected-identity-provider.tsx +135 -0
  33. package/template/src/components/device-profile-provider.tsx +41 -26
  34. package/template/src/components/follow-post-button.tsx +78 -0
  35. package/template/src/components/header.tsx +2 -0
  36. package/template/src/components/image-attachments.tsx +187 -0
  37. package/template/src/components/loader.tsx +48 -5
  38. package/template/src/components/notification-inbox.tsx +232 -0
  39. package/template/src/components/portal-footer.tsx +14 -0
  40. package/template/src/components/post-history.tsx +101 -0
  41. package/template/src/components/profile-menu.tsx +37 -0
  42. package/template/src/components/ui/components/command.tsx +30 -29
  43. package/template/src/components/ui/components/message-scroller.tsx +2 -2
  44. package/template/src/components/ui/components/scroll-area.tsx +0 -1
  45. package/template/src/components/ui/components/switch.tsx +29 -0
  46. package/template/src/components/ui/styles/globals.css +1 -1
  47. package/template/src/configured-feedback.test.ts +82 -0
  48. package/template/src/configured-feedback.ts +61 -0
  49. package/template/src/connected-identity.test.ts +148 -0
  50. package/template/src/connected-identity.ts +127 -0
  51. package/template/src/connected-participation.test.tsx +67 -0
  52. package/template/src/connected-runtime.test.ts +244 -0
  53. package/template/src/connected-runtime.ts +176 -0
  54. package/template/src/connected-social.test.tsx +158 -0
  55. package/template/src/feedback-cache-invalidation.ts +3 -0
  56. package/template/src/feedback-runtime.ts +123 -13
  57. package/template/src/feedbax/changelog/index.test.ts +35 -0
  58. package/template/src/feedbax/changelog/index.ts +5 -1
  59. package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
  60. package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
  61. package/template/src/feedbax/config/index.test.ts +73 -1
  62. package/template/src/feedbax/config/index.ts +205 -9
  63. package/template/src/feedbax/feedback/comment-storage.ts +38 -0
  64. package/template/src/feedbax/feedback/connected-feedback.ts +235 -0
  65. package/template/src/feedbax/feedback/connected.test.ts +272 -0
  66. package/template/src/feedbax/feedback/connected.ts +225 -0
  67. package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
  68. package/template/src/feedbax/feedback/email-delivery.ts +66 -0
  69. package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
  70. package/template/src/feedbax/feedback/github-comments.ts +390 -0
  71. package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
  72. package/template/src/feedbax/feedback/github-feedback.ts +581 -0
  73. package/template/src/feedbax/feedback/github-request.ts +120 -0
  74. package/template/src/feedbax/feedback/github-service-client.ts +56 -0
  75. package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
  76. package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
  77. package/template/src/feedbax/feedback/image-feedback.ts +114 -0
  78. package/template/src/feedbax/feedback/images.test.ts +223 -0
  79. package/template/src/feedbax/feedback/images.ts +350 -0
  80. package/template/src/feedbax/feedback/index.ts +226 -16
  81. package/template/src/feedbax/feedback/integration-auth.ts +138 -0
  82. package/template/src/feedbax/feedback/notion-comments.ts +2 -35
  83. package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
  84. package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
  85. package/template/src/feedbax/feedback/observed-history.ts +219 -0
  86. package/template/src/feedbax/feedback/ownership.ts +170 -0
  87. package/template/src/feedbax/feedback/participation.test.ts +653 -0
  88. package/template/src/feedbax/feedback/participation.ts +502 -0
  89. package/template/src/history-runtime.test.ts +80 -0
  90. package/template/src/history-runtime.ts +37 -0
  91. package/template/src/images-runtime.test.ts +133 -0
  92. package/template/src/images-runtime.ts +125 -0
  93. package/template/src/index.css +103 -440
  94. package/template/src/integration-runtime.test.ts +365 -0
  95. package/template/src/integration-runtime.ts +289 -0
  96. package/template/src/lazy-post-detail-overlay.tsx +13 -2
  97. package/template/src/my-feedback-page.test.tsx +152 -0
  98. package/template/src/my-feedback-page.tsx +398 -0
  99. package/template/src/my-feedback-runtime.test.ts +93 -0
  100. package/template/src/my-feedback-runtime.ts +154 -0
  101. package/template/src/overlay-post-detail.test.tsx +59 -8
  102. package/template/src/overlay-post-detail.tsx +20 -69
  103. package/template/src/participation-client.ts +63 -0
  104. package/template/src/participation-runtime.test.ts +35 -0
  105. package/template/src/participation-runtime.ts +255 -0
  106. package/template/src/portal-comments.ts +35 -5
  107. package/template/src/portal-feedback-form.tsx +29 -6
  108. package/template/src/portal-feedback-runtime.ts +3 -0
  109. package/template/src/portal-feedback-server-function.ts +6 -3
  110. package/template/src/portal-feedback-submission.ts +19 -0
  111. package/template/src/portal-server-entry.ts +13 -0
  112. package/template/src/post-creation-flow.tsx +2 -2
  113. package/template/src/post-detail-content.test.tsx +233 -0
  114. package/template/src/post-detail-content.tsx +162 -0
  115. package/template/src/post-detail-query.ts +34 -0
  116. package/template/src/post-results.tsx +1 -1
  117. package/template/src/public-cache-policy.test.ts +11 -4
  118. package/template/src/public-cache-policy.ts +1 -1
  119. package/template/src/public-feedback-index.tsx +9 -39
  120. package/template/src/public-feedback-page.test.tsx +65 -0
  121. package/template/src/public-feedback-page.ts +12 -1
  122. package/template/src/public-portal-error.tsx +1 -1
  123. package/template/src/public-post-detail-skeleton.tsx +54 -0
  124. package/template/src/public-post-detail.tsx +437 -271
  125. package/template/src/public-post-route.test.tsx +39 -3
  126. package/template/src/public-post-route.tsx +15 -28
  127. package/template/src/public-release-links.test.ts +120 -0
  128. package/template/src/public-release-links.ts +130 -0
  129. package/template/src/public-roadmap-route.test.tsx +38 -2
  130. package/template/src/public-roadmap-view.tsx +56 -23
  131. package/template/src/public-shell.test.tsx +26 -0
  132. package/template/src/routeTree.gen.ts +70 -3
  133. package/template/src/router.tsx +2 -0
  134. package/template/src/routes/__root.tsx +3 -5
  135. package/template/src/routes/email-preferences.tsx +77 -0
  136. package/template/src/routes/my-feedback.tsx +16 -0
  137. package/template/src/routes/submit.tsx +1 -1
  138. package/template/src/safe-public-failure.ts +3 -1
  139. package/template/src/server.ts +87 -2
  140. package/template/src/submission-intent.ts +26 -0
  141. package/template/src/trusted-feedback-handler.test.ts +1 -1
  142. package/template/src/vote-toggle.tsx +42 -9
  143. package/template/vite.config.ts +2 -0
@@ -1,10 +1,53 @@
1
- import { Loader2 } from "lucide-react";
1
+ import { Skeleton } from "@feedbax/ui/components/skeleton";
2
2
 
3
- export default function Loader() {
3
+ export default function Loader({
4
+ label = "Loading content",
5
+ }: { label?: string } = {}) {
4
6
  return (
5
- <div className="portal-loading" role="status">
6
- <Loader2 aria-hidden="true" />
7
- <span>Loading published feedback…</span>
7
+ <div
8
+ className="portal-loading"
9
+ role="status"
10
+ aria-label={label}
11
+ aria-busy="true"
12
+ >
13
+ <span className="sr-only">{label}…</span>
14
+ <div
15
+ aria-hidden="true"
16
+ className="mx-auto grid w-full max-w-7xl gap-10 px-6 py-10 md:grid-cols-[1fr_16rem] md:px-10"
17
+ >
18
+ <div className="space-y-8">
19
+ <div className="space-y-3">
20
+ <Skeleton className="h-8 w-40" />
21
+ <Skeleton className="h-4 w-64 max-w-full" />
22
+ </div>
23
+ <div className="flex gap-3">
24
+ <Skeleton className="h-10 w-24" />
25
+ <Skeleton className="h-10 w-24" />
26
+ </div>
27
+ <div className="divide-y border-y">
28
+ {[0, 1, 2].map((row) => (
29
+ <div
30
+ key={row}
31
+ data-post-skeleton-row
32
+ className="flex items-start gap-5 py-7"
33
+ >
34
+ <Skeleton className="size-10 shrink-0" />
35
+ <div className="flex-1 space-y-3">
36
+ <Skeleton className="h-4 w-3/5" />
37
+ <Skeleton className="h-3 w-4/5" />
38
+ <Skeleton className="h-3 w-24" />
39
+ </div>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ <div className="hidden space-y-6 pt-3 md:block">
45
+ <Skeleton className="h-4 w-20" />
46
+ <Skeleton className="h-40 w-full" />
47
+ <Skeleton className="h-4 w-20" />
48
+ <Skeleton className="h-28 w-full" />
49
+ </div>
50
+ </div>
8
51
  </div>
9
52
  );
10
53
  }
@@ -0,0 +1,232 @@
1
+ import { Bell, Loader2 } from "lucide-react";
2
+ import { useState } from "react";
3
+ import {
4
+ useInfiniteQuery,
5
+ useQuery,
6
+ useQueryClient,
7
+ } from "@tanstack/react-query";
8
+ import { Button } from "@feedbax/ui/components/button";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ } from "@feedbax/ui/components/dialog";
17
+ import { Label } from "@feedbax/ui/components/label";
18
+ import { Switch } from "@feedbax/ui/components/switch";
19
+ import { useConnectedIdentity } from "./connected-identity-provider";
20
+ import {
21
+ notificationQuery,
22
+ participationRequest,
23
+ preferencesQuery,
24
+ type EmailPreferences,
25
+ } from "../participation-client";
26
+ import { formatPublicDate } from "../public-date";
27
+
28
+ export function NotificationInbox() {
29
+ const identity = useConnectedIdentity();
30
+ if (!identity.enabled || !identity.displayName) return null;
31
+ return <ConnectedNotificationInbox />;
32
+ }
33
+ function ConnectedNotificationInbox() {
34
+ const identity = useConnectedIdentity();
35
+ const [open, setOpen] = useState(false);
36
+ const [error, setError] = useState<string>();
37
+ const [pending, setPending] = useState(false);
38
+ const queryClient = useQueryClient();
39
+ const enabled = identity.enabled && Boolean(identity.displayName);
40
+ const inbox = useInfiniteQuery({
41
+ ...notificationQuery,
42
+ enabled,
43
+ refetchInterval: enabled ? 60000 : false,
44
+ });
45
+ const preferences = useQuery({
46
+ ...preferencesQuery(),
47
+ enabled: enabled && open,
48
+ });
49
+ if (!enabled) return null;
50
+ const items = inbox.data?.pages.flatMap((page) => page.items) ?? [];
51
+ const unread = inbox.data?.pages[0]?.unreadCount ?? 0;
52
+ async function read(id?: string) {
53
+ setError(undefined);
54
+ try {
55
+ await participationRequest("notifications", id ? { id } : {});
56
+ await queryClient.invalidateQueries({
57
+ queryKey: notificationQuery.queryKey,
58
+ });
59
+ } catch (cause) {
60
+ setError(
61
+ cause instanceof Error
62
+ ? cause.message
63
+ : "Couldn’t mark updates as read.",
64
+ );
65
+ }
66
+ }
67
+ async function email(enabled: boolean) {
68
+ setError(undefined);
69
+ setPending(true);
70
+ try {
71
+ const next = await participationRequest<EmailPreferences>("preferences", {
72
+ emailEnabled: enabled,
73
+ });
74
+ queryClient.setQueryData(preferencesQuery().queryKey, next);
75
+ } catch (cause) {
76
+ setError(
77
+ cause instanceof Error
78
+ ? cause.message
79
+ : "Couldn’t save email preferences.",
80
+ );
81
+ } finally {
82
+ setPending(false);
83
+ }
84
+ }
85
+ return (
86
+ <Dialog open={open} onOpenChange={setOpen}>
87
+ <DialogTrigger
88
+ render={
89
+ <Button
90
+ variant="ghost"
91
+ size="icon"
92
+ aria-label={
93
+ unread ? `Notifications, ${unread} unread` : "Notifications"
94
+ }
95
+ className="relative"
96
+ />
97
+ }
98
+ >
99
+ <Bell aria-hidden="true" />
100
+ {unread ? (
101
+ <span className="absolute top-1 right-1 size-2 rounded-full bg-primary" />
102
+ ) : null}
103
+ </DialogTrigger>
104
+ <DialogContent className="max-h-[85svh] gap-0 overflow-hidden p-0 sm:max-w-lg">
105
+ <DialogHeader className="border-b p-5">
106
+ <DialogTitle className="text-lg">Notifications</DialogTitle>
107
+ <DialogDescription className="text-sm">
108
+ Updates on Posts you follow.
109
+ </DialogDescription>
110
+ </DialogHeader>
111
+ <div className="flex items-center justify-between gap-3 border-b px-5 py-3">
112
+ <span className="text-sm text-muted-foreground">{unread} unread</span>
113
+ <Button
114
+ size="sm"
115
+ variant="ghost"
116
+ disabled={!unread}
117
+ onClick={() => void read()}
118
+ >
119
+ Mark all read
120
+ </Button>
121
+ </div>
122
+ <div className="max-h-[45svh] overflow-y-auto">
123
+ {inbox.isPending ? (
124
+ <p role="status" className="flex items-center gap-2 p-5 text-sm">
125
+ <Loader2 className="size-4 animate-spin" />
126
+ Loading updates…
127
+ </p>
128
+ ) : inbox.isError ? (
129
+ <div className="space-y-3 p-5">
130
+ <p className="text-sm">Couldn’t load updates.</p>
131
+ <Button
132
+ size="sm"
133
+ variant="outline"
134
+ onClick={() => void inbox.refetch()}
135
+ >
136
+ Try again
137
+ </Button>
138
+ </div>
139
+ ) : items.length ? (
140
+ <ul className="divide-y">
141
+ {items.map((item) => (
142
+ <li
143
+ key={item.id}
144
+ className={item.read ? "p-5" : "bg-muted/40 p-5"}
145
+ >
146
+ <a
147
+ href={`/p/${encodeURIComponent(item.slug)}`}
148
+ className="block space-y-1 text-sm hover:underline"
149
+ onClick={() => {
150
+ void read(item.id);
151
+ setOpen(false);
152
+ }}
153
+ >
154
+ <span className="block font-medium">{item.title}</span>
155
+ <span className="block text-muted-foreground">
156
+ {item.message}
157
+ </span>
158
+ <time
159
+ className="block text-xs text-muted-foreground"
160
+ dateTime={new Date(item.createdAt).toISOString()}
161
+ >
162
+ {formatPublicDate(new Date(item.createdAt))}
163
+ </time>
164
+ </a>
165
+ {!item.read ? (
166
+ <Button
167
+ size="sm"
168
+ variant="ghost"
169
+ className="mt-2"
170
+ onClick={() => void read(item.id)}
171
+ >
172
+ Mark read
173
+ </Button>
174
+ ) : null}
175
+ </li>
176
+ ))}
177
+ </ul>
178
+ ) : (
179
+ <p className="p-5 text-sm text-muted-foreground">
180
+ Follow a Post to receive updates here.
181
+ </p>
182
+ )}
183
+ {inbox.hasNextPage ? (
184
+ <Button
185
+ variant="outline"
186
+ size="sm"
187
+ className="m-5"
188
+ disabled={inbox.isFetchingNextPage}
189
+ onClick={() => void inbox.fetchNextPage()}
190
+ >
191
+ {inbox.isFetchingNextPage ? "Loading…" : "View more"}
192
+ </Button>
193
+ ) : null}
194
+ </div>
195
+ <div className="space-y-2 border-t p-5">
196
+ <div className="flex items-center justify-between gap-4">
197
+ <Label htmlFor="notification-email">Email notifications</Label>
198
+ <Switch
199
+ id="notification-email"
200
+ checked={preferences.data?.emailEnabled ?? false}
201
+ disabled={pending || !preferences.data?.emailAvailable}
202
+ onCheckedChange={email}
203
+ />
204
+ </div>
205
+ {preferences.isError ? (
206
+ <Button
207
+ size="sm"
208
+ variant="ghost"
209
+ onClick={() => void preferences.refetch()}
210
+ >
211
+ Retry email preferences
212
+ </Button>
213
+ ) : !preferences.data?.emailAvailable && !preferences.isPending ? (
214
+ <p className="text-xs text-muted-foreground">
215
+ Email requires a verified address from your product account and a
216
+ configured sender.
217
+ </p>
218
+ ) : (
219
+ <p className="text-xs text-muted-foreground">
220
+ You can unsubscribe at any time.
221
+ </p>
222
+ )}
223
+ {error ? (
224
+ <p role="alert" className="text-sm text-destructive">
225
+ {error}
226
+ </p>
227
+ ) : null}
228
+ </div>
229
+ </DialogContent>
230
+ </Dialog>
231
+ );
232
+ }
@@ -0,0 +1,14 @@
1
+ export function PortalFooter() {
2
+ return (
3
+ <footer className="mt-auto flex justify-end border-t px-6 py-4 text-xs text-muted-foreground lg:px-10">
4
+ <a
5
+ href="https://github.com/jessequartey/feedbax"
6
+ target="_blank"
7
+ rel="noopener noreferrer"
8
+ className="transition-colors hover:text-foreground"
9
+ >
10
+ Powered by Feedbax
11
+ </a>
12
+ </footer>
13
+ );
14
+ }
@@ -0,0 +1,101 @@
1
+ import type { ObservedHistoryPage } from "@feedbax/feedback";
2
+ import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { Button } from "@feedbax/ui/components/button";
4
+ import { Skeleton } from "@feedbax/ui/components/skeleton";
5
+ import { formatPublicDate } from "../public-date";
6
+
7
+ export function PostHistory({ slug }: { slug: string }) {
8
+ const client = useQueryClient();
9
+ const clearPublicPost = () => {
10
+ client.setQueryData(["public-post", slug], null);
11
+ client.setQueryData(["public-post-comments", slug], {
12
+ pages: [{ items: [] }],
13
+ pageParams: [undefined],
14
+ });
15
+ void client.invalidateQueries({ queryKey: ["public-post-roadmap"] });
16
+ };
17
+ const query = useInfiniteQuery({
18
+ queryKey: ["public-post-history", slug],
19
+ initialPageParam: undefined as string | undefined,
20
+ queryFn: async ({ pageParam }) => {
21
+ const params = new URLSearchParams({
22
+ slug,
23
+ ...(pageParam ? { cursor: pageParam } : {}),
24
+ });
25
+ try {
26
+ const response = await fetch(`/internal/post-history?${params}`, {
27
+ cache: "no-store",
28
+ });
29
+ if (!response.ok) throw Error("Progress history is unavailable.");
30
+ const page = (await response.json()) as ObservedHistoryPage;
31
+ if (!page.available) clearPublicPost();
32
+ return page;
33
+ } catch (error) {
34
+ clearPublicPost();
35
+ throw error;
36
+ }
37
+ },
38
+ getNextPageParam: (page) => page.nextCursor,
39
+ staleTime: 0,
40
+ retry: false,
41
+ });
42
+ const events = query.isError
43
+ ? []
44
+ : (query.data?.pages.flatMap((page) => page.items) ?? []);
45
+ return (
46
+ <section className="space-y-3" aria-label="Activities">
47
+ <h2 className="sr-only">Activities</h2>
48
+ {query.isPending ? (
49
+ <Skeleton className="h-12 w-full" />
50
+ ) : query.isError ? (
51
+ <div role="alert" className="space-y-2">
52
+ <p className="text-sm">
53
+ Progress history is temporarily unavailable.
54
+ </p>
55
+ <Button
56
+ variant="outline"
57
+ size="sm"
58
+ onClick={() => void query.refetch()}
59
+ >
60
+ Retry history
61
+ </Button>
62
+ </div>
63
+ ) : events.length ? (
64
+ <ol className="space-y-4 border-l pl-4">
65
+ {events.map((event) => (
66
+ <li key={event.id} className="space-y-1 text-sm">
67
+ <time
68
+ dateTime={new Date(event.observedAt).toISOString()}
69
+ className="text-xs text-muted-foreground"
70
+ >
71
+ Observed {formatPublicDate(new Date(event.observedAt))}
72
+ </time>
73
+ {event.kind === "status" ? (
74
+ <p>
75
+ Moved to <span className="font-medium">{event.status}</span>
76
+ </p>
77
+ ) : (
78
+ <>
79
+ <p className="font-medium">Product Team update</p>
80
+ <p className="whitespace-pre-wrap">{event.body}</p>
81
+ </>
82
+ )}
83
+ </li>
84
+ ))}
85
+ </ol>
86
+ ) : (
87
+ <p className="text-sm text-muted-foreground">No activities yet.</p>
88
+ )}
89
+ {query.hasNextPage && !query.isError ? (
90
+ <Button
91
+ variant="outline"
92
+ size="sm"
93
+ disabled={query.isFetchingNextPage}
94
+ onClick={() => void query.fetchNextPage()}
95
+ >
96
+ {query.isFetchingNextPage ? "Loading history…" : "More history"}
97
+ </Button>
98
+ ) : null}
99
+ </section>
100
+ );
101
+ }
@@ -1,3 +1,4 @@
1
+ import { useConnectedIdentity } from "./connected-identity-provider";
1
2
  import { useTheme } from "next-themes";
2
3
  import { UserCircle } from "lucide-react";
3
4
 
@@ -20,8 +21,44 @@ import { useDeviceProfile } from "./device-profile-provider";
20
21
  export function ProfileMenu() {
21
22
  const { profile, openProfileSetup, clearProfile } = useDeviceProfile();
22
23
 
24
+ const identity = useConnectedIdentity();
25
+
23
26
  const initials = profile ? deriveDeviceProfileInitials(profile) : undefined;
24
27
 
28
+ if (identity.enabled)
29
+ return (
30
+ <div>
31
+ <Button variant="ghost" onClick={openProfileSetup}>
32
+ Contact profile
33
+ </Button>
34
+ {identity.displayName ? (
35
+ <>
36
+ <Button
37
+ nativeButton={false}
38
+ variant="ghost"
39
+ render={<a href="/my-feedback" />}
40
+ >
41
+ My feedback
42
+ </Button>
43
+ <span>{identity.displayName}</span>
44
+ <Button variant="outline" onClick={() => void identity.signOut()}>
45
+ Sign out
46
+ </Button>
47
+ </>
48
+ ) : (
49
+ <Button
50
+ variant="outline"
51
+ disabled={!identity.ready}
52
+ onClick={() => {
53
+ window.location.href = "/internal/identity/start";
54
+ }}
55
+ >
56
+ Sign in
57
+ </Button>
58
+ )}
59
+ {identity.error ? <p role="alert">{identity.error}</p> : null}
60
+ </div>
61
+ );
25
62
  return profile ? (
26
63
  <DropdownMenu>
27
64
  <DropdownMenuTrigger
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { Command as CommandPrimitive } from "cmdk";
3
-
4
3
  import { cn } from "@feedbax/ui/lib/utils";
4
+
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
@@ -9,10 +9,6 @@ import {
9
9
  DialogHeader,
10
10
  DialogTitle,
11
11
  } from "@feedbax/ui/components/dialog";
12
- import {
13
- InputGroup,
14
- InputGroupAddon,
15
- } from "@feedbax/ui/components/input-group";
16
12
  import { SearchIcon, CheckIcon } from "lucide-react";
17
13
 
18
14
  function Command({
@@ -47,17 +43,17 @@ function CommandDialog({
47
43
  }) {
48
44
  return (
49
45
  <Dialog {...props}>
50
- <DialogHeader className="sr-only">
51
- <DialogTitle>{title}</DialogTitle>
52
- <DialogDescription>{description}</DialogDescription>
53
- </DialogHeader>
54
46
  <DialogContent
55
47
  className={cn(
56
- "top-1/3 translate-y-0 overflow-hidden rounded-none p-0",
48
+ "top-24 translate-y-0 overflow-hidden rounded-none p-0 sm:top-32",
57
49
  className,
58
50
  )}
59
51
  showCloseButton={showCloseButton}
60
52
  >
53
+ <DialogHeader className="sr-only">
54
+ <DialogTitle>{title}</DialogTitle>
55
+ <DialogDescription>{description}</DialogDescription>
56
+ </DialogHeader>
61
57
  {children}
62
58
  </DialogContent>
63
59
  </Dialog>
@@ -69,20 +65,22 @@ function CommandInput({
69
65
  ...props
70
66
  }: React.ComponentProps<typeof CommandPrimitive.Input>) {
71
67
  return (
72
- <div data-slot="command-input-wrapper" className="border-b pb-0">
73
- <InputGroup className="h-11 border-none border-input/30 bg-input/30 shadow-none! md:h-8 *:data-[slot=input-group-addon]:pl-2!">
74
- <CommandPrimitive.Input
75
- data-slot="command-input"
76
- className={cn(
77
- "w-full text-base outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-xs",
78
- className,
79
- )}
80
- {...props}
81
- />
82
- <InputGroupAddon>
83
- <SearchIcon className="size-4 shrink-0 opacity-50" />
84
- </InputGroupAddon>
85
- </InputGroup>
68
+ <div
69
+ data-slot="command-input-wrapper"
70
+ className="flex h-14 shrink-0 items-center gap-3 border-b px-4"
71
+ >
72
+ <SearchIcon
73
+ className="size-5 shrink-0 text-muted-foreground"
74
+ aria-hidden="true"
75
+ />
76
+ <CommandPrimitive.Input
77
+ data-slot="command-input"
78
+ className={cn(
79
+ "h-full w-full min-w-0 border-0 bg-transparent py-4 text-base text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm",
80
+ className,
81
+ )}
82
+ {...props}
83
+ />
86
84
  </div>
87
85
  );
88
86
  }
@@ -95,7 +93,7 @@ function CommandList({
95
93
  <CommandPrimitive.List
96
94
  data-slot="command-list"
97
95
  className={cn(
98
- "no-scrollbar max-h-72 scroll-py-0 overflow-x-hidden overflow-y-auto outline-none",
96
+ "no-scrollbar max-h-80 scroll-py-2 overflow-x-hidden overflow-y-auto outline-none",
99
97
  className,
100
98
  )}
101
99
  {...props}
@@ -110,7 +108,10 @@ function CommandEmpty({
110
108
  return (
111
109
  <CommandPrimitive.Empty
112
110
  data-slot="command-empty"
113
- className={cn("py-6 text-center text-xs", className)}
111
+ className={cn(
112
+ "px-6 py-10 text-center text-sm text-muted-foreground",
113
+ className,
114
+ )}
114
115
  {...props}
115
116
  />
116
117
  );
@@ -124,7 +125,7 @@ function CommandGroup({
124
125
  <CommandPrimitive.Group
125
126
  data-slot="command-group"
126
127
  className={cn(
127
- "overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:text-muted-foreground",
128
+ "overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:text-muted-foreground",
128
129
  className,
129
130
  )}
130
131
  {...props}
@@ -139,7 +140,7 @@ function CommandSeparator({
139
140
  return (
140
141
  <CommandPrimitive.Separator
141
142
  data-slot="command-separator"
142
- className={cn("-mx-1 h-px bg-border", className)}
143
+ className={cn("my-1 h-px bg-border", className)}
143
144
  {...props}
144
145
  />
145
146
  );
@@ -154,7 +155,7 @@ function CommandItem({
154
155
  <CommandPrimitive.Item
155
156
  data-slot="command-item"
156
157
  className={cn(
157
- "group/command-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none in-data-[slot=dialog-content]:rounded-none! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
158
+ "group/command-item relative flex cursor-default items-center gap-2 rounded-none px-3 py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-none! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
158
159
  className,
159
160
  )}
160
161
  {...props}
@@ -5,8 +5,8 @@ import {
5
5
  useMessageScrollerScrollable,
6
6
  useMessageScrollerVisibility,
7
7
  } from "@shadcn/react/message-scroller";
8
-
9
8
  import { cn } from "@feedbax/ui/lib/utils";
9
+
10
10
  import { Button } from "@feedbax/ui/components/button";
11
11
  import { ArrowDownIcon } from "lucide-react";
12
12
 
@@ -40,7 +40,7 @@ function MessageScrollerViewport({
40
40
  <MessageScrollerPrimitive.Viewport
41
41
  data-slot="message-scroller-viewport"
42
42
  className={cn(
43
- "size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent",
43
+ "size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent data-pending-scroll:invisible",
44
44
  className,
45
45
  )}
46
46
  {...props}
@@ -1,5 +1,4 @@
1
1
  import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
-
3
2
  import { cn } from "@feedbax/ui/lib/utils";
4
3
 
5
4
  function ScrollArea({
@@ -0,0 +1,29 @@
1
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
+ import { cn } from "@feedbax/ui/lib/utils";
3
+
4
+ function Switch({
5
+ className,
6
+ size = "default",
7
+ ...props
8
+ }: SwitchPrimitive.Root.Props & {
9
+ size?: "sm" | "default";
10
+ }) {
11
+ return (
12
+ <SwitchPrimitive.Root
13
+ data-slot="switch"
14
+ data-size={size}
15
+ className={cn(
16
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <SwitchPrimitive.Thumb
22
+ data-slot="switch-thumb"
23
+ className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
24
+ />
25
+ </SwitchPrimitive.Root>
26
+ );
27
+ }
28
+
29
+ export { Switch };
@@ -2,7 +2,7 @@
2
2
  @import "tw-animate-css";
3
3
  @import "shadcn/tailwind.css";
4
4
  @import "@fontsource-variable/geist";
5
- @source "../../../apps/**/*.{ts,tsx}";
5
+ @source "../../../**/*.{ts,tsx}";
6
6
  @source "../**/*.{ts,tsx}";
7
7
 
8
8
  @custom-variant dark (&:is(.dark *));