create-feedbax 0.2.0 → 0.2.1

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 (99) hide show
  1. package/dist/cli.js +2042 -16
  2. package/dist/index.js +1997 -5
  3. package/package.json +1 -1
  4. package/template/.feedbax-template.sha256 +1 -1
  5. package/template/AGENTS.md +13 -0
  6. package/template/CONNECTED.md +404 -0
  7. package/template/README.md +58 -0
  8. package/template/components.json +1 -1
  9. package/template/eslint.config.mjs +23 -0
  10. package/template/migrations/0001_connected.sql +6 -0
  11. package/template/migrations/0002_participation.sql +8 -0
  12. package/template/migrations/0003_images.sql +3 -0
  13. package/template/migrations/0004_followed_status.sql +9 -0
  14. package/template/package.json +9 -5
  15. package/template/src/authorized-draft-post.tsx +1 -0
  16. package/template/src/changelog-page.tsx +63 -54
  17. package/template/src/cloudflare-workers.d.ts +6 -0
  18. package/template/src/comment-creation.test.tsx +42 -1
  19. package/template/src/comment-thread-page.ts +11 -1
  20. package/template/src/components/command-palette.tsx +59 -30
  21. package/template/src/components/connected-identity-provider.tsx +108 -0
  22. package/template/src/components/device-profile-provider.tsx +41 -26
  23. package/template/src/components/follow-post-button.tsx +89 -0
  24. package/template/src/components/header.tsx +2 -0
  25. package/template/src/components/image-attachments.tsx +187 -0
  26. package/template/src/components/loader.tsx +48 -5
  27. package/template/src/components/notification-inbox.tsx +232 -0
  28. package/template/src/components/portal-footer.tsx +14 -0
  29. package/template/src/components/profile-menu.tsx +30 -0
  30. package/template/src/components/ui/components/command.tsx +30 -29
  31. package/template/src/components/ui/components/message-scroller.tsx +2 -2
  32. package/template/src/components/ui/components/scroll-area.tsx +0 -1
  33. package/template/src/components/ui/components/switch.tsx +29 -0
  34. package/template/src/components/ui/styles/globals.css +1 -1
  35. package/template/src/connected-identity.test.ts +144 -0
  36. package/template/src/connected-identity.ts +122 -0
  37. package/template/src/connected-participation.test.tsx +67 -0
  38. package/template/src/connected-runtime.test.ts +241 -0
  39. package/template/src/connected-runtime.ts +176 -0
  40. package/template/src/connected-social.test.tsx +158 -0
  41. package/template/src/feedback-cache-invalidation.ts +1 -0
  42. package/template/src/feedback-runtime.ts +79 -12
  43. package/template/src/feedbax/config/index.test.ts +28 -0
  44. package/template/src/feedbax/config/index.ts +93 -0
  45. package/template/src/feedbax/feedback/connected-feedback.ts +206 -0
  46. package/template/src/feedbax/feedback/connected.test.ts +272 -0
  47. package/template/src/feedbax/feedback/connected.ts +215 -0
  48. package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
  49. package/template/src/feedbax/feedback/email-delivery.ts +66 -0
  50. package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
  51. package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
  52. package/template/src/feedbax/feedback/image-feedback.ts +101 -0
  53. package/template/src/feedbax/feedback/images.test.ts +223 -0
  54. package/template/src/feedbax/feedback/images.ts +324 -0
  55. package/template/src/feedbax/feedback/index.ts +55 -1
  56. package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
  57. package/template/src/feedbax/feedback/participation.test.ts +653 -0
  58. package/template/src/feedbax/feedback/participation.ts +502 -0
  59. package/template/src/images-runtime.test.ts +133 -0
  60. package/template/src/images-runtime.ts +125 -0
  61. package/template/src/index.css +97 -440
  62. package/template/src/lazy-post-detail-overlay.tsx +13 -2
  63. package/template/src/overlay-post-detail.test.tsx +59 -8
  64. package/template/src/overlay-post-detail.tsx +20 -69
  65. package/template/src/participation-client.ts +63 -0
  66. package/template/src/participation-runtime.test.ts +35 -0
  67. package/template/src/participation-runtime.ts +255 -0
  68. package/template/src/portal-comments.ts +24 -5
  69. package/template/src/portal-feedback-form.tsx +18 -3
  70. package/template/src/portal-feedback-runtime.ts +3 -0
  71. package/template/src/portal-feedback-server-function.ts +6 -3
  72. package/template/src/portal-feedback-submission.ts +10 -0
  73. package/template/src/portal-server-entry.ts +4 -0
  74. package/template/src/post-creation-flow.tsx +2 -2
  75. package/template/src/post-detail-content.test.tsx +233 -0
  76. package/template/src/post-detail-content.tsx +162 -0
  77. package/template/src/post-detail-query.ts +34 -0
  78. package/template/src/post-results.tsx +1 -1
  79. package/template/src/public-cache-policy.test.ts +11 -4
  80. package/template/src/public-cache-policy.ts +1 -1
  81. package/template/src/public-feedback-index.tsx +9 -39
  82. package/template/src/public-feedback-page.test.tsx +65 -0
  83. package/template/src/public-feedback-page.ts +12 -1
  84. package/template/src/public-post-detail-skeleton.tsx +54 -0
  85. package/template/src/public-post-detail.tsx +298 -208
  86. package/template/src/public-post-route.test.tsx +39 -3
  87. package/template/src/public-post-route.tsx +15 -28
  88. package/template/src/public-roadmap-route.test.tsx +38 -2
  89. package/template/src/public-roadmap-view.tsx +56 -23
  90. package/template/src/public-shell.test.tsx +26 -0
  91. package/template/src/routeTree.gen.ts +49 -3
  92. package/template/src/router.tsx +2 -0
  93. package/template/src/routes/__root.tsx +3 -5
  94. package/template/src/routes/email-preferences.tsx +77 -0
  95. package/template/src/routes/submit.tsx +1 -1
  96. package/template/src/safe-public-failure.ts +3 -1
  97. package/template/src/server.ts +41 -1
  98. package/template/src/trusted-feedback-handler.test.ts +1 -1
  99. package/template/src/vote-toggle.tsx +42 -9
@@ -1,6 +1,6 @@
1
1
  import type { PublicPostQuery, PublicPostPage } from "@feedbax/feedback";
2
2
  import type { PortalFeatures } from "@feedbax/config";
3
- import { Skeleton } from "@feedbax/ui/components/skeleton";
3
+ import Loader from "./components/loader";
4
4
  import { useAuthorizedDraftPosts } from "./draft-post-list";
5
5
  import {
6
6
  BoardNavigation,
@@ -39,11 +39,11 @@ export function PublicPostIndex({
39
39
  <section aria-labelledby="feedback-heading">
40
40
  <h1
41
41
  id="feedback-heading"
42
- className="text-3xl font-semibold tracking-tight"
42
+ className="text-2xl font-semibold tracking-tight"
43
43
  >
44
44
  Feedback
45
45
  </h1>
46
- <p className="mt-2 text-base text-muted-foreground">
46
+ <p className="mt-2 text-sm text-muted-foreground">
47
47
  {features.voting
48
48
  ? "Share ideas and vote on what matters."
49
49
  : "Share ideas and follow public progress."}
@@ -58,10 +58,11 @@ export function PublicPostIndex({
58
58
  features={features}
59
59
  />
60
60
  </div>
61
- <section className="mt-8" aria-live="polite">
62
- <div className="mb-4">
63
- <h2 className="text-lg font-medium">Posts</h2>
64
- </div>
61
+ <section
62
+ className="mt-8"
63
+ aria-live="polite"
64
+ aria-label="Feedback posts"
65
+ >
65
66
  <PostResults
66
67
  drafts={drafts}
67
68
  initialItems={page.items}
@@ -89,36 +90,5 @@ export function PublicPostIndex({
89
90
  }
90
91
 
91
92
  export function PublicPostFeedSkeleton() {
92
- return (
93
- <main
94
- className="feedback-index feedback-feed-skeleton"
95
- aria-label="Loading Posts"
96
- aria-busy="true"
97
- >
98
- <section className="feedback-intro" aria-hidden="true">
99
- <Skeleton className="feedback-skeleton-heading" />
100
- <Skeleton className="feedback-skeleton-deck" />
101
- </section>
102
- <div className="post-controls" aria-hidden="true">
103
- <Skeleton className="feedback-skeleton-control" />
104
- <Skeleton className="feedback-skeleton-control" />
105
- <Skeleton className="feedback-skeleton-control" />
106
- </div>
107
- <section className="feedback-results" aria-hidden="true">
108
- <Skeleton className="feedback-skeleton-section-heading" />
109
- <ol className="feedback-list">
110
- {Array.from({ length: 3 }, (_, index) => (
111
- <li key={index} data-post-skeleton-row>
112
- <div className="feedback-skeleton-row">
113
- <Skeleton className="feedback-skeleton-meta" />
114
- <Skeleton className="feedback-skeleton-title" />
115
- <Skeleton className="feedback-skeleton-copy" />
116
- </div>
117
- </li>
118
- ))}
119
- </ol>
120
- </section>
121
- <span className="sr-only">Loading Posts…</span>
122
- </main>
123
- );
93
+ return <Loader label="Loading Posts" />;
124
94
  }
@@ -16,6 +16,71 @@ import { authorizedDraftPostsQueryKey } from "./authorized-draft-query";
16
16
  afterEach(cleanup);
17
17
 
18
18
  describe("public feedback home page", () => {
19
+ it("keeps Shipped Posts out of default Top before pagination while preserving explicit discovery", async () => {
20
+ const feedback = createFeedbackModule({
21
+ initialItems: [
22
+ ...Array.from({ length: 26 }, (_, index) => ({
23
+ id: `active-${index}`,
24
+ slug: `active-${index}`,
25
+ title: `Active ${index}`,
26
+ description: "Feedback",
27
+ type: "Feature Request" as const,
28
+ status: "Planned" as const,
29
+ published: true,
30
+ voteCount: index,
31
+ createdAt: new Date(index * 1000),
32
+ updatedAt: new Date(index * 1000),
33
+ })),
34
+ ...Array.from({ length: 27 }, (_, index) => ({
35
+ id: `shipped-${index}`,
36
+ slug: `shipped-${index}`,
37
+ title: `Shipped ${index}`,
38
+ description: "Feedback",
39
+ type: "Feature Request" as const,
40
+ status: "Shipped" as const,
41
+ published: true,
42
+ voteCount: 1000,
43
+ createdAt: new Date(100000 + index * 1000),
44
+ updatedAt: new Date(100000 + index * 1000),
45
+ })),
46
+ ],
47
+ });
48
+ const first = await loadPublicPostPage({
49
+ feedback,
50
+ search: { sort: "top" },
51
+ });
52
+ expect(first.items).toHaveLength(25);
53
+ expect(first.items.every((post) => post.status !== "Shipped")).toBe(true);
54
+ const second = await loadPublicPostPage({
55
+ feedback,
56
+ search: { sort: "top", cursor: first.nextCursor },
57
+ });
58
+ expect(second.items).toHaveLength(1);
59
+ const legacy = await loadPublicPostPage({
60
+ feedback,
61
+ search: { sort: "trending" },
62
+ });
63
+ expect(legacy.items.map((post) => post.slug)).toEqual(
64
+ first.items.map((post) => post.slug),
65
+ );
66
+ const shipped = await loadPublicPostPage({
67
+ feedback,
68
+ search: { sort: "top", statuses: ["Shipped"] },
69
+ });
70
+ expect(shipped.items).toHaveLength(25);
71
+ expect(shipped.items.every((post) => post.status === "Shipped")).toBe(true);
72
+ const search = await loadPublicPostPage({
73
+ feedback,
74
+ search: { sort: "top", search: "Shipped" },
75
+ });
76
+ expect(search.items[0]?.status).toBe("Shipped");
77
+ const newest = await loadPublicPostPage({
78
+ feedback,
79
+ search: { sort: "new" },
80
+ });
81
+ expect(newest.items[0]?.status).toBe("Shipped");
82
+ });
83
+
19
84
  it("loads 25 Published Posts and an opaque next-page cursor", async () => {
20
85
  const feedback = createFeedbackModule({
21
86
  initialItems: Array.from({ length: 26 }, (_, index) => ({
@@ -30,7 +30,18 @@ export async function loadPublicPostPage({
30
30
  feedback: Pick<FeedbackModule, "listPublicPosts">;
31
31
  search: Record<string, unknown>;
32
32
  }): Promise<PublicPostPage> {
33
- return feedback.listPublicPosts(publicPostSearch(search));
33
+ const query = publicPostSearch(search);
34
+ const defaultTop =
35
+ query.sort !== "new" &&
36
+ !query.search &&
37
+ !query.status &&
38
+ !query.statuses?.length;
39
+ return feedback.listPublicPosts({
40
+ ...query,
41
+ ...(defaultTop
42
+ ? { statuses: postStatuses.filter((status) => status !== "Shipped") }
43
+ : {}),
44
+ });
34
45
  }
35
46
 
36
47
  export function publicPostSearch(
@@ -0,0 +1,54 @@
1
+ import { Skeleton } from "@feedbax/ui/components/skeleton";
2
+
3
+ export function PublicPostDetailSkeleton({
4
+ display = "page",
5
+ }: { display?: "overlay" | "page" } = {}) {
6
+ return (
7
+ <main
8
+ className="feedback-detail public-post-detail"
9
+ data-display={display}
10
+ aria-label="Loading Post details"
11
+ aria-busy="true"
12
+ >
13
+ {display === "page" ? (
14
+ <Skeleton aria-hidden="true" className="h-4 w-32" />
15
+ ) : null}
16
+ <div aria-hidden="true" className="post-detail-layout">
17
+ <div className="post-detail-primary space-y-7">
18
+ <div className="flex gap-2">
19
+ <Skeleton className="h-5 w-28" />
20
+ <Skeleton className="h-5 w-24" />
21
+ </div>
22
+ <div className="space-y-3">
23
+ <Skeleton
24
+ className={display === "overlay" ? "h-5 w-4/5" : "h-7 w-4/5"}
25
+ />
26
+ <Skeleton
27
+ className={display === "overlay" ? "h-5 w-3/5" : "h-7 w-3/5"}
28
+ />
29
+ </div>
30
+ <div className="space-y-3">
31
+ <Skeleton className="h-4 w-full" />
32
+ <Skeleton className="h-4 w-full" />
33
+ <Skeleton className="h-4 w-2/3" />
34
+ </div>
35
+ <Skeleton className="h-10 w-16" />
36
+ <div className="space-y-5 border-t pt-6">
37
+ <Skeleton className="h-4 w-24" />
38
+ <Skeleton className="h-24 w-full" />
39
+ </div>
40
+ </div>
41
+ <div className="post-detail-sidebar space-y-7">
42
+ <Skeleton className="h-4 w-20" />
43
+ {[0, 1, 2, 3].map((row) => (
44
+ <div className="flex justify-between gap-4" key={row}>
45
+ <Skeleton className="h-3 w-16" />
46
+ <Skeleton className="h-3 w-24" />
47
+ </div>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ <span className="sr-only">Loading Post details…</span>
52
+ </main>
53
+ );
54
+ }