npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-feedback-ui

v0.2.0

Published

Customizable React and React Native UI primitives for convex-feedback.

Readme

npm version Convex Component NPM License NPM Downloads GitHub forks GitHub Repo stars

Vite demo • Expo demo • React Native demo

convex-feedback-ui

| Expo | React Native | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Expo | React Native |

Optional React DOM, React Native, and Expo Router UI for convex-feedback.

This package provides the public feedback and roadmap experience. Internal priority and roadmap management live in the forkable Clerk admin panel, which uses the headless hooks directly.

The package is intentionally layered:

convex-feedback hooks
        ↓
compound UI primitives
        ↓
prebuilt FeedbackScreen
        ↓
optional Expo Router integration

Use the prebuilt screen for a complete feedback board, compose primitives for custom layouts, or ignore this package entirely and use the headless convex-feedback hooks directly.

Authentication, host business rules, and lifecycle callbacks stay in the headless host wrapper. The UI package receives the generated hooks; see the convex-feedback lifecycle callback reference for callback configuration and transaction behavior.

React DOM

import { FeedbackScreen } from "convex-feedback-ui";
import { feedbackHooks } from "./hooks/feedbackHooks";
import "convex-feedback-ui/styles.css";

<FeedbackScreen hooks={feedbackHooks} />;

Use RoadmapScreen for the public roadmap and its attached feedback entries:

import { RoadmapScreen } from "convex-feedback-ui";

<RoadmapScreen
  hooks={feedbackHooks}
  onEntryOpen={(entryId) => navigate(`/feedback/${entryId}`)}
  onUnauthenticated={() => openSignIn()}
/>;

Roadmap browsing and attached-entry reads work without authentication. The onUnauthenticated callback is used for create, vote, like, and comment actions in the prebuilt screens; the host decides how to present sign-in.

The default stylesheet:

  • has no Tailwind dependency;
  • uses namespaced cf-* classes;
  • uses low-specificity :where(...) selectors;
  • uses CSS custom properties for theme values;
  • allows normal host CSS, Tailwind, or CSS-module classes to override defaults;
  • can be omitted entirely if you want to provide your own styling.

On entry detail, the prebuilt feedback screens show an Edit action only when the server identifies the current viewer as the entry author. The form edits the title and details; entry type remains admin-controlled. The backend rechecks authorship when the update mutation runs, so hiding the action is only a presentation convenience and is not the authorization boundary. Stack-enabled Expo screens place this action in the native right toolbar; non-stacked native screens keep it alongside the entry details.

In stack mode, opening the action pushes a native form-sheet editor on iOS (and a modal stack screen on Android) with cancel and save controls in the top toolbar. Edit-screen copy is configurable through messages.form.

Key colors can be overridden directly:

<FeedbackScreen
  hooks={feedbackHooks}
  primaryColor="#6d5efc"
  backgroundColor="#0d0d10"
  surfaceColor="#17171b"
  inputColor="#1f1f25"
  textColor="#ffffff"
  mutedColor="#a0a0aa"
  borderColor="#2d2d34"
/>

Or provide theme tokens through FeedbackProvider.

React Native

Use the /native entry point for React Native projects that do not use Expo Router, or whenever you want the standard React Native implementation without native stack integration:

import { FeedbackScreen } from "convex-feedback-ui/native";

<FeedbackScreen
  hooks={feedbackHooks}
  theme={{
    colors: {
      primary: "#6d5efc",
      background: "#ffffff",
      surface: "#ffffff",
      input: "#f7f7fa",
    },
  }}
/>;

The native FeedbackScreen includes the feedback UI using React Native components.

The native roadmap exports also include the reusable RoadmapBoard and RoadmapScreenContent. Their topInset defaults to 0, while bottomInset defaults to the bottom safe-area inset. These props describe space occupied by host navigation chrome: an explicit bottomInset is the total space, including the safe area, and replaces the default even when set to 0. NativeTabs normally bounds content above its tab bar, so it should be left unset there.

Expo Router

Expo Router projects can use the /expo entry point:

import { FeedbackScreen } from "convex-feedback-ui/expo";

<FeedbackScreen
  hooks={feedbackHooks}
  theme={{
    colors: {
      primary: "#6d5efc",
      background: "#ffffff",
      surface: "#ffffff",
      input: "#f7f7fa",
    },
  }}
/>;

The Expo version uses the same native feedback UI, but adds optional Expo Router stack integration.

By default, useStack is enabled:

<FeedbackScreen hooks={feedbackHooks} useStack />

When enabled, the prebuilt screen can integrate with the current Expo Router stack, including native stack configuration, toolbar actions, back behavior, and native search UI.

[!WARNING] If the filter toolbar button is intermittently broken, especially on iOS 26+, pass Host from @expo/ui/swift-ui to BottomToolbarWrapper:

import { Host } from "@expo/ui/swift-ui";

<FeedbackScreen hooks={feedbackHooks} BottomToolbarWrapper={Host} />;

The same BottomToolbarWrapper={Host} prop is available on FeedbackStackLayout for routed Expo navigation.

Stack options can be customized through StackOptions:

<FeedbackScreen
  hooks={feedbackHooks}
  stackOptions={{
    headerLargeTitleEnabled: true,
    headerTransparent: true,
  }}
/>

iOS toolbar actions keep their package-provided SF Symbols. Android accepts host-provided image sources; when an image is omitted, the action falls back to its text label:

<FeedbackScreen
  hooks={feedbackHooks}
  androidToolbarIcons={{
    create: require("./assets/add.png"),
    edit: require("./assets/edit.png"),
    save: require("./assets/check.png"),
    back: require("./assets/back.png"),
  }}
/>

Roadmap stack insets

The stack-enabled Expo RoadmapScreen and routed RoadmapStackLayout use a transparent native header by default. The board automatically reserves the measured native-stack header height; if that context is unavailable, the fallback is 44 points on iOS or 56 points on Android, plus the top safe-area inset. If headerTransparent is false, the native stack already places the board below the header and the automatic top inset is 0. Set topInset to override that space, including with 0.

The routed and stack-enabled roadmap also renders its native bottom search toolbar on iOS 26 and newer and includes its standard 44-point height in the default bottomInset. Android and older iOS versions do not receive an empty toolbar. If your host draws a toolbar or tab bar over the screen, pass bottomInset as the total occupied height, including the safe area; an explicit value replaces the automatic default, including 0. NativeTabs content is already bounded above the tab bar, so omit bottomInset there.

Using the Expo screen without stack integration

Expo projects do not have to use the native stack integration.

Set useStack={false} to use the standard native feedback layout instead:

<FeedbackScreen hooks={feedbackHooks} useStack={false} />

When useStack={false}, the Expo convenience screen behaves like the regular React Native screen.

StackOptions is only available when stack integration is enabled.

Routed Expo navigation

For real stacked routes, use the additive routed API. Expo Router discovers routes from the application's app directory, so the host application supplies small route files while the package supplies their layouts and screens:

app/feedback/
├── _layout.tsx
├── index.tsx
├── [entryId]/
│   ├── index.tsx
│   └── edit.tsx
└── new/
    ├── _layout.tsx
    └── index.tsx

The layout owns the feedback providers and keeps them mounted across the board, entry-detail, and create-entry routes:

// app/feedback/_layout.tsx
import {
  FeedbackStackLayout,
  feedbackStackSettings,
} from "convex-feedback-ui/expo";
import { feedbackHooks } from "../../feedback";

export const unstable_settings = feedbackStackSettings;

export default function Layout() {
  return (
    <FeedbackStackLayout
      hooks={feedbackHooks}
      androidToolbarIcons={{
        create: require("../../assets/add.png"),
        edit: require("../../assets/edit.png"),
        save: require("../../assets/check.png"),
        back: require("../../assets/back.png"),
        close: require("../../assets/close.png"),
      }}
      screenOptions={{ headerTintColor: "#5b5bd6" }}
      boardOptions={{ headerLargeTitleEnabled: true }}
      entryOptions={{ headerBackTitle: "Feedback" }}
      createOptions={{ presentation: "formSheet" }}
    />
  );
}

Each page file only needs to re-export its package screen:

// app/feedback/index.tsx
export { FeedbackBoardScreen as default } from "convex-feedback-ui/expo";

// app/feedback/[entryId]/index.tsx
export { FeedbackEntryScreen as default } from "convex-feedback-ui/expo";

// app/feedback/[entryId]/edit.tsx
export { FeedbackEditScreen as default } from "convex-feedback-ui/expo";

// app/feedback/new/_layout.tsx
import {
  FeedbackCreateStackLayout,
  feedbackCreateStackSettings,
} from "convex-feedback-ui/expo";

export const unstable_settings = feedbackCreateStackSettings;
export default FeedbackCreateStackLayout;

// app/feedback/new/index.tsx
export { CreateFeedbackScreen as default } from "convex-feedback-ui/expo";

FeedbackStackLayout uses these route names by default:

{
  board: "index",
  entry: "[entryId]/index",
  edit: "[entryId]/edit",
  create: "new",
}

Names can be partially overridden when the files use a different structure.

const routes = {
  entry: "entry/[entryId]",
  edit: "entry/[entryId]/edit",
  create: "create",
};

export const unstable_settings = createFeedbackStackSettings(routes);

export default function Layout() {
  return <FeedbackStackLayout hooks={feedbackHooks} routes={routes} />;
}

The board's search and loaded list remain mounted when another screen is pushed, so returning restores the prior query and scroll position. The create route is a modal navigator by default. Duplicate suggestions push the main entry-detail route, with the create modal remaining available behind it. After creation, the complete modal is dismissed and the created entry replaces it on the board stack.

See packages/example-expo-routed for a complete application.

Routed public roadmap

The public roadmap has a matching routed Expo API. Create a board and item page under your app's route directory:

app/roadmap/
├── _layout.tsx
├── index.tsx
└── [roadmapId].tsx
// app/roadmap/_layout.tsx
import {
  RoadmapStackLayout,
  roadmapStackSettings,
} from "convex-feedback-ui/expo";
import { feedbackHooks } from "../../feedback";

export const unstable_settings = roadmapStackSettings;

export default function Layout() {
  return <RoadmapStackLayout hooks={feedbackHooks} />;
}

// app/roadmap/index.tsx
export { RoadmapBoardScreen as default } from "convex-feedback-ui/expo";

// app/roadmap/[roadmapId].tsx
export { RoadmapItemScreen as default } from "convex-feedback-ui/expo";

Use onEntryOpen on RoadmapStackLayout when attached feedback should push the host application's routed feedback detail page. The roadmap item page uses the existing entry-card and entry-detail implementations when no host entry route is supplied. The board title and search field are rendered by the Expo Router Stack, and RoadmapBoard is exported from the native entry points for sharing the admin-style stage layout with custom cards.

On this routed stack, topInset and bottomInset can be supplied on RoadmapStackLayout to replace the automatic header and bottom-toolbar spacing.

Diagnostic metadata

Prebuilt screens can collect creation-time diagnostic metadata for feedback entries. Collection is disabled by default and never applies to comments.

Enable the platform defaults for every entry kind:

<FeedbackScreen hooks={feedbackHooks} collectMetadata />

Or combine global defaults with per-kind behavior:

<FeedbackScreen
  hooks={feedbackHooks}
  collectMetadata={{
    standard: true,
    additional: { releaseChannel: "production" },
    kinds: {
      feedback: false,
      feature_request: {
        standard: ["platform", "appVersion"],
      },
      bug_report: {
        standard: true,
        additional: async ({ kind }) => ({
          kind,
          accountTier: await getAccountTier(),
        }),
      },
    },
  }}
/>
  • A kind-specific standard selection replaces the global selection. Global and kind-specific additional values are merged, with kind-specific keys taking precedence.
  • A kind set to false disables all metadata collection for that kind.
  • Collection happens only when the user finally submits.
  • Unavailable or failed collection sources are omitted without blocking feedback submission.
  • Web defaults include platform, user agent, language, timezone, screen and viewport dimensions, and device pixel ratio.
  • React Native defaults include platform, OS version, available device model, screen dimensions, pixel ratio, and font scale.
  • The Expo entry point additionally uses expo-constants for app version, build number, application ID, Expo runtime version, and execution environment when available.
  • Generic React Native apps can provide app version and build values through additional.

Metadata is stored in separate standard and additional sections. It is omitted from list and search results and from non-admin reads. When the host's server-side actor resolver identifies an admin, getEntry includes the metadata and the entry detail screen shows a metadata viewer.

The platform collectors and formatMetadataKey helper are exported from their respective package entry points for custom integrations.

Choosing an API

For a complete implementation with minimal setup:

Web                  → convex-feedback-ui
React Native         → convex-feedback-ui/native
Expo Router          → convex-feedback-ui/expo

For custom layouts, use the primitives exposed by the relevant entry point instead of the prebuilt FeedbackScreen.

The prebuilt screens are convenience APIs. They are built from the same public primitives available to consumers, so applications are not required to adopt the prebuilt layout or navigation behavior.

You can also import the prebuilt screen components directly if you only want certain parts without reaching for primitives.

Using primitives

Web and native expose the same conceptual compounds:

  • FeedbackBoard.*
  • FeedbackEntry.*
  • FeedbackForm.*
  • Comment.*

FeedbackBoard.State is the centered container used for loading and empty states. The prebuilt screen accepts optional loading and emptyState nodes. The loading node replaces the default loading indicator/copy; the empty-state node renders above the default empty-state message. Native implementations use ActivityIndicator by default when no loading node is supplied. ChoiceChips is the shared single-selection primitive used for entry kinds and the web/native board's Open/Closed filter.

Example:

<FeedbackEntry.Root entry={entry}>
  <FeedbackEntry.Upvote onToggle={setUpvote} />

  <FeedbackEntry.Content>
    <FeedbackEntry.Status />
    <FeedbackEntry.Title />
    <FeedbackEntry.Body />
    <FeedbackEntry.CommentCount />
  </FeedbackEntry.Content>
</FeedbackEntry.Root>

Containers accept arbitrary children, so layout remains controlled by the host application.

Native primitives can also be composed directly:

<FeedbackBoard.Root>
  <FeedbackBoard.Header>
    <FeedbackBoard.Title />
    <ChoiceChips
      options={[
        { value: "open", label: "Open" },
        { value: "closed", label: "Closed" },
      ]}
      value={status}
      onValueChange={setStatus}
    />
  </FeedbackBoard.Header>

  <FeedbackBoard.Search value={query} onValueChange={setQuery} />

  <FeedbackBoard.List>
    {entries.map((entry) => (
      <FeedbackEntry.Root key={entry.id} entry={entry}>
        {/* custom entry layout */}
      </FeedbackEntry.Root>
    ))}
  </FeedbackBoard.List>
</FeedbackBoard.Root>

Using primitives directly is the recommended escape hatch when the prebuilt screen's navigation, search, layout, or presentation does not fit the host application.

Replacing primitive implementations

Interactive leaf primitives accept typed render-function children. This is the escape hatch for shadcn, Radix-derived controls, Expo UI, or application-specific native controls.

<Comment.Like onToggle={setLiked}>
  {({ active, count, toggle }) => (
    <MyLikeControl active={active} count={count} onPress={toggle} />
  )}
</Comment.Like>

The render function replaces the entire visual implementation while preserving state and action wiring. Regular children replace only the default content where supported.

This pattern is intentionally used instead of relying on a DOM-specific asChild clone mechanism, so it works across React DOM and React Native.

Localization and copy customization

The package has no i18n runtime dependency. All package-owned text is represented by FeedbackMessages and has English defaults.

Override only what you need:

<FeedbackScreen
  hooks={feedbackHooks}
  messages={{
    board: {
      title: "Produktfeedback",
      searchPlaceholder: "Feedback durchsuchen…",
      statusFilter: "Status filtern",
    },
    comments: {
      viewReplies: (count) => `${count} Antworten anzeigen`,
    },
  }}
/>

This is compatible with i18next, react-intl, Lingui, custom dictionaries, or plain static objects. Call your application's translation function when building the messages object.

The board status filter uses messages.statuses.open and messages.statuses.closed for its two options, and messages.board.statusFilter for its accessible label.

Precedence for copy is:

local primitive label/children
→ FeedbackProvider / FeedbackScreen messages
→ built-in English messages

User-generated entry and comment content is never translated, normalized for display, or rewritten.

Lazy replies

The prebuilt screens preserve the backend lazy-loading model. Top-level comments mount one paginated query. A CommentBranch only mounts its ReplyList after the user expands that comment. Each reply with children repeats the same pattern.

This means deep threads do not create queries until the relevant branch is opened.

Sorting

Pass commentSort="top" | "newest" | "oldest" to the prebuilt screen for globally correct server ordering.

For custom presentation ordering, provide:

transformComments={(comments) =>
  [...comments].sort(myComparator)
}

That transform only affects comments already loaded at the current direct-child level. It is deliberately not described as a globally paginated server sort.

Localization invariant

When adding visible package copy:

  1. add it to FeedbackMessages;
  2. add an English default;
  3. consume it through the UI context;
  4. update localization contract tests if the shape changes.

Do not hardcode new visible sentences inside a primitive or prebuilt screen.

Monorepo development

From the repository root, run:

npm install
npm run codegen
npm run test:all