chore(design-sync): sync inputs for claude.ai/design trade-in DS #1984

Merged
lekss361 merged 1 commit from chore/design-sync-tradein into main 2026-06-27 11:42:15 +00:00
46 changed files with 843 additions and 0 deletions

79
.design-sync/AUTHORING.md Normal file
View file

@ -0,0 +1,79 @@
# Preview authoring guide (design-sync, tradein-mvp-frontend)
You author `.design-sync/previews/<Name>.tsx` for assigned components so their preview cards
render real, styled, plausible compositions. The bundle is already built; you only recompile
your own previews.
## Import contract (IMPORTANT)
- Import the component from the package name: `import { OfferCard } from 'tradein-mvp-frontend';`
(the build maps this to the shipped bundle global — do NOT import from a relative src path).
- Import realistic data from the shared fixtures: `import { FIXTURE_ESTIMATE } from './_fixtures';`
- Type-only imports (`import type {...} from '@/types/trade-in'`) are erased — fine to use for casts.
- A provider is ALREADY configured globally (the app's `Providers` = TanStack Query). You do NOT
wrap previews in QueryClientProvider yourself (a second copy breaks context identity).
## Shared fixtures available from `./_fixtures` (realistic ЕКБ 2-к secondary)
- `FIXTURE_ESTIMATE: AggregatedEstimate` — median 9.85M ₽, range 9.110.6M, confidence high,
analogs[3], actual_deals[2], cian_valuation, avito_imv, dkp_corridor, price_trend[6]. Covers any
`estimate: AggregatedEstimate` prop.
- `FIXTURE_INPUT: TradeInEstimateInput` — the quartira params (area 55.3, 2 rooms, floor 5/11, monolith, good).
- `FIXTURE_IMV: IMVBenchmarkResponse` — for IMVBenchmark.
- `FIXTURE_HOUSES: HouseInfoForEstimate[]` — for HouseInfoCard.
- `FIXTURE_PLACEMENT: PlacementHistoryItem[]` — placement history rows.
- `FIXTURE_ANALYTICS: HouseAnalyticsResponse` — kpi + price_history[4] + recent_sold[2].
- `FIXTURE_SELLTIME: SellTimeSensitivityResponse` — exposure-vs-premium buckets.
- `FIXTURE_SALES: SalesVsListingsResponse` — street deals/listings pairs.
Read `tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx` — it shows EXACT prop usage for
many components (the canonical "hero" composition). Port it.
## How to find props
Read each component's source `tradein-mvp/frontend/src/components/<group>/<Name>.tsx`. The `interface Props`
(or inline destructure) is the contract. Many take `estimate`; some take other fixtures; some take
callbacks (pass `() => {}`); some fetch via hooks by id (see "fetch-coupled" below).
## Recipe
- 1 canonical story (the docs/page.tsx usage) named `Default`. Add 13 more ONLY if they visibly
differ (sweep a real variant axis: a state, an enum, empty-vs-full). Budget 14 cells.
- Realistic content only (the ЕКБ fixtures) — never foo/test.
- Callbacks → `() => {}`. Booleans like `isLoading`/`isPending` → usually `false` for the full state.
- Keep visually-identical variants OUT (e.g. a flag that only changes a link, not the view) — one cell.
## Fetch-coupled components (PlacementHistoryCard, HouseAnalyticsSection, PhotoUpload, ListingsCard
maybe) take an `estimateId` and fetch internally. The global Provider's query cache is EMPTY, so they
render their loading/empty state. If the component accepts data via props too, prefer that. If it can
ONLY fetch and renders blank/loading, author the best you can and RECORD it in your learnings file as
"renders loading/empty — fetch-coupled" — do NOT fake data you can't pass.
## Your loop (per component)
1. Read source → write `.design-sync/previews/<Name>.tsx` (named exports, no marker line).
2. Rebuild ONLY your components:
`node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules tradein-mvp/frontend/node_modules --out ./ds-bundle --components <YOUR_COMMA_LIST>`
3. Capture ONLY your components:
`node .ds-sync/package-capture.mjs --out ./ds-bundle --components <YOUR_COMMA_LIST>`
4. READ each `ds-bundle/_screenshots/review/<group>__<Name>.png`. Grade each cell on the absolute
rubric: Styled (DS tokens/font visible) · Complete (renders whole, no missing children) · Plausible.
5. Write `.design-sync/.cache/review/<Name>.grade.json`:
`{"cells":{"<CellLabel>":{"verdict":"good"|"needs-work","note":"..."}}}` (keys = exact cell labels
from the capture log). `needs-work` → fix the .tsx, rebuild, recapture, regrade until `good`.
## HARD RULES (violating corrupts other agents' work)
- Edit ONLY your assigned `previews/<Name>.tsx`, your `.cache/review/<Name>.grade.json`, and your
`.design-sync/learnings/<BATCH>.md`. NOTHING else. Never touch config.json / NOTES.md / other previews.
- NEVER run `package-build.mjs` or `package-validate.mjs`. NEVER run `package-capture.mjs` without
`--components` (a full run prunes other agents' state). Only the two scoped commands above.
- If the SAME root cause hits 2+ of your components, or ANY config-level issue (a needed provider chain,
a missing token/css, a needed `cardMode`/`viewport` override, an import that won't resolve) → STOP on
those, record in your learnings file `.design-sync/learnings/<BATCH>.md`, and report to the orchestrator.
Config/NOTES changes are orchestrator-only.
## Wide/overlay components
Cards with `className="card"` are full-width and fine. If a card visibly overflows its grid cell or an
overlay (dialog/map modal) collapses, that needs a `cfg.overrides.<Name>.cardMode` change — you CANNOT
do that (config is orchestrator-only). Record it in learnings and move on.
## Calibration learnings from the solo set (HeroSummary, OfferCard, PriceRangeBar)
- The contract works: `import { X } from 'tradein-mvp-frontend'` + `./_fixtures` + global Provider.
- Cards render fully styled with the trade-in tokens/Manrope font.
- Components with analog photos show a grey placeholder (fixture photo_url=null, offline) — that is the
component's REAL no-photo state, grade it good (do not try to add external image URLs).
- Drop variants that don't change the view (brandSlug, isResubmitting rendered identical → single cell).

34
.design-sync/NOTES.md Normal file
View file

@ -0,0 +1,34 @@
# design-sync NOTES — tradein-mvp-frontend
Target: `tradein-mvp/frontend` (NOT the repo-root `frontend/`, which is the Site-Finder app). Project: gendesign-tradein.
## Build gotchas
- **No dist / no Storybook → synth-entry mode.** The package is a Next.js app, not a lib; `package.json` has no `main`/`module`/`exports`.
- **`--entry` must point at a NON-existent dist path** (`tradein-mvp/frontend/dist/index.js`). Two jobs:
1. PKG_DIR resolution walks up `dirname(--entry)` to the real named `package.json` → PKG_DIR=`tradein-mvp/frontend` (without `--entry`, PKG_DIR defaults to `node_modules/<pkg>` which doesn't self-install → ENOENT crash).
2. The path being absent makes `resolveDistEntry(soft)` return null → `synthEntry=true``deriveComponentsFromSrc` discovers components from `src/`. A path that EXISTS (e.g. package.json) suppresses synth mode → `[ZERO_MATCH]` tokens-only.
- **`cfg.*` path fields are PACKAGE-relative** (relative to PKG_DIR), not repo-relative. srcDir=`src`, cssEntry=`src/components/trade-in/trade-in.css`, tokensGlob=`src/app/globals.css`.
- `--node-modules tradein-mvp/frontend/node_modules` (react + @types/react live there).
## Styling
- Tokens in `src/app/globals.css` (`:root{--bg-app,--accent,…}`); component CSS in `src/components/trade-in/trade-in.css` (2277 lines). Manrope via remote Google Fonts `@import` (→ `[FONT_REMOTE]`, no action).
## Re-sync risks
- Synth scan over-includes non-component PascalCase exports (51 found vs ~37 files) — prune with `componentSrcMap: {Name: null}` as identified.
- Components are app-coupled (next/*, TanStack Query, data hooks); many need providers/mock props to render → expect floor cards / authored previews with composed props.
## Authoring (38 components, all authored-good)
- **Import contract:** preview imports `{X} from 'tradein-mvp-frontend'` (mapped to window.TradeInUI bundle) + data from `./_fixtures` (re-exports the repo's offline `app/ui-preview/estimate/fixture.ts`). Type imports erased.
- **Provider = seeded `PreviewProvider`** (`.design-sync/preview-provider.tsx`, wired via `cfg.extraEntries` + `cfg.provider`). Mirrors `app/ui-preview/estimate/page.tsx`'s seeded QueryClient: pre-fills `["auth","me"]` (fake admin user) + the estimate sub-queries (placement-history, house-analytics, sell-time-sensitivity, cian-price-changes=[], sales-vs-listings). This is what makes fetch-coupled cards (StreetDealsCard, HouseAnalyticsSection, PlacementHistoryCard) and auth-gated ones (RouteGuard, UserMenu) render offline.
- **CRITICAL:** an extraEntries module must NOT import anything that reads `process.env` at module top-level (e.g. `@/lib/useMe``@/lib/api`) — extraEntries evaluate BEFORE the synth-entry `.pkg-shim.mjs`, so `process` is undefined and the whole IIFE aborts (38/38 vanish from the global). The provider hardcodes `ME_QUERY_KEY = ["auth","me"]` instead of importing useMe.
- extraEntries path is PACKAGE-relative: `../../.design-sync/preview-provider.tsx`. Its `@/` aliases don't resolve from outside the tsconfig root → use relative `../tradein-mvp/frontend/src/...` for repo imports inside it.
- **Chart cards gate on analog count ≥8** (DistributionCard, ExposureCard): the shared fixture has 3 analogs → "not enough data" fallback. Those two previews extend `analogs` to 10 realistic ЕКБ lots INLINE in their own preview file (spread FIXTURE_ESTIMATE, override analogs+n_analogs) — never edit `_fixtures.ts`.
- **Admin/scraper panels** (DataQualitySection, ProviderProxySection, RunsTable, PacingSection, SystemHealthSection) fetch their own data with no repo fixture → they render their real styled LOADING/header state. Graded good as honest states; richer data would need admin fixtures that don't exist in the repo.
- **MapCard / MapPicker**: Leaflet from unpkg loads in the capture env; OSM raster tiles sometimes don't paint within the screenshot window (external tile fetch) — markers/popup/controls still render. Both graded good. MapPicker is a `createPortal` full-viewport overlay — rendered contained in capture; if a future capture viewport change makes it escape, add `cfg.overrides.MapPicker.cardMode`/`viewport`.
## Re-sync risks
- The `process` shim env defaults live in `.design-sync/overrides/source-kit.mjs` (synth entry). If the app reads NEW `process.env.NEXT_PUBLIC_*` vars, add them there or components throw.
- `PreviewProvider` seed keys are pinned to the fixture's `estimate_id` and address/area/rooms — if `fixture.ts` changes those, update `preview-provider.tsx` to match (else fetch-coupled cards re-floor).
- Synth scan re-includes Next route/layout/error files on every build → `componentSrcMap` nulls (13 entries) must persist. New app-router pages would need adding.
- `.d.ts` props are weak (`[key:string]:unknown`) — synth mode has no built types. Real prop contracts live in each component's source `interface Props`. A real `tsup`/`tsc` lib build would fix this (recommend if the agent needs strong API contracts).
- Grades clear on any `cfg.provider`/preview-affecting config change (expected) — re-grade from fresh sheets.

36
.design-sync/config.json Normal file
View file

@ -0,0 +1,36 @@
{
"projectId": "e4b235af-089b-4532-8025-5199a2695a12",
"shape": "package",
"pkg": "tradein-mvp-frontend",
"globalName": "TradeInUI",
"srcDir": "src",
"tsconfig": "tsconfig.json",
"cssEntry": "src/components/trade-in/trade-in.css",
"tokensGlob": "src/app/globals.css",
"buildCmd": "node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules tradein-mvp/frontend/node_modules --entry tradein-mvp/frontend/dist/index.js --out ./ds-bundle",
"libOverrides": {
"source-kit.mjs": "synth-entry process shim for Next.js app (process.env.NEXT_PUBLIC_* reads)"
},
"componentSrcMap": {
"Error": null,
"GlobalError": null,
"Providers": null,
"RootLayout": null,
"AvitoScraperPage": null,
"CachePage": null,
"CianScraperPage": null,
"HistoryPage": null,
"PreviewEstimatePage": null,
"ScraperPage": null,
"ScrapersUnifiedPage": null,
"TradeInPage": null,
"YandexScraperPage": null
},
"provider": {
"component": "PreviewProvider"
},
"extraEntries": [
"../../.design-sync/preview-provider.tsx"
],
"readmeHeader": ".design-sync/conventions.md"
}

View file

@ -0,0 +1,50 @@
# Trade-In UI — how to build with this design system
React components from the GenDesign **trade-in** product (real-estate trade-in valuation, RU/ЕКБ). Import everything from `tradein-mvp-frontend` (bound at `window.TradeInUI`). The cards are domain-specific and **prop-driven** — you compose them with data, you don't restyle their internals.
## Setup & wrapping (required for data components)
Most cards take their data as **props** and render standalone. But several read app state through **TanStack Query** hooks (`UserMenu`, `Topbar`, `RouteGuard``useMe`; `HouseAnalyticsSection`, `PlacementHistoryCard`, `StreetDealsCard` → estimate sub-queries). Those MUST be rendered inside a `QueryClientProvider` (the app ships one as `Providers`). Without it they throw "No QueryClient set"; with an empty client they render their loading/null state. Prop-only cards (`HeroSummary`, `OfferCard`, `PriceRangeBar`, `DealsCard`, `ListingsCard`, charts…) need no provider.
```tsx
import { Providers, HeroSummary, OfferCard } from "tradein-mvp-frontend";
<Providers>
<main className="page" style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<HeroSummary estimate={estimate} input={input} onResubmit={fn} />
<OfferCard estimate={estimate} brandSlug={null} />
</main>
</Providers>
```
Load `styles.css` once at the root — it pulls the component CSS + design tokens.
## Styling idiom — global stylesheet + CSS custom properties
This is **not Tailwind and not CSS-in-JS**. Styling is a **global stylesheet** of semantic class names plus **CSS custom-property design tokens**. Two rules:
1. **The components carry their own classes** (`.card`, `.card-head`, `.section-kicker`, `.count-strip`, `.pricebar`, `.source-chip`, `.control`, `.pill`, `.mono`, `.top-nav`, `.meta-grid`…). Don't reach inside them; compose via props. New class names you invent will not exist in the stylesheet.
2. **For your own layout/wrapper glue, use the token vars + inline styles**, on the 4/8/12/16/24/32 spacing scale (tabular-nums for numbers). Color/shape tokens, all defined in the shipped CSS:
| Group | Tokens |
|---|---|
| Surface | `--bg-app` `--bg-card` `--bg-card-alt` `--bg-headline` |
| Text | `--fg-primary` `--fg-secondary` `--fg-tertiary` `--fg-on-dark` |
| Brand/CTA | `--accent` `--accent-hover` `--accent-soft` `--accent-2` |
| Semantic | `--success` `--warn` `--danger` (`*-soft` variants) |
| Border | `--border-soft` `--border-card` `--border-strong` |
| Shape/type | `--radius` `--radius-sm` `--radius-lg` `--shadow-md` `--font-sans` (Manrope) `--font-mono` `--container` |
```tsx
<div style={{ background: "var(--bg-card)", border: "1px solid var(--border-card)",
borderRadius: "var(--radius)", padding: 16, color: "var(--fg-primary)",
fontFamily: "var(--font-sans)" }}>
<b style={{ color: "var(--accent)" }}>9 850 000 ₽</b>
</div>
```
## Where the truth is
- **Styles/tokens:** read the bound `styles.css` and its `@import` `_ds_bundle.css` — the authoritative class + token list.
- **Per component:** `<Name>.d.ts` (props) and `<Name>.prompt.md` (usage). NB: synth-built `.d.ts` props are loose (`[key: string]: unknown`); the `.prompt.md` + preview card show real prop shapes and realistic data.
- Money/area/dates are RU-formatted (`toLocaleString("ru-RU")`, `₽`, `м²`). Keep that idiom.

View file

@ -0,0 +1,178 @@
// Non-storybook `package` adapter. Bundles dist/ when present (the authoritative
// component list comes from shipped .d.ts; with no dist it synthesizes an
// entry from src/ as a last resort) and opportunistically enriches each
// component from src/ — JSDoc and dir-derived group. Every enrichment miss
// degrades to the plain-dist behaviour.
//
// Discovery is heuristic-based; each heuristic has a `.design-sync/config.json`
// override (ASSUMPTION comments below name them) so repos that don't match the
// defaults write config, not code. `componentSrcMap` is the single override
// knob for component inclusion: non-null value = add/pin src path, null =
// exclude a .d.ts-exported internal.
import { existsSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { Project, Node, ts } from 'ts-morph';
// forked from design-sync lib/source-kit.mjs — synth-entry process shim (Next.js app reads process.env.NEXT_PUBLIC_*)
import { leadingJsdoc, readText, slash, walk } from '../../.ds-sync/lib/common.mjs';
import { resolveDistEntry } from '../../.ds-sync/lib/bundle.mjs';
import { exportedNames, isComponentName } from '../../.ds-sync/lib/dts.mjs';
const NON_IMPL_RX = /\.(stories|test|spec)\./;
const SRC_IMPL_RX = /\.(tsx|jsx)$/;
// Dir names that don't usefully group components — skip so the emitted path
// is `components/<group>/<Name>` not `components/components/<Name>`.
const GENERIC_DIR = new Set(['components', 'component', 'src', 'lib', 'ui', 'packages', 'react']);
const slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'general';
// No .d.ts → scan src files for PascalCase value exports via ts-morph.
function deriveComponentsFromSrc(srcFiles) {
const project = new Project({
skipAddingFilesFromTsConfig: true,
compilerOptions: { jsx: ts.JsxEmit.Preserve, allowJs: true, skipLibCheck: true },
});
const seen = new Set();
for (const p of srcFiles) {
if (NON_IMPL_RX.test(p) || !SRC_IMPL_RX.test(p)) continue;
const sf = project.addSourceFileAtPathIfExists(p);
if (!sf) continue;
for (const [name, decls] of sf.getExportedDeclarations()) {
// `export default function Button()` is keyed as 'default' — recover
// the declared name from the function/class node.
const real = name === 'default'
? decls.map((d) => d.getName?.()).find((n) => n && n !== 'default')
: name;
if (!real || !/^[A-Z][A-Za-z0-9]*$/.test(real)) continue;
if (decls.some((d) => Node.isVariableDeclaration(d) || Node.isFunctionDeclaration(d) || Node.isClassDeclaration(d))) {
seen.add(real);
}
}
}
return [...seen].sort().map((name) => ({ name, group: 'general' }));
}
export async function resolvePackage(ctx) {
const { PKG_DIR, pkgJson, ENTRY_OVERRIDE, PKG, OUT, cfg } = ctx;
const srcMap = cfg.componentSrcMap ?? {};
// ── 1. src/ discovery (best-effort; feeds enrichment + synth-entry fallback).
// ASSUMPTION: source root is first of src/ | lib/ | components/. Override: cfg.srcDir.
const srcRoot = [cfg.srcDir, 'src', 'lib', 'components']
.map((d) => d && resolve(PKG_DIR, d))
.find((d) => d && existsSync(d));
const srcFiles = srcRoot ? walk(srcRoot, (n) => /\.(tsx|jsx|mdx?)$/.test(n)) : [];
// ── 2. entry: dist if it exists, else synthesize from src/ (last resort).
let entry = resolveDistEntry({ pkgDir: PKG_DIR, pkgJson, override: ENTRY_OVERRIDE, pkgName: PKG, soft: true });
let synthEntry = false;
if (!entry) {
if (!srcRoot) {
console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`);
process.exit(1);
}
const comps = srcFiles.filter((p) => SRC_IMPL_RX.test(p) && !NON_IMPL_RX.test(p));
// Next.js app code reads process.env.NEXT_PUBLIC_* at module top-level; in
// the browser IIFE `process` is undefined → every component throws. Emit a
// shim module and import it FIRST (ESM evaluates the first import's body
// before the re-exported component modules), so process exists before any
// component body runs.
const shim = resolve(OUT, '.pkg-shim.mjs');
writeFileSync(
shim,
'globalThis.process ??= { env: {} };\n' +
'globalThis.process.env ??= {};\n' +
'const __e = globalThis.process.env;\n' +
"__e.NODE_ENV ??= 'development';\n" +
"__e.NEXT_PUBLIC_ENABLE_PREVIEW ??= '1';\n" +
"__e.NEXT_PUBLIC_BASE_PATH ??= '';\n" +
"__e.NEXT_PUBLIC_API_BASE_URL ??= '';\n" +
"__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n",
);
entry = join(OUT, '.pkg-entry.mjs');
writeFileSync(
entry,
`import ${JSON.stringify(slash(shim))};\n` +
comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') +
'\n',
);
synthEntry = true;
console.error(
`[NO_DIST] no built entry — synthesizing from ${comps.length} src files (run the package's build for best results)`,
);
}
// ── 3. component list: from shipped .d.ts (authoritative when dist exists).
// ASSUMPTION: components = PascalCase value exports in the .d.ts tree.
// Override: cfg.componentSrcMap (non-null adds/pins, null excludes).
const exported = exportedNames(PKG_DIR, pkgJson);
const names = new Set([...exported].filter(isComponentName));
for (const [k, v] of Object.entries(srcMap)) {
if (v === null) { names.delete(k); continue; }
// Names reach `<script>` blocks in the emitted HTML — reject anything
// that isn't a plain PascalCase identifier.
if (!/^[A-Z][A-Za-z0-9]*$/.test(k)) {
console.error(`[CONFIG] componentSrcMap: "${k}" is not a valid component name (PascalCase identifiers only)`);
continue;
}
names.add(k);
}
let components = [...names].sort().map((name) => ({ name, group: 'general' }));
if (!components.length && synthEntry) {
components = deriveComponentsFromSrc(srcFiles).filter((c) => srcMap[c.name] !== null);
}
if (!components.length) {
if (cfg.cssEntry || existsSync(join(PKG_DIR, 'styles.css'))) {
console.error('[ZERO_MATCH] no component exports — treating as tokens-only DS');
return { shape: 'package', entry, components: [], tokensOnly: true };
}
console.error(`[ZERO_MATCH] no PascalCase exports in ${PKG} and no styles — nothing to sync`);
process.exit(1);
}
// ── 4. src/ enrichment per component. Every miss degrades to plain-dist.
if (srcRoot) {
for (const c of components) {
// Pinned via config → skip fuzzy-find entirely.
let hit = typeof srcMap[c.name] === 'string' ? slash(resolve(PKG_DIR, srcMap[c.name])) : null;
if (!hit) {
// ASSUMPTION: <Name>.tsx | <name>/<name>.tsx | <Name>/index.tsx |
// <kebab-name>.tsx, case-insensitive; dir-match ranks above
// bare-file match, then prefer one that actually exports `c.name`.
// Override: cfg.componentSrcMap.
const kebab = c.name.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
const nameRx = new RegExp(
`(?:^|/)(?:${c.name}/(?:index|${c.name})\\.(tsx|jsx)|(?:${c.name}|${kebab})\\.(tsx|jsx))$`,
'i',
);
const hits = srcFiles
.filter((p) => nameRx.test(p) && !NON_IMPL_RX.test(p))
.sort(
(a, b) =>
(b.toLowerCase().includes(`/${c.name.toLowerCase()}/`) ? 1 : 0) -
(a.toLowerCase().includes(`/${c.name.toLowerCase()}/`) ? 1 : 0),
);
const exportRx = new RegExp(`export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+${c.name}\\b`);
hit = hits.find((p) => exportRx.test(readText(p))) ?? hits[0];
}
if (!hit || !existsSync(hit)) continue;
c.srcPath = hit;
c.doc = leadingJsdoc(readText(hit), c.name) || undefined;
// group = last src/ path segment that isn't the component's own dir or
// a generic container name — else JSDoc @category — else 'general'.
c.group = slug(
slash(relative(srcRoot, dirname(hit)))
.split('/')
.filter((s) => s && s.toLowerCase() !== c.name.toLowerCase() && !GENERIC_DIR.has(s.toLowerCase()))
.at(-1)
|| (c.doc && /@category\s+(\S+)/.exec(c.doc)?.[1])
|| 'general',
);
}
}
console.error(
` package: ${components.length} components` +
(srcRoot ? ` (${components.filter((c) => c.srcPath).length} src-matched)` : ' (no src/ — dist-only)'),
);
return { shape: 'package', entry, components, synthEntry, exported };
}

View file

@ -0,0 +1,61 @@
"use client";
// Seeded provider for design-sync preview cards. Mirrors the repo's own offline
// preview client (tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx):
// a QueryClient pre-filled with the FIXTURE_* data + a fake authorized user, so
// fetch-coupled cards (HouseAnalyticsSection, PlacementHistoryCard, StreetDealsCard)
// and auth-gated ones (RouteGuard, UserMenu) render offline. Bundled into
// window.<GLOBAL> via cfg.extraEntries → shares the SAME react-query instance as
// the components (context identity), which a second copy in a preview would break.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import {
FIXTURE_ANALYTICS,
FIXTURE_ESTIMATE,
FIXTURE_PLACEMENT,
FIXTURE_SALES,
FIXTURE_SELLTIME,
} from "../tradein-mvp/frontend/src/app/ui-preview/estimate/fixture";
// Inlined to avoid importing @/lib/useMe → @/lib/api, which reads process.env
// at module top-level; in an extraEntries module that evaluates before the
// synth-entry process shim and aborts the whole bundle. Key must match
// useMe.ts's ME_QUERY_KEY exactly.
const ME_QUERY_KEY = ["auth", "me"] as const;
const PREVIEW_ID = FIXTURE_ESTIMATE.estimate_id;
const PREVIEW_USER = {
username: "preview",
role: "admin",
allowed_paths: ["/**"],
deny_paths: [],
brand: null,
};
export function PreviewProvider({ children }: { children: React.ReactNode }) {
const [client] = useState(() => {
const qc = new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity, refetchOnWindowFocus: false },
},
});
qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER);
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "placement-history"], FIXTURE_PLACEMENT);
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "house-analytics"], FIXTURE_ANALYTICS);
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "sell-time-sensitivity"], FIXTURE_SELLTIME);
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "cian-price-changes"], []);
qc.setQueryData(
[
"trade-in",
"sales-vs-listings",
FIXTURE_ESTIMATE.target_address,
FIXTURE_ESTIMATE.area_m2,
FIXTURE_ESTIMATE.rooms,
],
FIXTURE_SALES,
);
return qc;
});
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}

View file

@ -0,0 +1,12 @@
import { AddressInput } from 'tradein-mvp-frontend';
/** Combobox адреса ЕКБ с автокомплитом. Dropdown открывается по focus/набору
* (фетч suggest офлайн недоступен) карточка показывает заполненный control. */
export const Default = () => (
<AddressInput
value="Екатеринбург, ул. Репина, 75/2"
onChange={() => {}}
onPickCoords={() => {}}
placeholder="ул. Малышева, 30 · Куйбышева, 48…"
/>
);

View file

@ -0,0 +1,6 @@
import { CianValuationCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Компактный блок «Оценка Cian» продажа 9.70 млн + аренда 42 000 /мес,
* спарклайн за 6 мес и бейдж 3.2%. Узкий компонент (section, не full-width card). */
export const Default = () => <CianValuationCard data={FIXTURE_ESTIMATE.cian_valuation} />;

View file

@ -0,0 +1,6 @@
import { DataQualitySection } from 'tradein-mvp-frontend';
/** Coverage-секция: таблица заполнения полей по источникам + обогащение домов.
* Фетчит /admin/scraper/data-quality через useQuery (без data-prop). В превью
* без сети рендерит заголовок + hint + graceful-degradation. Fetch-coupled. */
export const Default = () => <DataQualitySection />;

View file

@ -0,0 +1,6 @@
import { DealsCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Секция 3 «Сделки» фактические ДКП-сделки Росреестра по аналогам (2 строки):
* count-strip (кол-во / медиана / диапазон) + чипы источников + таблица. */
export const Default = () => <DealsCard estimate={FIXTURE_ESTIMATE} />;

View file

@ -0,0 +1,25 @@
import { DistributionCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
// Канонический FIXTURE_ESTIMATE содержит лишь 3 аналога — DistributionCard рисует
// гистограмму только при ≥8 (иначе fallback-текст «мало аналогов»). Расширяем до 10
// правдоподобных ЕКБ-аналогов (₽/м² вокруг median 178 000) — чтобы показать сам график.
// Реалистичные ЕКБ-улицы/цены, не foo/test; правим только этот preview-файл.
const ekbAnalogs = [
{ address: 'ул. Репина, 73', area_m2: 54, rooms: 2, floor: 7, total_floors: 16, price_rub: 9_700_000, price_per_m2: 179_629, listing_date: '2026-05-12', days_on_market: 18, photo_url: null, source: 'avito', source_url: null, distance_m: 120, tier: null, lat: 56.8401, lon: 60.5702 },
{ address: 'ул. Викулова, 33', area_m2: 57, rooms: 2, floor: 4, total_floors: 10, price_rub: 9_950_000, price_per_m2: 174_561, listing_date: '2026-05-20', days_on_market: 10, photo_url: null, source: 'cian', source_url: null, distance_m: 340, tier: null, lat: 56.8389, lon: 60.5681 },
{ address: 'ул. Кирова, 28', area_m2: 53, rooms: 2, floor: 9, total_floors: 12, price_rub: 9_400_000, price_per_m2: 177_358, listing_date: '2026-05-04', days_on_market: 31, photo_url: null, source: 'avito', source_url: null, distance_m: 510, tier: null, lat: 56.8372, lon: 60.5749 },
{ address: 'ул. Серафимы Дерябиной, 24', area_m2: 56, rooms: 2, floor: 6, total_floors: 14, price_rub: 9_600_000, price_per_m2: 171_429, listing_date: '2026-05-08', days_on_market: 42, photo_url: null, source: 'cian', source_url: null, distance_m: 680, tier: null, lat: 56.8318, lon: 60.5666 },
{ address: 'ул. Ясная, 6', area_m2: 52, rooms: 2, floor: 11, total_floors: 16, price_rub: 9_600_000, price_per_m2: 184_615, listing_date: '2026-05-18', days_on_market: 14, photo_url: null, source: 'avito', source_url: null, distance_m: 430, tier: null, lat: 56.8295, lon: 60.5803 },
{ address: 'ул. Белореченская, 17', area_m2: 58, rooms: 2, floor: 3, total_floors: 9, price_rub: 9_800_000, price_per_m2: 168_966, listing_date: '2026-04-28', days_on_market: 55, photo_url: null, source: 'yandex', source_url: null, distance_m: 900, tier: null, lat: 56.8267, lon: 60.5611 },
{ address: 'ул. Гурзуфская, 16', area_m2: 55, rooms: 2, floor: 8, total_floors: 12, price_rub: 10_000_000, price_per_m2: 181_818, listing_date: '2026-05-22', days_on_market: 9, photo_url: null, source: 'cian', source_url: null, distance_m: 260, tier: null, lat: 56.8344, lon: 60.5727 },
{ address: 'ул. Посадская, 40', area_m2: 51, rooms: 2, floor: 5, total_floors: 10, price_rub: 9_600_000, price_per_m2: 188_235, listing_date: '2026-05-15', days_on_market: 22, photo_url: null, source: 'avito', source_url: null, distance_m: 770, tier: null, lat: 56.8231, lon: 60.5832 },
{ address: 'ул. Шаумяна, 86', area_m2: 60, rooms: 2, floor: 2, total_floors: 18, price_rub: 9_900_000, price_per_m2: 165_000, listing_date: '2026-04-25', days_on_market: 60, photo_url: null, source: 'yandex', source_url: null, distance_m: 1100, tier: null, lat: 56.8189, lon: 60.5598 },
{ address: 'ул. Чкалова, 124', area_m2: 54, rooms: 2, floor: 12, total_floors: 19, price_rub: 10_400_000, price_per_m2: 192_593, listing_date: '2026-05-24', days_on_market: 7, photo_url: null, source: 'cian', source_url: null, distance_m: 350, tier: null, lat: 56.8276, lon: 60.5689 },
];
const estimate = { ...FIXTURE_ESTIMATE, analogs: ekbAnalogs, n_analogs: ekbAnalogs.length };
/** Гистограмма распределения /м² по 10 аналогам с маркером «ваша квартира»
* на median 178 000 /м² (оранжевый столбец/референс-линия). */
export const Default = () => <DistributionCard estimate={estimate} />;

View file

@ -0,0 +1,13 @@
import { EstimateForm } from 'tradein-mvp-frontend';
/** Шаг 1/1 параметры квартиры (адрес + площадь/комнаты/этаж/тип/ремонт +
* свёрнутый CRM-блок). Полное «спокойное» состояние: не считает, без ошибок. */
export const Default = () => (
<EstimateForm
onSubmit={() => {}}
isPending={false}
error={null}
remaining={12}
limit={15}
/>
);

View file

@ -0,0 +1,25 @@
import { ExposureCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
// Канонический FIXTURE_ESTIMATE содержит лишь 3 аналога — ExposureCard рисует
// scatter (цена × срок экспозиции) только при ≥8 точках с days_on_market (иначе
// fallback-текст «недостаточно данных»). Расширяем до 10 правдоподобных ЕКБ-аналогов
// (у всех непустой days_on_market) — чтобы показать сам график. Правим только этот файл.
const ekbAnalogs = [
{ address: 'ул. Репина, 73', area_m2: 54, rooms: 2, floor: 7, total_floors: 16, price_rub: 9_700_000, price_per_m2: 179_629, listing_date: '2026-05-12', days_on_market: 18, photo_url: null, source: 'avito', source_url: null, distance_m: 120, tier: null, lat: 56.8401, lon: 60.5702 },
{ address: 'ул. Викулова, 33', area_m2: 57, rooms: 2, floor: 4, total_floors: 10, price_rub: 9_950_000, price_per_m2: 174_561, listing_date: '2026-05-20', days_on_market: 10, photo_url: null, source: 'cian', source_url: null, distance_m: 340, tier: null, lat: 56.8389, lon: 60.5681 },
{ address: 'ул. Кирова, 28', area_m2: 53, rooms: 2, floor: 9, total_floors: 12, price_rub: 9_400_000, price_per_m2: 177_358, listing_date: '2026-05-04', days_on_market: 31, photo_url: null, source: 'avito', source_url: null, distance_m: 510, tier: null, lat: 56.8372, lon: 60.5749 },
{ address: 'ул. Серафимы Дерябиной, 24', area_m2: 56, rooms: 2, floor: 6, total_floors: 14, price_rub: 9_600_000, price_per_m2: 171_429, listing_date: '2026-05-08', days_on_market: 42, photo_url: null, source: 'cian', source_url: null, distance_m: 680, tier: null, lat: 56.8318, lon: 60.5666 },
{ address: 'ул. Ясная, 6', area_m2: 52, rooms: 2, floor: 11, total_floors: 16, price_rub: 9_600_000, price_per_m2: 184_615, listing_date: '2026-05-18', days_on_market: 14, photo_url: null, source: 'avito', source_url: null, distance_m: 430, tier: null, lat: 56.8295, lon: 60.5803 },
{ address: 'ул. Белореченская, 17', area_m2: 58, rooms: 2, floor: 3, total_floors: 9, price_rub: 9_800_000, price_per_m2: 168_966, listing_date: '2026-04-28', days_on_market: 55, photo_url: null, source: 'yandex', source_url: null, distance_m: 900, tier: null, lat: 56.8267, lon: 60.5611 },
{ address: 'ул. Гурзуфская, 16', area_m2: 55, rooms: 2, floor: 8, total_floors: 12, price_rub: 10_000_000, price_per_m2: 181_818, listing_date: '2026-05-22', days_on_market: 9, photo_url: null, source: 'cian', source_url: null, distance_m: 260, tier: null, lat: 56.8344, lon: 60.5727 },
{ address: 'ул. Посадская, 40', area_m2: 51, rooms: 2, floor: 5, total_floors: 10, price_rub: 9_600_000, price_per_m2: 188_235, listing_date: '2026-05-15', days_on_market: 22, photo_url: null, source: 'avito', source_url: null, distance_m: 770, tier: null, lat: 56.8231, lon: 60.5832 },
{ address: 'ул. Шаумяна, 86', area_m2: 60, rooms: 2, floor: 2, total_floors: 18, price_rub: 9_900_000, price_per_m2: 165_000, listing_date: '2026-04-25', days_on_market: 60, photo_url: null, source: 'yandex', source_url: null, distance_m: 1100, tier: null, lat: 56.8189, lon: 60.5598 },
{ address: 'ул. Чкалова, 124', area_m2: 54, rooms: 2, floor: 12, total_floors: 19, price_rub: 10_400_000, price_per_m2: 192_593, listing_date: '2026-05-24', days_on_market: 7, photo_url: null, source: 'cian', source_url: null, distance_m: 350, tier: null, lat: 56.8276, lon: 60.5689 },
];
const estimate = { ...FIXTURE_ESTIMATE, analogs: ekbAnalogs, n_analogs: ekbAnalogs.length };
/** Scatter «цена × срок экспозиции» по 10 аналогам (дешевле быстрее),
* вертикальная референс-линия на median 9.85 млн . */
export const Default = () => <ExposureCard estimate={estimate} />;

View file

@ -0,0 +1,13 @@
import { HeroSummary } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from './_fixtures';
/** Секция 1 «Сводка» медиана + достоверность CV + параметры объекта.
* Фото-аналог = плейсхолдер (фикстура офлайн, photo_url: null). */
export const Default = () => (
<HeroSummary
estimate={FIXTURE_ESTIMATE}
input={FIXTURE_INPUT}
onResubmit={() => {}}
isResubmitting={false}
/>
);

View file

@ -0,0 +1,9 @@
import { HeroTransparency } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Блок «доверие + действия» в шапке результата CTA «заявка» (лид-инбокс задан
* в shim env), «Скачать PDF», свежесть данных и collapsible «Как рассчитано»
* (band достоверности, аналоги, точность адреса). Generic-бренд (brandSlug=null). */
export const Default = () => (
<HeroTransparency estimate={FIXTURE_ESTIMATE} brandSlug={null} brandName={null} />
);

View file

@ -0,0 +1,6 @@
import { HouseAnalyticsKpiRow } from 'tradein-mvp-frontend';
import { FIXTURE_ANALYTICS } from './_fixtures';
/** KPI-строка аналитики дома 3 карточки: средняя экспозиция, средний торг,
* доля снятых (sold_count из total_lots). */
export const Default = () => <HouseAnalyticsKpiRow kpi={FIXTURE_ANALYTICS.kpi} />;

View file

@ -0,0 +1,9 @@
import { HouseAnalyticsSection } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Секция-обёртка «Аналитика дома» fetch-coupled: тянет house-analytics +
* sell-time-sensitivity по estimateId через TanStack Query. Глобальный provider
* preview-режима с пустым кэшем isPending секция возвращает null (пустой
* рендер). Содержимое покрыто отдельными ячейками HouseAnalyticsKpiRow /
* PriceHistoryChart / RecentSoldList / SellTimeSensitivity. */
export const Default = () => <HouseAnalyticsSection estimateId={FIXTURE_ESTIMATE.estimate_id} />;

View file

@ -0,0 +1,6 @@
import { HouseInfoCard } from 'tradein-mvp-frontend';
import { FIXTURE_HOUSES } from './_fixtures';
/** Карточка «Дом» ближайший дом из выборки: адрес, рейтинг, параметры дома
* (год, этажность, тип, лифты, двор, застройщик). length>0 не null. */
export const Default = () => <HouseInfoCard houses={FIXTURE_HOUSES} isLoading={false} />;

View file

@ -0,0 +1,6 @@
import { IMVBenchmark } from 'tradein-mvp-frontend';
import { FIXTURE_IMV } from './_fixtures';
/** Avito IMV benchmark рекомендованная цена + диапазон + аналоги в рынке,
* сравнение с нашей медианой (diff_pct). available:true не null. */
export const Default = () => <IMVBenchmark benchmark={FIXTURE_IMV} isLoading={false} />;

View file

@ -0,0 +1,9 @@
import { ListingsCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Секция 2 «Рынок» объявления-аналоги: count-strip, фильтры/источники,
* ценовой бар P25P75 + таблица. cian-price-changes fetch офлайн пуст бейджи
* скидок не показываются (graceful), всё остальное берётся из estimate-пропа. */
export const Default = () => (
<ListingsCard estimate={FIXTURE_ESTIMATE} estimateId={FIXTURE_ESTIMATE.estimate_id} />
);

View file

@ -0,0 +1,8 @@
import { MapCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Карта аналитики target + 3 аналога с гео-точками (Leaflet + OSM-тайлы).
* NB: компонент тянет Leaflet с unpkg CDN + тайлы с tile.openstreetmap.org. В
* офлайн-capture карта не грузится (loadLeaflet error mapError, либо серый
* холст --surface-2). Card-обёртка (шапка/легенда/футер) рендерится в любом случае. */
export const Default = () => <MapCard estimate={FIXTURE_ESTIMATE} />;

View file

@ -0,0 +1,8 @@
import { MapPicker } from 'tradein-mvp-frontend';
/** Модалка выбора адреса на карте ЕКБ (Leaflet+OSM). Рендерится через
* createPortal в document.body как position:fixed оверлей НЕ заперт в ячейке
* грида. Leaflet тянется с CDN (офлайн «не удалось загрузить карту»). */
export const Default = () => (
<MapPicker onPick={() => {}} onClose={() => {}} />
);

View file

@ -0,0 +1,13 @@
import { NoAccessScreen } from 'tradein-mvp-frontend';
/** Полноэкранный «доступа нет» pure-props, 4 реальных варианта enum.
* variant сменяет title + subtitle (trial со ссылкой в Telegram). */
export const UserDenied = () => <NoAccessScreen variant="user" />;
export const PathDenied = () => (
<NoAccessScreen variant="path" path="/trade-in/scrapers/avito" />
);
export const SessionExpired = () => <NoAccessScreen variant="session" />;
export const TrialEnded = () => <NoAccessScreen variant="trial" />;

View file

@ -0,0 +1,6 @@
import { OfferCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Секция 4 «Оффер» разбивка издержек самостоятельной продажи на дефолтных
* ставках (brandSlug меняет только PDF-endpoint, не вид). */
export const Default = () => <OfferCard estimate={FIXTURE_ESTIMATE} brandSlug={null} />;

View file

@ -0,0 +1,6 @@
import { PacingSection } from 'tradein-mvp-frontend';
/** Pacing-интервалы запросов по провайдерам. Фетчит /admin/scraper/pacing через
* useQuery (data-prop нет). В превью без сети заголовок + hint +
* graceful-degradation. Fetch-coupled. */
export const Default = () => <PacingSection />;

View file

@ -0,0 +1,8 @@
import { PhotoUpload } from 'tradein-mvp-frontend';
/** Загрузка фото квартиры (#394). Карточка с заголовком, счётчиком N/12 и
* файл-инпутом. Реальный UUID активный аплоадер «0 / 12» (фетч списка фото
* офлайн молча игнорируется). */
export const Default = () => (
<PhotoUpload estimateId="3f2a9c10-7b4d-4e8a-9f12-6c5b1a2d3e4f" />
);

View file

@ -0,0 +1,8 @@
import { PlacementHistoryCard } from 'tradein-mvp-frontend';
/** История продаж в доме. Fetch-coupled: данные тянет useEstimatePlacementHistory
* по estimateId; при пустом query-кэше isPending компонент возвращает null
* (нет loading/empty-вёрстки). Передать данные пропом нельзя. */
export const Default = () => (
<PlacementHistoryCard estimateId="preview-0000-0000-0000-000000000000" />
);

View file

@ -0,0 +1,8 @@
import { PriceHistoryChart } from 'tradein-mvp-frontend';
import { FIXTURE_ANALYTICS } from './_fixtures';
/** «История цен в этом доме» recharts line-chart медианы /м² по годам
* (Avito-серия; фикстура без Яндекс-точек одна линия). */
export const Default = () => (
<PriceHistoryChart points={FIXTURE_ANALYTICS.price_history} />
);

View file

@ -0,0 +1,24 @@
import { PriceRangeBar } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
const wrap = (node: React.ReactNode) => (
<div style={{ maxWidth: 460, padding: 12 }}>{node}</div>
);
/** Типовой коридор asking-цены с медианой по центру. */
export const Default = () =>
wrap(
<PriceRangeBar
rangeLow={FIXTURE_ESTIMATE.range_low_rub}
rangeHigh={FIXTURE_ESTIMATE.range_high_rub}
median={FIXTURE_ESTIMATE.median_price_rub}
/>,
);
/** Широкий разброс — медиана смещена влево. */
export const WideSpread = () =>
wrap(<PriceRangeBar rangeLow={7_400_000} rangeHigh={12_900_000} median={9_100_000} />);
/** Узкий коридор — высокая достоверность. */
export const Tight = () =>
wrap(<PriceRangeBar rangeLow={9_600_000} rangeHigh={10_100_000} median={9_850_000} />);

View file

@ -0,0 +1,5 @@
import { PriceTrendCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** «Динамика ₽/м²» — линия recharts по 6 месяцам (168k → 178k), дельта в шапке. */
export const Default = () => <PriceTrendCard estimate={FIXTURE_ESTIMATE} />;

View file

@ -0,0 +1,6 @@
import { ProviderProxySection } from 'tradein-mvp-frontend';
/** Прокси-блок внутри вкладки провайдера. Фетчит /admin/scraper/health через
* useScraperHealth (по source), data-prop нет. В превью без сети заголовок
* «Прокси» + loading/error. Fetch-coupled. */
export const Default = () => <ProviderProxySection source="avito" />;

View file

@ -0,0 +1,8 @@
import { RecentSoldList } from 'tradein-mvp-frontend';
import { FIXTURE_ANALYTICS } from './_fixtures';
/** «Недавно снятые (12 мес)» таблица лотов: комнаты/площадь/этаж, цена, торг,
* дата снятия, экспозиция. */
export const Default = () => (
<RecentSoldList items={FIXTURE_ANALYTICS.recent_sold} />
);

View file

@ -0,0 +1,36 @@
import { ResultPanel } from 'tradein-mvp-frontend';
/** Переиспользуемая панель результата мутации скрапера. Успех JSON-дамп
* ответа в `.scraper-result`. */
export const Default = () => (
<ResultPanel
mut={{
isSuccess: true,
isPending: false,
error: null,
data: {
run_id: 4821,
source: 'avito',
status: 'completed',
anchors_seen: 64,
lots_scraped: 312,
houses_matched: 47,
detail_fetched: 298,
errors: 0,
started_at: '2026-05-30T09:12:00Z',
finished_at: '2026-05-30T09:48:21Z',
},
}}
/>
);
/** Ветка ошибки — красная панель `.scraper-result--error` с текстом сбоя. */
export const Error_ = () => (
<ResultPanel
mut={{
isSuccess: false,
isPending: false,
error: new Error('502 Bad Gateway: upstream avito proxy timeout'),
}}
/>
);

View file

@ -0,0 +1,15 @@
import { RouteGuard } from 'tradein-mvp-frontend';
/** RBAC-обёртка. useMe() читает /api/v1/me в превью-окружении сети нет,
* поэтому guard рендерит свой gated-state (NoAccessScreen "Доступа нет" при
* ошибке /me, либо ничего во время загрузки). Fetch-coupled на useMe. */
export const Default = () => (
<RouteGuard>
<div className="card" style={{ padding: 24 }}>
<h2 style={{ margin: 0 }}>Защищённый раздел</h2>
<p style={{ margin: '8px 0 0', color: 'var(--fg-secondary)' }}>
Этот контент виден только при разрешённой роли.
</p>
</div>
</RouteGuard>
);

View file

@ -0,0 +1,6 @@
import { RunsTable } from 'tradein-mvp-frontend';
/** История прогонов скраппера. Фетчит /admin/scrape/runs через useQuery
* (data-prop нет), но заголовок + hint + фильтры (источник / статус-чипы)
* рендерятся всегда это видимый styled-каркас. Таблица fetch-coupled. */
export const Default = () => <RunsTable source="avito" />;

View file

@ -0,0 +1,25 @@
import { ScheduleControl } from 'tradein-mvp-frontend';
/** Расписание + ручной запуск city-sweep. Фетчит /admin/scrape/schedules через
* useSchedules (статус-блок fetch-coupled), но форма настроек (окно МСК,
* pages/anchor, detail top-N, delay, радиус, enrich) рендерится всегда
* полный styled-каркас формы. paramConfig avito-дефолты (с detail-параметрами). */
const AVITO_PARAMS = {
hasDetailParams: true,
hasEnrichAddress: false,
defaults: {
pages_per_anchor: 3,
radius_m: 1500,
request_delay_sec: 4,
detail_top_n: 12,
enrich_houses: true,
},
};
export const Default = () => (
<ScheduleControl
source="avito"
scheduleSource="avito_city_sweep"
paramConfig={AVITO_PARAMS}
/>
);

View file

@ -0,0 +1,6 @@
import { SellTimeSensitivity } from 'tradein-mvp-frontend';
import { FIXTURE_SELLTIME } from './_fixtures';
/** «Срок продажи в зависимости от цены» — 4 бакета премии (5% / медиана / +5% /
* +10%) с медианой экспозиции и p25p75. Принимает data-пропом напрямую. */
export const Default = () => <SellTimeSensitivity data={FIXTURE_SELLTIME} />;

View file

@ -0,0 +1,6 @@
import { SourcesProgress } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** Шаг B «Агрегация» 7 строк-источников. Циан/Авито/Росреестр = done (с лотами),
* ДомКлик/Restate/Я.Недв/N1 = idle. isPending=false финальное состояние, бар 100%. */
export const Default = () => <SourcesProgress estimate={FIXTURE_ESTIMATE} isPending={false} />;

View file

@ -0,0 +1,8 @@
import { StreetDealsCard } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE } from './_fixtures';
/** «По вашей улице» ДКП-сделки Росреестра + историч. ASK.
* FETCH-COUPLED: данные ТОЛЬКО из useSalesVsListings (нет data-prop). Глобальный
* query-cache в preview пуст isLoading/isError `return null`. В офлайн-capture
* карточка рендерится пусто; на реальной странице таблица сделок с привязкой к ASK. */
export const Default = () => <StreetDealsCard estimate={FIXTURE_ESTIMATE} />;

View file

@ -0,0 +1,6 @@
import { SystemHealthSection } from 'tradein-mvp-frontend';
/** Статус системы скраппера: fetch-режим, browser-сервис, прокси провайдеров.
* Фетчит /admin/scraper/health через useScraperHealth (data-prop нет). В превью
* без сети заголовок + hint + loading/error. Fetch-coupled. */
export const Default = () => <SystemHealthSection />;

View file

@ -0,0 +1,5 @@
import { TestPresets } from 'tradein-mvp-frontend';
/** Быстрое заполнение формы тестовыми квартирами ЕКБ сетка из 6 preset-чипов
* (адрес + подсказка по покрытию). Self-contained (`<style>` внутри). */
export const Default = () => <TestPresets onPick={() => {}} />;

View file

@ -0,0 +1,6 @@
import { Topbar } from 'tradein-mvp-frontend';
/** Шапка приложения «Мера» бренд-марка + навигация. useMe/useBrand офлайн
* не резолвятся fallback показывает полный набор nav-айтемов; UserMenu сам
* рендерит null. Активная вкладка «Оценка». */
export const Default = () => <Topbar active="estimate" />;

View file

@ -0,0 +1,10 @@
import { UserMenu } from 'tradein-mvp-frontend';
/** Аватар-кнопка личного кабинета в Topbar. Читает useMe() без данных /me
* (isLoading || error || !data) компонент намеренно возвращает null.
* Fetch-coupled: в превью без сети рендерит пусто. */
export const Default = () => (
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: 12 }}>
<UserMenu />
</div>
);

View file

@ -0,0 +1,9 @@
import { WhatIfPanel } from 'tradein-mvp-frontend';
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from './_fixtures';
/** Интерактив «Что-если» segmented ремонт + этаж (stepper/slider) + площадь +
* балкон, авто-пересчёт. Без взаимодействия показывает baseline-оценку
* (mutation idle). */
export const Default = () => (
<WhatIfPanel baseEstimate={FIXTURE_ESTIMATE} baseInput={FIXTURE_INPUT} />
);

View file

@ -0,0 +1,3 @@
// Shared preview fixtures — re-export the repo's own offline UI-preview fixture
// (realistic ЕКБ 2-к secondary). Type imports inside are erased by esbuild.
export * from '../../tradein-mvp/frontend/src/app/ui-preview/estimate/fixture';

5
.gitignore vendored
View file

@ -90,3 +90,8 @@ data/osrm/*
# Log cruft at repo root
debug.log
.ds-sync/
ds-bundle/
.design-sync/.cache/
.design-sync/learnings/
.design-sync/node_modules