From e70f5da82b8ddf5045ab94431e645d014ad1b30f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 27 Jun 2026 14:40:50 +0300 Subject: [PATCH] chore(design-sync): sync inputs for claude.ai/design trade-in DS Imports tradein-mvp/frontend (38 components) to a claude.ai/design design-system project. Synth-entry build (Next.js app, no dist): - overrides/source-kit.mjs: process.env shim for browser IIFE - preview-provider.tsx: seeded TanStack Query provider (mirrors the repo's offline ui-preview client) so fetch-coupled + auth-gated cards render - previews/*.tsx: authored preview compositions (real fixture data) - conventions.md: README header for the design agent - config.json + NOTES.md: reproducible re-sync inputs --- .design-sync/AUTHORING.md | 79 ++++++++ .design-sync/NOTES.md | 34 ++++ .design-sync/config.json | 36 ++++ .design-sync/conventions.md | 50 +++++ .design-sync/overrides/source-kit.mjs | 178 ++++++++++++++++++ .design-sync/preview-provider.tsx | 61 ++++++ .design-sync/previews/AddressInput.tsx | 12 ++ .design-sync/previews/CianValuationCard.tsx | 6 + .design-sync/previews/DataQualitySection.tsx | 6 + .design-sync/previews/DealsCard.tsx | 6 + .design-sync/previews/DistributionCard.tsx | 25 +++ .design-sync/previews/EstimateForm.tsx | 13 ++ .design-sync/previews/ExposureCard.tsx | 25 +++ .design-sync/previews/HeroSummary.tsx | 13 ++ .design-sync/previews/HeroTransparency.tsx | 9 + .../previews/HouseAnalyticsKpiRow.tsx | 6 + .../previews/HouseAnalyticsSection.tsx | 9 + .design-sync/previews/HouseInfoCard.tsx | 6 + .design-sync/previews/IMVBenchmark.tsx | 6 + .design-sync/previews/ListingsCard.tsx | 9 + .design-sync/previews/MapCard.tsx | 8 + .design-sync/previews/MapPicker.tsx | 8 + .design-sync/previews/NoAccessScreen.tsx | 13 ++ .design-sync/previews/OfferCard.tsx | 6 + .design-sync/previews/PacingSection.tsx | 6 + .design-sync/previews/PhotoUpload.tsx | 8 + .../previews/PlacementHistoryCard.tsx | 8 + .design-sync/previews/PriceHistoryChart.tsx | 8 + .design-sync/previews/PriceRangeBar.tsx | 24 +++ .design-sync/previews/PriceTrendCard.tsx | 5 + .../previews/ProviderProxySection.tsx | 6 + .design-sync/previews/RecentSoldList.tsx | 8 + .design-sync/previews/ResultPanel.tsx | 36 ++++ .design-sync/previews/RouteGuard.tsx | 15 ++ .design-sync/previews/RunsTable.tsx | 6 + .design-sync/previews/ScheduleControl.tsx | 25 +++ .design-sync/previews/SellTimeSensitivity.tsx | 6 + .design-sync/previews/SourcesProgress.tsx | 6 + .design-sync/previews/StreetDealsCard.tsx | 8 + .design-sync/previews/SystemHealthSection.tsx | 6 + .design-sync/previews/TestPresets.tsx | 5 + .design-sync/previews/Topbar.tsx | 6 + .design-sync/previews/UserMenu.tsx | 10 + .design-sync/previews/WhatIfPanel.tsx | 9 + .design-sync/previews/_fixtures.ts | 3 + .gitignore | 5 + 46 files changed, 843 insertions(+) create mode 100644 .design-sync/AUTHORING.md create mode 100644 .design-sync/NOTES.md create mode 100644 .design-sync/config.json create mode 100644 .design-sync/conventions.md create mode 100644 .design-sync/overrides/source-kit.mjs create mode 100644 .design-sync/preview-provider.tsx create mode 100644 .design-sync/previews/AddressInput.tsx create mode 100644 .design-sync/previews/CianValuationCard.tsx create mode 100644 .design-sync/previews/DataQualitySection.tsx create mode 100644 .design-sync/previews/DealsCard.tsx create mode 100644 .design-sync/previews/DistributionCard.tsx create mode 100644 .design-sync/previews/EstimateForm.tsx create mode 100644 .design-sync/previews/ExposureCard.tsx create mode 100644 .design-sync/previews/HeroSummary.tsx create mode 100644 .design-sync/previews/HeroTransparency.tsx create mode 100644 .design-sync/previews/HouseAnalyticsKpiRow.tsx create mode 100644 .design-sync/previews/HouseAnalyticsSection.tsx create mode 100644 .design-sync/previews/HouseInfoCard.tsx create mode 100644 .design-sync/previews/IMVBenchmark.tsx create mode 100644 .design-sync/previews/ListingsCard.tsx create mode 100644 .design-sync/previews/MapCard.tsx create mode 100644 .design-sync/previews/MapPicker.tsx create mode 100644 .design-sync/previews/NoAccessScreen.tsx create mode 100644 .design-sync/previews/OfferCard.tsx create mode 100644 .design-sync/previews/PacingSection.tsx create mode 100644 .design-sync/previews/PhotoUpload.tsx create mode 100644 .design-sync/previews/PlacementHistoryCard.tsx create mode 100644 .design-sync/previews/PriceHistoryChart.tsx create mode 100644 .design-sync/previews/PriceRangeBar.tsx create mode 100644 .design-sync/previews/PriceTrendCard.tsx create mode 100644 .design-sync/previews/ProviderProxySection.tsx create mode 100644 .design-sync/previews/RecentSoldList.tsx create mode 100644 .design-sync/previews/ResultPanel.tsx create mode 100644 .design-sync/previews/RouteGuard.tsx create mode 100644 .design-sync/previews/RunsTable.tsx create mode 100644 .design-sync/previews/ScheduleControl.tsx create mode 100644 .design-sync/previews/SellTimeSensitivity.tsx create mode 100644 .design-sync/previews/SourcesProgress.tsx create mode 100644 .design-sync/previews/StreetDealsCard.tsx create mode 100644 .design-sync/previews/SystemHealthSection.tsx create mode 100644 .design-sync/previews/TestPresets.tsx create mode 100644 .design-sync/previews/Topbar.tsx create mode 100644 .design-sync/previews/UserMenu.tsx create mode 100644 .design-sync/previews/WhatIfPanel.tsx create mode 100644 .design-sync/previews/_fixtures.ts diff --git a/.design-sync/AUTHORING.md b/.design-sync/AUTHORING.md new file mode 100644 index 00000000..08a2bec4 --- /dev/null +++ b/.design-sync/AUTHORING.md @@ -0,0 +1,79 @@ +# Preview authoring guide (design-sync, tradein-mvp-frontend) + +You author `.design-sync/previews/.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//.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/.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 ` +3. Capture ONLY your components: + `node .ds-sync/package-capture.mjs --out ./ds-bundle --components ` +4. READ each `ds-bundle/_screenshots/review/__.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/.grade.json`: + `{"cells":{"":{"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/.tsx`, your `.cache/review/.grade.json`, and your + `.design-sync/learnings/.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/.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..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). diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 00000000..3dc9d62e --- /dev/null +++ b/.design-sync/NOTES.md @@ -0,0 +1,34 @@ +# design-sync NOTES — tradein-mvp-frontend + +Target: `tradein-mvp/frontend` (NOT the repo-root `frontend/`, which is the Site-Finder app). Project: gendesign-tradein. + +## Build gotchas +- **No dist / no Storybook → synth-entry mode.** The package is a Next.js app, not a lib; `package.json` has no `main`/`module`/`exports`. +- **`--entry` must point at a NON-existent dist path** (`tradein-mvp/frontend/dist/index.js`). Two jobs: + 1. PKG_DIR resolution walks up `dirname(--entry)` to the real named `package.json` → PKG_DIR=`tradein-mvp/frontend` (without `--entry`, PKG_DIR defaults to `node_modules/` 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. diff --git a/.design-sync/config.json b/.design-sync/config.json new file mode 100644 index 00000000..cb6cd504 --- /dev/null +++ b/.design-sync/config.json @@ -0,0 +1,36 @@ +{ + "projectId": "e4b235af-089b-4532-8025-5199a2695a12", + "shape": "package", + "pkg": "tradein-mvp-frontend", + "globalName": "TradeInUI", + "srcDir": "src", + "tsconfig": "tsconfig.json", + "cssEntry": "src/components/trade-in/trade-in.css", + "tokensGlob": "src/app/globals.css", + "buildCmd": "node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules tradein-mvp/frontend/node_modules --entry tradein-mvp/frontend/dist/index.js --out ./ds-bundle", + "libOverrides": { + "source-kit.mjs": "synth-entry process shim for Next.js app (process.env.NEXT_PUBLIC_* reads)" + }, + "componentSrcMap": { + "Error": null, + "GlobalError": null, + "Providers": null, + "RootLayout": null, + "AvitoScraperPage": null, + "CachePage": null, + "CianScraperPage": null, + "HistoryPage": null, + "PreviewEstimatePage": null, + "ScraperPage": null, + "ScrapersUnifiedPage": null, + "TradeInPage": null, + "YandexScraperPage": null + }, + "provider": { + "component": "PreviewProvider" + }, + "extraEntries": [ + "../../.design-sync/preview-provider.tsx" + ], + "readmeHeader": ".design-sync/conventions.md" +} diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md new file mode 100644 index 00000000..90d33bff --- /dev/null +++ b/.design-sync/conventions.md @@ -0,0 +1,50 @@ +# Trade-In UI — how to build with this design system + +React components from the GenDesign **trade-in** product (real-estate trade-in valuation, RU/ЕКБ). Import everything from `tradein-mvp-frontend` (bound at `window.TradeInUI`). The cards are domain-specific and **prop-driven** — you compose them with data, you don't restyle their internals. + +## Setup & wrapping (required for data components) + +Most cards take their data as **props** and render standalone. But several read app state through **TanStack Query** hooks (`UserMenu`, `Topbar`, `RouteGuard` → `useMe`; `HouseAnalyticsSection`, `PlacementHistoryCard`, `StreetDealsCard` → estimate sub-queries). Those MUST be rendered inside a `QueryClientProvider` (the app ships one as `Providers`). Without it they throw "No QueryClient set"; with an empty client they render their loading/null state. Prop-only cards (`HeroSummary`, `OfferCard`, `PriceRangeBar`, `DealsCard`, `ListingsCard`, charts…) need no provider. + +```tsx +import { Providers, HeroSummary, OfferCard } from "tradein-mvp-frontend"; + + +
+ + +
+
+``` + +Load `styles.css` once at the root — it pulls the component CSS + design tokens. + +## Styling idiom — global stylesheet + CSS custom properties + +This is **not Tailwind and not CSS-in-JS**. Styling is a **global stylesheet** of semantic class names plus **CSS custom-property design tokens**. Two rules: + +1. **The components carry their own classes** (`.card`, `.card-head`, `.section-kicker`, `.count-strip`, `.pricebar`, `.source-chip`, `.control`, `.pill`, `.mono`, `.top-nav`, `.meta-grid`…). Don't reach inside them; compose via props. New class names you invent will not exist in the stylesheet. +2. **For your own layout/wrapper glue, use the token vars + inline styles**, on the 4/8/12/16/24/32 spacing scale (tabular-nums for numbers). Color/shape tokens, all defined in the shipped CSS: + +| Group | Tokens | +|---|---| +| Surface | `--bg-app` `--bg-card` `--bg-card-alt` `--bg-headline` | +| Text | `--fg-primary` `--fg-secondary` `--fg-tertiary` `--fg-on-dark` | +| Brand/CTA | `--accent` `--accent-hover` `--accent-soft` `--accent-2` | +| Semantic | `--success` `--warn` `--danger` (`*-soft` variants) | +| Border | `--border-soft` `--border-card` `--border-strong` | +| Shape/type | `--radius` `--radius-sm` `--radius-lg` `--shadow-md` `--font-sans` (Manrope) `--font-mono` `--container` | + +```tsx +
+ 9 850 000 ₽ +
+``` + +## Where the truth is + +- **Styles/tokens:** read the bound `styles.css` and its `@import` `_ds_bundle.css` — the authoritative class + token list. +- **Per component:** `.d.ts` (props) and `.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. diff --git a/.design-sync/overrides/source-kit.mjs b/.design-sync/overrides/source-kit.mjs new file mode 100644 index 00000000..6faac436 --- /dev/null +++ b/.design-sync/overrides/source-kit.mjs @@ -0,0 +1,178 @@ +// Non-storybook `package` adapter. Bundles dist/ when present (the authoritative +// component list comes from shipped .d.ts; with no dist it synthesizes an +// entry from src/ as a last resort) and opportunistically enriches each +// component from src/ — JSDoc and dir-derived group. Every enrichment miss +// degrades to the plain-dist behaviour. +// +// Discovery is heuristic-based; each heuristic has a `.design-sync/config.json` +// override (ASSUMPTION comments below name them) so repos that don't match the +// defaults write config, not code. `componentSrcMap` is the single override +// knob for component inclusion: non-null value = add/pin src path, null = +// exclude a .d.ts-exported internal. + +import { existsSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { Project, Node, ts } from 'ts-morph'; +// forked from design-sync lib/source-kit.mjs — synth-entry process shim (Next.js app reads process.env.NEXT_PUBLIC_*) +import { leadingJsdoc, readText, slash, walk } from '../../.ds-sync/lib/common.mjs'; +import { resolveDistEntry } from '../../.ds-sync/lib/bundle.mjs'; +import { exportedNames, isComponentName } from '../../.ds-sync/lib/dts.mjs'; + +const NON_IMPL_RX = /\.(stories|test|spec)\./; +const SRC_IMPL_RX = /\.(tsx|jsx)$/; +// Dir names that don't usefully group components — skip so the emitted path +// is `components//` not `components/components/`. +const GENERIC_DIR = new Set(['components', 'component', 'src', 'lib', 'ui', 'packages', 'react']); +const slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'general'; + +// No .d.ts → scan src files for PascalCase value exports via ts-morph. +function deriveComponentsFromSrc(srcFiles) { + const project = new Project({ + skipAddingFilesFromTsConfig: true, + compilerOptions: { jsx: ts.JsxEmit.Preserve, allowJs: true, skipLibCheck: true }, + }); + const seen = new Set(); + for (const p of srcFiles) { + if (NON_IMPL_RX.test(p) || !SRC_IMPL_RX.test(p)) continue; + const sf = project.addSourceFileAtPathIfExists(p); + if (!sf) continue; + for (const [name, decls] of sf.getExportedDeclarations()) { + // `export default function Button()` is keyed as 'default' — recover + // the declared name from the function/class node. + const real = name === 'default' + ? decls.map((d) => d.getName?.()).find((n) => n && n !== 'default') + : name; + if (!real || !/^[A-Z][A-Za-z0-9]*$/.test(real)) continue; + if (decls.some((d) => Node.isVariableDeclaration(d) || Node.isFunctionDeclaration(d) || Node.isClassDeclaration(d))) { + seen.add(real); + } + } + } + return [...seen].sort().map((name) => ({ name, group: 'general' })); +} + +export async function resolvePackage(ctx) { + const { PKG_DIR, pkgJson, ENTRY_OVERRIDE, PKG, OUT, cfg } = ctx; + const srcMap = cfg.componentSrcMap ?? {}; + + // ── 1. src/ discovery (best-effort; feeds enrichment + synth-entry fallback). + // ASSUMPTION: source root is first of src/ | lib/ | components/. Override: cfg.srcDir. + const srcRoot = [cfg.srcDir, 'src', 'lib', 'components'] + .map((d) => d && resolve(PKG_DIR, d)) + .find((d) => d && existsSync(d)); + const srcFiles = srcRoot ? walk(srcRoot, (n) => /\.(tsx|jsx|mdx?)$/.test(n)) : []; + + // ── 2. entry: dist if it exists, else synthesize from src/ (last resort). + let entry = resolveDistEntry({ pkgDir: PKG_DIR, pkgJson, override: ENTRY_OVERRIDE, pkgName: PKG, soft: true }); + let synthEntry = false; + if (!entry) { + if (!srcRoot) { + console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`); + process.exit(1); + } + const comps = srcFiles.filter((p) => SRC_IMPL_RX.test(p) && !NON_IMPL_RX.test(p)); + // Next.js app code reads process.env.NEXT_PUBLIC_* at module top-level; in + // the browser IIFE `process` is undefined → every component throws. Emit a + // shim module and import it FIRST (ESM evaluates the first import's body + // before the re-exported component modules), so process exists before any + // component body runs. + const shim = resolve(OUT, '.pkg-shim.mjs'); + writeFileSync( + shim, + 'globalThis.process ??= { env: {} };\n' + + 'globalThis.process.env ??= {};\n' + + 'const __e = globalThis.process.env;\n' + + "__e.NODE_ENV ??= 'development';\n" + + "__e.NEXT_PUBLIC_ENABLE_PREVIEW ??= '1';\n" + + "__e.NEXT_PUBLIC_BASE_PATH ??= '';\n" + + "__e.NEXT_PUBLIC_API_BASE_URL ??= '';\n" + + "__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n", + ); + entry = join(OUT, '.pkg-entry.mjs'); + writeFileSync( + entry, + `import ${JSON.stringify(slash(shim))};\n` + + comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') + + '\n', + ); + synthEntry = true; + console.error( + `[NO_DIST] no built entry — synthesizing from ${comps.length} src files (run the package's build for best results)`, + ); + } + + // ── 3. component list: from shipped .d.ts (authoritative when dist exists). + // ASSUMPTION: components = PascalCase value exports in the .d.ts tree. + // Override: cfg.componentSrcMap (non-null adds/pins, null excludes). + const exported = exportedNames(PKG_DIR, pkgJson); + const names = new Set([...exported].filter(isComponentName)); + for (const [k, v] of Object.entries(srcMap)) { + if (v === null) { names.delete(k); continue; } + // Names reach `