chore(design-sync): durable-инпуты tradein DS-панели — 6 floor-card превью, бренд-шрифты Manrope/Plex, source-kit fork-фикс (#2267)
This commit is contained in:
parent
11861e5dc0
commit
375d59c48d
65 changed files with 1271 additions and 2 deletions
79
.design-sync/AUTHORING.md
Normal file
79
.design-sync/AUTHORING.md
Normal 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.1–10.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 1–3 more ONLY if they visibly
|
||||||
|
differ (sweep a real variant axis: a state, an enum, empty-vs-full). Budget 1–4 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).
|
||||||
71
.design-sync/NOTES.md
Normal file
71
.design-sync/NOTES.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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` (новый анкор).
|
||||||
53
.design-sync/config.json
Normal file
53
.design-sync/config.json
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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": {
|
||||||
|
"component": "PreviewProvider"
|
||||||
|
},
|
||||||
|
"extraEntries": [
|
||||||
|
"../../.design-sync/preview-provider.tsx"
|
||||||
|
],
|
||||||
|
"readmeHeader": ".design-sync/conventions.md",
|
||||||
|
"extraFonts": [
|
||||||
|
"../../.design-sync/fonts/brand-fonts.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
56
.design-sync/conventions.md
Normal file
56
.design-sync/conventions.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# 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` |
|
||||||
|
|
||||||
|
**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
|
||||||
|
<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.
|
||||||
BIN
.design-sync/fonts/IBMPlexMono-300-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-300-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/IBMPlexMono-300-latin.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-300-latin.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/IBMPlexMono-400-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-400-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/IBMPlexMono-400-latin.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-400-latin.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/IBMPlexMono-500-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-500-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/IBMPlexMono-500-latin.woff2
Normal file
BIN
.design-sync/fonts/IBMPlexMono-500-latin.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/Inter-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/Inter-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/Inter-latin.woff2
Normal file
BIN
.design-sync/fonts/Inter-latin.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/JetBrainsMono-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/JetBrainsMono-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/JetBrainsMono-latin.woff2
Normal file
BIN
.design-sync/fonts/JetBrainsMono-latin.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/Manrope-cyrillic.woff2
Normal file
BIN
.design-sync/fonts/Manrope-cyrillic.woff2
Normal file
Binary file not shown.
BIN
.design-sync/fonts/Manrope-latin.woff2
Normal file
BIN
.design-sync/fonts/Manrope-latin.woff2
Normal file
Binary file not shown.
112
.design-sync/fonts/brand-fonts.css
Normal file
112
.design-sync/fonts/brand-fonts.css
Normal 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;
|
||||||
|
}
|
||||||
213
.design-sync/overrides/source-kit.mjs
Normal file
213
.design-sync/overrides/source-kit.mjs
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
// 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, readFileSync } 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);
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
// 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');
|
||||||
|
// `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(
|
||||||
|
entry,
|
||||||
|
`import ${JSON.stringify(slash(shim))};\n` +
|
||||||
|
comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') +
|
||||||
|
'\n' +
|
||||||
|
defaultReexports.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 (synthEntry) {
|
||||||
|
// 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 (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 };
|
||||||
|
}
|
||||||
73
.design-sync/preview-provider.tsx
Normal file
73
.design-sync/preview-provider.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"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}>
|
||||||
|
{/* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
.design-sync/previews/AddressInput.tsx
Normal file
12
.design-sync/previews/AddressInput.tsx
Normal 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…"
|
||||||
|
/>
|
||||||
|
);
|
||||||
36
.design-sync/previews/BuildingListingsDrawer.tsx
Normal file
36
.design-sync/previews/BuildingListingsDrawer.tsx
Normal 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={() => {}} />
|
||||||
|
);
|
||||||
6
.design-sync/previews/CianValuationCard.tsx
Normal file
6
.design-sync/previews/CianValuationCard.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/DataQualitySection.tsx
Normal file
6
.design-sync/previews/DataQualitySection.tsx
Normal 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 />;
|
||||||
6
.design-sync/previews/DealsCard.tsx
Normal file
6
.design-sync/previews/DealsCard.tsx
Normal 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} />;
|
||||||
25
.design-sync/previews/DistributionCard.tsx
Normal file
25
.design-sync/previews/DistributionCard.tsx
Normal 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} />;
|
||||||
13
.design-sync/previews/EstimateForm.tsx
Normal file
13
.design-sync/previews/EstimateForm.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
25
.design-sync/previews/ExposureCard.tsx
Normal file
25
.design-sync/previews/ExposureCard.tsx
Normal 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} />;
|
||||||
13
.design-sync/previews/HeroSummary.tsx
Normal file
13
.design-sync/previews/HeroSummary.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
9
.design-sync/previews/HeroTransparency.tsx
Normal file
9
.design-sync/previews/HeroTransparency.tsx
Normal 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} />
|
||||||
|
);
|
||||||
6
.design-sync/previews/HouseAnalyticsKpiRow.tsx
Normal file
6
.design-sync/previews/HouseAnalyticsKpiRow.tsx
Normal 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} />;
|
||||||
9
.design-sync/previews/HouseAnalyticsSection.tsx
Normal file
9
.design-sync/previews/HouseAnalyticsSection.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/HouseInfoCard.tsx
Normal file
6
.design-sync/previews/HouseInfoCard.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/IMVBenchmark.tsx
Normal file
6
.design-sync/previews/IMVBenchmark.tsx
Normal 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} />;
|
||||||
9
.design-sync/previews/ListingsCard.tsx
Normal file
9
.design-sync/previews/ListingsCard.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ListingsCard } from 'tradein-mvp-frontend';
|
||||||
|
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||||
|
|
||||||
|
/** Секция 2 «Рынок» — объявления-аналоги: count-strip, фильтры/источники,
|
||||||
|
* ценовой бар P25–P75 + таблица. cian-price-changes fetch офлайн пуст → бейджи
|
||||||
|
* скидок не показываются (graceful), всё остальное берётся из estimate-пропа. */
|
||||||
|
export const Default = () => (
|
||||||
|
<ListingsCard estimate={FIXTURE_ESTIMATE} estimateId={FIXTURE_ESTIMATE.estimate_id} />
|
||||||
|
);
|
||||||
25
.design-sync/previews/LocationDrawer.tsx
Normal file
25
.design-sync/previews/LocationDrawer.tsx
Normal 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>
|
||||||
|
);
|
||||||
8
.design-sync/previews/MapCard.tsx
Normal file
8
.design-sync/previews/MapCard.tsx
Normal 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} />;
|
||||||
8
.design-sync/previews/MapPicker.tsx
Normal file
8
.design-sync/previews/MapPicker.tsx
Normal 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={() => {}} />
|
||||||
|
);
|
||||||
13
.design-sync/previews/NoAccessScreen.tsx
Normal file
13
.design-sync/previews/NoAccessScreen.tsx
Normal 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" />;
|
||||||
6
.design-sync/previews/OfferCard.tsx
Normal file
6
.design-sync/previews/OfferCard.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/PacingSection.tsx
Normal file
6
.design-sync/previews/PacingSection.tsx
Normal 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 />;
|
||||||
8
.design-sync/previews/PhotoUpload.tsx
Normal file
8
.design-sync/previews/PhotoUpload.tsx
Normal 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" />
|
||||||
|
);
|
||||||
8
.design-sync/previews/PlacementHistoryCard.tsx
Normal file
8
.design-sync/previews/PlacementHistoryCard.tsx
Normal 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" />
|
||||||
|
);
|
||||||
8
.design-sync/previews/PriceHistoryChart.tsx
Normal file
8
.design-sync/previews/PriceHistoryChart.tsx
Normal 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} />
|
||||||
|
);
|
||||||
24
.design-sync/previews/PriceRangeBar.tsx
Normal file
24
.design-sync/previews/PriceRangeBar.tsx
Normal 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} />);
|
||||||
5
.design-sync/previews/PriceTrendCard.tsx
Normal file
5
.design-sync/previews/PriceTrendCard.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/ProviderProxySection.tsx
Normal file
6
.design-sync/previews/ProviderProxySection.tsx
Normal 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" />;
|
||||||
8
.design-sync/previews/RecentSoldList.tsx
Normal file
8
.design-sync/previews/RecentSoldList.tsx
Normal 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} />
|
||||||
|
);
|
||||||
10
.design-sync/previews/ResultPanel.tsx
Normal file
10
.design-sync/previews/ResultPanel.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ResultPanel } from 'tradein-mvp-frontend';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResultPanel (v2) — центральная панель результата оценки на `/trade-in/v2`:
|
||||||
|
* одна цифра-герой (ожидаемая цена продажи / «оценка»), тиры median + ДКП,
|
||||||
|
* доверительные диапазоны, источники и мини-гистограмма распределения.
|
||||||
|
* Данные по умолчанию берутся из встроенной `RESULT_FIXTURE` компонента,
|
||||||
|
* поэтому карточке достаточно передать `onNavigate` (навигация по секциям).
|
||||||
|
*/
|
||||||
|
export const Default = () => <ResultPanel onNavigate={() => {}} />;
|
||||||
15
.design-sync/previews/RouteGuard.tsx
Normal file
15
.design-sync/previews/RouteGuard.tsx
Normal 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>
|
||||||
|
);
|
||||||
6
.design-sync/previews/RunsTable.tsx
Normal file
6
.design-sync/previews/RunsTable.tsx
Normal 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" />;
|
||||||
54
.design-sync/previews/SaleShareControls.tsx
Normal file
54
.design-sync/previews/SaleShareControls.tsx
Normal 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), тепловая
|
||||||
|
* окраска столбцов green→red повторяет цвет маркеров карты. Фильтры: город,
|
||||||
|
* цена, год постройки, тип дома, мин. объявлений, сортировка. */
|
||||||
|
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={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
50
.design-sync/previews/SaleShareList.tsx
Normal file
50
.design-sync/previews/SaleShareList.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
30
.design-sync/previews/SaleShareMap.tsx
Normal file
30
.design-sync/previews/SaleShareMap.tsx
Normal 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={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
25
.design-sync/previews/ScheduleControl.tsx
Normal file
25
.design-sync/previews/ScheduleControl.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
39
.design-sync/previews/SectionOverlay.tsx
Normal file
39
.design-sync/previews/SectionOverlay.tsx
Normal 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>
|
||||||
|
);
|
||||||
6
.design-sync/previews/SellTimeSensitivity.tsx
Normal file
6
.design-sync/previews/SellTimeSensitivity.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { SellTimeSensitivity } from 'tradein-mvp-frontend';
|
||||||
|
import { FIXTURE_SELLTIME } from './_fixtures';
|
||||||
|
|
||||||
|
/** «Срок продажи в зависимости от цены» — 4 бакета премии (−5% / медиана / +5% /
|
||||||
|
* +10%) с медианой экспозиции и p25–p75. Принимает data-пропом напрямую. */
|
||||||
|
export const Default = () => <SellTimeSensitivity data={FIXTURE_SELLTIME} />;
|
||||||
6
.design-sync/previews/SourcesProgress.tsx
Normal file
6
.design-sync/previews/SourcesProgress.tsx
Normal 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} />;
|
||||||
8
.design-sync/previews/StreetDealsCard.tsx
Normal file
8
.design-sync/previews/StreetDealsCard.tsx
Normal 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} />;
|
||||||
6
.design-sync/previews/SystemHealthSection.tsx
Normal file
6
.design-sync/previews/SystemHealthSection.tsx
Normal 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 />;
|
||||||
5
.design-sync/previews/TestPresets.tsx
Normal file
5
.design-sync/previews/TestPresets.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { TestPresets } from 'tradein-mvp-frontend';
|
||||||
|
|
||||||
|
/** Быстрое заполнение формы тестовыми квартирами ЕКБ — сетка из 6 preset-чипов
|
||||||
|
* (адрес + подсказка по покрытию). Self-contained (`<style>` внутри). */
|
||||||
|
export const Default = () => <TestPresets onPick={() => {}} />;
|
||||||
6
.design-sync/previews/Topbar.tsx
Normal file
6
.design-sync/previews/Topbar.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Topbar } from 'tradein-mvp-frontend';
|
||||||
|
|
||||||
|
/** Шапка приложения «Мера» — бренд-марка + навигация. useMe/useBrand офлайн
|
||||||
|
* не резолвятся → fallback показывает полный набор nav-айтемов; UserMenu сам
|
||||||
|
* рендерит null. Активная вкладка — «Оценка». */
|
||||||
|
export const Default = () => <Topbar active="estimate" />;
|
||||||
10
.design-sync/previews/UserMenu.tsx
Normal file
10
.design-sync/previews/UserMenu.tsx
Normal 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>
|
||||||
|
);
|
||||||
9
.design-sync/previews/WhatIfPanel.tsx
Normal file
9
.design-sync/previews/WhatIfPanel.tsx
Normal 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} />
|
||||||
|
);
|
||||||
3
.design-sync/previews/_fixtures.ts
Normal file
3
.design-sync/previews/_fixtures.ts
Normal 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';
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
@ -90,3 +89,8 @@ data/osrm/*
|
||||||
|
|
||||||
# Log cruft at repo root
|
# Log cruft at repo root
|
||||||
debug.log
|
debug.log
|
||||||
|
.ds-sync/
|
||||||
|
ds-bundle/
|
||||||
|
.design-sync/.cache/
|
||||||
|
.design-sync/learnings/
|
||||||
|
.design-sync/node_modules
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue