Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package-lock.json
*.tsbuildinfo
app/src-tauri/target
app/src-tauri/gen
app/src-old/

# SVGs have no Prettier parser and are either hand-crafted art or generated
# from upstream icon sets; leave them untouched.
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Port selection depends on mode: Tauri binds **1420** (hard-coded in `tauri.conf.
### Three-workspace monorepo

- `shared/` (`@recrest/shared`) — constants, types, pure utils. Compiled to `dist/` and consumed as a normal npm dep. `postinstall` and `predev` build it automatically; `app/tsconfig.app.json` has it as a TS project reference so composite builds work.
- `app/` (`@recrest/app`) — React 19 + Vite + Tailwind v4 frontend, and the Rust Tauri backend in `app/src-tauri/`.
- `app/` (`@recrest/app`) — React 19 + Vite + MUI v9 + Emotion frontend, and the Rust Tauri backend in `app/src-tauri/`.
- `tests/` (`@recrest/tests`) — Playwright E2E.

Do **not** add path aliases pointing `@recrest/shared` at the source files. `shared/` has `composite: true` and emits to `dist/`; the rest of the repo resolves it via `node_modules` (yarn symlink → shared's `package.json` main/types). Source imports would break `tsc -b`. For Vitest we instead use explicit `resolve.alias` in `app/vitest.config.ts`, because `vite-tsconfig-paths` would pick up the Solution `tsconfig.json` (which holds only references) and miss the app's real paths.
Expand Down Expand Up @@ -70,8 +70,9 @@ Rust commands are registered in `app/src-tauri/src/lib.rs::run()`. DTOs use `#[s
- TypeScript is strict with `noUncheckedIndexedAccess` and `noImplicitOverride`. Array index access returns `T | undefined` — guard or coalesce.
- Imports are sorted by `@trivago/prettier-plugin-sort-imports`; don't reorder manually (prettier will overwrite).
- React components avoid nested interactive elements. Row selectors use `<div role="button" tabIndex={0}>` with keyboard handlers so action buttons inside rows stay legal.
- Do not reintroduce `postcss.config.js` or `autoprefixer` / `postcss` as deps — Tailwind v4 runs through `@tailwindcss/vite` and handles vendor prefixes internally.
- Styling goes through MUI v9 + Emotion `styled()` components only. Never use the `sx` prop — every style collection must live in a `styled()` component (see `feedback_no_sx_always_styled` memory). Tailwind, PostCSS, and Autoprefixer were removed in the Phase 2 migration — do not reintroduce them.
- When adding a Tauri command: declare it in the matching `commands/*.rs`, wire it into `generate_handler![...]` in `lib.rs`, mirror the return type as a TS DTO on the `@recrest/shared` side, and consume it through `invoke<T>` in a thunk (not directly in components).
- **No magic strings.** Every `data-testid`, `recrest:*` storage key, Tauri command name, and IPC event channel must come from a constant in `app/src/lib/constants/` (or `@recrest/shared`). ESLint's `no-restricted-syntax` block enforces this — see `app/src/lib/constants/README.md` for the full layering and how to add a new constant. The only sanctioned inline exception is the anti-flash `<script>` in `app/index.html`, because it runs before any module loads.

## Known scope gaps (not bugs)

Expand Down
4 changes: 2 additions & 2 deletions app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dist-ssr
._*
.Spotlight-V100
.Trashes
.VolumeIcon.icns
.VolumeIcon.icns

# build output
build/
Expand All @@ -36,4 +36,4 @@ build/
storybook-static/

# Superpowers brainstorm mockups
.superpowers/
.superpowers/
16 changes: 12 additions & 4 deletions app/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrderParserPlugins": ["typescript", "jsx"],
"importOrder": [
"^react(-dom)?$",
"^react$",
"^react-dom",
"^react-redux$",
"^react-router",
"^react-",
"^@mui/",
"^@reduxjs/toolkit",
"^redux",
"^@tauri-apps/",
"<THIRD_PARTY_MODULES>",
"^@recrest/(.*)$",
"^@/(.*)$",
"^[./]"
"<THIRD_PARTY_MODULES>",
"^@/",
"^\\.",
"^.+\\.css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
Expand Down
73 changes: 58 additions & 15 deletions app/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
import { Provider as ReduxProvider } from "react-redux";

import { MemoryRouter } from "react-router-dom";

import { I18nextProvider } from "react-i18next";

import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";

import { configureStore } from "@reduxjs/toolkit";
import type { Preview } from "@storybook/react-vite";

import { TooltipProvider } from "@/components/molecules/compounds/Tooltip";
import { THEMES, type ThemeId } from "@/lib/constants/theme.constants";
import i18n from "@/locales";
import { providersReducer } from "@/store/reducers/providersReducer";
import { prsReducer } from "@/store/reducers/prsReducer";
import { remoteImportReducer } from "@/store/reducers/remoteImportReducer";
import { reposReducer } from "@/store/reducers/reposReducer";
import { settingsReducer } from "@/store/reducers/settingsReducer";
import { uiReducer } from "@/store/reducers/uiReducer";
import { getTheme } from "@/theme";

import "@/i18n";
import "@/styles/globals.css";
const store = configureStore({
reducer: {
ui: uiReducer,
settings: settingsReducer,
providers: providersReducer,
repos: reposReducer,
prs: prsReducer,
remoteImport: remoteImportReducer,
},
});

const preview: Preview = {
parameters: {
controls: { expanded: true },
layout: "centered",
backgrounds: {
default: "app",
values: [
{ name: "app", value: "var(--surface)" },
{ name: "dark", value: "#0f1115" },
{ name: "light", value: "#ffffff" },
],
},
globalTypes: {
themeId: {
description: "App theme",
defaultValue: "light",
toolbar: {
title: "Theme",
icon: "paintbrush",
items: THEMES.map((t) => ({ value: t.id, title: t.label })),
dynamicTitle: true,
},
},
},
decorators: [
(Story) => (
<TooltipProvider delayDuration={250}>
<Story />
</TooltipProvider>
),
(Story, ctx) => {
const themeId = (ctx.globals.themeId ?? "light") as ThemeId;
const theme = getTheme(themeId);
return (
<ReduxProvider store={store}>
<I18nextProvider i18n={i18n}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<MemoryRouter
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
>
<Story />
</MemoryRouter>
</ThemeProvider>
</I18nextProvider>
</ReduxProvider>
);
},
],
};

Expand Down
76 changes: 73 additions & 3 deletions app/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Everything the user sees or runs: the React 19 frontend in `src/` and the Rust T

From the repo root via `yarn workspace @recrest/app <script>`:

- `dev` — Vite only (port 1420). Tauri IPC no-ops via `isTauri()`. Use this for pure UI work.
- `tauri:dev` — full desktop shell. Requires Rust toolchain.
- `dev` — Vite only. Tauri IPC no-ops via `isTauri()`. Use this for pure UI work. Port follows `TAURI_ENV_PLATFORM`: **3000** outside Tauri (matches the root `yarn dev:web` flow and Playwright's base URL), **1420** when invoked via `tauri:dev`. `strictPort: true` on both — no silent fallback.
- `tauri:dev` — full desktop shell on port **1420**. Requires Rust toolchain.
- `build` — `tsc -b && vite build` (production bundle).
- `tauri:build` — wraps the desktop installer. **Will fail until `src-tauri/icons/` contains PNGs** — that's a known scope gap.
- `test` — vitest (jsdom).
Expand Down Expand Up @@ -65,9 +65,79 @@ Two icon sets live under `src-tauri/`:
## UI conventions

- No nested interactive elements. Clickable rows use `<div role="button" tabIndex={0}>` with an `onKeyDown` that handles Enter/Space, so hover-revealed action `<button>`s inside them remain legal HTML.
- Tailwind v4 via `@tailwindcss/vite` — **do not reintroduce** `postcss.config.js`, `postcss`, or `autoprefixer`. The plugin handles everything; the original scaffold failed because of a leftover PostCSS config referring to an uninstalled `@tailwindcss/postcss`.
- Styling is MUI v9 + Emotion (`@mui/material` + `@emotion/styled`) only. The phase-two migration removed Tailwind, PostCSS, and SCSS modules — **do not reintroduce** `postcss.config.js`, `postcss`, `autoprefixer`, or `tailwindcss`. If a third-party widget pulls in a competing styling layer, contain it in its own folder and don't let the imports leak into shared atoms/molecules.
- **No `sx` prop.** Every styled collection lives in a `styled()` component — either inline at the top of the file or extracted to `<Name>.styles.tsx`. The `sx={{ ... }}` shorthand is forbidden because it bypasses static extraction, fragments style ownership, and makes the theme contract opaque. Single dynamic offsets that genuinely can't be styled (e.g. an animated `transform` driven by motion state) belong in a `style={{}}` prop, not `sx`.
- `useDevice` (backed by `device-type-detection`) drives `useResponsiveSidebar` in `AppShell`. Auto-collapse preserves the user's manual preference and restores it on wider viewports.

## Component conventions

Full reorganisation plan: `docs/plans/PLAN_COMPONENTS_REFACTOR.md`. The rules below are what new code must follow.

### Folder layout

```
ComponentName/
├── index.tsx ← component, props interface, default export
├── ComponentName.styles.tsx ← when styled-blocks exceed ~200 LOC
└── parts/ ← sub-components only used by this component
└── PartName/index.tsx
```

- Folder name = default export name. No `BrandIcon/index.tsx` exporting `GeneralBrandIcon`.
- One component per file. SVG decoration helpers (e.g. inside `Mascot`) are the only sanctioned exception.
- The `<Foo>/<Foo>.tsx + <Foo>/index.ts` re-export shim is deprecated — use `<Foo>/index.tsx` directly.
- Inline render-helpers belong in `<parent>/parts/<Name>/`. Promote to `components/` only once another route imports them.

### Atom / molecule / organism

- `atoms/` — single-element primitives. No business state.
- `molecules/` — atoms composed with limited internal state. No data fetching.
- `organisms/` — composed UI with state, Redux selectors, listeners.
- `pages/app/` — route-level. Owns thunk dispatch and page-level effects.

Card chrome lives in `molecules/cards/GeneralCard`, never in `organisms/cards/*`.

### General-prefix primitives

`GeneralX` is reserved for cross-cutting primitives every page composes — `GeneralButton`, `GeneralButtonGroup`, `GeneralIconButton`, `GeneralCard`, `GeneralTooltip`, `GeneralSparkline`, `GeneralSwitchInput`, `GeneralSearchInput`, `GeneralAvatar`, `GeneralDrawer`, `GeneralModal`, `GeneralLoader`, `GeneralCircularLoader`, `GeneralLinearLoader`, `GeneralSkeletonLoader`. Domain specialisations of those primitives drop the prefix: `AuthorAvatar` and `RepoAvatar` compose `GeneralAvatar`, `ConfirmationModal` composes `GeneralModal`, `MrDetailDrawer` composes `GeneralDrawer`, etc. Brand atoms (`Logo`, `Mascot`) and tag/icon helpers (`BrandIcon`, `IdeIcon`, `ShellIcon`, `TerminalIcon`) don't carry the prefix because they aren't substitutable primitives.

**Modal vs. drawer vs. dialog naming:** there is no `dialogs/` folder, and modals/drawers each live in one folder regardless of complexity. Every full-screen overlay that asks for input or confirmation is a *modal* — all of them live in `molecules/modals/` (`GeneralModal`, `ConfirmationModal`, `AddRepoModal`, etc.); no separate `organisms/modals/`. Side-pane overlays are *drawers*; specialisations sit under `molecules/drawers/` alongside `GeneralDrawer`.

**Buttons & button groups** live in `atoms/buttons/`. `ScopeButtonGroup` (formerly the `ScopeToggle` "molecule") sits next to `GeneralButton`/`GeneralButtonGroup`/`GeneralIconButton` because it is, structurally, a `GeneralButtonGroup` composition with two scope items — not a separate molecule kind. There is no `molecules/toggles/` folder.

**Icon-only buttons** (clear, close, info-tooltip, row actions, sidebar collapse, etc.) compose `GeneralIconButton` — never an inline `styled("button")`. The primitive owns the 4 standard sizes (`XS`/`SM`/`MD`/`LG`), `variant` (`ghost`/`subtle`/`outline`), `shape` (`circle`/`square`), and `tone` (`neutral`/`primary`/`danger`). Pass the icon via the `icon` prop, not as children, so the surface stays single-responsibility. Need a new size? Extend `IconButtonSize` in `lib/constants/iconButton.constants` — don't add a one-off inline button.

**Asset folders** are split:

- `app/src/assets/logos/` — Recrest brand-mark SVGs (`recrest-icon-*.svg`).
- `app/src/assets/icons/` — every other icon-related asset and the React wrappers that consume them. Domain subfolders hold the raw `*.svg` files (`assets/icons/ides/`, `assets/icons/shells/`, `assets/icons/terminals/`); sibling folders hold the React wrapper components that pick the right SVG by id and add prop-driven theming (`assets/icons/BrandIcon/`, `assets/icons/IdeIcon/`, `assets/icons/ShellIcon/`, `assets/icons/TerminalIcon/`). The wrappers used to live under `atoms/icons/` — that namespace is gone; everything icon-shaped now lives under `assets/icons/`.

When building anything interactive, compose the `GeneralX` primitive first. Adding props to `GeneralButton` is preferred over re-implementing `styled("button")` inline.

### Styling primitives

| Need | Use |
| ------------------------------------- | ------------------------------------------------------------------ |
| Body text, headings, captions | `<Typography variant component>` or `styled(Typography)` |
| Layout wrapper | `<Box sx>` or `styled(Box)` |
| Visual primitive with custom props | `styled(Box, { shouldForwardProp })` + strict prop type |
| Interactive button | `<GeneralButton variant>` |
| Native chrome (titlebar caption etc.) | `styled("button")` **with** `// eslint-disable-next-line` + reason |

`styled("h1..h6"|"p"|"span"|"div"|"button")` for text or layout is forbidden — reach for `Typography` (text) or `Box` (layout). The remaining native-chrome exceptions need an inline `eslint-disable-next-line` with a one-line reason.

### Size ceiling

No `.tsx` over 800 LOC. When approaching it: extract inline sub-components into `parts/<Name>/`, split styled-blocks into `<Name>.styles.tsx` at ~200 LOC, and lift data hooks into a sibling `hooks/` folder.

### Helpers

Generic helpers go to `app/src/lib/utils/<topic>.utils.ts`. Don't inline `timeAgo`-style functions next to a component. Domain logic (activity aggregation, repo enrichment) stays under `app/src/lib/<domain>/`.

### Comments

Default to writing none. Keep only the comments that explain **why** — a constraint, a workaround, or a subtle invariant. Delete visual section markers (`/* ─── Section ─── */`), JSDoc that restates the component name, and "what" comments the JSX already shows.

## Testing

Vitest with `jsdom`. `src/test-setup.ts` mocks `@tauri-apps/api/core` and `@tauri-apps/api/event` globally — without those mocks, importing the store crashes in tests because jsdom has no `__TAURI_INTERNALS__`.
Expand Down
21 changes: 0 additions & 21 deletions app/components.json

This file was deleted.

Loading
Loading