π Storybook β’ π R-KIT β’ π§ R-KIT Dev
- Overview
- Tech Stack
- Quick Start
- Available Scripts
- Configuration
- Localization & Sidebar Menus
- Feature Flags & Module Configs
- Module Generator
- Project Structure
- Conventions & Tooling
This repository provides a batteries-included dashboard scaffold using the Roketin design system. It ships with authenticated layouts, reusable UI primitives, strict linting, and testing utilities so teams can focus on feature delivery instead of project setup.
- Framework: React 19 + Vite 7
- State & Data: Zustand, React Query, Immer
- Routing & Auth: React Router v7, custom guards, permission helpers
- UI & Styling: Tailwind CSS (via
@tailwindcss/vite), Lucide icons - Forms & Validation: React Hook Form, Yup, reusable form primitives
- Testing: Vitest, @testing-library, MSW
- Quality: ESLint (strict config), Prettier, Husky hooks, lint-staged, Commitlint
Requires Node.js β₯ 18 and pnpm β₯ 9.
# Install dependencies
pnpm install
# Start the dev server
pnpm dev
# Create a production build
pnpm build
# Run the test suite
pnpm testEnvironment variables live in .env (example values are committed). Define new feature switches in feature-flags.config.ts and flip them per environment with VITE_FEATURE_<FLAG> in your .env.
| Command | Description |
|---|---|
pnpm dev |
Launches Vite dev server on port 5177. |
pnpm build |
Type-checks and bundles the app for production. |
pnpm preview |
Serves the production build locally. |
pnpm lint |
Runs ESLint against the entire project. |
pnpm test |
Executes unit/integration tests with Vitest. |
pnpm test:coverage |
Generates coverage reports under coverage/. |
pnpm test:ui |
Starts the Vitest UI runner. |
pnpm commit |
Interactive commit message helper (Commitlint prompt). |
pnpm roketin |
Custom CLI for scaffolding modules and associated files. |
pnpm storybook |
Runs Storybook on http://localhost:6006 for base components. |
pnpm storybook:build |
Produces the static Storybook build under storybook-static/. |
Application-level knobs reside in roketin.config.ts. Adjusting this file lets you rebrand, resize the sidebar, change filter persistence, switch admin prefixes, and more without touching component code.
| Section | Keys | Purpose |
|---|---|---|
app |
name, shortName, tagline |
Populates branding components such as RBrand and copy used around the shell. |
sidebar.settings |
stateStorage.type, stateStorage.key, width, widthMobile, widthIcon, keyboardShortcut |
Persists sidebar open/close state, controls widths for desktop/mobile/icon modes, and binds the keyboard toggle key. |
filters.persistence |
enabled, strategy, keyPrefix, debounceMs |
Configures how RFilter remembers selections (e.g., local-storage, custom key prefixes, debounce window). |
routes.admin |
basePath |
Base path prepended to every authenticated route (default /r-admin). |
languages |
enabled, debug, supported[] (code, label, isDefault) |
Toggles multi-language support, i18n debug logging, and declares the supported locale list. |
app: Supplies the product name, short label, and tagline shown in default headers/footers. Perfect for rebranding a white-label deployment.sidebar.settings:stateStorage.type: Where to persist open/closed state (local-storageby default).stateStorage.key: Storage namespace so multiple apps can coexist on one domain.width,widthMobile,widthIcon: CSS-ready values exposed as custom properties.keyboardShortcut: Letter bound toβ/Ctrl + keyto toggle the sidebar.
filters.persistence: Enable or disable persistence. Choose a storagestrategy, optionally set akeyPrefix, and throttle writes withdebounceMs.routes.admin: Change the admin prefix (e.g.,/app) to relocate every nested feature route.languages:enabled: Hides the language dropdown when set tofalse.debug: Mirrorsi18nextdebug mode; handy during localisation tweaks.supported: Each locale entry must define acode(used by i18next),label(UI display), and optionalisDefault.
- Update
src/modules/app/locales/app.<lang>.jsonwhen adding languages toroketin.config.ts. - Each feature module owns its sidebar structure through
<feature>.config.ts. The config exposesmenuentries pointing to translation keys (e.g.,sampleForm:title).AppSidebaraggregates every module config automatically, so translations just need to exist in the featureβs locale file. When a config setsparentModuleId, its menu is appended under the parent's menu tree (with strict inheritanceβno parent, no child). - Example locale snippet:
{
"menu": {
"dashboard": "Dashboard",
"sampleForm": "Sample Form"
}
}- Single source of truth: All flag definitions live in
feature-flags.config.ts. UsedefineFeatureFlags({ MY_FEATURE: { env: 'VITE_FEATURE_MY_FEATURE', defaultEnabled: true }, })to register a key, optional description, and default behaviour. - Environment toggles: Set
VITE_FEATURE_<FLAG>in.env,.env.local, or deployment secrets. Truthy values (true,1,on) enable the feature; falsy values disable it. Missing entries fall back todefaultEnabled. - Runtime helpers: Import from
@/modules/app/libs/feature-flag:isFeatureEnabled('SOME_FLAG')for synchronous checks (menu generation, guards, data fetching).useFeatureFlag('SOME_FLAG')hook for component rendering without duplicating logic.
- Route guards: Add
handle.featureFlag = 'SOME_FLAG'(optionallyhandle.featureFlagFallback) in any route object to automatically short-circuit toAppNotFoundwhen the flag is disabledβthis happens before auth/permission guards run. - Module configs: Every module exports
<module>.config.tswithdefineModuleConfig({ moduleId, parentModuleId?, featureFlag, menu? }). The loadersrc/modules/app/libs/module-config.lib.tsglobs@/modules/**/*.config.ts, so nested modules are supported out of the box (src/modules/billing/modules/invoice/invoice.config.ts, etc.). - Sidebar menus:
menuentries live next to the module and can describe tree menus, icons, and permissions. Parent modules typically define the container menu (omitnameto keep it non-clickable). Children inherit the parent automatically by declaringparentModuleId. You can omit themenufield or set it tofalsefor child modules that shouldn't appear directly in the sidebar. Useorder(lower first) to customize the ordering of menus at any level.AppSidebarconsumesAPP_MODULE_CONFIGS, nests children beneath their parent, and skips children whenever the parent is disabled. - CLI automation:
pnpm roketin module ...(standard preset) now scaffolds<feature>.config.tsand automatically appends the correspondingVITE_FEATURE_<FLAG>entry tofeature-flags.config.ts. Review the stub (icon, permissions, translation namespace) and adjust to your needs before shipping.
Example config:
// src/modules/sample-form/sample-form.config.ts
import { defineModuleConfig } from '@/modules/app/types/module-config.type';
export const sampleFormModuleConfig = defineModuleConfig({
moduleId: 'sample-form',
// parentModuleId: 'billing', // Optional: attach under another moduleβs menu.
featureFlag: 'SAMPLE_FORM',
// menu: false, // Uncomment to skip sidebar entries (useful for child modules).
menu: {
title: 'sampleForm:title',
name: 'SampleFormIndex',
order: 10,
// icon: SomeIcon,
// permission: 'SAMPLE_FORM_VIEW',
},
});// Parent module acts as a collapsible header (no `name` β non-clickable).
export const userModuleConfig = defineModuleConfig({
moduleId: 'user',
featureFlag: 'USER',
menu: {
title: 'user:title',
icon: Users,
order: 1,
},
});
// Child module is rendered under the parent (strict: hidden when parent is disabled).
export const userGuardModuleConfig = defineModuleConfig({
moduleId: 'user-guard',
parentModuleId: 'user',
featureFlag: 'USER_GUARD',
menu: {
title: 'user:menu.guard',
name: 'UserGuardIndex',
},
});Notes:
- Children attach to the first menu entry defined by the parent config. Remove
nameon the parent menu to render it as a toggle-only container, or setnameif the parent should be clickable too. - When a parentβs feature flag is
false, all children referencing thatparentModuleIdare hidden automatically (strict inheritance). - Parents that act purely as containers (no
name) disappear automatically when every child is filtered out (no orphan headers are shown). - Use the optional
orderfield to enforce deterministic ordering (lower values first, ties fall back to declaration order). - CLI scaffolding:
pnpm roketin module user/guardauto-populatesparentModuleIdand a childmenuentry; tweak the title, icon, permissions, or order as needed.
The CLI backing pnpm roketin lives in bin/roketin.js and scaffolds feature modules under src/modules. It understands nested structures, child routes, and optional assets.
# Create a new feature module (e.g., src/modules/reporting)
pnpm roketin module reporting
# Create a nested module (prompts whether it should be treated as a child route)
pnpm roketin module reporting/summary
# Force explicit child route generation via the legacy alias
pnpm roketin module-child reporting/summary
# Inspect registered generators, presets, and restricted modules
pnpm roketin info- Greeting: The CLI renders a Roketin banner with CFonts.
- Path Parsing: It splits the provided path (e.g.,
reporting/summary) and builds the target directory. Nested segments are placed under a cascadingmodulesfolder (src/modules/reporting/modules/summary/β¦) so each feature can contain its own sub-modules. - Generation Mode: You choose between:
Standard: Generates module config + feature flag entry, pages, routes, locale stub, types, and services (the default set).All folders: Scaffolds every supported artifact (hooks, contexts, stores, etc.).Custom: Lets you pick specific file types via a checkbox prompt.
- Child Routes & Auto-Linking: For nested paths, the CLI asks if the final segment should be treated as a child route. All routes use a unified naming convention (
<module>.routes.tsx) regardless of nesting level. The generator automatically imports and spreads child routes inside the parent route file (including grandparents, recursively). If you already customized the parent route structure, skim the resulting diff to confirm the insertion landed where you expect. - Auto-Scaffold Parents: When a parent route/config is missing (e.g., you run
pnpm roketin module master-data/salesbeforemaster-dataexists), the CLI now creates both the lightweight parent route scaffold and the parent module config (complete with its own feature flag). Only minimal shells are generatedβno placeholder page componentβso you retain full control over the actual content and menu labels. - Idempotent Files: Existing files are never overwritten unless you opted in at the overwrite prompt. Skipped items are logged for visibility.
- Feature flags on autopilot: Whenever the
configgenerator runs, the CLI adds (or reuses) aVITE_FEATURE_<FLAG>entry infeature-flags.config.tsso toggling a module is as simple as flipping the env value.
The application uses a smart route discovery system:
- Top-level modules (
src/modules/*/routes/*.routes.tsx) are auto-discovered byapp.routes.tsx - Nested modules (
src/modules/*/modules/*/routes/*.routes.tsx) are imported by their parent module's route file - All route files use the same naming convention:
<module>.routes.tsx(no.childsuffix needed)
This means when you create a nested module like master-data/client, the CLI:
- Creates
src/modules/master-data/modules/client/routes/client.routes.tsx - Auto-imports
clientRoutesintomaster-data.routes.tsx - The parent route is discovered by
app.routes.tsx, which loads all children automatically
Depending on your selections, the generator can produce:
components/pages/<feature>.tsxβ Page skeleton.routes/<feature>.routes.tsxβ Route config usingcreateAppRoutes.locales/<feature>.en.jsonβ Locale stub for feature-specific translations.services/<feature>.service.tsβ API placeholder.types/<feature>.type.tsβ Type definitions scaffold.- Optional extras: hooks, contexts, stores, libs, constants, etc.
All generated route containers ship with a handle.breadcrumbOptions.disabled flag. This keeps parent breadcrumbs (e.g., βMaster Dataβ) displayed but non-clickable until you intentionally enable navigation for them.
Use module:move to relocate modules between different locations:
# Promote a child module to standalone (top-level)
pnpm roketin module:move master-data/client client
# Move a module to a different parent
pnpm roketin module:move master-data/client user-management/client
# Demote a standalone module to become a child
pnpm roketin module:move client master-data/client
# Interactive mode (prompts for destination)
pnpm roketin module:move master-data/clientThe command automatically handles:
- Moving the folder to the new location
- Updating all import paths across the codebase
- Removing the module from its old parent route (if it was a child)
- Adding the module to its new parent route (if becoming a child)
- Updating route file comments to reflect the new location
- Cleaning up empty directories
Routes can drive breadcrumbs through the handle property, and components can register dynamic labels with a lightweight hook.
export const productsRoutes = createAppRoutes([
{
path: 'products/:sku',
element: <ProductDetail />,
handle: {
breadcrumb: (match) => ({
type: 'product',
id: match.params.sku ?? '',
}),
breadcrumbOptions: {
disabled: false,
hide: false,
},
},
},
]);breadcrumbaccepts either a translation key/string or a function returning{ type, id }for resolver-based labels.breadcrumbOptions.disabledkeeps the crumb visible but non-clickable.breadcrumbOptions.hideremoves the crumb entirely.
import { useBreadcrumbLabel } from '@/modules/app/hooks/use-breadcrumb-resolver';
export default function ProductDetail({ sku }: { sku: string }) {
useBreadcrumbLabel('product', (id) => productCache[id]?.name ?? id);
return <ProductDetailView sku={sku} />;
}useBreadcrumbLabel(type, resolver) registers the resolver while the component is mounted and cleans it up automatically. Call useResetBreadcrumbResolvers() when leaving a flow that should clear every resolver.
Routes remain the primary source of truth, but sometimes a page needs extra context (dynamic breadcrumbs, title overrides, or even feature guards). The optional useOverridePageConfig hook lets you inject those hints straight from the page component:
import { useOverridePageConfig } from '@/modules/app/hooks/use-page-config';
export default function SampleFormPage() {
useOverridePageConfig(({ params }) => ({
title: params?.id ? 'sampleForm:actions.edit' : 'sampleForm:menu.createNew',
breadcrumbs: [
{ label: 'sampleForm:title', href: '/admin/sample-form' },
{
label: params?.id
? 'sampleForm:actions.edit'
: 'sampleForm:menu.createNew',
},
],
permissions: ['SAMPLE_FORM_VIEW'],
featureFlag: 'SAMPLE_FORM',
}));
return <div>...</div>;
}- The hook is optional: if you skip it, the layout falls back to the route handles defined in
*.routes.tsx. - Returning
permissionsorfeatureFlagautomatically rendersAppForbidden/AppNotFoundwhen access is denied, so sensitive pages stay guarded even without duplicating route metadata. - Breadcrumbs accept arrays or
(ctx) => [], and theyβre merged into the global breadcrumb component for a consistent UX.
Two structures are supported; pick the one that fits your teamβs workflow.
src/modules/
βββ config/
βββ config.config.ts
βββ routes/config.routes.tsx
βββ modules/
βββ user/
βββ user.config.ts
βββ routes/user.routes.tsx
βββ components/pages/user.tsx
- Every level mirrors the eventual route segment (
config β user β detail), so React Router automatically nests segments andRBreadcrumbscan infer a fallback trail directly from the URL when neither the route handle noruseOverridePageConfigoverrides exist. moduleId/parentModuleIdmatch the folder hierarchy, so sidebar ordering is deterministic and global feature-flag inheritance (parent off β children hidden) works without extra bookkeeping.- New pages only need to live inside the relevant module folder: the glob loader picks up
*.routes.tsx, and breadcrumbs/title fallbacks already align with the directory structure.
src/modules/
βββ config/
βββ user/
βββ role/
- Keep modules top-level but express nesting through
defineModuleConfig:
// src/modules/user/user.config.ts
export default defineModuleConfig({
moduleId: 'config-user',
parentModuleId: 'config',
menu: {
title: 'user:title',
name: 'UserIndex',
order: 20,
},
});- Because the file system no longer mirrors URL depth, add explicit breadcrumbs either via the route
handleor, for richer/dynamic needs,useOverridePageConfig. This ensuresRBreadcrumbsrenders the intended hierarchy even though the folders are flat.
// src/modules/user/components/pages/user-detail.tsx
import { useOverridePageConfig } from '@/modules/app/hooks/use-page-config';
export default function UserDetailPage() {
useOverridePageConfig(({ params }) => ({
breadcrumbs: [
{ label: 'Config', href: '/admin/config' },
{ label: 'user:title', href: '/admin/config/user' },
{ label: params?.id ?? 'Detail' },
],
}));
return <div>...</div>;
}Module configs still drive the menus (parent/child relationships + order control). Breadcrumbs are stitched together via the snippets above so even though folders are flat, the UI shows the desired nesting.
- Menus are still ordered via the
orderfield inside each config, so you can rearrange siblings without touching the folder tree.
Both approaches can coexist: legacy modules can stay flat while newer ones adopt the hierarchical layout for auto-generated breadcrumbs.
reactjs-base-project/
βββ roketin.config.ts # Central app/theme/sidebar configuration
βββ package.json
βββ pnpm-lock.yaml
βββ vite.config.ts # Vite + Vitest configuration
βββ tsconfig.json # Root TS config (references app/node configs)
βββ .husky/ # Git hooks (Commitlint, lint-staged)
βββ bin/
β βββ roketin.js # Module generator CLI
βββ public/ # Static assets served by Vite
βββ dist/ # Build output (generated)
βββ coverage/ # Test coverage reports (generated)
βββ tests/
β βββ setup.ts # Vitest setup
β βββ setup-msw.ts # MSW server bootstrap
β βββ unit/ # Example unit tests
βββ src/
βββ main.tsx # App entry point
βββ vite-env.d.ts
βββ @types/ # Project-level TypeScript declarations
βββ plugins/ # Custom Vite plugins (e.g., i18n type generator)
βββ modules/
βββ app/ # App shell, layouts, shared libs, locales
βββ auth/ # Authentication routes and layouts
βββ dashboard/ # Example dashboard module
βββ sample-form/ # Rich form examples and reusable widgets
modules/[feature]breakdown
assets/# [Only app folder] static assets scoped to the shell (e.g., global CSS).components/base/# [Only app folder] reusable atoms/molecules prefixed withr-(r-form.tsx,r-filter.tsx, etc.).layouts/# top-level layout pieces (app-layout.tsx,app-sidebar.tsx).pages/# entry-point screens for the shell (app-entry-point.tsx, error states).ui/# [Only app folder] shared components; variant tokens live underui/variants/.
constants/# shared constants and enums (permissions, menus) using kebab-case filenames.contexts/# React contexts; filename convention:<feature>-context.ts.hooks/# custom hooks (use-*.ts/x), camel-cased afteruse.libs/# utility functions (storage, crypto, routing) using kebab-case names.locales/# i18n resources (app.<lang>.json, validation bundles).routes/# shell route aggregator (app.routes.tsx).stores/# Zustand stores (*.store.tssuffix).types/# shared TypeScript definitions (*.type.tssuffix).validators/# schema or validation helpers (*.validator.ts).
Directories such as
dist/,coverage/, andnode_modules/are generated and can be cleaned safely.
- Linting: ESLint with strict TypeScript rules and hook validation.
- Formatting: Prettier runs automatically via Husky and lint-staged on staged files.
- Commits: Commitlint enforces Conventional Commit messages; use
pnpm commitfor an interactive flow. - Testing: Vitest mimics Jest APIs, with MSW mocking network calls. Tests bootstrap via
tests/setup.ts. - Module Generation:
pnpm roketin module <feature>/[sub-feature]scaffolds new modules following project conventions (config, feature flags, hierarchy-aware menus).
Have fun building! Contributions and issues are welcome. π
