create-feedbax 0.1.0 → 0.2.0

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 (210) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +11 -1
  3. package/dist/cli.js +1261 -26
  4. package/dist/index.js +1402 -160
  5. package/package.json +40 -14
  6. package/template/.feedbax-template.sha256 +1 -0
  7. package/template/.gitignore.template +6 -0
  8. package/template/.npmignore.template +5 -0
  9. package/template/.prettierignore.template +4 -0
  10. package/template/LICENSE +201 -0
  11. package/template/components.json +25 -0
  12. package/template/eslint.config.mjs +25 -0
  13. package/template/package.json +70 -14
  14. package/template/public/feedbax-mark.svg +4 -0
  15. package/template/public/robots.txt +3 -0
  16. package/template/src/authorized-draft-post.test.tsx +212 -0
  17. package/template/src/authorized-draft-post.tsx +204 -0
  18. package/template/src/authorized-draft-query.ts +74 -0
  19. package/template/src/browser-post-state.test.ts +120 -0
  20. package/template/src/browser-post-state.ts +134 -0
  21. package/template/src/browser-vote-state.test.ts +14 -0
  22. package/template/src/browser-vote-state.ts +27 -0
  23. package/template/src/capability-authorization-error.ts +3 -0
  24. package/template/src/changelog-page.test.tsx +139 -0
  25. package/template/src/changelog-page.tsx +219 -0
  26. package/template/src/changelog-query.ts +13 -0
  27. package/template/src/changelog-runtime.ts +27 -0
  28. package/template/src/cloudflare-turnstile.ts +31 -0
  29. package/template/src/cloudflare-workers-test.ts +3 -0
  30. package/template/src/cloudflare-workers.d.ts +18 -0
  31. package/template/src/comment-creation.test.tsx +270 -0
  32. package/template/src/comment-profile.ts +5 -0
  33. package/template/src/comment-thread-page.ts +18 -0
  34. package/template/src/components/command-palette-context.tsx +71 -0
  35. package/template/src/components/command-palette.tsx +246 -0
  36. package/template/src/components/create-post-trigger.tsx +44 -0
  37. package/template/src/components/device-profile-provider.tsx +309 -0
  38. package/template/src/components/header.tsx +95 -0
  39. package/template/src/components/loader.tsx +10 -0
  40. package/template/src/components/profile-menu.tsx +89 -0
  41. package/template/src/components/ui/components/alert-dialog.tsx +185 -0
  42. package/template/src/components/ui/components/attachment.tsx +207 -0
  43. package/template/src/components/ui/components/avatar.tsx +107 -0
  44. package/template/src/components/ui/components/badge.tsx +52 -0
  45. package/template/src/components/ui/components/bubble.tsx +128 -0
  46. package/template/src/components/ui/components/button-group.tsx +87 -0
  47. package/template/src/components/ui/components/button.tsx +56 -0
  48. package/template/src/components/ui/components/card.tsx +103 -0
  49. package/template/src/components/ui/components/checkbox.tsx +26 -0
  50. package/template/src/components/ui/components/command.tsx +194 -0
  51. package/template/src/components/ui/components/dialog.tsx +154 -0
  52. package/template/src/components/ui/components/drawer.tsx +229 -0
  53. package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
  54. package/template/src/components/ui/components/empty.tsx +101 -0
  55. package/template/src/components/ui/components/field.tsx +236 -0
  56. package/template/src/components/ui/components/input-group.tsx +155 -0
  57. package/template/src/components/ui/components/input.tsx +20 -0
  58. package/template/src/components/ui/components/item.tsx +201 -0
  59. package/template/src/components/ui/components/kbd.tsx +26 -0
  60. package/template/src/components/ui/components/label.tsx +18 -0
  61. package/template/src/components/ui/components/marker.tsx +71 -0
  62. package/template/src/components/ui/components/message-scroller.tsx +128 -0
  63. package/template/src/components/ui/components/message.tsx +92 -0
  64. package/template/src/components/ui/components/native-select.tsx +65 -0
  65. package/template/src/components/ui/components/popover.tsx +88 -0
  66. package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
  67. package/template/src/components/ui/components/radio-group.tsx +36 -0
  68. package/template/src/components/ui/components/scroll-area.tsx +52 -0
  69. package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
  70. package/template/src/components/ui/components/select.tsx +199 -0
  71. package/template/src/components/ui/components/separator.tsx +23 -0
  72. package/template/src/components/ui/components/sheet.tsx +135 -0
  73. package/template/src/components/ui/components/skeleton.tsx +13 -0
  74. package/template/src/components/ui/components/sonner.tsx +43 -0
  75. package/template/src/components/ui/components/spinner.tsx +16 -0
  76. package/template/src/components/ui/components/tabs.tsx +80 -0
  77. package/template/src/components/ui/components/textarea.tsx +18 -0
  78. package/template/src/components/ui/components/tooltip.tsx +64 -0
  79. package/template/src/components/ui/hooks/.gitkeep +0 -0
  80. package/template/src/components/ui/lib/utils.ts +6 -0
  81. package/template/src/components/ui/styles/globals.css +151 -0
  82. package/template/src/create-post-overlay.tsx +82 -0
  83. package/template/src/draft-post-list.tsx +39 -0
  84. package/template/src/feedback-cache-invalidation.test.ts +185 -0
  85. package/template/src/feedback-cache-invalidation.ts +97 -0
  86. package/template/src/feedback-runtime.ts +38 -0
  87. package/template/src/feedbax/changelog/index.test.ts +156 -0
  88. package/template/src/feedbax/changelog/index.ts +206 -0
  89. package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
  90. package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
  91. package/template/src/feedbax/config/index.test.ts +111 -0
  92. package/template/src/feedbax/config/index.ts +271 -0
  93. package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
  94. package/template/src/feedbax/feedback/index.test.ts +900 -0
  95. package/template/src/feedbax/feedback/index.ts +942 -0
  96. package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
  97. package/template/src/feedbax/feedback/notion-comments.ts +231 -0
  98. package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
  99. package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
  100. package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
  101. package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
  102. package/template/src/feedbax/feedback/notion-request.ts +74 -0
  103. package/template/src/feedbax.test-fixture.ts +64 -0
  104. package/template/src/feedbax.ts +69 -0
  105. package/template/src/index.css +2138 -0
  106. package/template/src/lazy-create-post-overlay.tsx +6 -0
  107. package/template/src/lazy-post-detail-overlay.tsx +10 -0
  108. package/template/src/masked-post-link.tsx +27 -0
  109. package/template/src/overlay-post-detail.test.tsx +130 -0
  110. package/template/src/overlay-post-detail.tsx +92 -0
  111. package/template/src/portal-comments.test.ts +375 -0
  112. package/template/src/portal-comments.ts +384 -0
  113. package/template/src/portal-features.test.tsx +150 -0
  114. package/template/src/portal-feedback-form.tsx +515 -0
  115. package/template/src/portal-feedback-runtime.test.ts +32 -0
  116. package/template/src/portal-feedback-runtime.ts +55 -0
  117. package/template/src/portal-feedback-server-function.ts +93 -0
  118. package/template/src/portal-feedback-submission.test.ts +307 -0
  119. package/template/src/portal-feedback-submission.ts +214 -0
  120. package/template/src/portal-head.ts +58 -0
  121. package/template/src/portal-server-entry.ts +64 -0
  122. package/template/src/portal-voting.test.ts +106 -0
  123. package/template/src/portal-voting.ts +149 -0
  124. package/template/src/post-badges.tsx +27 -0
  125. package/template/src/post-creation-flow.test.tsx +230 -0
  126. package/template/src/post-creation-flow.tsx +88 -0
  127. package/template/src/post-detail-overlay.test.tsx +114 -0
  128. package/template/src/post-detail-overlay.tsx +67 -0
  129. package/template/src/post-feed-controls.test.tsx +239 -0
  130. package/template/src/post-feed-controls.tsx +298 -0
  131. package/template/src/post-presentation.ts +37 -0
  132. package/template/src/post-queries.test.ts +158 -0
  133. package/template/src/post-queries.ts +26 -0
  134. package/template/src/post-results.tsx +165 -0
  135. package/template/src/public-cache-policy.test.ts +141 -0
  136. package/template/src/public-cache-policy.ts +119 -0
  137. package/template/src/public-cache-tags.ts +7 -0
  138. package/template/src/public-changelog-page.ts +15 -0
  139. package/template/src/public-changelog-server-function.ts +27 -0
  140. package/template/src/public-date.ts +11 -0
  141. package/template/src/public-failure-state.test.tsx +69 -0
  142. package/template/src/public-feedback-index.tsx +124 -0
  143. package/template/src/public-feedback-page.test.tsx +290 -0
  144. package/template/src/public-feedback-page.ts +75 -0
  145. package/template/src/public-feedback-server-function.ts +22 -0
  146. package/template/src/public-portal-error.tsx +27 -0
  147. package/template/src/public-post-detail.tsx +682 -0
  148. package/template/src/public-post-page.test.tsx +173 -0
  149. package/template/src/public-post-page.ts +15 -0
  150. package/template/src/public-post-route.test.tsx +214 -0
  151. package/template/src/public-post-route.tsx +171 -0
  152. package/template/src/public-post-server-function.ts +48 -0
  153. package/template/src/public-post-unavailable.tsx +10 -0
  154. package/template/src/public-roadmap-page.test.tsx +139 -0
  155. package/template/src/public-roadmap-page.ts +24 -0
  156. package/template/src/public-roadmap-route.test.tsx +218 -0
  157. package/template/src/public-roadmap-route.tsx +50 -0
  158. package/template/src/public-roadmap-server-function.ts +44 -0
  159. package/template/src/public-roadmap-view.tsx +378 -0
  160. package/template/src/public-shell.test.tsx +518 -0
  161. package/template/src/roadmap-query.ts +21 -0
  162. package/template/src/root-branding.test.ts +53 -0
  163. package/template/src/routeTree.gen.ts +131 -0
  164. package/template/src/routed-post-creation-form.tsx +28 -0
  165. package/template/src/router.tsx +25 -0
  166. package/template/src/routes/__root.tsx +116 -0
  167. package/template/src/routes/changelog.tsx +39 -0
  168. package/template/src/routes/index.tsx +61 -0
  169. package/template/src/routes/p.$slug.tsx +16 -0
  170. package/template/src/routes/roadmap.tsx +11 -0
  171. package/template/src/routes/submit.tsx +28 -0
  172. package/template/src/safe-public-failure.ts +45 -0
  173. package/template/src/server.ts +111 -0
  174. package/template/src/similar-posts.tsx +45 -0
  175. package/template/src/submit-route.test.tsx +134 -0
  176. package/template/src/trusted-feedback-handler.test.ts +425 -0
  177. package/template/src/trusted-feedback-handler.ts +181 -0
  178. package/template/src/trusted-feedback-runtime.ts +17 -0
  179. package/template/src/turnstile-configuration.test.ts +37 -0
  180. package/template/src/turnstile-configuration.ts +19 -0
  181. package/template/src/use-media-query.ts +17 -0
  182. package/template/src/vote-toggle.test.tsx +171 -0
  183. package/template/src/vote-toggle.tsx +211 -0
  184. package/template/tsconfig.json +45 -0
  185. package/template/vite.config.ts +20 -0
  186. package/template/vitest.config.ts +4 -0
  187. package/template/vitest.setup.ts +3 -0
  188. package/template/wrangler.jsonc +35 -0
  189. package/dist/cli.d.ts +0 -3
  190. package/dist/cli.d.ts.map +0 -1
  191. package/dist/cli.js.map +0 -1
  192. package/dist/index.d.ts +0 -21
  193. package/dist/index.d.ts.map +0 -1
  194. package/dist/index.js.map +0 -1
  195. package/template/.env.example +0 -4
  196. package/template/README.md +0 -7
  197. package/template/apps/portal/components.json +0 -20
  198. package/template/apps/portal/package.json +0 -31
  199. package/template/apps/portal/src/lib/utils.ts +0 -6
  200. package/template/apps/portal/src/routeTree.gen.ts +0 -59
  201. package/template/apps/portal/src/router.tsx +0 -10
  202. package/template/apps/portal/src/routes/__root.tsx +0 -31
  203. package/template/apps/portal/src/routes/index.tsx +0 -35
  204. package/template/apps/portal/src/styles.css +0 -91
  205. package/template/apps/portal/tsconfig.json +0 -15
  206. package/template/apps/portal/vite.config.ts +0 -13
  207. package/template/feedbax.config.ts +0 -13
  208. package/template/feedbax.jsonc +0 -11
  209. package/template/pnpm-workspace.yaml +0 -3
  210. package/template/turbo.json +0 -8
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+ import { mergeProps } from "@base-ui/react/merge-props";
3
+ import { useRender } from "@base-ui/react/use-render";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "@feedbax/ui/lib/utils";
7
+
8
+ const markerVariants = cva(
9
+ "group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-xs text-muted-foreground [&_svg:not([class*='size-'])]:size-3.5 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "",
14
+ separator:
15
+ "before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
16
+ border: "border-b border-border pb-2",
17
+ },
18
+ },
19
+ },
20
+ );
21
+
22
+ function Marker({
23
+ className,
24
+ variant = "default",
25
+ render,
26
+ ...props
27
+ }: useRender.ComponentProps<"div"> & VariantProps<typeof markerVariants>) {
28
+ return useRender({
29
+ defaultTagName: "div",
30
+ props: mergeProps<"div">(
31
+ {
32
+ className: cn(markerVariants({ variant, className })),
33
+ },
34
+ props,
35
+ ),
36
+ render,
37
+ state: {
38
+ slot: "marker",
39
+ variant,
40
+ },
41
+ });
42
+ }
43
+
44
+ function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) {
45
+ return (
46
+ <span
47
+ data-slot="marker-icon"
48
+ aria-hidden="true"
49
+ className={cn(
50
+ "size-3.5 shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function MarkerContent({ className, ...props }: React.ComponentProps<"span">) {
59
+ return (
60
+ <span
61
+ data-slot="marker-content"
62
+ className={cn(
63
+ "min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ );
69
+ }
70
+
71
+ export { Marker, MarkerIcon, MarkerContent, markerVariants };
@@ -0,0 +1,128 @@
1
+ import * as React from "react";
2
+ import {
3
+ MessageScroller as MessageScrollerPrimitive,
4
+ useMessageScroller,
5
+ useMessageScrollerScrollable,
6
+ useMessageScrollerVisibility,
7
+ } from "@shadcn/react/message-scroller";
8
+
9
+ import { cn } from "@feedbax/ui/lib/utils";
10
+ import { Button } from "@feedbax/ui/components/button";
11
+ import { ArrowDownIcon } from "lucide-react";
12
+
13
+ function MessageScrollerProvider(
14
+ props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>,
15
+ ) {
16
+ return <MessageScrollerPrimitive.Provider {...props} />;
17
+ }
18
+
19
+ function MessageScroller({
20
+ className,
21
+ ...props
22
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
23
+ return (
24
+ <MessageScrollerPrimitive.Root
25
+ data-slot="message-scroller"
26
+ className={cn(
27
+ "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function MessageScrollerViewport({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
39
+ return (
40
+ <MessageScrollerPrimitive.Viewport
41
+ data-slot="message-scroller-viewport"
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",
44
+ className,
45
+ )}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function MessageScrollerContent({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
55
+ return (
56
+ <MessageScrollerPrimitive.Content
57
+ data-slot="message-scroller-content"
58
+ className={cn("flex h-max min-h-full flex-col gap-6", className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function MessageScrollerItem({
65
+ className,
66
+ scrollAnchor = false,
67
+ ...props
68
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
69
+ return (
70
+ <MessageScrollerPrimitive.Item
71
+ data-slot="message-scroller-item"
72
+ scrollAnchor={scrollAnchor}
73
+ className={cn(
74
+ "min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
75
+ className,
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ function MessageScrollerButton({
83
+ direction = "end",
84
+ className,
85
+ children,
86
+ render,
87
+ variant = "secondary",
88
+ size = "icon-sm",
89
+ ...props
90
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
91
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
92
+ return (
93
+ <MessageScrollerPrimitive.Button
94
+ data-slot="message-scroller-button"
95
+ data-direction={direction}
96
+ data-variant={variant}
97
+ data-size={size}
98
+ direction={direction}
99
+ className={cn(
100
+ "absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
101
+ className,
102
+ )}
103
+ render={render ?? <Button variant={variant} size={size} />}
104
+ {...props}
105
+ >
106
+ {children ?? (
107
+ <>
108
+ <ArrowDownIcon />
109
+ <span className="sr-only">
110
+ {direction === "end" ? "Scroll to end" : "Scroll to start"}
111
+ </span>
112
+ </>
113
+ )}
114
+ </MessageScrollerPrimitive.Button>
115
+ );
116
+ }
117
+
118
+ export {
119
+ MessageScrollerProvider,
120
+ MessageScroller,
121
+ MessageScrollerViewport,
122
+ MessageScrollerContent,
123
+ MessageScrollerItem,
124
+ MessageScrollerButton,
125
+ useMessageScroller,
126
+ useMessageScrollerScrollable,
127
+ useMessageScrollerVisibility,
128
+ };
@@ -0,0 +1,92 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@feedbax/ui/lib/utils";
4
+
5
+ function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="message-group"
9
+ className={cn("flex min-w-0 flex-col gap-1.5", className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function Message({
16
+ className,
17
+ align = "start",
18
+ ...props
19
+ }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
20
+ return (
21
+ <div
22
+ data-slot="message"
23
+ data-align={align}
24
+ className={cn(
25
+ "group/message relative flex w-full min-w-0 gap-1.5 text-xs data-[align=end]:flex-row-reverse",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
34
+ return (
35
+ <div
36
+ data-slot="message-avatar"
37
+ className={cn(
38
+ "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
47
+ return (
48
+ <div
49
+ data-slot="message-content"
50
+ className={cn(
51
+ "flex w-full min-w-0 flex-col gap-2 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="message-header"
63
+ className={cn(
64
+ "flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0",
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="message-footer"
76
+ className={cn(
77
+ "flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ export {
86
+ MessageGroup,
87
+ Message,
88
+ MessageAvatar,
89
+ MessageContent,
90
+ MessageFooter,
91
+ MessageHeader,
92
+ };
@@ -0,0 +1,65 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@feedbax/ui/lib/utils";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+
6
+ type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
7
+ size?: "sm" | "default";
8
+ };
9
+
10
+ function NativeSelect({
11
+ className,
12
+ size = "default",
13
+ ...props
14
+ }: NativeSelectProps) {
15
+ return (
16
+ <div
17
+ className={cn(
18
+ "group/native-select relative w-fit has-[select:disabled]:opacity-50",
19
+ className,
20
+ )}
21
+ data-slot="native-select-wrapper"
22
+ data-size={size}
23
+ >
24
+ <select
25
+ data-slot="native-select"
26
+ data-size={size}
27
+ className="h-11 w-full min-w-0 appearance-none rounded-none border border-input bg-transparent py-1 pr-8 pl-2.5 text-base transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[size=sm]:h-11 data-[size=sm]:rounded-none data-[size=sm]:py-0.5 md:h-8 md:text-xs md:data-[size=sm]:h-7 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
28
+ {...props}
29
+ />
30
+ <ChevronDownIcon
31
+ className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
32
+ aria-hidden="true"
33
+ data-slot="native-select-icon"
34
+ />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ function NativeSelectOption({
40
+ className,
41
+ ...props
42
+ }: React.ComponentProps<"option">) {
43
+ return (
44
+ <option
45
+ data-slot="native-select-option"
46
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function NativeSelectOptGroup({
53
+ className,
54
+ ...props
55
+ }: React.ComponentProps<"optgroup">) {
56
+ return (
57
+ <optgroup
58
+ data-slot="native-select-optgroup"
59
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
3
+
4
+ import { cn } from "@feedbax/ui/lib/utils";
5
+
6
+ function Popover({ ...props }: PopoverPrimitive.Root.Props) {
7
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
8
+ }
9
+
10
+ function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
11
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
12
+ }
13
+
14
+ function PopoverContent({
15
+ className,
16
+ align = "center",
17
+ alignOffset = 0,
18
+ side = "bottom",
19
+ sideOffset = 4,
20
+ ...props
21
+ }: PopoverPrimitive.Popup.Props &
22
+ Pick<
23
+ PopoverPrimitive.Positioner.Props,
24
+ "align" | "alignOffset" | "side" | "sideOffset"
25
+ >) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Positioner
29
+ align={align}
30
+ alignOffset={alignOffset}
31
+ side={side}
32
+ sideOffset={sideOffset}
33
+ className="isolate z-50"
34
+ >
35
+ <PopoverPrimitive.Popup
36
+ data-slot="popover-content"
37
+ className={cn(
38
+ "z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-none bg-popover p-2.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ </PopoverPrimitive.Positioner>
44
+ </PopoverPrimitive.Portal>
45
+ );
46
+ }
47
+
48
+ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="popover-header"
52
+ className={cn("flex flex-col gap-1 text-xs", className)}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
59
+ return (
60
+ <PopoverPrimitive.Title
61
+ data-slot="popover-title"
62
+ className={cn("text-sm font-medium", className)}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function PopoverDescription({
69
+ className,
70
+ ...props
71
+ }: PopoverPrimitive.Description.Props) {
72
+ return (
73
+ <PopoverPrimitive.Description
74
+ data-slot="popover-description"
75
+ className={cn("text-xs/relaxed text-muted-foreground", className)}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ export {
82
+ Popover,
83
+ PopoverContent,
84
+ PopoverDescription,
85
+ PopoverHeader,
86
+ PopoverTitle,
87
+ PopoverTrigger,
88
+ };
@@ -0,0 +1,85 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { Avatar, AvatarFallback } from "@feedbax/ui/components/avatar";
5
+ import {
6
+ Command,
7
+ CommandInput,
8
+ CommandItem,
9
+ } from "@feedbax/ui/components/command";
10
+ import { Field, FieldError, FieldLabel } from "@feedbax/ui/components/field";
11
+ import { Kbd } from "@feedbax/ui/components/kbd";
12
+ import { RadioGroup, RadioGroupItem } from "@feedbax/ui/components/radio-group";
13
+ import { ScrollArea } from "@feedbax/ui/components/scroll-area";
14
+ import { Sheet, SheetTrigger } from "@feedbax/ui/components/sheet";
15
+ import { Spinner } from "@feedbax/ui/components/spinner";
16
+
17
+ describe("redesigned portal registry primitives", () => {
18
+ it("renders a field with an associated label", () => {
19
+ const markup = renderToStaticMarkup(
20
+ <Field>
21
+ <FieldLabel htmlFor="title">Title</FieldLabel>
22
+ </Field>,
23
+ );
24
+ expect(markup).toContain("<label");
25
+ expect(markup).toContain("Title");
26
+ });
27
+
28
+ it("renders a field error message", () => {
29
+ const markup = renderToStaticMarkup(
30
+ <FieldError errors={[{ message: "Title is required" }]} />,
31
+ );
32
+ expect(markup).toContain("Title is required");
33
+ });
34
+
35
+ it("renders a command palette shell with an input and items", () => {
36
+ const markup = renderToStaticMarkup(
37
+ <Command>
38
+ <CommandInput placeholder="Search feedback…" />
39
+ <CommandItem>Keyboard-first search</CommandItem>
40
+ </Command>,
41
+ );
42
+ expect(markup).toContain("Search feedback…");
43
+ expect(markup).toContain("Keyboard-first search");
44
+ });
45
+
46
+ it("renders an avatar fallback with initials", () => {
47
+ const markup = renderToStaticMarkup(
48
+ <Avatar>
49
+ <AvatarFallback>MC</AvatarFallback>
50
+ </Avatar>,
51
+ );
52
+ expect(markup).toContain("MC");
53
+ });
54
+
55
+ it("renders a sheet trigger", () => {
56
+ const markup = renderToStaticMarkup(
57
+ <Sheet>
58
+ <SheetTrigger>Filters</SheetTrigger>
59
+ </Sheet>,
60
+ );
61
+ expect(markup).toContain("Filters");
62
+ });
63
+
64
+ it("renders a kbd hint", () => {
65
+ expect(renderToStaticMarkup(<Kbd>⌘K</Kbd>)).toContain("⌘K");
66
+ });
67
+
68
+ it("renders a radio group with an item", () => {
69
+ const markup = renderToStaticMarkup(
70
+ <RadioGroup>
71
+ <RadioGroupItem value="feature" />
72
+ </RadioGroup>,
73
+ );
74
+ expect(markup).toContain("radiogroup");
75
+ });
76
+
77
+ it("renders a scroll area with content", () => {
78
+ const markup = renderToStaticMarkup(<ScrollArea>Posts</ScrollArea>);
79
+ expect(markup).toContain("Posts");
80
+ });
81
+
82
+ it("renders a spinner", () => {
83
+ expect(renderToStaticMarkup(<Spinner />)).toContain("svg");
84
+ });
85
+ });
@@ -0,0 +1,36 @@
1
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3
+
4
+ import { cn } from "@feedbax/ui/lib/utils";
5
+
6
+ function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
7
+ return (
8
+ <RadioGroupPrimitive
9
+ data-slot="radio-group"
10
+ className={cn("grid w-full gap-2", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
17
+ return (
18
+ <RadioPrimitive.Root
19
+ data-slot="radio-group-item"
20
+ className={cn(
21
+ "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
22
+ className,
23
+ )}
24
+ {...props}
25
+ >
26
+ <RadioPrimitive.Indicator
27
+ data-slot="radio-group-indicator"
28
+ className="flex size-4 items-center justify-center"
29
+ >
30
+ <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
31
+ </RadioPrimitive.Indicator>
32
+ </RadioPrimitive.Root>
33
+ );
34
+ }
35
+
36
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,52 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
+
3
+ import { cn } from "@feedbax/ui/lib/utils";
4
+
5
+ function ScrollArea({
6
+ className,
7
+ children,
8
+ ...props
9
+ }: ScrollAreaPrimitive.Root.Props) {
10
+ return (
11
+ <ScrollAreaPrimitive.Root
12
+ data-slot="scroll-area"
13
+ className={cn("relative", className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport
17
+ data-slot="scroll-area-viewport"
18
+ className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
19
+ >
20
+ {children}
21
+ </ScrollAreaPrimitive.Viewport>
22
+ <ScrollBar />
23
+ <ScrollAreaPrimitive.Corner />
24
+ </ScrollAreaPrimitive.Root>
25
+ );
26
+ }
27
+
28
+ function ScrollBar({
29
+ className,
30
+ orientation = "vertical",
31
+ ...props
32
+ }: ScrollAreaPrimitive.Scrollbar.Props) {
33
+ return (
34
+ <ScrollAreaPrimitive.Scrollbar
35
+ data-slot="scroll-area-scrollbar"
36
+ data-orientation={orientation}
37
+ orientation={orientation}
38
+ className={cn(
39
+ "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
40
+ className,
41
+ )}
42
+ {...props}
43
+ >
44
+ <ScrollAreaPrimitive.Thumb
45
+ data-slot="scroll-area-thumb"
46
+ className="relative flex-1 rounded-none bg-border"
47
+ />
48
+ </ScrollAreaPrimitive.Scrollbar>
49
+ );
50
+ }
51
+
52
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,23 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ Attachment,
6
+ AttachmentAction,
7
+ } from "@feedbax/ui/components/attachment";
8
+ import { InputGroupButton } from "@feedbax/ui/components/input-group";
9
+
10
+ describe("shared secondary form actions", () => {
11
+ it.each([
12
+ ["attachment action", <AttachmentAction />],
13
+ ["input group button", <InputGroupButton />],
14
+ ])("defaults the %s to a non-submitting button", (_name, component) => {
15
+ expect(renderToStaticMarkup(component)).toContain('type="button"');
16
+ });
17
+
18
+ it("preserves horizontal as the observable default attachment orientation", () => {
19
+ expect(renderToStaticMarkup(<Attachment />)).toContain(
20
+ 'data-orientation="horizontal"',
21
+ );
22
+ });
23
+ });