chore(design-sync): durable-инпуты DS-панели tradein — 6 floor-card превью, шрифты Manrope/Plex, source-kit fork-фикс (#2267) #2274

Merged
lekss361 merged 3 commits from chore/design-sync-tradein into main 2026-07-03 07:38:42 +00:00
26 changed files with 469 additions and 43 deletions

View file

@ -32,3 +32,40 @@ Target: `tradein-mvp/frontend` (NOT the repo-root `frontend/`, which is the Site
- 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. - 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). - `.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. - Grades clear on any `cfg.provider`/preview-affecting config change (expected) — re-grade from fresh sheets.
## Re-sync 2026-07-02 — v2 dashboard sync (main was 223 commits ahead; harness authored on June components)
**Two source-kit.mjs fork fixes were REQUIRED for the evolved v2 codebase (both committed in the override, declared in cfg.libOverrides):**
1. **Exclude `next/font` importers from the synth-entry.** `src/app/v2/layout.tsx` calls `Manrope()`/`IBM_Plex_Mono()` from `next/font/google` at MODULE TOP-LEVEL. esbuild can't resolve the Next-only loader → stubs it `(void 0)``undefined()` aborts the whole browser IIFE → `window.TradeInUI` empty → ALL 58 components vanish (`[RENDER] root empty` everywhere, `[BUNDLE_EXPORT]`). Fix: `comps` filter drops any file whose content matches `/from\s+['"]next\/font/`. If a NEW file top-level-calls a Next build-time loader, same class of crash — extend the filter.
2. **Re-export default-exported components.** `export * from <path>` does NOT re-export a module's `default`. The v2 views/nav/overlay/panel are authored `export default function <Name>` (AnalyticsView, HeroBar, HistoryView, ParamsPanel, SectionOverlay, SourcesView, TopNav) → absent from `window.TradeInUI``[BUNDLE_EXPORT] not a component`. Fix: entry now also emits `export { default as <Name> } from <path>` for each `export default function/class <Name>`. Named-export components (`export function X`) were always fine.
**componentSrcMap:** added `TradeInV2Layout / TradeInV2Page / SaleShareLayout / SaleSharePage` = null (Next route files, never DS components — same as RootLayout et al.).
**overrides (grid):** MapPicker `{cardMode:single, primaryStory:Default}` (portal/fixed), StreetDealsCard + Topbar `{cardMode:column}` (wider than a grid cell).
**Floor-card components (6, authorable on any future re-sync):** SaleShareControls, SaleShareList, SaleShareMap, SectionOverlay, LocationDrawer, BuildingListingsDrawer — overlays/drawers whose props don't seed rich data via PreviewProvider, so they show the honest typographic floor. All the OTHER v2 components (views/nav/hero/panel) render richly because the provider seeds their data.
**Known render warn:** ResultPanel — `variants identical` (Default vs Error cells render near-identically; not broken, the Error cell just doesn't diverge visually enough). Recorded here so re-syncs don't read it as new.
**Font note (re-sync risk):** the v2 HUD's real typefaces are **Manrope + IBM_Plex_Mono** (loaded by the excluded `v2/layout.tsx` via next/font → CSS vars `--font-manrope`/`--font-plex-mono`). `cfg.extraFonts` ships **Inter + JetBrains Mono** (June brand fonts). v2 previews therefore render Manrope-slot text in the shipped fallback. If brand-exact v2 rendering is wanted, add Manrope + IBM Plex Mono woff2 to `.design-sync/fonts/` + `cfg.extraFonts` and map the `--font-manrope`/`--font-plex-mono` vars.
**ResultPanel NAME COLLISION (fixed 2026-07-02):** two components named `ResultPanel` in src — `app/scrapers/_components/ScraperPage.tsx` (`export function ResultPanel`, scraper mut-panel) and `components/trade-in/v2/ResultPanel.tsx` (`export default function ResultPanel`, the v2 estimate result / honest hero). The default-re-export fix makes the v2 one win `window.TradeInUI.ResultPanel` (explicit `export {default as ResultPanel}` beats the star export). But the June authored preview `previews/ResultPanel.tsx` was the SCRAPER one (`mut` props) → the v2 component rendered fine (reads data from context) but its prompt.md documented the wrong (scraper) API. FIX: (1) `cfg.componentSrcMap.ResultPanel = "src/components/trade-in/v2/ResultPanel.tsx"` pins enrichment to v2; (2) re-authored `previews/ResultPanel.tsx``<ResultPanel onNavigate={()=>{}} />` (v2 API; `data` defaults to the component's built-in RESULT_FIXTURE = honest-hero). If a re-sync ever shows ResultPanel with scraper markup, the pin/preview regressed. Any NEW duplicate PascalCase component name will hit the same class of issue — the default-re-export makes the default-export win; pin + author the intended one.
**Upload gotcha (this machine):** after a follow-up single-component rebuild, `resync-verdict.json upload.deletePaths` came back with ALL other components (318 paths) — a stale-anchor diff artifact, NOT real deletions (ResultPanel + audit/uploads were absent from it). Do NOT feed that deletePaths to delete_files (would nuke the project). For a focused single-component re-upload: write just that component's `components/<group>/<Name>/*` + `_preview/<Name>.js` + `_ds_sync.json` (sentinel-fenced), deletes=[].
- **CORRECTION (2026-07-03):** the 318 deletePaths were NOT a diff artifact — they were REAL local deletions caused by a source-kit fork bug (see below): the July-02 `componentSrcMap.ResultPanel` pin collapsed the synth build to 1 component, so the local ds-bundle genuinely lost the other 53. The "don't feed deletePaths blindly" advice stands (it saved the project), but the root cause is fixed now.
## Re-sync 2026-07-03 — 6 floor cards authored + v2 brand fonts (#2267)
**source-kit.mjs fork fix #3 (REQUIRED): componentSrcMap pin must AUGMENT synth discovery, not replace it.** In synth mode there is no shipped `.d.ts`, so `exportedNames()` is empty and `names` contains ONLY the non-null `componentSrcMap` pins. The old guard `if (!components.length && synthEntry) components = deriveComponentsFromSrc(...)` therefore never ran once a single pin existed (ResultPanel, added 2026-07-02) → the whole bundle collapsed to 1 component (`(stale preview: X — component no longer exported)` for everything else; verdict shows all others as `removed` + bogus deletePaths). Fix in the fork: when `synthEntry`, always union `deriveComponentsFromSrc(srcFiles)` (minus `null`-excluded) with the pinned names. Any future pin would have re-triggered this. NB: the fork edit re-keys EVERY component's sourceKey → full re-grade pass (done: 47/54 renderHashes byte-identical to the 2026-07-02 anchor → carry-forward grades; the rest eyeballed).
**6 floor cards → authored (all graded good):**
- `SaleShareControls` — inline `SaleShareSummary` (histogram 7 корзин, coverage 92.1%); порог 8% приглушает нижнюю корзину; все фильтры.
- `SaleShareList` — 6 инлайн-домов ЕКБ (heat-бейджи, «аварийный», over_100 «возможно, несколько корпусов», selected-row) + Empty cell. `cardMode: column`.
- `SaleShareMap` — 8 heat-маркеров + открытый popup выбранного дома; OSM-тайлы офлайн не красятся (известно, как MapCard).
- `SectionOverlay` (v2) — рендерится contained в relative-«артборде» (absolute-позиционирование против ближайшего positioned ancestor; wrapper height 680 + v2-градиент). 2 cells: HistoryView (04) / AnalyticsView (06) на их встроенных fixtures (data-props не переданы). `cardMode: column`.
- `LocationDrawer` (v2) — open=true в таком же contained-артборде (height 640). `cardMode: column`.
- `BuildingListingsDrawer``createPortal` + `.ss-drawer-overlay` = position:fixed → `cardMode: single` (прецедент MapPicker). Шапка богатая (props), тело fetch-coupled (`useBuildingListings`, data-prop нет) → офлайн честный error-state «Не удалось загрузить объявления дома». Сидировать можно было бы ключом `["sale-share","listings",<house_id>]` в PreviewProvider — сознательно НЕ сделано (кэш-ключ завязан на house_id фикстуры превью; хрупко).
**v2 brand fonts shipped (Manrope + IBM Plex Mono).** woff2 (latin+cyrillic; Manrope variable 200-800, Plex Mono static 300/400/500 — веса из `app/v2/layout.tsx`) скачаны с Google Fonts → `.design-sync/fonts/`, @font-face добавлены в `brand-fonts.css`. **ГОЧА: extraFonts-пайплайн (`css.mjs extractFonts`) извлекает ТОЛЬКО `@font-face`-блоки — `:root{}` из brand-fonts.css молча выбрасывается.** Маппинг `--font-manrope`/`--font-plex-mono` поэтому живёт в `preview-provider.tsx` (`<style>` в провайдере — капчер-путь) + задокументирован для design-консюмеров в `conventions.md` (сниппет `:root{...}`). После фикса v2-цифры реально в Plex Mono (проверено по sheets ResultPanel/SectionOverlay).
**Upload 2026-07-03: НЕ выполнен из worker-сессии** — DesignSync MCP-тулов в ней нет (ToolSearch пуст). Verdict готов и чист: ok=true, pendingGrade=0, deletePaths=0 (легитимно — remote-анкор полный, local снова 54 компонента), upload.any=true, components=54 (все re-key'нуты форк-фиксом), bundle+styling+aux=true. Main-сессия: залить ds-bundle по verdict'у (deletePaths пуст — ничего не удалять) и после успеха скопировать свежий `ds-bundle/_ds_sync.json``.design-sync/.cache/remote-sync.json` (новый анкор).

View file

@ -24,7 +24,21 @@
"ScraperPage": null, "ScraperPage": null,
"ScrapersUnifiedPage": null, "ScrapersUnifiedPage": null,
"TradeInPage": null, "TradeInPage": null,
"YandexScraperPage": null "YandexScraperPage": null,
"TradeInV2Layout": null,
"TradeInV2Page": null,
"SaleShareLayout": null,
"SaleSharePage": null,
"ResultPanel": "src/components/trade-in/v2/ResultPanel.tsx"
},
"overrides": {
"MapPicker": { "cardMode": "single", "primaryStory": "Default" },
"StreetDealsCard": { "cardMode": "column" },
"Topbar": { "cardMode": "column" },
"SaleShareList": { "cardMode": "column" },
"SectionOverlay": { "cardMode": "column" },
"LocationDrawer": { "cardMode": "column" },
"BuildingListingsDrawer": { "cardMode": "single", "primaryStory": "Default" }
}, },
"provider": { "provider": {
"component": "PreviewProvider" "component": "PreviewProvider"
@ -32,5 +46,8 @@
"extraEntries": [ "extraEntries": [
"../../.design-sync/preview-provider.tsx" "../../.design-sync/preview-provider.tsx"
], ],
"readmeHeader": ".design-sync/conventions.md" "readmeHeader": ".design-sync/conventions.md",
"extraFonts": [
"../../.design-sync/fonts/brand-fonts.css"
]
} }

View file

@ -35,6 +35,12 @@ This is **not Tailwind and not CSS-in-JS**. Styling is a **global stylesheet** o
| Border | `--border-soft` `--border-card` `--border-strong` | | Border | `--border-soft` `--border-card` `--border-strong` |
| Shape/type | `--radius` `--radius-sm` `--radius-lg` `--shadow-md` `--font-sans` (Manrope) `--font-mono` `--container` | | Shape/type | `--radius` `--radius-sm` `--radius-lg` `--shadow-md` `--font-sans` (Manrope) `--font-mono` `--container` |
**v2 (МЕРА HUD) fonts:** the `v2/*` components read `var(--font-manrope)` / `var(--font-plex-mono)` (in the app these come from `next/font`). The woff2 for both families ships in `fonts/fonts.css`; define the vars once at your design root:
```css
:root { --font-manrope: 'Manrope'; --font-plex-mono: 'IBM Plex Mono'; }
```
```tsx ```tsx
<div style={{ background: "var(--bg-card)", border: "1px solid var(--border-card)", <div style={{ background: "var(--bg-card)", border: "1px solid var(--border-card)",
borderRadius: "var(--radius)", padding: 16, color: "var(--fg-primary)", borderRadius: "var(--radius)", padding: 16, color: "var(--fg-primary)",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,112 @@
/* Brand fonts shipped with the trade-in DS bundle.
* June set: Inter + JetBrains Mono (variable woff2).
* v2 HUD set (2026-07-03): Manrope + IBM Plex Mono the МЕРА v2 typefaces,
* loaded in the app via next/font in app/v2/layout.tsx (CSS vars
* --font-manrope / --font-plex-mono). next/font is excluded from the synth
* bundle, so we ship the woff2 here and map the vars in :root below.
* All fonts: latin + cyrillic subsets (ЕКБ addresses are Cyrillic). */
:root {
--font-manrope: 'Manrope';
--font-plex-mono: 'IBM Plex Mono';
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 200 800;
font-display: swap;
src: url('./Manrope-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 200 800;
font-display: swap;
src: url('./Manrope-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('./IBMPlexMono-300-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('./IBMPlexMono-300-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./IBMPlexMono-400-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./IBMPlexMono-400-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('./IBMPlexMono-500-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('./IBMPlexMono-500-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('./Inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('./Inter-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100 800;
font-display: swap;
src: url('./JetBrainsMono-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100 800;
font-display: swap;
src: url('./JetBrainsMono-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

View file

@ -10,7 +10,7 @@
// knob for component inclusion: non-null value = add/pin src path, null = // knob for component inclusion: non-null value = add/pin src path, null =
// exclude a .d.ts-exported internal. // exclude a .d.ts-exported internal.
import { existsSync, writeFileSync } from 'node:fs'; import { existsSync, writeFileSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path'; import { dirname, join, relative, resolve } from 'node:path';
import { Project, Node, ts } from 'ts-morph'; 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_*) // forked from design-sync lib/source-kit.mjs — synth-entry process shim (Next.js app reads process.env.NEXT_PUBLIC_*)
@ -70,7 +70,18 @@ export async function resolvePackage(ctx) {
console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`); console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`);
process.exit(1); process.exit(1);
} }
const comps = srcFiles.filter((p) => SRC_IMPL_RX.test(p) && !NON_IMPL_RX.test(p)); // Next route files (app/**/layout.tsx, page.tsx) that call `next/font`
// loaders (Manrope(), IBM_Plex_Mono()) at module top-level crash the browser
// IIFE: esbuild can't resolve the Next-only loader → stubs it to `undefined`
// → `undefined()` aborts the whole bundle → window.<global> stays empty and
// every component vanishes. These files are never DS components anyway, so
// drop any `next/font`-importing module from the synth-entry set.
const comps = srcFiles.filter(
(p) =>
SRC_IMPL_RX.test(p) &&
!NON_IMPL_RX.test(p) &&
!/from\s+['"]next\/font/.test(readFileSync(p, 'utf8')),
);
// Next.js app code reads process.env.NEXT_PUBLIC_* at module top-level; in // 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 // the browser IIFE `process` is undefined → every component throws. Emit a
// shim module and import it FIRST (ESM evaluates the first import's body // shim module and import it FIRST (ESM evaluates the first import's body
@ -89,10 +100,25 @@ export async function resolvePackage(ctx) {
"__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n", "__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n",
); );
entry = join(OUT, '.pkg-entry.mjs'); entry = join(OUT, '.pkg-entry.mjs');
// `export *` does NOT re-export a module's default. Components authored as
// `export default function <Name>` (the v2 views/nav/overlay/panel) would be
// absent from window.<global> → [BUNDLE_EXPORT] "not a component". Re-export
// each named default under its declared name so default-exported components
// reach the global alongside the named ones.
const defaultReexports = comps
.map((p) => {
const m = readFileSync(p, 'utf8').match(
/export\s+default\s+(?:async\s+)?(?:function|class)\s+([A-Z][A-Za-z0-9]*)/,
);
return m ? `export { default as ${m[1]} } from ${JSON.stringify(p)};` : null;
})
.filter(Boolean);
writeFileSync( writeFileSync(
entry, entry,
`import ${JSON.stringify(slash(shim))};\n` + `import ${JSON.stringify(slash(shim))};\n` +
comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') + comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') +
'\n' +
defaultReexports.join('\n') +
'\n', '\n',
); );
synthEntry = true; synthEntry = true;
@ -117,8 +143,17 @@ export async function resolvePackage(ctx) {
names.add(k); names.add(k);
} }
let components = [...names].sort().map((name) => ({ name, group: 'general' })); let components = [...names].sort().map((name) => ({ name, group: 'general' }));
if (!components.length && synthEntry) { if (synthEntry) {
components = deriveComponentsFromSrc(srcFiles).filter((c) => srcMap[c.name] !== null); // Synth mode has no shipped .d.ts → `names` holds only componentSrcMap
// pins. A non-null pin must AUGMENT src discovery (it exists to pin
// enrichment to a specific file), not REPLACE it — with the old
// `!components.length` guard a single pin (ResultPanel, 2026-07-02)
// collapsed the whole synth bundle to just the pinned component.
const derived = deriveComponentsFromSrc(srcFiles).filter((c) => srcMap[c.name] !== null);
const have = new Set(components.map((c) => c.name));
components = components
.concat(derived.filter((c) => !have.has(c.name)))
.sort((a, b) => a.name.localeCompare(b.name));
} }
if (!components.length) { if (!components.length) {
if (cfg.cssEntry || existsSync(join(PKG_DIR, 'styles.css'))) { if (cfg.cssEntry || existsSync(join(PKG_DIR, 'styles.css'))) {

View file

@ -57,5 +57,17 @@ export function PreviewProvider({ children }: { children: React.ReactNode }) {
); );
return qc; return qc;
}); });
return <QueryClientProvider client={client}>{children}</QueryClientProvider>; return (
<QueryClientProvider client={client}>
{/* v2 HUD font vars. The app defines --font-manrope/--font-plex-mono via
next/font in app/v2/layout.tsx, which is excluded from the synth
bundle (next/font loader crash) so the vars never exist in capture
and tokens.font.* fell back. The @font-face for both families ships
in .design-sync/fonts/brand-fonts.css (cfg.extraFonts), but that
pipeline extracts @font-face blocks ONLY (a :root{} there is
dropped) map the vars here instead. */}
<style>{":root{--font-manrope:'Manrope';--font-plex-mono:'IBM Plex Mono'}"}</style>
{children}
</QueryClientProvider>
);
} }

View file

@ -0,0 +1,36 @@
import { BuildingListingsDrawer } from 'tradein-mvp-frontend';
// Правый drawer с активными объявлениями выбранного дома (/trade-in/sale-share):
// createPortal в document.body, .ss-drawer-overlay = position:fixed по всему
// вьюпорту (как MapPicker) → cardMode:single. Шапка — адрес + тепловой бейдж
// доли + «N из M квартир» (из props). Тело FETCH-COUPLED: useBuildingListings
// (GET /buildings/{id}/listings) — data-prop нет, глобальный query-cache пуст →
// в офлайн-capture честный pending/error-state («Загрузка объявлений…» /
// «Не удалось загрузить объявления дома»). На реальной странице — список
// объявлений с ценой, ₽/м², этажом и ссылкой на оригинал.
const building = {
house_id: 3101,
address: 'ул. Викулова, 46',
lat: 56.8412,
lon: 60.5556,
sale_share_pct: 34.6,
sale_share_pct_45d: 41.2,
listings_45d: 33,
over_100: false,
active_secondary: 27,
flat_count_effective: 78,
gar_match_method: 'cadastre',
median_price_rub: 4_950_000,
median_price_per_m2: 158_500,
avg_days_on_market: 74,
year_built: 1972,
house_type: 'panel',
total_floors: 9,
series_name: '1-468',
is_emergency: false,
};
/** Открытый drawer дома «ул. Викулова, 46» (34.6% в продаже, 27 из 78 квартир). */
export const Default = () => (
<BuildingListingsDrawer building={building} onClose={() => {}} />
);

View file

@ -0,0 +1,25 @@
import { LocationDrawer } from 'tradein-mvp-frontend';
// «ПОЯСНЕНИЕ К РАСЧЁТУ» — правый drawer HUD «МЕРА Оценка» (/trade-in/v2),
// открывается с «?» у «КОЭФ. ЛОКАЦИИ» в HeroBar. Честная методика: как
// агрегируются источники (Циан/Я.Недвижимость/Авито/Домклик + Росреестр) и
// явная плашка «коэффициент локации в разработке». Контент статичный —
// данных не принимает, только open/onClose. Drawer absolute-позиционирован
// (width 452 + scrim) — рендерим contained внутри relative-обёртки.
/** Открытое состояние: scrim + выдвинутая панель с методикой и info-плашкой
* о локации. */
export const Default = () => (
<div
style={{
position: 'relative',
height: 640,
borderRadius: 10,
overflow: 'hidden',
background:
'radial-gradient(1100px 520px at 30% -10%, #f7fbff 0%, #eef4fa 55%, #e6eef7 100%)',
}}
>
<LocationDrawer open={true} onClose={() => {}} />
</div>
);

View file

@ -1,36 +1,10 @@
import { ResultPanel } from 'tradein-mvp-frontend'; import { ResultPanel } from 'tradein-mvp-frontend';
/** Переиспользуемая панель результата мутации скрапера. Успех JSON-дамп /**
* ответа в `.scraper-result`. */ * ResultPanel (v2) центральная панель результата оценки на `/trade-in/v2`:
export const Default = () => ( * одна цифра-герой (ожидаемая цена продажи / «оценка»), тиры median + ДКП,
<ResultPanel * доверительные диапазоны, источники и мини-гистограмма распределения.
mut={{ * Данные по умолчанию берутся из встроенной `RESULT_FIXTURE` компонента,
isSuccess: true, * поэтому карточке достаточно передать `onNavigate` (навигация по секциям).
isPending: false, */
error: null, export const Default = () => <ResultPanel onNavigate={() => {}} />;
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,54 @@
import { SaleShareControls } from 'tradein-mvp-frontend';
// Панель «Порог и фильтры» страницы /trade-in/sale-share. Controlled-компонент:
// состояние живёт в page.tsx и приходит через props. Сводка (гистограмма
// распределения sale_share_pct) — реалистичные значения по покрытию ЕКБ
// (view v_building_sale_share ≈ 6.7k домов вторички, знаменатель ГАР у ~92%).
const summary = {
total_secondary_buildings: 6667,
buildings_with_denominator: 6143,
coverage_pct: 92.1,
max_pct: 42.0,
p95_pct: 11.3,
histogram: [
{ bucket: '0-5', count: 4980 },
{ bucket: '5-10', count: 642 },
{ bucket: '10-20', count: 298 },
{ bucket: '20-30', count: 121 },
{ bucket: '30-50', count: 57 },
{ bucket: '50-100', count: 18 },
{ bucket: '100+', count: 6 },
],
};
const HOUSE_TYPES = ['panel', 'brick', 'monolith', 'monolith_brick', 'block', 'stalin'];
/** Порог 8% на слайдере: корзины ниже порога приглушены (opacity), тепловая
* окраска столбцов greenred повторяет цвет маркеров карты. Фильтры: город,
* цена, год постройки, тип дома, мин. объявлений, сортировка. */
export const Default = () => (
<SaleShareControls
summary={summary}
minPct={8}
onMinPct={() => {}}
city="Екатеринбург"
onCity={() => {}}
priceMin=""
onPriceMin={() => {}}
priceMax=""
onPriceMax={() => {}}
yearMin=""
onYearMin={() => {}}
yearMax=""
onYearMax={() => {}}
houseType=""
onHouseType={() => {}}
houseTypeOptions={HOUSE_TYPES}
sort="share_desc"
onSort={() => {}}
window="now"
onWindow={() => {}}
minCount={3}
onMinCount={() => {}}
/>
);

View file

@ -0,0 +1,50 @@
import { SaleShareList } from 'tradein-mvp-frontend';
// Список домов /trade-in/sale-share, отсортирован по доле в продаже (server-side
// share_desc). Реалистичные дома ЕКБ (view v_building_sale_share): адрес,
// тепловой бейдж доли, «N из M квартир», медиана + ₽/м², экспозиция,
// год/тип/этажность/серия. Включены оба edge-бейджа: «аварийный» и
// «возможно, несколько корпусов» (over_100 — ГАР-коллизия адреса).
const buildings = [
{ house_id: 4212, address: 'ул. Космонавтов, 52', lat: 56.8890, lon: 60.6132, sale_share_pct: 128.0, sale_share_pct_45d: 131.5, listings_45d: 46, over_100: true, active_secondary: 32, flat_count_effective: 25, gar_match_method: 'address', median_price_rub: 3_650_000, median_price_per_m2: 121_700, avg_days_on_market: 96, year_built: 1961, house_type: 'brick', total_floors: 5, series_name: null, is_emergency: false },
{ house_id: 3387, address: 'пер. Сапёров, 5', lat: 56.8271, lon: 60.6198, sale_share_pct: 48.9, sale_share_pct_45d: 52.4, listings_45d: 24, over_100: false, active_secondary: 22, flat_count_effective: 45, gar_match_method: 'cadastre', median_price_rub: 2_990_000, median_price_per_m2: 98_400, avg_days_on_market: 148, year_built: 1957, house_type: 'brick', total_floors: 3, series_name: null, is_emergency: true },
{ house_id: 3101, address: 'ул. Викулова, 46', lat: 56.8412, lon: 60.5556, sale_share_pct: 34.6, sale_share_pct_45d: 41.2, listings_45d: 33, over_100: false, active_secondary: 27, flat_count_effective: 78, gar_match_method: 'cadastre', median_price_rub: 4_950_000, median_price_per_m2: 158_500, avg_days_on_market: 74, year_built: 1972, house_type: 'panel', total_floors: 9, series_name: '1-468', is_emergency: false },
{ house_id: 2874, address: 'ул. Бебеля, 138', lat: 56.8664, lon: 60.5721, sale_share_pct: 22.4, sale_share_pct_45d: 25.0, listings_45d: 20, over_100: false, active_secondary: 17, flat_count_effective: 76, gar_match_method: 'cadastre', median_price_rub: 5_400_000, median_price_per_m2: 149_200, avg_days_on_market: 61, year_built: 1978, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
{ house_id: 5530, address: 'ул. Малышева, 84', lat: 56.8380, lon: 60.6203, sale_share_pct: 12.1, sale_share_pct_45d: 13.8, listings_45d: 16, over_100: false, active_secondary: 14, flat_count_effective: 116, gar_match_method: 'cadastre', median_price_rub: 7_850_000, median_price_per_m2: 172_300, avg_days_on_market: 47, year_built: 1954, house_type: 'stalin', total_floors: 6, series_name: null, is_emergency: false },
{ house_id: 6119, address: 'ул. Щербакова, 20', lat: 56.7791, lon: 60.6120, sale_share_pct: 6.8, sale_share_pct_45d: 8.1, listings_45d: 22, over_100: false, active_secondary: 18, flat_count_effective: 264, gar_match_method: 'cadastre', median_price_rub: 8_900_000, median_price_per_m2: 164_800, avg_days_on_market: 38, year_built: 2016, house_type: 'monolith', total_floors: 25, series_name: null, is_emergency: false },
];
/** Полная таблица (6 домов, выбран «ул. Викулова, 46» строка is-selected),
* сортировка по доле, кнопка «Объявления » ведёт в drawer. */
export const Default = () => (
<SaleShareList
buildings={buildings}
sort="share_desc"
onSort={() => {}}
selectedHouseId={3101}
hoveredHouseId={null}
onSelect={() => {}}
onHover={() => {}}
isLoading={false}
isError={false}
minPct={5}
window="now"
/>
);
/** Пустой результат — фильтры отсекли все дома (честный empty-state). */
export const Empty = () => (
<SaleShareList
buildings={[]}
sort="share_desc"
onSort={() => {}}
selectedHouseId={null}
hoveredHouseId={null}
onSelect={() => {}}
onHover={() => {}}
isLoading={false}
isError={false}
minPct={25}
window="now"
/>
);

View file

@ -0,0 +1,30 @@
import { SaleShareMap } from 'tradein-mvp-frontend';
// Тепловая карта домов /trade-in/sale-share (Leaflet + OSM с CDN, как
// MapCard/MapPicker). Цвет и радиус circleMarker растут с sale_share_pct
// (green → red, over_100 → тёмно-красный). 8 домов ЕКБ с координатами;
// выбранный дом (selectedHouseId) получает открытый popup. OSM-тайлы —
// внешний fetch: в офлайн-capture подложка может не прогрузиться,
// маркеры/popup/контролы рендерятся всегда.
const buildings = [
{ house_id: 4212, address: 'ул. Космонавтов, 52', lat: 56.8890, lon: 60.6132, sale_share_pct: 128.0, sale_share_pct_45d: 131.5, listings_45d: 46, over_100: true, active_secondary: 32, flat_count_effective: 25, gar_match_method: 'address', median_price_rub: 3_650_000, median_price_per_m2: 121_700, avg_days_on_market: 96, year_built: 1961, house_type: 'brick', total_floors: 5, series_name: null, is_emergency: false },
{ house_id: 3387, address: 'пер. Сапёров, 5', lat: 56.8271, lon: 60.6198, sale_share_pct: 48.9, sale_share_pct_45d: 52.4, listings_45d: 24, over_100: false, active_secondary: 22, flat_count_effective: 45, gar_match_method: 'cadastre', median_price_rub: 2_990_000, median_price_per_m2: 98_400, avg_days_on_market: 148, year_built: 1957, house_type: 'brick', total_floors: 3, series_name: null, is_emergency: true },
{ house_id: 3101, address: 'ул. Викулова, 46', lat: 56.8412, lon: 60.5556, sale_share_pct: 34.6, sale_share_pct_45d: 41.2, listings_45d: 33, over_100: false, active_secondary: 27, flat_count_effective: 78, gar_match_method: 'cadastre', median_price_rub: 4_950_000, median_price_per_m2: 158_500, avg_days_on_market: 74, year_built: 1972, house_type: 'panel', total_floors: 9, series_name: '1-468', is_emergency: false },
{ house_id: 2874, address: 'ул. Бебеля, 138', lat: 56.8664, lon: 60.5721, sale_share_pct: 22.4, sale_share_pct_45d: 25.0, listings_45d: 20, over_100: false, active_secondary: 17, flat_count_effective: 76, gar_match_method: 'cadastre', median_price_rub: 5_400_000, median_price_per_m2: 149_200, avg_days_on_market: 61, year_built: 1978, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
{ house_id: 5530, address: 'ул. Малышева, 84', lat: 56.8380, lon: 60.6203, sale_share_pct: 12.1, sale_share_pct_45d: 13.8, listings_45d: 16, over_100: false, active_secondary: 14, flat_count_effective: 116, gar_match_method: 'cadastre', median_price_rub: 7_850_000, median_price_per_m2: 172_300, avg_days_on_market: 47, year_built: 1954, house_type: 'stalin', total_floors: 6, series_name: null, is_emergency: false },
{ house_id: 6119, address: 'ул. Щербакова, 20', lat: 56.7791, lon: 60.6120, sale_share_pct: 6.8, sale_share_pct_45d: 8.1, listings_45d: 22, over_100: false, active_secondary: 18, flat_count_effective: 264, gar_match_method: 'cadastre', median_price_rub: 8_900_000, median_price_per_m2: 164_800, avg_days_on_market: 38, year_built: 2016, house_type: 'monolith', total_floors: 25, series_name: null, is_emergency: false },
{ house_id: 5871, address: 'ул. Крауля, 44', lat: 56.8443, lon: 60.5610, sale_share_pct: 17.9, sale_share_pct_45d: 19.6, listings_45d: 14, over_100: false, active_secondary: 12, flat_count_effective: 67, gar_match_method: 'cadastre', median_price_rub: 5_150_000, median_price_per_m2: 152_900, avg_days_on_market: 58, year_built: 1980, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
{ house_id: 6402, address: 'ул. 8 Марта, 190', lat: 56.8043, lon: 60.6094, sale_share_pct: 3.4, sale_share_pct_45d: 4.0, listings_45d: 11, over_100: false, active_secondary: 9, flat_count_effective: 262, gar_match_method: 'cadastre', median_price_rub: 9_300_000, median_price_per_m2: 176_400, avg_days_on_market: 33, year_built: 2019, house_type: 'monolith', total_floors: 26, series_name: null, is_emergency: false },
];
/** Карта ЕКБ: 8 тепловых маркеров, выбран «ул. Викулова, 46» открыт popup
* (адрес, % в продаже, N из M квартир). */
export const Default = () => (
<SaleShareMap
buildings={buildings}
selectedHouseId={3101}
hoveredHouseId={null}
onSelect={() => {}}
onHover={() => {}}
/>
);

View file

@ -0,0 +1,39 @@
import { SectionOverlay } from 'tradein-mvp-frontend';
// Glass/blur оверлей секций HUD «МЕРА Оценка» (/trade-in/v2): скобки-уголки,
// шапка «номер секции + заголовок + ← К ОЦЕНКЕ», скроллируемое тело со
// сменной view. Позиционируется absolute относительно артборда — в preview
// рендерим contained внутри relative-обёртки с фоном v2-страницы.
// Данные не передаём: каждая view падает на свой встроенный fixture-набор
// (HISTORY_FIXTURE / ANALYTICS_FIXTURE …) — как storybook/unwired usage.
const Artboard = ({ children }: { children?: unknown }) => (
<div
style={{
position: 'relative',
height: 680,
borderRadius: 10,
overflow: 'hidden',
background:
'radial-gradient(1100px 520px at 30% -10%, #f7fbff 0%, #eef4fa 55%, #e6eef7 100%)',
}}
>
{children as React.ReactNode}
</div>
);
/** Секция 04 «ПРОДАЖИ В ДОМЕ» (active=1 HistoryView на fixture-данных):
* таблица ДКП-продаж дома с ценами и датами. */
export const HistorySection = () => (
<Artboard>
<SectionOverlay active={1} onClose={() => {}} onNavigate={() => {}} />
</Artboard>
);
/** Секция 06 «АНАЛИТИКА ДОМА» (active=3 AnalyticsView): KPI дома,
* динамика цены, недавние продажи. */
export const AnalyticsSection = () => (
<Artboard>
<SectionOverlay active={3} onClose={() => {}} onNavigate={() => {}} />
</Artboard>
);

3
.gitignore vendored
View file

@ -1,4 +1,4 @@
# Memory graph (hardlink to C:\mcp\.aim\memory-gendesign.jsonl) # Memory graph (hardlink to C:\mcp\.aim\memory-gendesign.jsonl)
*.jsonl *.jsonl
!memory/memory-gendesign.jsonl !memory/memory-gendesign.jsonl
@ -44,7 +44,6 @@ tradein-mvp/scripts/.*cookies*.json
# Agents and rules ARE tracked: they are team-shared config. # Agents and rules ARE tracked: they are team-shared config.
# Explicit per-path list — `.claude/` + `!.claude/agents/` whitelist pattern # Explicit per-path list — `.claude/` + `!.claude/agents/` whitelist pattern
# does not work because git cannot re-include files under an ignored parent. # does not work because git cannot re-include files under an ignored parent.
.claude/settings.json
.claude/settings.local.json .claude/settings.local.json
.claude/cache/ .claude/cache/
.claude/sessions/ .claude/sessions/