diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/LICENSE b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/LICENSE new file mode 100644 index 0000000..db9ad82 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 uixmat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/README.md b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/README.md new file mode 100644 index 0000000..066a873 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/README.md @@ -0,0 +1,192 @@ +![hero](https://github.com/bklit/bklit-ui/blob/main/apps/web/app/opengraph-image.png?raw=true) + + +
+

+vercel-oss 2 +

+
+

+ A collection of Open Source charts and utility components that you can customize and extend. +
+
+ GitHub Stars + CI + Discord Members + X Follow + Vercel OSS Program Member +

+

+ + +## Premium Sponsors + +Bklit UI is supported by our premium sponsors. + +
+ +OpenPanel + + +**[OpenPanel](https://openpanel.dev)** — OpenPanel is an open source analytics platform that combines web analytics and product analytics in one privacy-first tool. Track pageviews, events, funnels, retention, and user journeys — all without cookies. + +## Charts + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Area Chart + +
+ Area Chart
+ Docs · Studio +
+ + Bar Chart + +
+ Bar Chart
+ Docs · Studio +
+ + Candlestick Chart + +
+ Candlestick Chart
+ Docs · Studio +
+ + Choropleth Chart + +
+ Choropleth Chart
+ Docs · Studio +
+ + Composed Chart + +
+ Composed Chart
+ Docs · Studio +
+ + Funnel Chart + +
+ Funnel Chart
+ Docs · Studio +
+ + Gauge + +
+ Gauge
+ Docs · Studio +
+ + Line Chart + +
+ Line Chart
+ Docs · Studio +
+ + Live Line Chart + +
+ Live Line Chart
+ Docs · Studio +
+ + Pie Chart + +
+ Pie Chart
+ Docs · Studio +
+ + Radar Chart + +
+ Radar Chart
+ Docs · Studio +
+ + Ring Chart + +
+ Ring Chart
+ Docs · Studio +
+ + Scatter Chart + +
+ Scatter Chart
+ Docs · Studio +
+ + Sankey Chart + +
+ Sankey Chart
+ Docs · Studio +
+ +## Studio + +Studio is Bklit's interactive chart playground. Pick a chart, tweak styling and animation controls in real time, then copy the generated React code or export a registry JSON for your project. + +

+ Bklit Studio +

+ +[Open Studio →](https://ui.bklit.com/studio) + +## Getting Started + +Bklit UI is a shadcn registry. Initialize shadcn in your project, then add a chart: + +```bash +npx shadcn@latest init +npx shadcn@latest add @bklit/line-chart +``` + +See [Installation](https://ui.bklit.com/docs/installation) for registry setup and all components. + +## Repo activity + +![Alt](https://repobeats.axiom.co/api/embed/c591b93fd9e7bfa8f4dc8bddb716699615ee5fde.svg "Repobeats analytics image") + +## License + +**Chart components** (`packages/ui`, shadcn registry) are [MIT licensed](./LICENSE). + +**Bklit Studio** (`packages/studio`, hosted at [ui.bklit.com/studio](https://ui.bklit.com/studio)) +is proprietary. See [LICENSE-STUDIO.md](./LICENSE-STUDIO.md). You may not reuse, +resell, or redistribute Studio without written permission. diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/UPSTREAM.md b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/UPSTREAM.md new file mode 100644 index 0000000..d6ad130 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/UPSTREAM.md @@ -0,0 +1,35 @@ +# Bklit UI Vendor Snapshot + +Source: https://github.com/bklit/bklit-ui +Commit: 86149e76b509b661aca26af95461b18952eda646 +Snapshot date: 2026-06-28 + +Included: + +- `LICENSE` +- `README.md` +- `packages/ui` +- `apps/web/public/svgs` + +Excluded: + +- `.git` +- `node_modules` +- `apps/studio` +- `packages/studio` + +License notes: + +- The root Bklit UI repository is MIT licensed. +- Bklit Studio has a separate restrictive license and is intentionally not vendored here. +- Treat this directory as a local source/reference snapshot. Do not import it into the runtime directly until the selected chart components and their dependencies are copied into the app layer intentionally. + +Candidate SEO module uses: + +- `funnel-chart`: pipeline/readiness from context review to market demand, SEO plan, rewrite, validation. +- `bar-chart`: top clusters, pages, intents, and demand buckets. +- `scatter-chart`: opportunity map by demand, relevance, confidence, and implementation effort. +- `sankey-chart`: context/intent to keyword/page/field flow for dev-mode explainability. +- `gauge-chart` or `ring-chart`: compact quality/readiness scores. +- `area-chart`, `line-chart`, `composed-chart`: historical demand, forecast, and validation trend screens after time-series data exists. +- `heatmap-chart`: keyword/page coverage matrix if the table becomes too dense. diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/area-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/area-chart.svg new file mode 100644 index 0000000..82646ea --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/area-chart.svg @@ -0,0 +1,44 @@ +
Jan 1
Jan 3
Jan 6
Jan 9
Jan 12
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/bar-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/bar-chart.svg new file mode 100644 index 0000000..8f86808 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/bar-chart.svg @@ -0,0 +1,44 @@ +
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/candlestick-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/candlestick-chart.svg new file mode 100644 index 0000000..64c7264 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/candlestick-chart.svg @@ -0,0 +1,44 @@ +
Jan 1
Jan 8
Jan 15
Jan 22
Jan 30
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/choropleth-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/choropleth-chart.svg new file mode 100644 index 0000000..2ba32d9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/choropleth-chart.svg @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/composed-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/composed-chart.svg new file mode 100644 index 0000000..49aa48e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/composed-chart.svg @@ -0,0 +1,44 @@ +
Jan 1
Jan 5
Jan 10
Jan 15
Jan 20
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/funnel-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/funnel-chart.svg new file mode 100644 index 0000000..e660491 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/funnel-chart.svg @@ -0,0 +1,44 @@ +
10,000
100%
Visitors
4,200
42%
Sign-ups
2,100
21%
Activated
890
9%
Subscribed
520
5%
Retained
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/gauge-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/gauge-chart.svg new file mode 100644 index 0000000..48e2c2e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/gauge-chart.svg @@ -0,0 +1,44 @@ +
$284,920Total Revenue
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/line-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/line-chart.svg new file mode 100644 index 0000000..115c257 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/line-chart.svg @@ -0,0 +1,44 @@ +
Jan 1
Jan 3
Jan 6
Jan 9
Jan 12
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/live-line-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/live-line-chart.svg new file mode 100644 index 0000000..907fac9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/live-line-chart.svg @@ -0,0 +1,44 @@ +
12:18:52
12:19:00
12:19:07
12:19:15
12:19:22
87.50
90.00
92.50
95.00
97.50
100.00
102.50
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/pie-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/pie-chart.svg new file mode 100644 index 0000000..a0900ad --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/pie-chart.svg @@ -0,0 +1,44 @@ +
925Total
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/radar-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/radar-chart.svg new file mode 100644 index 0000000..8495524 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/radar-chart.svg @@ -0,0 +1,23 @@ +20406080100SpeedReliabilityComfortEfficiencySafety \ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/ring-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/ring-chart.svg new file mode 100644 index 0000000..164fcac --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/ring-chart.svg @@ -0,0 +1,44 @@ +
11,050Channels
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/sankey-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/sankey-chart.svg new file mode 100644 index 0000000..5c32eb4 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/sankey-chart.svg @@ -0,0 +1,23 @@ +A60 sessionsB65 sessionsX70 sessionsY55 sessionsZ125 sessions \ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/scatter-chart.svg b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/scatter-chart.svg new file mode 100644 index 0000000..5208203 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/scatter-chart.svg @@ -0,0 +1,44 @@ +
Jan 1
Jan 8
Jan 15
Jan 22
Jan 30
\ No newline at end of file diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/typescript-config/react-library.json b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/typescript-config/react-library.json new file mode 100644 index 0000000..c90a0f3 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/typescript-config/react-library.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + } +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/eslint.config.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/eslint.config.mjs new file mode 100644 index 0000000..0a63d82 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/eslint.config.mjs @@ -0,0 +1,2 @@ +// biome-ignore lint/performance/noBarrelFile: ESLint config re-export +export { config as default } from "@bklitui/eslint-config/react-internal"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/package.json b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/package.json new file mode 100644 index 0000000..b86f0ea --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/package.json @@ -0,0 +1,81 @@ +{ + "name": "@bklitui/ui", + "version": "0.0.0", + "license": "MIT", + "private": true, + "sideEffects": false, + "exports": { + "./charts": "./src/charts/index.ts", + "./lib/utils": "./src/lib/utils.ts", + "./components/shimmering-text": "./src/components/shimmering-text.tsx", + "./button": "./src/button.tsx", + "./card": "./src/card.tsx", + "./code": "./src/code.tsx", + "./chart-grid": "./src/chart-grid.tsx", + "./chart-marker": "./src/chart-marker.tsx", + "./line-chart": "./src/line-chart.tsx", + "./tooltip": "./src/tooltip.tsx", + "./*": "./src/*.tsx" + }, + "scripts": { + "lint": "eslint . --max-warnings 0", + "check-types": "tsc --noEmit", + "test": "node scripts/run-tests.mjs", + "registry:build": "node scripts/verify-registry-files.mjs && node scripts/verify-registry-deps.mjs && node scripts/generate-v0-examples.mjs && node scripts/generate-block-registry.mjs && shadcn build --output ../../apps/web/public/r && cp registry.json ../../apps/web/public/r/registry.json", + "registry:verify": "node scripts/verify-registry-files.mjs && node scripts/verify-registry-deps.mjs && node scripts/verify-registry-sync.mjs" + }, + "devDependencies": { + "@bklitui/eslint-config": "workspace:*", + "@bklitui/typescript-config": "workspace:*", + "@types/d3-array": "^3.2.1", + "@types/d3-geo": "^3.1.0", + "@types/d3-sankey": "^0.12.5", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.6", + "@types/geojson": "^7946.0.16", + "@types/node": "^22.15.3", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.2", + "@types/topojson-client": "^3.1.5", + "@types/topojson-specification": "^1.0.5", + "eslint": "^9.39.1", + "shadcn": "^2.5.0", + "tailwindcss": "^4.1.8", + "tsx": "^4.19.4", + "typescript": "5.9.2" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "dependencies": { + "@bklitui/icons": "workspace:*", + "@base-ui/react": "^1.0.0-alpha.8", + "@number-flow/react": "^0.5.4", + "@visx/brush": "3.12.0", + "@visx/curve": "4.0.1-alpha.0", + "@visx/event": "4.0.1-alpha.0", + "@visx/geo": "4.0.1-alpha.0", + "@visx/gradient": "4.0.1-alpha.0", + "@visx/grid": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/heatmap": "4.0.1-alpha.0", + "@visx/pattern": "4.0.1-alpha.0", + "@visx/responsive": "4.0.1-alpha.0", + "@visx/sankey": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "@visx/zoom": "4.0.1-alpha.0", + "clsx": "^2.1.1", + "d3-array": "3.2.4", + "d3-geo": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "^3.2.0", + "motion": "^12.27.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-use-measure": "^2.1.7", + "tailwind-merge": "^2.6.0", + "topojson-client": "3.1.0" + } +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry.json b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry.json new file mode 100644 index 0000000..ceb52d1 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry.json @@ -0,0 +1,2307 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry.json", + "name": "bklit-ui", + "homepage": "https://ui.bklit.com", + "items": [ + { + "name": "utils", + "type": "registry:lib", + "title": "Utilities", + "description": "Utility functions for className merging with clsx and tailwind-merge", + "dependencies": [ + "clsx", + "tailwind-merge" + ], + "files": [ + { + "path": "src/lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + } + ] + }, + { + "name": "shimmering-text", + "type": "registry:component", + "title": "Shimmering Text", + "description": "Per-character shimmer animation for loading labels and badges", + "registryDependencies": [ + "@bklit/utils" + ], + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "src/components/shimmering-text.tsx", + "type": "registry:component", + "target": "components/shimmering-text.tsx" + } + ] + }, + { + "name": "chart-utils", + "type": "registry:lib", + "title": "Chart Utilities", + "description": "Shared Intl formatters, time-series decimation, and RAF tooltip scheduling for Bklit charts", + "files": [ + { + "path": "src/charts/chart-formatters.ts", + "type": "registry:lib", + "target": "components/charts/chart-formatters.ts" + }, + { + "path": "src/charts/decimate-time-series.ts", + "type": "registry:lib", + "target": "components/charts/decimate-time-series.ts" + }, + { + "path": "src/charts/use-scheduled-tooltip.ts", + "type": "registry:lib", + "target": "components/charts/use-scheduled-tooltip.ts" + } + ] + }, + { + "name": "chart-context", + "type": "registry:component", + "title": "Chart Context", + "description": "Shared context and hooks for Bklit chart components", + "registryDependencies": [ + "@bklit/utils", + "@bklit/chart-utils" + ], + "dependencies": [ + "@visx/event@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/scale@4.0.1-alpha.0", + "d3-array", + "motion" + ], + "cssVars": { + "light": { + "--chart-1": "oklch(0.32 0 none)", + "--chart-2": "oklch(0.41 0 none)", + "--chart-3": "oklch(0.54 0 none)", + "--chart-4": "oklch(0.71 0 none)", + "--chart-5": "oklch(0.89 0 none)", + "--chart-background": "oklch(1 0 0)", + "--chart-foreground": "oklch(0.145 0.004 285)", + "--chart-foreground-muted": "oklch(0.55 0.014 260)", + "--chart-line-primary": "var(--chart-1)", + "--chart-line-secondary": "var(--chart-2)", + "--chart-crosshair": "oklch(0.4 0.1828 274.34)", + "--chart-grid": "oklch(0.9 0 0)", + "--chart-brush-border": "var(--chart-grid)", + "--chart-tooltip-background": "oklch(0.21 0.006 285 / 0.8)", + "--chart-tooltip-foreground": "oklch(0.985 0 0)", + "--chart-tooltip-muted": "oklch(0.65 0.01 260)", + "--chart-marker-background": "oklch(0.97 0.005 260)", + "--chart-marker-border": "oklch(0.85 0.01 260)", + "--chart-marker-foreground": "oklch(0.3 0.01 260)", + "--chart-label": "oklch(0.45 0.01 260)", + "--chart-scale-01": "oklch(0.98 0.003 106)", + "--chart-scale-02": "oklch(0.92 0.008 106)", + "--chart-scale-03": "oklch(0.82 0.015 106)", + "--chart-scale-04": "oklch(0.68 0.02 106)", + "--chart-scale-05": "oklch(0.55 0.025 106)", + "--chart-scale-pattern-color": "oklch(0.96 0.005 106)" + }, + "dark": { + "--chart-1": "oklch(1 0 none)", + "--chart-2": "oklch(0.73 0 none)", + "--chart-3": "oklch(0.51 0 none)", + "--chart-4": "oklch(0.39 0 none)", + "--chart-5": "oklch(0.32 0 none)", + "--chart-background": "oklch(0.145 0 0)", + "--chart-foreground": "oklch(0.45 0 0)", + "--chart-foreground-muted": "oklch(0.65 0.01 260)", + "--chart-crosshair": "oklch(0.45 0 0)", + "--chart-grid": "oklch(0.25 0 0)", + "--chart-brush-border": "var(--chart-grid)", + "--chart-marker-background": "oklch(0.25 0.01 260)", + "--chart-marker-border": "oklch(0.4 0.01 260)", + "--chart-marker-foreground": "oklch(0.9 0 0)", + "--chart-label": "oklch(0.75 0.01 260)", + "--chart-scale-01": "oklch(0.28 0.03 270)", + "--chart-scale-02": "oklch(0.38 0.04 270)", + "--chart-scale-03": "oklch(0.48 0.05 270)", + "--chart-scale-04": "oklch(0.58 0.06 270)", + "--chart-scale-05": "oklch(0.68 0.07 270)", + "--chart-scale-pattern-color": "oklch(0.145 0 0)" + } + }, + "files": [ + { + "path": "src/charts/chart-context.tsx", + "type": "registry:component", + "target": "components/charts/chart-context.tsx" + }, + { + "path": "src/charts/reference-area-config.ts", + "type": "registry:lib", + "target": "components/charts/reference-area-config.ts" + }, + { + "path": "src/charts/use-chart-interaction.ts", + "type": "registry:component", + "target": "components/charts/use-chart-interaction.ts" + }, + { + "path": "src/charts/y-axis-scales.ts", + "type": "registry:lib", + "target": "components/charts/y-axis-scales.ts" + }, + { + "path": "src/charts/y-axis-ticks.ts", + "type": "registry:lib", + "target": "components/charts/y-axis-ticks.ts" + }, + { + "path": "src/charts/chart-phase.ts", + "type": "registry:component", + "target": "components/charts/chart-phase.ts" + }, + { + "path": "src/charts/y-domain-utils.ts", + "type": "registry:lib", + "target": "components/charts/y-domain-utils.ts" + }, + { + "path": "src/charts/filter-data-by-x-domain.ts", + "type": "registry:lib", + "target": "components/charts/filter-data-by-x-domain.ts" + }, + { + "path": "src/charts/generate-chart-skeleton-data.ts", + "type": "registry:lib", + "target": "components/charts/generate-chart-skeleton-data.ts" + }, + { + "path": "src/charts/use-animated-y-domains.ts", + "type": "registry:component", + "target": "components/charts/use-animated-y-domains.ts" + }, + { + "path": "src/charts/use-chart-phase-orchestrator.ts", + "type": "registry:component", + "target": "components/charts/use-chart-phase-orchestrator.ts" + }, + { + "path": "src/charts/line-loading-timing.ts", + "type": "registry:lib", + "target": "components/charts/line-loading-timing.ts" + } + ] + }, + { + "name": "chart-animation", + "type": "registry:component", + "title": "Chart Animation", + "description": "Shared motion helpers for chart enter transitions, clip reveals, and staggered animations", + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "src/charts/animation.ts", + "type": "registry:component", + "target": "components/charts/animation.ts" + }, + { + "path": "src/charts/motion-utils.ts", + "type": "registry:component", + "target": "components/charts/motion-utils.ts" + }, + { + "path": "src/charts/use-mount-progress.ts", + "type": "registry:component", + "target": "components/charts/use-mount-progress.ts" + }, + { + "path": "src/charts/use-enter-complete.ts", + "type": "registry:component", + "target": "components/charts/use-enter-complete.ts" + }, + { + "path": "src/charts/chart-reveal-clip.tsx", + "type": "registry:component", + "target": "components/charts/chart-reveal-clip.tsx" + }, + { + "path": "src/charts/static-chart-preview-context.tsx", + "type": "registry:component", + "target": "components/charts/static-chart-preview-context.tsx" + }, + { + "path": "src/charts/chart-defs.ts", + "type": "registry:component", + "target": "components/charts/chart-defs.ts" + } + ] + }, + { + "name": "chart-series", + "type": "registry:component", + "title": "Chart Series Layer", + "description": "Shared line and area series rendering helpers (paths, markers, highlights, dash tails)", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/path-stroke-utils.ts", + "type": "registry:component", + "target": "components/charts/path-stroke-utils.ts" + }, + { + "path": "src/charts/highlight-segment-bounds.ts", + "type": "registry:component", + "target": "components/charts/highlight-segment-bounds.ts" + }, + { + "path": "src/charts/highlight-segment.tsx", + "type": "registry:component", + "target": "components/charts/highlight-segment.tsx" + }, + { + "path": "src/charts/dash-tail-stroke.tsx", + "type": "registry:component", + "target": "components/charts/dash-tail-stroke.tsx" + }, + { + "path": "src/charts/series-dash-tail-overlay.tsx", + "type": "registry:component", + "target": "components/charts/series-dash-tail-overlay.tsx" + }, + { + "path": "src/charts/series-highlight-layer.tsx", + "type": "registry:component", + "target": "components/charts/series-highlight-layer.tsx" + }, + { + "path": "src/charts/chart-legend-hover.tsx", + "type": "registry:component", + "target": "components/charts/chart-legend-hover.tsx" + }, + { + "path": "src/charts/series-hover-dim.tsx", + "type": "registry:component", + "target": "components/charts/series-hover-dim.tsx" + }, + { + "path": "src/charts/series-point-marker.tsx", + "type": "registry:component", + "target": "components/charts/series-point-marker.tsx" + }, + { + "path": "src/charts/series-markers.tsx", + "type": "registry:component", + "target": "components/charts/series-markers.tsx" + }, + { + "path": "src/charts/use-highlight-segment.ts", + "type": "registry:component", + "target": "components/charts/use-highlight-segment.ts" + }, + { + "path": "src/charts/area-gradient-defs.tsx", + "type": "registry:component", + "target": "components/charts/area-gradient-defs.tsx" + }, + { + "path": "src/charts/fade-edges.ts", + "type": "registry:component", + "target": "components/charts/fade-edges.ts" + } + ] + }, + { + "name": "grid", + "type": "registry:component", + "title": "Chart Grid", + "description": "Grid lines for charts", + "registryDependencies": [ + "@bklit/chart-context" + ], + "dependencies": [ + "@visx/grid@4.0.1-alpha.0" + ], + "files": [ + { + "path": "src/charts/grid.tsx", + "type": "registry:component", + "target": "components/charts/grid.tsx" + }, + { + "path": "src/charts/use-grid-shimmer.ts", + "type": "registry:component", + "target": "components/charts/use-grid-shimmer.ts" + } + ] + }, + { + "name": "reference-area", + "type": "registry:component", + "title": "Reference Area", + "description": "Shaded data-coordinate band for target ranges and thresholds on time-series charts", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/background" + ], + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "src/charts/reference-area.tsx", + "type": "registry:component", + "target": "components/charts/reference-area.tsx" + }, + { + "path": "src/charts/reference-area-geometry.ts", + "type": "registry:component", + "target": "components/charts/reference-area-geometry.ts" + }, + { + "path": "src/charts/reference-area-registration-context.tsx", + "type": "registry:component", + "target": "components/charts/reference-area-registration-context.tsx" + } + ] + }, + { + "name": "projection-line", + "type": "registry:component", + "title": "Projection Line", + "description": "Forecast segment extending a line series past the last data point", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/line-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/projection-line.tsx", + "type": "registry:component", + "target": "components/charts/projection-line.tsx" + }, + { + "path": "src/charts/projection-line-end-marker.tsx", + "type": "registry:component", + "target": "components/charts/projection-line-end-marker.tsx" + }, + { + "path": "src/charts/projection-utils.ts", + "type": "registry:component", + "target": "components/charts/projection-utils.ts" + }, + { + "path": "src/charts/line-series-terminal-marker.tsx", + "type": "registry:component", + "target": "components/charts/line-series-terminal-marker.tsx" + } + ] + }, + { + "name": "background", + "type": "registry:component", + "title": "Chart Background", + "description": "Pattern fill for the plot area when grid lines are hidden", + "registryDependencies": [ + "@bklit/chart-context" + ], + "dependencies": [ + "@visx/pattern@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/background.tsx", + "type": "registry:component", + "target": "components/charts/background.tsx" + }, + { + "path": "src/charts/pattern-preset.tsx", + "type": "registry:component", + "target": "components/charts/pattern-preset.tsx" + }, + { + "path": "src/charts/visx-pattern.tsx", + "type": "registry:component", + "target": "components/charts/visx-pattern.tsx" + } + ] + }, + { + "name": "x-axis", + "type": "registry:component", + "title": "X Axis", + "description": "X-axis component for time-series charts", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/utils" + ], + "files": [ + { + "path": "src/charts/x-axis.tsx", + "type": "registry:component", + "target": "components/charts/x-axis.tsx" + } + ] + }, + { + "name": "y-axis", + "type": "registry:component", + "title": "Y Axis", + "description": "Y-axis component for value labels in line and area charts", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/utils" + ], + "files": [ + { + "path": "src/charts/y-axis.tsx", + "type": "registry:component", + "target": "components/charts/y-axis.tsx" + }, + { + "path": "src/charts/reference-area-geometry.ts", + "type": "registry:lib", + "target": "components/charts/reference-area-geometry.ts" + } + ] + }, + { + "name": "chart-tooltip", + "type": "registry:component", + "title": "Chart Tooltip", + "description": "Composable tooltip components for charts", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/utils" + ], + "dependencies": [ + "@number-flow/react", + "motion" + ], + "files": [ + { + "path": "src/charts/chart-config-context.tsx", + "type": "registry:component", + "target": "components/charts/chart-config-context.tsx" + }, + { + "path": "src/charts/indicator-fade.ts", + "type": "registry:lib", + "target": "components/charts/indicator-fade.ts" + }, + { + "path": "src/charts/tooltip/chart-tooltip.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/chart-tooltip.tsx" + }, + { + "path": "src/charts/tooltip/tooltip-box.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/tooltip-box.tsx" + }, + { + "path": "src/charts/tooltip/tooltip-content.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/tooltip-content.tsx" + }, + { + "path": "src/charts/tooltip/tooltip-dot.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/tooltip-dot.tsx" + }, + { + "path": "src/charts/tooltip/tooltip-indicator.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/tooltip-indicator.tsx" + }, + { + "path": "src/charts/tooltip/date-ticker.tsx", + "type": "registry:component", + "target": "components/charts/tooltip/date-ticker.tsx" + }, + { + "path": "src/charts/tooltip/index.ts", + "type": "registry:component", + "target": "components/charts/tooltip/index.ts" + } + ] + }, + { + "name": "legend", + "type": "registry:component", + "title": "Chart Legend", + "description": "Composable legend components for charts", + "registryDependencies": [ + "@bklit/utils", + "@bklit/chart-utils" + ], + "dependencies": [ + "@base-ui/react", + "@number-flow/react" + ], + "cssVars": { + "light": { + "--legend": "oklch(1 0 0)", + "--legend-foreground": "oklch(0.141 0.005 285.823)", + "--legend-muted": "oklch(0.967 0.001 286.375)", + "--legend-muted-foreground": "oklch(0.552 0.016 285.938)", + "--legend-track": "oklch(0.92 0.004 286.32)" + }, + "dark": { + "--legend": "oklch(0.21 0.006 285.885)", + "--legend-foreground": "oklch(0.985 0 0)", + "--legend-muted": "oklch(0.274 0.006 286.033)", + "--legend-muted-foreground": "oklch(0.705 0.015 286.067)", + "--legend-track": "oklch(0.274 0.006 286.033)" + } + }, + "files": [ + { + "path": "src/charts/legend/legend.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend.tsx" + }, + { + "path": "src/charts/legend/legend-context.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-context.tsx" + }, + { + "path": "src/charts/legend/legend-item.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-item.tsx" + }, + { + "path": "src/charts/legend/legend-label.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-label.tsx" + }, + { + "path": "src/charts/legend/legend-marker.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-marker.tsx" + }, + { + "path": "src/charts/legend/legend-progress.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-progress.tsx" + }, + { + "path": "src/charts/legend/legend-value.tsx", + "type": "registry:component", + "target": "components/charts/legend/legend-value.tsx" + }, + { + "path": "src/charts/legend/index.ts", + "type": "registry:component", + "target": "components/charts/legend/index.ts" + } + ] + }, + { + "name": "markers", + "type": "registry:component", + "title": "Chart Markers", + "description": "Marker components for highlighting data points on charts", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-tooltip", + "@bklit/utils" + ], + "files": [ + { + "path": "src/charts/markers/chart-markers.tsx", + "type": "registry:component", + "target": "components/charts/markers/chart-markers.tsx" + }, + { + "path": "src/charts/markers/marker-group.tsx", + "type": "registry:component", + "target": "components/charts/markers/marker-group.tsx" + }, + { + "path": "src/charts/markers/index.ts", + "type": "registry:component", + "target": "components/charts/markers/index.ts" + } + ] + }, + { + "name": "area-chart", + "type": "registry:component", + "title": "Area Chart", + "description": "A composable area chart with gradient fills and animations", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/chart-series", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + "@bklit/shimmering-text", + "@bklit/utils" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/gradient@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/area-chart.tsx", + "type": "registry:component", + "target": "components/charts/area-chart.tsx" + }, + { + "path": "src/charts/time-series-chart-shell.tsx", + "type": "registry:component", + "target": "components/charts/time-series-chart-shell.tsx" + }, + { + "path": "src/charts/reference-area-registration-context.tsx", + "type": "registry:component", + "target": "components/charts/reference-area-registration-context.tsx" + }, + { + "path": "src/charts/projection-config.ts", + "type": "registry:component", + "target": "components/charts/projection-config.ts" + }, + { + "path": "src/charts/projection-utils.ts", + "type": "registry:component", + "target": "components/charts/projection-utils.ts" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:component", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/series-bar-layout.ts", + "type": "registry:component", + "target": "components/charts/series-bar-layout.ts" + }, + { + "path": "src/charts/area.tsx", + "type": "registry:component", + "target": "components/charts/area.tsx" + }, + { + "path": "src/charts/area-chart-loading.tsx", + "type": "registry:component", + "target": "components/charts/area-chart-loading.tsx" + }, + { + "path": "src/charts/line-loading-pulse.tsx", + "type": "registry:component", + "target": "components/charts/line-loading-pulse.tsx" + }, + { + "path": "src/charts/loading-sweep.tsx", + "type": "registry:component", + "target": "components/charts/loading-sweep.tsx" + }, + { + "path": "src/charts/line-loading-timing.ts", + "type": "registry:component", + "target": "components/charts/line-loading-timing.ts" + }, + { + "path": "src/charts/chart-loading-label.tsx", + "type": "registry:component", + "target": "components/charts/chart-loading-label.tsx" + }, + { + "path": "src/charts/pattern-area.tsx", + "type": "registry:component", + "target": "components/charts/pattern-area.tsx" + } + ] + }, + { + "name": "composed-chart", + "type": "registry:component", + "title": "Composed Chart", + "description": "Time-series chart composing Line, Area, and SeriesBar on one shared scale (Recharts ComposedChart-style)", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/y-axis", + "@bklit/chart-tooltip", + "@bklit/utils", + "@bklit/line-chart", + "@bklit/area-chart" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/scale@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "d3-array", + "motion" + ], + "files": [ + { + "path": "src/charts/composed-chart.tsx", + "type": "registry:component", + "target": "components/charts/composed-chart.tsx" + }, + { + "path": "src/charts/series-bar.tsx", + "type": "registry:component", + "target": "components/charts/series-bar.tsx" + }, + { + "path": "src/charts/series-bar-layout.ts", + "type": "registry:component", + "target": "components/charts/series-bar-layout.ts" + }, + { + "path": "src/charts/time-series-chart-shell.tsx", + "type": "registry:component", + "target": "components/charts/time-series-chart-shell.tsx" + }, + { + "path": "src/charts/reference-area-registration-context.tsx", + "type": "registry:component", + "target": "components/charts/reference-area-registration-context.tsx" + }, + { + "path": "src/charts/projection-config.ts", + "type": "registry:component", + "target": "components/charts/projection-config.ts" + }, + { + "path": "src/charts/projection-utils.ts", + "type": "registry:component", + "target": "components/charts/projection-utils.ts" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:component", + "target": "components/charts/chart-child-passthrough.ts" + } + ] + }, + { + "name": "bar-chart", + "type": "registry:component", + "title": "Bar Chart", + "description": "A composable bar chart with horizontal/vertical orientations and animations", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/grid", + "@bklit/chart-tooltip", + "@bklit/utils" + ], + "dependencies": [ + "@visx/gradient@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/bar-chart.tsx", + "type": "registry:component", + "target": "components/charts/bar-chart.tsx" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:lib", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/chart-legend-hover.tsx", + "type": "registry:component", + "target": "components/charts/chart-legend-hover.tsx" + }, + { + "path": "src/charts/bar.tsx", + "type": "registry:component", + "target": "components/charts/bar.tsx" + }, + { + "path": "src/charts/bar-depth-geometry.ts", + "type": "registry:lib", + "target": "components/charts/bar-depth-geometry.ts" + }, + { + "path": "src/charts/bar-x-axis.tsx", + "type": "registry:component", + "target": "components/charts/bar-x-axis.tsx" + }, + { + "path": "src/charts/bar-y-axis.tsx", + "type": "registry:component", + "target": "components/charts/bar-y-axis.tsx" + }, + { + "path": "src/charts/loading-sweep.tsx", + "type": "registry:component", + "target": "components/charts/loading-sweep.tsx" + }, + { + "path": "src/charts/bar-chart-loading.tsx", + "type": "registry:component", + "target": "components/charts/bar-chart-loading.tsx" + } + ] + }, + { + "name": "line-chart", + "type": "registry:component", + "title": "Line Chart", + "description": "A composable line chart with multiple series support", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/chart-series", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + "@bklit/shimmering-text", + "@bklit/utils" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/line-chart.tsx", + "type": "registry:component", + "target": "components/charts/line-chart.tsx" + }, + { + "path": "src/charts/time-series-chart-shell.tsx", + "type": "registry:component", + "target": "components/charts/time-series-chart-shell.tsx" + }, + { + "path": "src/charts/reference-area-registration-context.tsx", + "type": "registry:component", + "target": "components/charts/reference-area-registration-context.tsx" + }, + { + "path": "src/charts/projection-config.ts", + "type": "registry:component", + "target": "components/charts/projection-config.ts" + }, + { + "path": "src/charts/projection-utils.ts", + "type": "registry:component", + "target": "components/charts/projection-utils.ts" + }, + { + "path": "src/charts/projection-line.tsx", + "type": "registry:component", + "target": "components/charts/projection-line.tsx" + }, + { + "path": "src/charts/projection-line-end-marker.tsx", + "type": "registry:component", + "target": "components/charts/projection-line-end-marker.tsx" + }, + { + "path": "src/charts/line-series-terminal-marker.tsx", + "type": "registry:component", + "target": "components/charts/line-series-terminal-marker.tsx" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:component", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/series-bar-layout.ts", + "type": "registry:component", + "target": "components/charts/series-bar-layout.ts" + }, + { + "path": "src/charts/line.tsx", + "type": "registry:component", + "target": "components/charts/line.tsx" + }, + { + "path": "src/charts/line-chart-loading.tsx", + "type": "registry:component", + "target": "components/charts/line-chart-loading.tsx" + }, + { + "path": "src/charts/line-loading-pulse.tsx", + "type": "registry:component", + "target": "components/charts/line-loading-pulse.tsx" + }, + { + "path": "src/charts/loading-sweep.tsx", + "type": "registry:component", + "target": "components/charts/loading-sweep.tsx" + }, + { + "path": "src/charts/line-loading-timing.ts", + "type": "registry:component", + "target": "components/charts/line-loading-timing.ts" + }, + { + "path": "src/charts/chart-loading-label.tsx", + "type": "registry:component", + "target": "components/charts/chart-loading-label.tsx" + } + ] + }, + { + "name": "profit-loss-line", + "type": "registry:component", + "title": "Profit/Loss Line", + "description": "Sign-colored line segments for profit and loss on LineChart", + "registryDependencies": [ + "@bklit/line-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + "@bklit/legend", + "@bklit/utils" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0" + ], + "files": [ + { + "path": "src/charts/profit-loss-line.tsx", + "type": "registry:component", + "target": "components/charts/profit-loss-line.tsx" + }, + { + "path": "src/charts/profit-loss-segments.ts", + "type": "registry:lib", + "target": "lib/profit-loss-segments.ts" + }, + { + "path": "src/charts/profit-loss-legend-hover.tsx", + "type": "registry:component", + "target": "components/charts/profit-loss-legend-hover.tsx" + }, + { + "path": "src/charts/profit-loss-legend.tsx", + "type": "registry:component", + "target": "components/charts/profit-loss-legend.tsx" + } + ] + }, + { + "name": "bar-depth", + "type": "registry:component", + "title": "Bar Depth", + "description": "3D depth and glossy glass surfaces for BarChart bars", + "registryDependencies": [ + "@bklit/bar-chart" + ], + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "src/charts/bar-depth.tsx", + "type": "registry:component", + "target": "components/charts/bar-depth.tsx" + } + ] + }, + { + "name": "scatter-chart", + "type": "registry:component", + "title": "Scatter Chart", + "description": "A composable time-series scatter chart with offset rings, hover dimming, and animated enter", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/chart-series", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + "@bklit/utils" + ], + "dependencies": [ + "d3-array", + "d3-scale", + "motion", + "react-use-measure" + ], + "files": [ + { + "path": "src/charts/scatter-chart.tsx", + "type": "registry:component", + "target": "components/charts/scatter-chart.tsx" + }, + { + "path": "src/charts/scatter-chart-shell.tsx", + "type": "registry:component", + "target": "components/charts/scatter-chart-shell.tsx" + }, + { + "path": "src/charts/time-series-chart-shell.tsx", + "type": "registry:component", + "target": "components/charts/time-series-chart-shell.tsx" + }, + { + "path": "src/charts/reference-area-registration-context.tsx", + "type": "registry:component", + "target": "components/charts/reference-area-registration-context.tsx" + }, + { + "path": "src/charts/projection-config.ts", + "type": "registry:component", + "target": "components/charts/projection-config.ts" + }, + { + "path": "src/charts/projection-utils.ts", + "type": "registry:component", + "target": "components/charts/projection-utils.ts" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:component", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/series-bar-layout.ts", + "type": "registry:component", + "target": "components/charts/series-bar-layout.ts" + }, + { + "path": "src/charts/scatter.tsx", + "type": "registry:component", + "target": "components/charts/scatter.tsx" + }, + { + "path": "src/charts/use-scatter-chart-interaction.ts", + "type": "registry:component", + "target": "components/charts/use-scatter-chart-interaction.ts" + }, + { + "path": "src/charts/scatter-svg.ts", + "type": "registry:component", + "target": "components/charts/scatter-svg.ts" + } + ] + }, + { + "name": "live-line-chart", + "type": "registry:component", + "title": "Live Line Chart", + "description": "Real-time streaming line chart with smooth scrolling, crosshair, and animated axes", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-tooltip", + "@bklit/utils" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/scale@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/event@4.0.1-alpha.0", + "d3-array", + "motion" + ], + "files": [ + { + "path": "src/charts/live-line-chart.tsx", + "type": "registry:component", + "target": "components/charts/live-line-chart.tsx" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:lib", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/live-line.tsx", + "type": "registry:component", + "target": "components/charts/live-line.tsx" + }, + { + "path": "src/charts/live-x-axis.tsx", + "type": "registry:component", + "target": "components/charts/live-x-axis.tsx" + }, + { + "path": "src/charts/live-y-axis.tsx", + "type": "registry:component", + "target": "components/charts/live-y-axis.tsx" + } + ] + }, + { + "name": "candlestick-chart", + "type": "registry:component", + "title": "Candlestick Chart", + "description": "A composable OHLC candlestick chart with gradients, patterns, tooltips, and hover interactions", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/y-axis", + "@bklit/chart-tooltip", + "@bklit/utils" + ], + "dependencies": [ + "@visx/scale@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "d3-array", + "motion" + ], + "files": [ + { + "path": "src/charts/candlestick-chart.tsx", + "type": "registry:component", + "target": "components/charts/candlestick-chart.tsx" + }, + { + "path": "src/charts/chart-child-passthrough.ts", + "type": "registry:lib", + "target": "components/charts/chart-child-passthrough.ts" + }, + { + "path": "src/charts/chart-legend-hover.tsx", + "type": "registry:component", + "target": "components/charts/chart-legend-hover.tsx" + }, + { + "path": "src/charts/candlestick.tsx", + "type": "registry:component", + "target": "components/charts/candlestick.tsx" + } + ] + }, + { + "name": "pie-chart", + "type": "registry:component", + "title": "Pie Chart", + "description": "A composable pie chart with animations and customizable slices", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils" + ], + "dependencies": [ + "@number-flow/react", + "@visx/group@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "d3-shape", + "motion" + ], + "files": [ + { + "path": "src/charts/pie-chart.tsx", + "type": "registry:component", + "target": "components/charts/pie-chart.tsx" + }, + { + "path": "src/charts/pie-context.tsx", + "type": "registry:component", + "target": "components/charts/pie-context.tsx" + }, + { + "path": "src/charts/pie-slice.tsx", + "type": "registry:component", + "target": "components/charts/pie-slice.tsx" + }, + { + "path": "src/charts/chart-stat-flow.tsx", + "type": "registry:component", + "target": "components/charts/chart-stat-flow.tsx" + }, + { + "path": "src/charts/pie-center-shell.tsx", + "type": "registry:component", + "target": "components/charts/pie-center-shell.tsx" + }, + { + "path": "src/charts/pie-center.tsx", + "type": "registry:component", + "target": "components/charts/pie-center.tsx" + }, + { + "path": "src/charts/chart-center-typography.ts", + "type": "registry:component", + "target": "components/charts/chart-center-typography.ts" + } + ] + }, + { + "name": "radar-chart", + "type": "registry:component", + "title": "Radar Chart", + "description": "A composable radar/spider chart with multiple data series", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils" + ], + "dependencies": [ + "@visx/group@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/scale@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "d3-shape", + "motion" + ], + "files": [ + { + "path": "src/charts/radar-chart.tsx", + "type": "registry:component", + "target": "components/charts/radar-chart.tsx" + }, + { + "path": "src/charts/radar-context.tsx", + "type": "registry:component", + "target": "components/charts/radar-context.tsx" + }, + { + "path": "src/charts/radar-area.tsx", + "type": "registry:component", + "target": "components/charts/radar-area.tsx" + }, + { + "path": "src/charts/radar-axis.tsx", + "type": "registry:component", + "target": "components/charts/radar-axis.tsx" + }, + { + "path": "src/charts/radar-grid.tsx", + "type": "registry:component", + "target": "components/charts/radar-grid.tsx" + }, + { + "path": "src/charts/radar-labels.tsx", + "type": "registry:component", + "target": "components/charts/radar-labels.tsx" + } + ] + }, + { + "name": "ring-chart", + "type": "registry:component", + "title": "Ring Chart", + "description": "A composable donut/ring chart with progress indicators", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils" + ], + "dependencies": [ + "@visx/group@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "@number-flow/react", + "motion" + ], + "files": [ + { + "path": "src/charts/ring-chart.tsx", + "type": "registry:component", + "target": "components/charts/ring-chart.tsx" + }, + { + "path": "src/charts/ring-context.tsx", + "type": "registry:component", + "target": "components/charts/ring-context.tsx" + }, + { + "path": "src/charts/ring.tsx", + "type": "registry:component", + "target": "components/charts/ring.tsx" + }, + { + "path": "src/charts/chart-stat-flow.tsx", + "type": "registry:component", + "target": "components/charts/chart-stat-flow.tsx" + }, + { + "path": "src/charts/ring-center.tsx", + "type": "registry:component", + "target": "components/charts/ring-center.tsx" + }, + { + "path": "src/charts/chart-center-typography.ts", + "type": "registry:component", + "target": "components/charts/chart-center-typography.ts" + } + ] + }, + { + "name": "gauge-chart", + "type": "registry:component", + "title": "Gauge", + "description": "Notch-based radial gauge with PieCenter-style center, patterns, and optional arc gradients", + "registryDependencies": [ + "@bklit/utils" + ], + "dependencies": [ + "@visx/responsive@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@number-flow/react", + "motion", + "d3-shape" + ], + "files": [ + { + "path": "src/charts/chart-stat-flow.tsx", + "type": "registry:component", + "target": "components/charts/chart-stat-flow.tsx" + }, + { + "path": "src/charts/pie-context.tsx", + "type": "registry:component", + "target": "components/charts/pie-context.tsx" + }, + { + "path": "src/charts/pie-center-shell.tsx", + "type": "registry:component", + "target": "components/charts/pie-center-shell.tsx" + }, + { + "path": "src/charts/pie-center.tsx", + "type": "registry:component", + "target": "components/charts/pie-center.tsx" + }, + { + "path": "src/charts/gauge.tsx", + "type": "registry:component", + "target": "components/charts/gauge.tsx" + }, + { + "path": "src/charts/chart-center-typography.ts", + "type": "registry:component", + "target": "components/charts/chart-center-typography.ts" + } + ] + }, + { + "name": "heatmap-chart", + "type": "registry:component", + "title": "Heatmap Chart", + "description": "A contribution heatmap with animated cells and configurable level colors", + "registryDependencies": [ + "@bklit/chart-context", + "@bklit/chart-animation", + "@bklit/utils", + "@bklit/chart-utils", + "@bklit/chart-tooltip", + "@bklit/background", + "@bklit/shimmering-text" + ], + "dependencies": [ + "@visx/group@4.0.1-alpha.0", + "@visx/heatmap@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/scale@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "src/charts/heatmap/heatmap-chart.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-chart.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-context.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-context.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-cells.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-cells.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-x-axis.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-x-axis.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-y-axis.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-y-axis.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-legend.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-legend.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-legend-swatch.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-legend-swatch.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-tooltip.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-tooltip.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-colors.ts", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-colors.ts" + }, + { + "path": "src/charts/chart-scale.ts", + "type": "registry:component", + "target": "components/charts/chart-scale.ts" + }, + { + "path": "src/charts/heatmap/heatmap-utils.ts", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-utils.ts" + }, + { + "path": "src/charts/heatmap/heatmap-animation.ts", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-animation.ts" + }, + { + "path": "src/charts/heatmap/heatmap-pattern-defs.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-pattern-defs.tsx" + }, + { + "path": "src/charts/heatmap/heatmap-chart-loading.tsx", + "type": "registry:component", + "target": "components/charts/heatmap/heatmap-chart-loading.tsx" + }, + { + "path": "src/charts/heatmap/generate-heatmap-skeleton-data.ts", + "type": "registry:component", + "target": "components/charts/heatmap/generate-heatmap-skeleton-data.ts" + }, + { + "path": "src/charts/heatmap/index.ts", + "type": "registry:component", + "target": "components/charts/heatmap/index.ts" + }, + { + "path": "src/charts/chart-loading-label.tsx", + "type": "registry:component", + "target": "components/charts/chart-loading-label.tsx" + } + ] + }, + { + "name": "choropleth-chart", + "type": "registry:component", + "title": "Choropleth Map", + "description": "A geographic map chart with zoom, pan, and data visualization", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils", + "@bklit/chart-utils", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@types/geojson", + "@visx/geo@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/zoom@4.0.1-alpha.0", + "d3-geo", + "topojson-client", + "motion" + ], + "files": [ + { + "path": "src/charts/choropleth/choropleth-chart.tsx", + "type": "registry:component", + "target": "components/charts/choropleth/choropleth-chart.tsx" + }, + { + "path": "src/charts/choropleth/choropleth-context.tsx", + "type": "registry:component", + "target": "components/charts/choropleth/choropleth-context.tsx" + }, + { + "path": "src/charts/chart-scale.ts", + "type": "registry:component", + "target": "components/charts/chart-scale.ts" + }, + { + "path": "src/charts/choropleth/choropleth-feature.tsx", + "type": "registry:component", + "target": "components/charts/choropleth/choropleth-feature.tsx" + }, + { + "path": "src/charts/choropleth/choropleth-graticule.tsx", + "type": "registry:component", + "target": "components/charts/choropleth/choropleth-graticule.tsx" + }, + { + "path": "src/charts/choropleth/choropleth-tooltip.tsx", + "type": "registry:component", + "target": "components/charts/choropleth/choropleth-tooltip.tsx" + }, + { + "path": "src/charts/choropleth/index.ts", + "type": "registry:component", + "target": "components/charts/choropleth/index.ts" + } + ] + }, + { + "name": "funnel-chart", + "type": "registry:component", + "title": "Funnel Chart", + "description": "An animated funnel chart with multi-layer halo rings, hover interactions, and staggered entrance animations", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils", + "@bklit/chart-utils" + ], + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "src/charts/funnel-chart.tsx", + "type": "registry:component", + "target": "components/charts/funnel-chart.tsx" + } + ] + }, + { + "name": "sankey-chart", + "type": "registry:component", + "title": "Sankey Diagram", + "description": "A flow diagram for visualizing data transfers between nodes", + "registryDependencies": [ + "@bklit/chart-animation", + "@bklit/utils", + "@bklit/chart-utils", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/event@4.0.1-alpha.0", + "@visx/gradient@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@visx/responsive@4.0.1-alpha.0", + "@visx/sankey@4.0.1-alpha.0", + "d3-sankey", + "motion" + ], + "files": [ + { + "path": "src/charts/sankey/sankey-chart.tsx", + "type": "registry:component", + "target": "components/charts/sankey/sankey-chart.tsx" + }, + { + "path": "src/charts/sankey/sankey-context.tsx", + "type": "registry:component", + "target": "components/charts/sankey/sankey-context.tsx" + }, + { + "path": "src/charts/sankey/sankey-node.tsx", + "type": "registry:component", + "target": "components/charts/sankey/sankey-node.tsx" + }, + { + "path": "src/charts/sankey/sankey-link.tsx", + "type": "registry:component", + "target": "components/charts/sankey/sankey-link.tsx" + }, + { + "path": "src/charts/sankey/sankey-tooltip.tsx", + "type": "registry:component", + "target": "components/charts/sankey/sankey-tooltip.tsx" + }, + { + "path": "src/charts/sankey/index.ts", + "type": "registry:component", + "target": "components/charts/sankey/index.ts" + } + ] + }, + { + "name": "area-chart-example", + "type": "registry:example", + "title": "Area Chart Example", + "description": "Composable area-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/area-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/area-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/area-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "bar-chart-example", + "type": "registry:example", + "title": "Bar Chart Example", + "description": "Composable bar-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/bar-chart", + "@bklit/grid", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/gradient@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/bar-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/bar-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "line-chart-example", + "type": "registry:example", + "title": "Line Chart Example", + "description": "Composable line-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/line-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/line-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/line-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "scatter-chart-example", + "type": "registry:example", + "title": "Scatter Chart Example", + "description": "Composable scatter-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/scatter-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "d3-scale", + "d3-array", + "motion", + "react-use-measure" + ], + "files": [ + { + "path": "registry/examples/scatter-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/scatter-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "pie-chart-example", + "type": "registry:example", + "title": "Pie Chart Example", + "description": "Composable pie-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/pie-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/pie-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/pie-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "gauge-chart-example", + "type": "registry:example", + "title": "Gauge Chart Example", + "description": "Composable gauge-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/gauge-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/gauge-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/gauge-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "heatmap-chart-example", + "type": "registry:example", + "title": "Heatmap Chart Example", + "description": "Composable heatmap-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/heatmap-chart" + ], + "dependencies": [ + "@visx/heatmap@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/heatmap-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/heatmap-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "ring-chart-example", + "type": "registry:example", + "title": "Ring Chart Example", + "description": "Composable ring-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/ring-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/ring-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/ring-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "radar-chart-example", + "type": "registry:example", + "title": "Radar Chart Example", + "description": "Composable radar-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/radar-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/radar-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/radar-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "composed-chart-example", + "type": "registry:example", + "title": "Composed Chart Example", + "description": "Composable composed-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/composed-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/composed-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/composed-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "funnel-chart-example", + "type": "registry:example", + "title": "Funnel Chart Example", + "description": "Composable funnel-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/funnel-chart" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/funnel-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/funnel-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "sankey-chart-example", + "type": "registry:example", + "title": "Sankey Chart Example", + "description": "Composable sankey-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/sankey-chart" + ], + "dependencies": [ + "@visx/sankey@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/sankey-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/sankey-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "candlestick-chart-example", + "type": "registry:example", + "title": "Candlestick Chart Example", + "description": "Composable candlestick-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/candlestick-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/y-axis", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/candlestick-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/candlestick-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "choropleth-chart-example", + "type": "registry:example", + "title": "Choropleth Chart Example", + "description": "Composable choropleth-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/choropleth-chart" + ], + "dependencies": [ + "@visx/geo@4.0.1-alpha.0", + "d3-geo", + "topojson-client" + ], + "files": [ + { + "path": "registry/examples/choropleth-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/choropleth-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "live-line-chart-example", + "type": "registry:example", + "title": "Live Line Chart Example", + "description": "Composable live-line-chart demo for Open in v0", + "registryDependencies": [ + "@bklit/live-line-chart", + "@bklit/chart-tooltip" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion" + ], + "files": [ + { + "path": "registry/examples/live-line-chart.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/live-line-chart-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "chart-stat-flow", + "type": "registry:component", + "title": "Chart Stat Flow", + "description": "Animated stat value and label for chart overlays", + "registryDependencies": [ + "@bklit/utils" + ], + "dependencies": [ + "@number-flow/react", + "motion" + ], + "files": [ + { + "path": "src/charts/chart-stat-flow.tsx", + "type": "registry:component", + "target": "components/charts/chart-stat-flow.tsx" + } + ] + }, + { + "name": "stat-card-area-01", + "type": "registry:block", + "title": "Stat Card Area", + "description": "Revenue stat card with gradient area sparkline, NumberFlow, and trend badge", + "registryDependencies": [ + "@bklit/area-chart", + "@bklit/chart-stat-flow", + "card", + "badge" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/gradient@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all" + ], + "files": [ + { + "path": "registry/blocks/stat-card-area-01/components/stat-card-area.tsx", + "type": "registry:component", + "target": "components/stat-card-area.tsx" + }, + { + "path": "registry/blocks/stat-card-area-01/components/stat-card-chart.tsx", + "type": "registry:component", + "target": "components/stat-card-chart.tsx" + }, + { + "path": "registry/blocks/stat-card-area-01/components/stat-card-hover-bridge.tsx", + "type": "registry:component", + "target": "components/stat-card-hover-bridge.tsx" + }, + { + "path": "registry/blocks/stat-card-area-01/components/trend-badge.tsx", + "type": "registry:component", + "target": "components/trend-badge.tsx" + }, + { + "path": "registry/blocks/stat-card-area-01/data/revenue-series.ts", + "type": "registry:lib", + "target": "data/revenue-series.ts" + }, + { + "path": "registry/examples/stat-card-area-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "stat-card-area-01-example", + "type": "registry:example", + "title": "Stat Card Area Example", + "description": "Revenue stat card with gradient area sparkline, NumberFlow, and trend badge — demo for Open in v0", + "registryDependencies": [ + "@bklit/stat-card-area-01" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@visx/gradient@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all" + ], + "files": [ + { + "path": "registry/examples/stat-card-area-01.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/stat-card-area-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "stat-card-line-01", + "type": "registry:block", + "title": "Stat Card Line", + "description": "Sessions stat card with line sparkline, overlaid NumberFlow, and trend badge", + "registryDependencies": [ + "@bklit/line-chart", + "@bklit/chart-stat-flow", + "card", + "badge" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all" + ], + "files": [ + { + "path": "registry/blocks/stat-card-line-01/components/stat-card-line.tsx", + "type": "registry:component", + "target": "components/stat-card-line.tsx" + }, + { + "path": "registry/blocks/stat-card-line-01/components/stat-card-chart.tsx", + "type": "registry:component", + "target": "components/stat-card-chart.tsx" + }, + { + "path": "registry/blocks/stat-card-line-01/components/stat-card-hover-bridge.tsx", + "type": "registry:component", + "target": "components/stat-card-hover-bridge.tsx" + }, + { + "path": "registry/blocks/stat-card-line-01/components/trend-badge.tsx", + "type": "registry:component", + "target": "components/trend-badge.tsx" + }, + { + "path": "registry/blocks/stat-card-line-01/data/sessions-series.ts", + "type": "registry:lib", + "target": "data/sessions-series.ts" + }, + { + "path": "registry/examples/stat-card-line-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "stat-card-line-01-example", + "type": "registry:example", + "title": "Stat Card Line Example", + "description": "Sessions stat card with line sparkline, overlaid NumberFlow, and trend badge — demo for Open in v0", + "registryDependencies": [ + "@bklit/stat-card-line-01" + ], + "dependencies": [ + "@visx/curve@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all" + ], + "files": [ + { + "path": "registry/examples/stat-card-line-01.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/stat-card-line-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "stat-card-choropleth-01", + "type": "registry:block", + "title": "Stat Card Choropleth", + "description": "Visitor map stat card with choropleth sparkline, NumberFlow, and trend badge", + "registryDependencies": [ + "@bklit/choropleth-chart", + "@bklit/chart-stat-flow", + "card", + "badge" + ], + "dependencies": [ + "@number-flow/react", + "@types/geojson", + "@types/topojson-specification", + "@central-icons-react/all", + "topojson-client" + ], + "files": [ + { + "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth.tsx", + "type": "registry:component", + "target": "components/stat-card-choropleth.tsx" + }, + { + "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth-hover-bridge.tsx", + "type": "registry:component", + "target": "components/stat-card-choropleth-hover-bridge.tsx" + }, + { + "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-chart.tsx", + "type": "registry:component", + "target": "components/stat-card-chart.tsx" + }, + { + "path": "registry/blocks/stat-card-choropleth-01/components/trend-badge.tsx", + "type": "registry:component", + "target": "components/trend-badge.tsx" + }, + { + "path": "registry/blocks/stat-card-choropleth-01/data/visitors.ts", + "type": "registry:lib", + "target": "data/visitors.ts" + }, + { + "path": "registry/blocks/stat-card-choropleth-01/lib/use-world-data.tsx", + "type": "registry:component", + "target": "lib/use-world-data.tsx" + }, + { + "path": "registry/examples/stat-card-choropleth-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + }, + { + "name": "stat-card-choropleth-01-example", + "type": "registry:example", + "title": "Stat Card Choropleth Example", + "description": "Visitor map stat card with choropleth sparkline, NumberFlow, and trend badge — demo for Open in v0", + "registryDependencies": [ + "@bklit/stat-card-choropleth-01" + ], + "dependencies": [ + "@number-flow/react", + "@types/geojson", + "@types/topojson-specification", + "@central-icons-react/all", + "topojson-client" + ], + "files": [ + { + "path": "registry/examples/stat-card-choropleth-01.tsx", + "type": "registry:page", + "target": "app/page.tsx" + }, + { + "path": "registry/examples/stat-card-choropleth-01-index.ts", + "type": "registry:lib", + "target": "components/charts/index.ts" + } + ] + } + ] +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-area.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-area.tsx new file mode 100644 index 0000000..3fcaebd --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-area.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { + Area, + AreaChart, + ChartStatFlow, + LinearGradient, +} from "@/components/charts"; +import { curveCardinal } from "@visx/curve"; +import { useState } from "react"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { revenueSeries, revenueStats } from "../data/revenue-series"; +import { + StatCardChart, + statCardLabelClassName, + statCardValueClassName, +} from "./stat-card-chart"; +import { + formatStatCardMonth, + StatCardHoverBridge, + type StatCardHoverState, +} from "./stat-card-hover-bridge"; +import { TrendBadge } from "./trend-badge"; + +export function StatCardArea() { + const [hover, setHover] = useState({ + value: null, + label: null, + trend: null, + }); + const displayValue = hover.value ?? revenueStats.average; + const displayLabel = hover.label ?? "Avg"; + const displayTrend = hover.trend ?? revenueStats.trend; + + return ( + + + Total Revenue + + + + + + + + + + + + + + + + + + ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-chart.tsx new file mode 100644 index 0000000..2d3d90d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-chart.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +export interface StatCardHoverState { + value: number | null; + label: string | null; + trend: number | null; +} + +export const statCardValueClassName = + "text-3xl font-semibold leading-none tracking-tight"; + +export const statCardLabelClassName = "mt-0 text-xs"; + +export const statCardChartHeights = { + sm: "[--stat-card-chart-h:96px]", + md: "[--stat-card-chart-h:190px]", + lg: "[--stat-card-chart-h:420px]", +} as const; + +/** Bleeds charts edge-to-edge inside stat card content padding. */ +export function StatCardChart({ + children, + className, + size = "sm", +}: { + children: ReactNode; + className?: string; + size?: keyof typeof statCardChartHeights; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-hover-bridge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-hover-bridge.tsx new file mode 100644 index 0000000..4bb3bec --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/stat-card-hover-bridge.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useChart } from "@/components/charts"; +import { useEffect } from "react"; +import type { StatCardHoverState } from "./stat-card-chart"; + +export type { StatCardHoverState } from "./stat-card-chart"; + +export function formatStatCardMonth(date: Date) { + return date.toLocaleDateString("en-US", { month: "short" }); +} + +export function formatStatCardWeekday(date: Date) { + return date.toLocaleDateString("en-US", { weekday: "long" }); +} + +function parsePointDate(raw: unknown): Date | null { + if (raw instanceof Date) { + return raw; + } + if (typeof raw === "string") { + return new Date(raw); + } + return null; +} + +function computePeriodTrend( + data: Record[], + index: number, + dataKey: string +): number | null { + if (index <= 0) { + return null; + } + + const current = data[index]?.[dataKey]; + const previous = data[index - 1]?.[dataKey]; + + if ( + typeof current !== "number" || + typeof previous !== "number" || + previous === 0 + ) { + return null; + } + + return ((current - previous) / previous) * 100; +} + +/** Syncs hovered chart values, labels, and trend into stat card UI. */ +export function StatCardHoverBridge({ + dataKey, + dateKey = "date", + formatLabel, + onHoverChange, +}: { + dataKey: string; + dateKey?: string; + formatLabel: (date: Date) => string; + onHoverChange: (state: StatCardHoverState) => void; +}) { + const { data, tooltipData } = useChart(); + + useEffect(() => { + if (!tooltipData?.point) { + onHoverChange({ value: null, label: null, trend: null }); + return; + } + + const raw = tooltipData.point[dataKey]; + const value = typeof raw === "number" ? raw : null; + const date = parsePointDate(tooltipData.point[dateKey]); + const label = date ? formatLabel(date) : null; + const trend = computePeriodTrend(data, tooltipData.index, dataKey); + + onHoverChange({ value, label, trend }); + }, [data, dataKey, dateKey, formatLabel, onHoverChange, tooltipData]); + + return null; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/trend-badge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/trend-badge.tsx new file mode 100644 index 0000000..eb800e9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-area-01/components/trend-badge.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { CentralIcon } from "@central-icons-react/all"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export function TrendBadge({ + value, + className, +}: { + value: number; + className?: string; +}) { + const positive = value >= 0; + + return ( + + + {positive ? "+" : ""} + {value.toFixed(1)}% + + ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-chart.tsx new file mode 100644 index 0000000..2d3d90d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-chart.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +export interface StatCardHoverState { + value: number | null; + label: string | null; + trend: number | null; +} + +export const statCardValueClassName = + "text-3xl font-semibold leading-none tracking-tight"; + +export const statCardLabelClassName = "mt-0 text-xs"; + +export const statCardChartHeights = { + sm: "[--stat-card-chart-h:96px]", + md: "[--stat-card-chart-h:190px]", + lg: "[--stat-card-chart-h:420px]", +} as const; + +/** Bleeds charts edge-to-edge inside stat card content padding. */ +export function StatCardChart({ + children, + className, + size = "sm", +}: { + children: ReactNode; + className?: string; + size?: keyof typeof statCardChartHeights; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth-hover-bridge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth-hover-bridge.tsx new file mode 100644 index 0000000..3a17690 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth-hover-bridge.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useChoropleth } from "@/components/charts"; +import { useEffect } from "react"; +import { computeVisitorTrend, getVisitorValue } from "../data/visitors"; +import type { StatCardHoverState } from "./stat-card-chart"; + +/** Syncs hovered choropleth feature into stat card NumberFlow and trend badge. */ +export function StatCardChoroplethHoverBridge({ + onHoverChange, +}: { + onHoverChange: (state: StatCardHoverState) => void; +}) { + const { tooltipData } = useChoropleth(); + + useEffect(() => { + if (!tooltipData?.feature) { + onHoverChange({ value: null, label: null, trend: null }); + return; + } + + const feature = tooltipData.feature; + const label = (feature.properties?.name as string | undefined) ?? "Unknown"; + const visitors = getVisitorValue(feature); + const value = visitors ?? 0; + const trend = visitors === undefined ? null : computeVisitorTrend(visitors); + + onHoverChange({ value, label, trend }); + }, [onHoverChange, tooltipData]); + + return null; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth.tsx new file mode 100644 index 0000000..37fc413 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth.tsx @@ -0,0 +1,91 @@ +"use client"; + +import type { ChoroplethFeature } from "@/components/charts"; +import { + ChartStatFlow, + ChoroplethChart, + ChoroplethFeatureComponent, + ChoroplethTooltip, +} from "@/components/charts"; +import { useState } from "react"; +import { useWorldDataStandalone } from "@/lib/use-world-data"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + getVisitorColor, + getVisitorValue, + visitorStats, +} from "../data/visitors"; +import { + StatCardChart, + type StatCardHoverState, + statCardLabelClassName, + statCardValueClassName, +} from "./stat-card-chart"; +import { StatCardChoroplethHoverBridge } from "./stat-card-choropleth-hover-bridge"; +import { TrendBadge } from "./trend-badge"; + +export function StatCardChoropleth() { + const { worldData, isLoading } = useWorldDataStandalone(); + const [hover, setHover] = useState({ + value: null, + label: null, + trend: null, + }); + const displayValue = hover.value ?? visitorStats.total; + const displayLabel = hover.label ?? "Total"; + const displayTrend = hover.trend ?? visitorStats.trend; + + return ( + + +
+ Unique Visitors + +
+ + + +
+ + + {isLoading || !worldData ? ( + +
+ Loading map… +
+
+ ) : ( + + + + + getVisitorColor(feature) + } + /> + + + + )} +
+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/trend-badge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/trend-badge.tsx new file mode 100644 index 0000000..eb800e9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/components/trend-badge.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { CentralIcon } from "@central-icons-react/all"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export function TrendBadge({ + value, + className, +}: { + value: number; + className?: string; +}) { + const positive = value >= 0; + + return ( + + + {positive ? "+" : ""} + {value.toFixed(1)}% + + ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/lib/use-world-data.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/lib/use-world-data.tsx new file mode 100644 index 0000000..f4763b7 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-choropleth-01/lib/use-world-data.tsx @@ -0,0 +1,143 @@ +"use client"; + +import type { FeatureCollection, Geometry } from "geojson"; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useState, +} from "react"; +import { feature } from "topojson-client"; +import type { GeometryCollection, Topology } from "topojson-specification"; + +interface CountryProperties { + name: string; + [key: string]: unknown; +} + +interface WorldTopology extends Topology { + objects: { + [key: string]: GeometryCollection; + }; +} + +const WORLD_DATA_URL = + "https://raw.githubusercontent.com/subyfly/topojson/refs/heads/master/world-countries.json"; + +// Global cache to avoid refetching across component mounts +let globalWorldDataCache: FeatureCollection< + Geometry, + CountryProperties +> | null = null; +let globalFetchPromise: Promise | null> | null = null; + +function fetchWorldData(): Promise | null> { + // Return cached data if available + if (globalWorldDataCache) { + return Promise.resolve(globalWorldDataCache); + } + + // Return existing promise if fetch is in progress + if (globalFetchPromise) { + return globalFetchPromise; + } + + // Start new fetch + globalFetchPromise = (async () => { + try { + const response = await fetch(WORLD_DATA_URL); + const topology = (await response.json()) as WorldTopology; + const objectKey = Object.keys(topology.objects)[0]; + if (!objectKey) { + throw new Error("No objects found in topology"); + } + const geoObject = topology.objects[objectKey]; + if (!geoObject) { + throw new Error("Object not found in topology"); + } + const geojson = feature( + topology, + geoObject + ) as unknown as FeatureCollection; + globalWorldDataCache = geojson; + return geojson; + } catch (error) { + console.error("Failed to fetch world data:", error); + return null; + } + })(); + + return globalFetchPromise; +} + +interface WorldDataContextValue { + worldData: FeatureCollection | null; + isLoading: boolean; +} + +const WorldDataContext = createContext({ + worldData: null, + isLoading: true, +}); + +export function WorldDataProvider({ children }: { children: ReactNode }) { + const [worldData, setWorldData] = useState | null>(globalWorldDataCache); + const [isLoading, setIsLoading] = useState(!globalWorldDataCache); + + useEffect(() => { + if (globalWorldDataCache) { + setWorldData(globalWorldDataCache); + setIsLoading(false); + return; + } + + fetchWorldData().then((data) => { + setWorldData(data); + setIsLoading(false); + }); + }, []); + + return ( + + {children} + + ); +} + +export function useWorldData() { + return useContext(WorldDataContext); +} + +// Standalone hook for components that don't have the provider +export function useWorldDataStandalone() { + const [worldData, setWorldData] = useState | null>(globalWorldDataCache); + const [isLoading, setIsLoading] = useState(!globalWorldDataCache); + + useEffect(() => { + if (globalWorldDataCache) { + setWorldData(globalWorldDataCache); + setIsLoading(false); + return; + } + + fetchWorldData().then((data) => { + setWorldData(data); + setIsLoading(false); + }); + }, []); + + return { worldData, isLoading }; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-chart.tsx new file mode 100644 index 0000000..2d3d90d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-chart.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +export interface StatCardHoverState { + value: number | null; + label: string | null; + trend: number | null; +} + +export const statCardValueClassName = + "text-3xl font-semibold leading-none tracking-tight"; + +export const statCardLabelClassName = "mt-0 text-xs"; + +export const statCardChartHeights = { + sm: "[--stat-card-chart-h:96px]", + md: "[--stat-card-chart-h:190px]", + lg: "[--stat-card-chart-h:420px]", +} as const; + +/** Bleeds charts edge-to-edge inside stat card content padding. */ +export function StatCardChart({ + children, + className, + size = "sm", +}: { + children: ReactNode; + className?: string; + size?: keyof typeof statCardChartHeights; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-hover-bridge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-hover-bridge.tsx new file mode 100644 index 0000000..4bb3bec --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-hover-bridge.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useChart } from "@/components/charts"; +import { useEffect } from "react"; +import type { StatCardHoverState } from "./stat-card-chart"; + +export type { StatCardHoverState } from "./stat-card-chart"; + +export function formatStatCardMonth(date: Date) { + return date.toLocaleDateString("en-US", { month: "short" }); +} + +export function formatStatCardWeekday(date: Date) { + return date.toLocaleDateString("en-US", { weekday: "long" }); +} + +function parsePointDate(raw: unknown): Date | null { + if (raw instanceof Date) { + return raw; + } + if (typeof raw === "string") { + return new Date(raw); + } + return null; +} + +function computePeriodTrend( + data: Record[], + index: number, + dataKey: string +): number | null { + if (index <= 0) { + return null; + } + + const current = data[index]?.[dataKey]; + const previous = data[index - 1]?.[dataKey]; + + if ( + typeof current !== "number" || + typeof previous !== "number" || + previous === 0 + ) { + return null; + } + + return ((current - previous) / previous) * 100; +} + +/** Syncs hovered chart values, labels, and trend into stat card UI. */ +export function StatCardHoverBridge({ + dataKey, + dateKey = "date", + formatLabel, + onHoverChange, +}: { + dataKey: string; + dateKey?: string; + formatLabel: (date: Date) => string; + onHoverChange: (state: StatCardHoverState) => void; +}) { + const { data, tooltipData } = useChart(); + + useEffect(() => { + if (!tooltipData?.point) { + onHoverChange({ value: null, label: null, trend: null }); + return; + } + + const raw = tooltipData.point[dataKey]; + const value = typeof raw === "number" ? raw : null; + const date = parsePointDate(tooltipData.point[dateKey]); + const label = date ? formatLabel(date) : null; + const trend = computePeriodTrend(data, tooltipData.index, dataKey); + + onHoverChange({ value, label, trend }); + }, [data, dataKey, dateKey, formatLabel, onHoverChange, tooltipData]); + + return null; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-line.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-line.tsx new file mode 100644 index 0000000..1a4f757 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/stat-card-line.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { ChartStatFlow, Line, LineChart } from "@/components/charts"; +import { curveBasis } from "@visx/curve"; +import { useState } from "react"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { sessionsSeries, sessionsStats } from "../data/sessions-series"; +import { + StatCardChart, + statCardLabelClassName, + statCardValueClassName, +} from "./stat-card-chart"; +import { + formatStatCardWeekday, + StatCardHoverBridge, + type StatCardHoverState, +} from "./stat-card-hover-bridge"; +import { TrendBadge } from "./trend-badge"; + +export function StatCardLine() { + const [hover, setHover] = useState({ + value: null, + label: null, + trend: null, + }); + const average = Math.round(sessionsStats.average); + const displayValue = hover.value === null ? average : Math.round(hover.value); + const displayLabel = hover.label ?? "Avg"; + const displayTrend = hover.trend ?? sessionsStats.trend; + + return ( + + + Active Sessions + + + + + + + +
+ +
+ + + + + +
+
+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/trend-badge.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/trend-badge.tsx new file mode 100644 index 0000000..eb800e9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/blocks/stat-card-line-01/components/trend-badge.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { CentralIcon } from "@central-icons-react/all"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export function TrendBadge({ + value, + className, +}: { + value: number; + className?: string; +}) { + const positive = value >= 0; + + return ( + + + {positive ? "+" : ""} + {value.toFixed(1)}% + + ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart-index.ts new file mode 100644 index 0000000..3f27eb7 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { AreaChart } from "./area-chart"; +export { Area } from "./area"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart.tsx new file mode 100644 index 0000000..682be4d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart.tsx @@ -0,0 +1,30 @@ +"use client" + +// In your app (monorepo/npm): import { AreaChart, Area, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts" +import { AreaChart, Area, Grid, XAxis, ChartTooltip } from "@/components/charts" + +import { curveNatural } from "@visx/curve"; + +const chartData = [ + { date: new Date("2024-01-01"), desktop: 186 }, + { date: new Date("2024-02-01"), desktop: 305 }, + { date: new Date("2024-03-01"), desktop: 237 }, + { date: new Date("2024-04-01"), desktop: 73 }, + { date: new Date("2024-05-01"), desktop: 209 }, + { date: new Date("2024-06-01"), desktop: 214 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart-index.ts new file mode 100644 index 0000000..7e8fa70 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { BarChart } from "./bar-chart"; +export { Bar } from "./bar"; +export { BarXAxis } from "./bar-x-axis"; +export { Grid } from "./grid"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart.tsx new file mode 100644 index 0000000..2fe5a50 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart.tsx @@ -0,0 +1,29 @@ +"use client" + +// In your app (monorepo/npm): import { BarChart, Bar, BarXAxis, Grid, ChartTooltip } from "@bklitui/ui/charts" +import { BarChart, Bar, BarXAxis, Grid, ChartTooltip } from "@/components/charts" + +const data = [ + { month: "Jan", revenue: 12000, profit: 4500 }, + { month: "Feb", revenue: 15500, profit: 5200 }, + { month: "Mar", revenue: 11000, profit: 3800 }, + { month: "Apr", revenue: 18500, profit: 7100 }, + { month: "May", revenue: 16800, profit: 5400 }, + { month: "Jun", revenue: 21200, profit: 8800 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart-index.ts new file mode 100644 index 0000000..143206b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart-index.ts @@ -0,0 +1,7 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { CandlestickChart } from "./candlestick-chart"; +export { Candlestick } from "./candlestick"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { YAxis } from "./y-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart.tsx new file mode 100644 index 0000000..619194e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart.tsx @@ -0,0 +1,27 @@ +"use client" + +// In your app (monorepo/npm): import { CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip } from "@bklitui/ui/charts" +import { CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip } from "@/components/charts" + +const ohlcData = [ + { date: new Date("2024-01-02"), open: 100, high: 108, low: 98, close: 105 }, + { date: new Date("2024-01-03"), open: 105, high: 110, low: 102, close: 103 }, + { date: new Date("2024-01-04"), open: 103, high: 112, low: 101, close: 110 }, + { date: new Date("2024-01-05"), open: 110, high: 115, low: 107, close: 108 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart-index.ts new file mode 100644 index 0000000..d161d0a --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart-index.ts @@ -0,0 +1,2 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "./choropleth"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart.tsx new file mode 100644 index 0000000..3eb65c9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart.tsx @@ -0,0 +1,23 @@ +"use client" + +// In your app (monorepo/npm): import { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "@bklitui/ui/charts" +import { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "@/components/charts" + +const features = [ + { id: "US", name: "United States", value: 120 }, + { id: "CA", name: "Canada", value: 45 }, + { id: "GB", name: "United Kingdom", value: 62 }, +]; + +export default function Component() { + return ( +
+
+ + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart-index.ts new file mode 100644 index 0000000..0b65b3c --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart-index.ts @@ -0,0 +1,8 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { ComposedChart } from "./composed-chart"; +export { SeriesBar } from "./series-bar"; +export { Area } from "./area"; +export { Line } from "./line"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart.tsx new file mode 100644 index 0000000..9e6f796 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart.tsx @@ -0,0 +1,30 @@ +"use client" + +// In your app (monorepo/npm): import { ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts" +import { ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip } from "@/components/charts" + +import { curveNatural } from "@visx/curve"; + +const chartData = [ + { date: new Date("2024-01-01"), revenue: 4200, runRate: 3800 }, + { date: new Date("2024-02-01"), revenue: 5100, runRate: 4600 }, + { date: new Date("2024-03-01"), revenue: 4800, runRate: 5200 }, + { date: new Date("2024-04-01"), revenue: 5500, runRate: 5000 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart-index.ts new file mode 100644 index 0000000..24608cb --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart-index.ts @@ -0,0 +1,2 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { FunnelChart } from "./funnel-chart"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart.tsx new file mode 100644 index 0000000..7275420 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart.tsx @@ -0,0 +1,21 @@ +"use client" + +// In your app (monorepo/npm): import { FunnelChart } from "@bklitui/ui/charts" +import { FunnelChart } from "@/components/charts" + +const funnelData = [ + { label: "Visitors", value: 12000 }, + { label: "Signups", value: 4800 }, + { label: "Activated", value: 2100 }, + { label: "Paid", value: 840 }, +]; + +export default function Component() { + return ( +
+
+ +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart-index.ts new file mode 100644 index 0000000..540287e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart-index.ts @@ -0,0 +1,2 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { Gauge } from "./gauge"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart.tsx new file mode 100644 index 0000000..4fdb1ce --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart.tsx @@ -0,0 +1,22 @@ +"use client" + +// In your app (monorepo/npm): import { Gauge } from "@bklitui/ui/charts" +import { Gauge } from "@/components/charts" + + + +export default function Component() { + return ( +
+
+ +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart-index.ts new file mode 100644 index 0000000..a489340 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart-index.ts @@ -0,0 +1,11 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { + HeatmapCells, + HeatmapChart, + HeatmapInteractionBoundary, + HeatmapInteractionProvider, + HeatmapLegend, + HeatmapTooltip, + HeatmapXAxis, + HeatmapYAxis, +} from "./heatmap"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart.tsx new file mode 100644 index 0000000..38c4321 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart.tsx @@ -0,0 +1,53 @@ +"use client" + +// In your app (monorepo/npm): import { HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis } from "@bklitui/ui/charts" +import { HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis } from "@/components/charts" + +const data = [ + { + bin: 0, + bins: [ + { bin: 0, count: 2, date: new Date(2024, 0, 1) }, + { bin: 1, count: 0, date: new Date(2024, 0, 2) }, + { bin: 2, count: 3, date: new Date(2024, 0, 3) }, + { bin: 3, count: 1, date: new Date(2024, 0, 4) }, + { bin: 4, count: 4, date: new Date(2024, 0, 5) }, + { bin: 5, count: 0, date: new Date(2024, 0, 6) }, + { bin: 6, count: 1, date: new Date(2024, 0, 7) }, + ], + }, + { + bin: 1, + bins: [ + { bin: 0, count: 1, date: new Date(2024, 0, 8) }, + { bin: 1, count: 2, date: new Date(2024, 0, 9) }, + { bin: 2, count: 0, date: new Date(2024, 0, 10) }, + { bin: 3, count: 3, date: new Date(2024, 0, 11) }, + { bin: 4, count: 2, date: new Date(2024, 0, 12) }, + { bin: 5, count: 1, date: new Date(2024, 0, 13) }, + { bin: 6, count: 0, date: new Date(2024, 0, 14) }, + ], + }, +]; + +export default function Component() { + return ( +
+
+ + +
+ + + + + + + +
+
+
+
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart-index.ts new file mode 100644 index 0000000..73f33e2 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { LineChart } from "./line-chart"; +export { Line } from "./line"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart.tsx new file mode 100644 index 0000000..c2c83a7 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart.tsx @@ -0,0 +1,30 @@ +"use client" + +// In your app (monorepo/npm): import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts" +import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@/components/charts" + +import { curveNatural } from "@visx/curve"; + +const chartData = [ + { date: new Date("2024-01-01"), users: 1200 }, + { date: new Date("2024-02-01"), users: 1350 }, + { date: new Date("2024-03-01"), users: 1100 }, + { date: new Date("2024-04-01"), users: 1450 }, + { date: new Date("2024-05-01"), users: 1380 }, + { date: new Date("2024-06-01"), users: 1520 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart-index.ts new file mode 100644 index 0000000..42993cf --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { LiveLineChart } from "./live-line-chart"; +export { LiveLine } from "./live-line"; +export { LiveXAxis } from "./live-x-axis"; +export { LiveYAxis } from "./live-y-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart.tsx new file mode 100644 index 0000000..7a5f5c6 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart.tsx @@ -0,0 +1,32 @@ +"use client" + +// In your app (monorepo/npm): import { LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip } from "@bklitui/ui/charts" +import { LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip } from "@/components/charts" + +import { curveNatural } from "@visx/curve"; + +const initialData = Array.from({ length: 24 }, (_, i) => ({ + time: Date.now() - (23 - i) * 60_000, + value: 50 + Math.sin(i / 3) * 20, +})); + +export default function Component() { + return ( +
+
+ + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart-index.ts new file mode 100644 index 0000000..fc94e92 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart-index.ts @@ -0,0 +1,4 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { PieChart } from "./pie-chart"; +export { PieSlice } from "./pie-slice"; +export { PieCenter } from "./pie-center"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart.tsx new file mode 100644 index 0000000..a63f233 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart.tsx @@ -0,0 +1,26 @@ +"use client" + +// In your app (monorepo/npm): import { PieChart, PieSlice, PieCenter } from "@bklitui/ui/charts" +import { PieChart, PieSlice, PieCenter } from "@/components/charts" + +const pieData = [ + { label: "Direct", value: 320 }, + { label: "Organic", value: 280 }, + { label: "Referral", value: 190 }, + { label: "Social", value: 140 }, +]; + +export default function Component() { + return ( +
+
+ + {pieData.map((item, i) => ( + + ))} + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line-index.ts new file mode 100644 index 0000000..a5538cb --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line-index.ts @@ -0,0 +1,13 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { LineChart } from "./line-chart"; +export { Line } from "./line"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip"; +export { ProfitLossLine } from "./profit-loss-line"; +export { ProfitLossLegend } from "./profit-loss-legend"; +export { ProfitLossLegendHoverProvider } from "./profit-loss-legend-hover"; +export { + profitLossColor, + resolveProfitLossTooltipLabel, +} from "./profit-loss-line"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line.tsx new file mode 100644 index 0000000..972c7b5 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line.tsx @@ -0,0 +1,72 @@ +"use client" + +// In your app (monorepo/npm): import from "@bklitui/ui/charts" +import { + ChartTooltip, + Grid, + Line, + LineChart, + ProfitLossLegend, + ProfitLossLegendHoverProvider, + ProfitLossLine, + profitLossColor, + resolveProfitLossTooltipLabel, + XAxis, +} from "@/components/charts" +import { curveLinear } from "@visx/curve"; +import { useState } from "react"; + +const chartData = [ + { date: new Date("2024-01-01"), pnl: 420 }, + { date: new Date("2024-01-05"), pnl: 180 }, + { date: new Date("2024-01-10"), pnl: -240 }, + { date: new Date("2024-01-15"), pnl: -90 }, + { date: new Date("2024-01-20"), pnl: 310 }, + { date: new Date("2024-01-25"), pnl: 520 }, +]; + +export default function Component() { + const [legendHoveredIndex, setLegendHoveredIndex] = useState( + null + ); + + return ( +
+
+ + + + + + + + + profitLossColor((point.pnl as number) ?? 0)} + rows={(point) => { + const value = (point.pnl as number) ?? 0; + return [ + { + color: profitLossColor(value), + label: resolveProfitLossTooltipLabel(""), + value, + }, + ]; + }} + /> + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart-index.ts new file mode 100644 index 0000000..d0b73a8 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { RadarChart } from "./radar-chart"; +export { RadarGrid } from "./radar-grid"; +export { RadarAxis } from "./radar-axis"; +export { RadarLabels } from "./radar-labels"; +export { RadarArea } from "./radar-area"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart.tsx new file mode 100644 index 0000000..43f2d90 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart.tsx @@ -0,0 +1,32 @@ +"use client" + +// In your app (monorepo/npm): import { RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea } from "@bklitui/ui/charts" +import { RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea } from "@/components/charts" + +const metrics = [ + { key: "speed", label: "Speed" }, + { key: "reliability", label: "Reliability" }, + { key: "comfort", label: "Comfort" }, + { key: "safety", label: "Safety" }, + { key: "efficiency", label: "Efficiency" }, +]; +const data = [ + { id: "a", speed: 80, reliability: 70, comfort: 60, safety: 90, efficiency: 75 }, +]; + +export default function Component() { + return ( +
+
+ + + + + {data.map((row, i) => ( + + ))} + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart-index.ts new file mode 100644 index 0000000..7d478dc --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart-index.ts @@ -0,0 +1,4 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { RingChart } from "./ring-chart"; +export { Ring } from "./ring"; +export { RingCenter } from "./ring-center"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart.tsx new file mode 100644 index 0000000..bf74009 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart.tsx @@ -0,0 +1,26 @@ +"use client" + +// In your app (monorepo/npm): import { RingChart, Ring, RingCenter } from "@bklitui/ui/charts" +import { RingChart, Ring, RingCenter } from "@/components/charts" + +const ringData = [ + { label: "Email", value: 42 }, + { label: "Social", value: 28 }, + { label: "Direct", value: 18 }, + { label: "Other", value: 12 }, +]; + +export default function Component() { + return ( +
+
+ + {ringData.map((item, i) => ( + + ))} + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart-index.ts new file mode 100644 index 0000000..d6be2f3 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart-index.ts @@ -0,0 +1,2 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "./sankey"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart.tsx new file mode 100644 index 0000000..3b87a56 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart.tsx @@ -0,0 +1,34 @@ +"use client" + +// In your app (monorepo/npm): import { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "@bklitui/ui/charts" +import { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "@/components/charts" + +const data = { + nodes: [ + { name: "Ads" }, + { name: "Organic" }, + { name: "Landing" }, + { name: "Product" }, + { name: "Checkout" }, + ], + links: [ + { source: 0, target: 2, value: 40 }, + { source: 1, target: 2, value: 30 }, + { source: 2, target: 3, value: 50 }, + { source: 3, target: 4, value: 35 }, + ], +}; + +export default function Component() { + return ( +
+
+ + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart-index.ts new file mode 100644 index 0000000..bfa680b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart-index.ts @@ -0,0 +1,6 @@ +// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install +export { ScatterChart } from "./scatter-chart"; +export { Scatter } from "./scatter"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart.tsx new file mode 100644 index 0000000..84da754 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart.tsx @@ -0,0 +1,29 @@ +"use client" + +// In your app (monorepo/npm): import { ScatterChart, Scatter, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts" +import { ScatterChart, Scatter, Grid, XAxis, ChartTooltip } from "@/components/charts" + +const chartData = [ + { date: new Date("2024-01-01"), sessions: 420, conversions: 28 }, + { date: new Date("2024-02-01"), sessions: 510, conversions: 34 }, + { date: new Date("2024-03-01"), sessions: 390, conversions: 22 }, + { date: new Date("2024-04-01"), sessions: 580, conversions: 41 }, + { date: new Date("2024-05-01"), sessions: 620, conversions: 38 }, + { date: new Date("2024-06-01"), sessions: 710, conversions: 52 }, +]; + +export default function Component() { + return ( +
+
+ + + + + + + +
+
+ ) +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01-index.ts new file mode 100644 index 0000000..b226fda --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01-index.ts @@ -0,0 +1,6 @@ +/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */ +export { AreaChart } from "./area-chart"; +export { Area } from "./area"; +export { ChartStatFlow } from "./chart-stat-flow"; +export { useChart } from "./chart-context"; +export { LinearGradient } from "@visx/gradient"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01.tsx new file mode 100644 index 0000000..4ce5c9c --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { StatCardArea } from "@/components/stat-card-area"; + +export default function Component() { + return ( +
+
+ +
+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01-index.ts new file mode 100644 index 0000000..7f9e635 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01-index.ts @@ -0,0 +1,3 @@ +/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */ +export { type ChoroplethFeature, ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip, useChoropleth } from "./choropleth"; +export { ChartStatFlow } from "./chart-stat-flow"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01.tsx new file mode 100644 index 0000000..c819a92 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-choropleth-01.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { StatCardChoropleth } from "@/components/stat-card-choropleth"; + +export default function Component() { + return ( +
+
+ +
+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01-index.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01-index.ts new file mode 100644 index 0000000..d8b5f8b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01-index.ts @@ -0,0 +1,5 @@ +/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */ +export { LineChart } from "./line-chart"; +export { Line } from "./line"; +export { ChartStatFlow } from "./chart-stat-flow"; +export { useChart } from "./chart-context"; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01.tsx new file mode 100644 index 0000000..4642050 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { StatCardLine } from "@/components/stat-card-line"; + +export default function Component() { + return ( +
+
+ +
+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-block-registry.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-block-registry.mjs new file mode 100644 index 0000000..6f5c0f3 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-block-registry.mjs @@ -0,0 +1,328 @@ +/** + * Syncs stat-card blocks from apps/web/blocks into registry sources and registry.json + * (shadcn add + Open in v0). Run from packages/ui after generate-v0-examples.mjs. + */ +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const uiRoot = join(__dirname, ".."); +const webBlocksRoot = join(uiRoot, "../../apps/web/blocks"); +const registryBlocksRoot = join(uiRoot, "registry/blocks"); +const examplesDir = join(uiRoot, "registry/examples"); +const registryPath = join(uiRoot, "registry.json"); + +const TREND_BADGE_REGISTRY = `"use client"; + +import { CentralIcon } from "@central-icons-react/all"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export function TrendBadge({ + value, + className, +}: { + value: number; + className?: string; +}) { + const positive = value >= 0; + + return ( + + + {positive ? "+" : ""} + {value.toFixed(1)}% + + ); +} +`; + +const BLOCK_CHART_INDEX = { + "stat-card-area-01": `export { AreaChart } from "./area-chart"; +export { Area } from "./area"; +export { ChartStatFlow } from "./chart-stat-flow"; +export { useChart } from "./chart-context"; +export { LinearGradient } from "@visx/gradient";`, + "stat-card-line-01": `export { LineChart } from "./line-chart"; +export { Line } from "./line"; +export { ChartStatFlow } from "./chart-stat-flow"; +export { useChart } from "./chart-context";`, + "stat-card-choropleth-01": `export { type ChoroplethFeature, ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip, useChoropleth } from "./choropleth"; +export { ChartStatFlow } from "./chart-stat-flow";`, +}; + +/** @type {Record} */ +const BLOCKS = { + "stat-card-area-01": { + title: "Stat Card Area", + description: + "Revenue stat card with gradient area sparkline, NumberFlow, and trend badge", + previewMaxWidth: "max-w-md", + registryDependencies: [ + "@bklit/area-chart", + "@bklit/chart-stat-flow", + "card", + "badge", + ], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@visx/gradient@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all", + ], + files: [ + "components/stat-card-area.tsx", + "components/stat-card-chart.tsx", + "components/stat-card-hover-bridge.tsx", + "components/trend-badge.tsx", + "data/revenue-series.ts", + ], + }, + "stat-card-line-01": { + title: "Stat Card Line", + description: + "Sessions stat card with line sparkline, overlaid NumberFlow, and trend badge", + previewMaxWidth: "max-w-md", + registryDependencies: [ + "@bklit/line-chart", + "@bklit/chart-stat-flow", + "card", + "badge", + ], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@number-flow/react", + "@central-icons-react/all", + ], + files: [ + "components/stat-card-line.tsx", + "components/stat-card-chart.tsx", + "components/stat-card-hover-bridge.tsx", + "components/trend-badge.tsx", + "data/sessions-series.ts", + ], + }, + "stat-card-choropleth-01": { + title: "Stat Card Choropleth", + description: + "Visitor map stat card with choropleth sparkline, NumberFlow, and trend badge", + previewMaxWidth: "max-w-3xl", + registryDependencies: [ + "@bklit/choropleth-chart", + "@bklit/chart-stat-flow", + "card", + "badge", + ], + dependencies: [ + "@number-flow/react", + "@types/geojson", + "@types/topojson-specification", + "@central-icons-react/all", + "topojson-client", + ], + files: [ + "components/stat-card-choropleth.tsx", + "components/stat-card-choropleth-hover-bridge.tsx", + "components/stat-card-chart.tsx", + "components/trend-badge.tsx", + "data/visitors.ts", + "lib/use-world-data.tsx", + ], + }, +}; + +const COMPONENT_EXPORTS = { + "stat-card-area-01": "StatCardArea", + "stat-card-line-01": "StatCardLine", + "stat-card-choropleth-01": "StatCardChoropleth", +}; + +function transformForRegistry(content, filePath) { + if (filePath.endsWith("trend-badge.tsx")) { + return TREND_BADGE_REGISTRY; + } + + return content + .replaceAll("@bklitui/ui/charts", "@/components/charts") + .replaceAll("@/components/docs/use-world-data", "@/lib/use-world-data"); +} + +function copyBlockFile(blockId, relativePath) { + const sourcePath = join(webBlocksRoot, blockId, "files", relativePath); + const targetPath = join(registryBlocksRoot, blockId, relativePath); + + if (relativePath === "lib/use-world-data.tsx") { + const sharedSource = join( + uiRoot, + "../../apps/web/components/docs/use-world-data.tsx" + ); + mkdirSync(dirname(targetPath), { recursive: true }); + cpSync(sharedSource, targetPath); + return; + } + + if (!existsSync(sourcePath)) { + throw new Error(`Block source not found: ${sourcePath}`); + } + + mkdirSync(dirname(targetPath), { recursive: true }); + const content = transformForRegistry( + readFileSync(sourcePath, "utf8"), + relativePath + ); + writeFileSync(targetPath, content); +} + +function blockRegistryFiles(blockId, relativePaths) { + return relativePaths.map((relativePath) => ({ + path: `registry/blocks/${blockId}/${relativePath}`, + type: relativePath.endsWith(".ts") ? "registry:lib" : "registry:component", + target: relativePath, + })); +} + +function blockChartIndexFile(blockId) { + if (!BLOCK_CHART_INDEX[blockId]) { + return null; + } + + return { + path: `registry/examples/${blockId}-index.ts`, + type: "registry:lib", + target: "components/charts/index.ts", + }; +} + +const chartStatFlowItem = { + name: "chart-stat-flow", + type: "registry:component", + title: "Chart Stat Flow", + description: "Animated stat value and label for chart overlays", + registryDependencies: ["@bklit/utils"], + dependencies: ["@number-flow/react", "motion"], + files: [ + { + path: "src/charts/chart-stat-flow.tsx", + type: "registry:component", + target: "components/charts/chart-stat-flow.tsx", + }, + ], +}; + +mkdirSync(examplesDir, { recursive: true }); +mkdirSync(registryBlocksRoot, { recursive: true }); + +for (const [blockId, spec] of Object.entries(BLOCKS)) { + for (const relativePath of spec.files) { + copyBlockFile(blockId, relativePath); + } + + const exportName = COMPONENT_EXPORTS[blockId]; + const mainComponentPath = spec.files.find((file) => + file.startsWith("components/stat-card-") + ); + const importPath = `@/components/${mainComponentPath + ?.replace("components/", "") + .replace(".tsx", "")}`; + + const exampleContent = `"use client"; + +import { ${exportName} } from "${importPath}"; + +export default function Component() { + return ( +
+
+ <${exportName} /> +
+
+ ); +} +`; + + writeFileSync(join(examplesDir, `${blockId}.tsx`), exampleContent); + + const chartIndex = BLOCK_CHART_INDEX[blockId]; + if (chartIndex) { + writeFileSync( + join(examplesDir, `${blockId}-index.ts`), + `/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */\n${chartIndex}\n` + ); + } +} + +const registry = JSON.parse(readFileSync(registryPath, "utf8")); + +registry.items = registry.items.filter( + (item) => + item.name !== "chart-stat-flow" && !item.name.startsWith("stat-card-") +); + +registry.items.push(chartStatFlowItem); + +for (const [blockId, spec] of Object.entries(BLOCKS)) { + const blockFiles = blockRegistryFiles(blockId, spec.files); + const chartIndexFile = blockChartIndexFile(blockId); + if (chartIndexFile) { + blockFiles.push(chartIndexFile); + } + + registry.items.push({ + name: blockId, + type: "registry:block", + title: spec.title, + description: spec.description, + registryDependencies: spec.registryDependencies, + dependencies: spec.dependencies, + files: blockFiles, + }); + + const exampleFiles = [ + { + path: `registry/examples/${blockId}.tsx`, + type: "registry:page", + target: "app/page.tsx", + }, + ]; + + if (chartIndexFile) { + exampleFiles.push(chartIndexFile); + } + + registry.items.push({ + name: `${blockId}-example`, + type: "registry:example", + title: `${spec.title} Example`, + description: `${spec.description} — demo for Open in v0`, + registryDependencies: [`@bklit/${blockId}`], + dependencies: spec.dependencies, + files: exampleFiles, + }); +} + +writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`); +console.log( + `Synced ${Object.keys(BLOCKS).length} stat-card blocks into registry (${Object.keys(BLOCKS).length * 2 + 1} items).` +); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-v0-examples.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-v0-examples.mjs new file mode 100644 index 0000000..c07437b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-v0-examples.mjs @@ -0,0 +1,520 @@ +/** + * Generates registry example entries for Open in v0 (registry:example + registry:page). + * Run from packages/ui: node scripts/generate-v0-examples.mjs + */ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const examplesDir = join(root, "registry/examples"); + +/** Per-chart barrel for installed @/components/charts/* paths in v0. */ +const CHART_INDEX_EXPORTS = { + "area-chart": `export { AreaChart } from "./area-chart"; +export { Area } from "./area"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip";`, + "bar-chart": `export { BarChart } from "./bar-chart"; +export { Bar } from "./bar"; +export { BarXAxis } from "./bar-x-axis"; +export { Grid } from "./grid"; +export { ChartTooltip } from "./tooltip";`, + "line-chart": `export { LineChart } from "./line-chart"; +export { Line } from "./line"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip";`, + "scatter-chart": `export { ScatterChart } from "./scatter-chart"; +export { Scatter } from "./scatter"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip";`, + "pie-chart": `export { PieChart } from "./pie-chart"; +export { PieSlice } from "./pie-slice"; +export { PieCenter } from "./pie-center";`, + "gauge-chart": `export { Gauge } from "./gauge";`, + "heatmap-chart": `export { + HeatmapCells, + HeatmapChart, + HeatmapInteractionBoundary, + HeatmapInteractionProvider, + HeatmapLegend, + HeatmapTooltip, + HeatmapXAxis, + HeatmapYAxis, +} from "./heatmap";`, + "ring-chart": `export { RingChart } from "./ring-chart"; +export { Ring } from "./ring"; +export { RingCenter } from "./ring-center";`, + "radar-chart": `export { RadarChart } from "./radar-chart"; +export { RadarGrid } from "./radar-grid"; +export { RadarAxis } from "./radar-axis"; +export { RadarLabels } from "./radar-labels"; +export { RadarArea } from "./radar-area";`, + "composed-chart": `export { ComposedChart } from "./composed-chart"; +export { SeriesBar } from "./series-bar"; +export { Area } from "./area"; +export { Line } from "./line"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { ChartTooltip } from "./tooltip";`, + "funnel-chart": `export { FunnelChart } from "./funnel-chart";`, + "sankey-chart": `export { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "./sankey";`, + "candlestick-chart": `export { CandlestickChart } from "./candlestick-chart"; +export { Candlestick } from "./candlestick"; +export { Grid } from "./grid"; +export { XAxis } from "./x-axis"; +export { YAxis } from "./y-axis"; +export { ChartTooltip } from "./tooltip";`, + "choropleth-chart": `export { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "./choropleth";`, + "live-line-chart": `export { LiveLineChart } from "./live-line-chart"; +export { LiveLine } from "./live-line"; +export { LiveXAxis } from "./live-x-axis"; +export { LiveYAxis } from "./live-y-axis"; +export { ChartTooltip } from "./tooltip";`, +}; + +/** @type {Record} */ +const EXAMPLES = { + "area-chart": { + registryDependencies: [ + "@bklit/area-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + ], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: "AreaChart, Area, Grid, XAxis, ChartTooltip", + data: `const chartData = [ + { date: new Date("2024-01-01"), desktop: 186 }, + { date: new Date("2024-02-01"), desktop: 305 }, + { date: new Date("2024-03-01"), desktop: 237 }, + { date: new Date("2024-04-01"), desktop: 73 }, + { date: new Date("2024-05-01"), desktop: 209 }, + { date: new Date("2024-06-01"), desktop: 214 }, +];`, + body: ` + + + + +`, + extraImports: `import { curveNatural } from "@visx/curve";`, + }, + "bar-chart": { + registryDependencies: [ + "@bklit/bar-chart", + "@bklit/grid", + "@bklit/chart-tooltip", + ], + dependencies: [ + "@visx/gradient@4.0.1-alpha.0", + "@visx/pattern@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: "BarChart, Bar, BarXAxis, Grid, ChartTooltip", + data: `const data = [ + { month: "Jan", revenue: 12000, profit: 4500 }, + { month: "Feb", revenue: 15500, profit: 5200 }, + { month: "Mar", revenue: 11000, profit: 3800 }, + { month: "Apr", revenue: 18500, profit: 7100 }, + { month: "May", revenue: 16800, profit: 5400 }, + { month: "Jun", revenue: 21200, profit: 8800 }, +];`, + body: ` + + + + + +`, + }, + "line-chart": { + registryDependencies: [ + "@bklit/line-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + ], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: "LineChart, Line, Grid, XAxis, ChartTooltip", + data: `const chartData = [ + { date: new Date("2024-01-01"), users: 1200 }, + { date: new Date("2024-02-01"), users: 1350 }, + { date: new Date("2024-03-01"), users: 1100 }, + { date: new Date("2024-04-01"), users: 1450 }, + { date: new Date("2024-05-01"), users: 1380 }, + { date: new Date("2024-06-01"), users: 1520 }, +];`, + body: ` + + + + +`, + extraImports: `import { curveNatural } from "@visx/curve";`, + }, + "scatter-chart": { + registryDependencies: [ + "@bklit/scatter-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + ], + dependencies: ["d3-scale", "d3-array", "motion", "react-use-measure"], + importFrom: "ScatterChart, Scatter, Grid, XAxis, ChartTooltip", + data: `const chartData = [ + { date: new Date("2024-01-01"), sessions: 420, conversions: 28 }, + { date: new Date("2024-02-01"), sessions: 510, conversions: 34 }, + { date: new Date("2024-03-01"), sessions: 390, conversions: 22 }, + { date: new Date("2024-04-01"), sessions: 580, conversions: 41 }, + { date: new Date("2024-05-01"), sessions: 620, conversions: 38 }, + { date: new Date("2024-06-01"), sessions: 710, conversions: 52 }, +];`, + body: ` + + + + + +`, + }, + "pie-chart": { + registryDependencies: ["@bklit/pie-chart"], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: "PieChart, PieSlice, PieCenter", + data: `const pieData = [ + { label: "Direct", value: 320 }, + { label: "Organic", value: 280 }, + { label: "Referral", value: 190 }, + { label: "Social", value: 140 }, +];`, + body: ` + {pieData.map((item, i) => ( + + ))} + +`, + }, + "gauge-chart": { + registryDependencies: ["@bklit/gauge-chart"], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: "Gauge", + data: "", + body: ``, + }, + "heatmap-chart": { + registryDependencies: ["@bklit/heatmap-chart"], + dependencies: [ + "@visx/heatmap@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: + "HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis", + data: `const data = [ + { + bin: 0, + bins: [ + { bin: 0, count: 2, date: new Date(2024, 0, 1) }, + { bin: 1, count: 0, date: new Date(2024, 0, 2) }, + { bin: 2, count: 3, date: new Date(2024, 0, 3) }, + { bin: 3, count: 1, date: new Date(2024, 0, 4) }, + { bin: 4, count: 4, date: new Date(2024, 0, 5) }, + { bin: 5, count: 0, date: new Date(2024, 0, 6) }, + { bin: 6, count: 1, date: new Date(2024, 0, 7) }, + ], + }, + { + bin: 1, + bins: [ + { bin: 0, count: 1, date: new Date(2024, 0, 8) }, + { bin: 1, count: 2, date: new Date(2024, 0, 9) }, + { bin: 2, count: 0, date: new Date(2024, 0, 10) }, + { bin: 3, count: 3, date: new Date(2024, 0, 11) }, + { bin: 4, count: 2, date: new Date(2024, 0, 12) }, + { bin: 5, count: 1, date: new Date(2024, 0, 13) }, + { bin: 6, count: 0, date: new Date(2024, 0, 14) }, + ], + }, +];`, + body: ` + +
+ + + + + + + +
+
+
`, + }, + "ring-chart": { + registryDependencies: ["@bklit/ring-chart"], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: "RingChart, Ring, RingCenter", + data: `const ringData = [ + { label: "Email", value: 42 }, + { label: "Social", value: 28 }, + { label: "Direct", value: 18 }, + { label: "Other", value: 12 }, +];`, + body: ` + {ringData.map((item, i) => ( + + ))} + +`, + }, + "radar-chart": { + registryDependencies: ["@bklit/radar-chart"], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: "RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea", + data: `const metrics = [ + { key: "speed", label: "Speed" }, + { key: "reliability", label: "Reliability" }, + { key: "comfort", label: "Comfort" }, + { key: "safety", label: "Safety" }, + { key: "efficiency", label: "Efficiency" }, +]; +const data = [ + { id: "a", speed: 80, reliability: 70, comfort: 60, safety: 90, efficiency: 75 }, +];`, + body: ` + + + + {data.map((row, i) => ( + + ))} +`, + }, + "composed-chart": { + registryDependencies: [ + "@bklit/composed-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/chart-tooltip", + ], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: + "ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip", + data: `const chartData = [ + { date: new Date("2024-01-01"), revenue: 4200, runRate: 3800 }, + { date: new Date("2024-02-01"), revenue: 5100, runRate: 4600 }, + { date: new Date("2024-03-01"), revenue: 4800, runRate: 5200 }, + { date: new Date("2024-04-01"), revenue: 5500, runRate: 5000 }, +];`, + body: ` + + + + + + +`, + extraImports: `import { curveNatural } from "@visx/curve";`, + }, + "funnel-chart": { + registryDependencies: ["@bklit/funnel-chart"], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: "FunnelChart", + data: `const funnelData = [ + { label: "Visitors", value: 12000 }, + { label: "Signups", value: 4800 }, + { label: "Activated", value: 2100 }, + { label: "Paid", value: 840 }, +];`, + body: ``, + }, + "sankey-chart": { + registryDependencies: ["@bklit/sankey-chart"], + dependencies: [ + "@visx/sankey@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: "SankeyChart, SankeyLink, SankeyNode, SankeyTooltip", + data: `const data = { + nodes: [ + { name: "Ads" }, + { name: "Organic" }, + { name: "Landing" }, + { name: "Product" }, + { name: "Checkout" }, + ], + links: [ + { source: 0, target: 2, value: 40 }, + { source: 1, target: 2, value: 30 }, + { source: 2, target: 3, value: 50 }, + { source: 3, target: 4, value: 35 }, + ], +};`, + body: ` + + + +`, + }, + "candlestick-chart": { + registryDependencies: [ + "@bklit/candlestick-chart", + "@bklit/grid", + "@bklit/x-axis", + "@bklit/y-axis", + "@bklit/chart-tooltip", + ], + dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"], + importFrom: + "CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip", + data: `const ohlcData = [ + { date: new Date("2024-01-02"), open: 100, high: 108, low: 98, close: 105 }, + { date: new Date("2024-01-03"), open: 105, high: 110, low: 102, close: 103 }, + { date: new Date("2024-01-04"), open: 103, high: 112, low: 101, close: 110 }, + { date: new Date("2024-01-05"), open: 110, high: 115, low: 107, close: 108 }, +];`, + body: ` + + + + + +`, + }, + "choropleth-chart": { + registryDependencies: ["@bklit/choropleth-chart"], + dependencies: ["@visx/geo@4.0.1-alpha.0", "d3-geo", "topojson-client"], + importFrom: + "ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip", + data: `const features = [ + { id: "US", name: "United States", value: 120 }, + { id: "CA", name: "Canada", value: 45 }, + { id: "GB", name: "United Kingdom", value: 62 }, +];`, + body: ` + + +`, + }, + "live-line-chart": { + registryDependencies: ["@bklit/live-line-chart", "@bklit/chart-tooltip"], + dependencies: [ + "@visx/curve@4.0.1-alpha.0", + "@visx/shape@4.0.1-alpha.0", + "motion", + ], + importFrom: "LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip", + data: `const initialData = Array.from({ length: 24 }, (_, i) => ({ + time: Date.now() - (23 - i) * 60_000, + value: 50 + Math.sin(i / 3) * 20, +}));`, + body: ` + + + + +`, + extraImports: `import { curveNatural } from "@visx/curve";`, + }, +}; + +mkdirSync(examplesDir, { recursive: true }); + +for (const [slug, spec] of Object.entries(EXAMPLES)) { + const extra = spec.extraImports ? `${spec.extraImports}\n\n` : ""; + const content = `"use client" + +// In your app (monorepo/npm): import { ${spec.importFrom} } from "@bklitui/ui/charts" +import { ${spec.importFrom} } from "@/components/charts" + +${extra}${spec.data} + +export default function Component() { + return ( +
+
+ ${spec.body} +
+
+ ) +} +`; + writeFileSync(join(examplesDir, `${slug}.tsx`), content); + + const indexExports = CHART_INDEX_EXPORTS[slug]; + if (indexExports) { + writeFileSync( + join(examplesDir, `${slug}-index.ts`), + `// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install\n${indexExports}\n` + ); + } +} + +const registryPath = join(root, "registry.json"); +const registry = JSON.parse(readFileSync(registryPath, "utf8")); + +registry.items = registry.items.filter( + (item) => !item.name.endsWith("-example") +); + +const exampleItems = Object.entries(EXAMPLES).map(([slug, spec]) => { + const files = [ + { + path: `registry/examples/${slug}.tsx`, + type: "registry:page", + target: "app/page.tsx", + }, + ]; + if (CHART_INDEX_EXPORTS[slug]) { + files.push({ + path: `registry/examples/${slug}-index.ts`, + type: "registry:lib", + target: "components/charts/index.ts", + }); + } + return { + name: `${slug}-example`, + type: "registry:example", + title: `${slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} Example`, + description: `Composable ${slug} demo for Open in v0`, + registryDependencies: spec.registryDependencies, + dependencies: spec.dependencies, + files, + }; +}); + +registry.items.push(...exampleItems); + +writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`); +console.log(`Wrote ${Object.keys(EXAMPLES).length} v0 example registry items`); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/run-tests.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/run-tests.mjs new file mode 100644 index 0000000..ad965a5 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/run-tests.mjs @@ -0,0 +1,24 @@ +import { spawnSync } from "node:child_process"; +import { readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const testDir = join(packageRoot, "src/charts/__tests__"); + +const tests = readdirSync(testDir) + .filter((name) => name.endsWith(".test.ts")) + .map((name) => join(testDir, name)); + +if (tests.length === 0) { + console.error(`No test files found in ${testDir}`); + process.exit(1); +} + +const result = spawnSync( + process.execPath, + ["--import", "tsx", "--test", ...tests], + { stdio: "inherit", cwd: packageRoot } +); + +process.exit(result.status ?? 1); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-deps.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-deps.mjs new file mode 100644 index 0000000..5acf555 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-deps.mjs @@ -0,0 +1,169 @@ +#!/usr/bin/env node +/** + * Verifies registry item `dependencies` cover all npm imports in the item's + * file tree plus transitive @bklit registryDependencies (shadcn / v0 install closure). + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const uiRoot = path.join(__dirname, ".."); +const registryPath = path.join(uiRoot, "registry.json"); + +const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); +const byName = Object.fromEntries( + registry.items.map((item) => [item.name, item]) +); + +const importRe = + /(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g; +const DEP_VERSION_SUFFIX_RE = /@[\d.]+(-[\w.]+)?$/; +const BKLIT_REGISTRY_DEP_RE = /^@bklit\/(.+)$/; + +function getPackageName(spec) { + if (spec.startsWith(".") || spec.startsWith("@/")) { + return null; + } + if (spec.startsWith("@")) { + const parts = spec.split("/"); + return `${parts[0]}/${parts[1]}`; + } + return spec.split("/")[0]; +} + +function normalizeDep(dep) { + return dep.replace(DEP_VERSION_SUFFIX_RE, ""); +} + +/** Import specifiers that are satisfied by @types/* registry dependencies. */ +const TYPES_ALIASES = { + geojson: ["geojson", "@types/geojson"], +}; + +function isDeclared(pkg, declared) { + if (declared.has(pkg)) { + return true; + } + const aliases = TYPES_ALIASES[pkg]; + if (aliases) { + return aliases.some((name) => declared.has(name)); + } + return false; +} + +function scanFiles(files) { + const used = new Set(); + for (const file of files ?? []) { + const fullPath = path.join(uiRoot, file.path); + if (!fs.existsSync(fullPath)) { + throw new Error(`Registry file not found: ${file.path}`); + } + const content = fs.readFileSync(fullPath, "utf8"); + importRe.lastIndex = 0; + let match = importRe.exec(content); + while (match) { + const pkg = getPackageName(match[1]); + if (pkg && pkg !== "react" && pkg !== "react-dom") { + used.add(pkg); + } + match = importRe.exec(content); + } + } + return used; +} + +function getRegistryDepName(dep) { + const m = dep.match(BKLIT_REGISTRY_DEP_RE); + return m ? m[1] : null; +} + +function collectDeclared(name, visited = new Set()) { + const declared = new Set(); + const visit = (itemName) => { + if (visited.has(itemName)) { + return; + } + visited.add(itemName); + const item = byName[itemName]; + if (!item) { + return; + } + for (const dep of item.dependencies ?? []) { + declared.add(normalizeDep(dep)); + } + for (const regDep of item.registryDependencies ?? []) { + const nested = getRegistryDepName(regDep); + if (nested) { + visit(nested); + } + } + }; + visit(name); + return declared; +} + +function collectUsed(name, visited = new Set()) { + const used = new Set(); + const visit = (itemName) => { + if (visited.has(itemName)) { + return; + } + visited.add(itemName); + const item = byName[itemName]; + if (!item) { + return; + } + for (const pkg of scanFiles(item.files)) { + used.add(pkg); + } + for (const regDep of item.registryDependencies ?? []) { + const nested = getRegistryDepName(regDep); + if (nested) { + visit(nested); + } + } + }; + visit(name); + return used; +} + +const chartItems = registry.items.filter( + (item) => + item.type === "registry:component" && + (item.name.endsWith("-chart") || + item.name === "chart-context" || + item.name === "chart-animation") +); + +const utilityItems = registry.items.filter((item) => + ["grid", "x-axis", "y-axis", "chart-tooltip", "legend", "markers"].includes( + item.name + ) +); + +const failures = []; + +for (const item of [...chartItems, ...utilityItems]) { + const declared = collectDeclared(item.name); + const used = collectUsed(item.name); + const missing = [...used].filter((pkg) => !isDeclared(pkg, declared)).sort(); + if (missing.length > 0) { + failures.push({ name: item.name, missing }); + } +} + +if (failures.length > 0) { + console.error("Registry dependency closure check failed:\n"); + for (const { name, missing } of failures) { + console.error(` ${name}: missing ${missing.join(", ")}`); + } + console.error( + "\nAdd missing packages to `dependencies` in packages/ui/registry.json (on the item or a registryDependency)." + ); + process.exit(1); +} + +console.log( + `Registry dependency closure OK (${chartItems.length + utilityItems.length} items checked).` +); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-files.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-files.mjs new file mode 100644 index 0000000..c3c7b5c --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-files.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/** + * Verifies every installable registry item ships all local `./` imports in its + * transitive file closure (item files + registryDependencies). + * + * Also rejects CommonJS `require("react-dom")` in registry source files — it + * breaks Vite/ESM consumers even though Next.js/webpack may tolerate it. + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const uiRoot = path.join(__dirname, ".."); +const registryPath = path.join(uiRoot, "registry.json"); + +const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); +const byName = Object.fromEntries( + registry.items.map((item) => [item.name, item]) +); + +const importRe = + /(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"](\.[^'"]+)['"]/g; +const BKLIT_REGISTRY_DEP_RE = /^@bklit\/(.+)$/; + +const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]; + +function resolveRelativeImport(fromFilePath, spec) { + const dir = path.dirname(fromFilePath); + const base = path.normalize(path.join(dir, spec)); + const candidates = [ + base, + ...RESOLVE_EXTENSIONS.map((ext) => base + ext), + ...RESOLVE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)), + ]; + + for (const candidate of candidates) { + const fullPath = path.join(uiRoot, candidate); + if (fs.existsSync(fullPath)) { + return candidate; + } + } + + return null; +} + +function collectItemFiles(itemName, visited = new Set()) { + const files = new Set(); + const visit = (name) => { + if (visited.has(name)) { + return; + } + visited.add(name); + const item = byName[name]; + if (!item) { + return; + } + for (const file of item.files ?? []) { + files.add(file.path); + } + for (const regDep of item.registryDependencies ?? []) { + const nested = regDep.match(BKLIT_REGISTRY_DEP_RE)?.[1]; + if (nested) { + visit(nested); + } + } + }; + visit(itemName); + return files; +} + +function scanRelativeImports(filePath) { + // v0/shadcn example barrels re-export co-installed chart files from the + // target `components/charts/` directory — not from registry source paths. + if ( + filePath.startsWith("registry/examples/") && + filePath.endsWith("-index.ts") + ) { + return { imports: [], content: "" }; + } + + const fullPath = path.join(uiRoot, filePath); + if (!fs.existsSync(fullPath)) { + throw new Error(`Registry file not found: ${filePath}`); + } + const content = fs.readFileSync(fullPath, "utf8"); + const imports = []; + importRe.lastIndex = 0; + let match = importRe.exec(content); + while (match) { + imports.push(match[1]); + match = importRe.exec(content); + } + return { imports, content }; +} + +const INSTALLABLE_TYPES = new Set([ + "registry:component", + "registry:lib", + "registry:block", +]); + +const installableItems = registry.items.filter((item) => + INSTALLABLE_TYPES.has(item.type) +); + +const closureFailures = []; +const requireFailures = []; + +for (const item of installableItems) { + const closure = collectItemFiles(item.name); + const missing = new Map(); + + for (const filePath of closure) { + const { imports, content } = scanRelativeImports(filePath); + + if (content.includes('require("react-dom")')) { + requireFailures.push({ item: item.name, file: filePath }); + } + + for (const spec of imports) { + const resolved = resolveRelativeImport(filePath, spec); + if (!resolved) { + const key = `${filePath} -> ${spec}`; + if (!missing.has(key)) { + missing.set(key, "missing on disk"); + } + continue; + } + if (!closure.has(resolved)) { + const key = `${filePath} -> ${spec} (${resolved})`; + if (!missing.has(key)) { + missing.set(key, "not in install closure"); + } + } + } + } + + if (missing.size > 0) { + closureFailures.push({ name: item.name, missing: [...missing.entries()] }); + } +} + +let failed = false; + +if (closureFailures.length > 0) { + failed = true; + console.error("Registry local import closure check failed:\n"); + for (const { name, missing } of closureFailures) { + console.error(` ${name}:`); + for (const [ref, reason] of missing) { + console.error(` - ${ref} (${reason})`); + } + console.error(""); + } + console.error( + "Add missing files to packages/ui/registry.json (on the item or a registryDependency)." + ); +} + +if (requireFailures.length > 0) { + failed = true; + console.error('Registry source must not use require("react-dom"):\n'); + for (const { item, file } of requireFailures) { + console.error(` ${item}: ${file}`); + } + console.error( + '\nUse `import { createPortal } from "react-dom"` in "use client" components instead.' + ); +} + +if (failed) { + process.exit(1); +} + +console.log( + `Registry file closure OK (${installableItems.length} installable items checked).` +); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-sync.mjs b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-sync.mjs new file mode 100644 index 0000000..68c3f0d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-sync.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node +/** + * Ensures shadcn registry output matches chart source when packages/ui/src/charts changes. + * Run via `pnpm registry:verify` (locally or in CI). + */ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(__dirname, "../../.."); + +const CHARTS_SRC = "packages/ui/src/charts"; +const REGISTRY_PATHS = [ + "apps/web/public/r", + "packages/ui/registry.json", + "packages/ui/registry/examples", + "packages/ui/registry/blocks", +]; + +/** Build scripts may reformat these; compare parsed JSON, not raw text. */ +const SEMANTIC_JSON_PATHS = [ + "packages/ui/registry.json", + "apps/web/public/r/registry.json", +]; + +function run(cmd, { inherit = false } = {}) { + return execSync(cmd, { + cwd: repoRoot, + encoding: "utf8", + stdio: inherit ? "inherit" : "pipe", + }); +} + +function readJson(relativePath) { + return JSON.parse(fs.readFileSync(join(repoRoot, relativePath), "utf8")); +} + +function jsonEqual(a, b) { + return JSON.stringify(a) === JSON.stringify(b); +} + +function resolveDiffRange() { + if (process.env.REGISTRY_SYNC_DIFF_RANGE) { + return process.env.REGISTRY_SYNC_DIFF_RANGE; + } + + if (process.env.GITHUB_EVENT_NAME === "pull_request") { + const base = process.env.GITHUB_BASE_REF || "main"; + return `origin/${base}...HEAD`; + } + + if ( + process.env.GITHUB_EVENT_NAME === "push" && + process.env.GITHUB_EVENT_BEFORE && + process.env.GITHUB_SHA + ) { + return `${process.env.GITHUB_EVENT_BEFORE}...${process.env.GITHUB_SHA}`; + } + + return "origin/main...HEAD"; +} + +function chartsChanged(diffRange) { + const changed = run( + `git diff --name-only ${diffRange} -- ${CHARTS_SRC}` + ).trim(); + return changed.length > 0; +} + +function listDirtyRegistryFiles() { + const excludeArgs = SEMANTIC_JSON_PATHS.map((p) => `':!${p}'`).join(" "); + const paths = REGISTRY_PATHS.join(" "); + const status = run( + `git status --porcelain -- ${paths} ${excludeArgs}` + ).trim(); + if (!status) { + return []; + } + return status.split("\n").filter(Boolean); +} + +function listSemanticRegistryDrift(beforeByPath) { + const drift = []; + for (const relativePath of SEMANTIC_JSON_PATHS) { + const after = readJson(relativePath); + if (!jsonEqual(beforeByPath[relativePath], after)) { + drift.push(relativePath); + } + } + return drift; +} + +function main() { + const diffRange = resolveDiffRange(); + + if (!chartsChanged(diffRange)) { + console.log( + `No changes in ${CHARTS_SRC} (${diffRange}); registry sync check skipped.` + ); + process.exit(0); + } + + console.log( + `Chart source changed (${diffRange}); rebuilding registry to verify sync...` + ); + + const registryBefore = Object.fromEntries( + SEMANTIC_JSON_PATHS.map((relativePath) => [ + relativePath, + readJson(relativePath), + ]) + ); + + run("pnpm registry:build", { inherit: true }); + + const semanticDrift = listSemanticRegistryDrift(registryBefore); + const dirty = listDirtyRegistryFiles(); + const failures = [ + ...semanticDrift.map((path) => `${path} (content changed)`), + ...dirty, + ]; + + if (failures.length > 0) { + console.error("\nRegistry is out of sync with chart source changes.\n"); + console.error( + "Run `pnpm registry:build` from the repo root and commit the updated registry files:\n" + ); + for (const line of failures) { + console.error(` ${line}`); + } + process.exit(1); + } + + console.log("Registry is in sync with chart source."); +} + +main(); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/button.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/button.tsx new file mode 100644 index 0000000..9c8e325 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/button.tsx @@ -0,0 +1,21 @@ +"use client"; + +import type { ReactNode } from "react"; + +interface ButtonProps { + children: ReactNode; + className?: string; + appName: string; +} + +export const Button = ({ children, className, appName }: ButtonProps) => { + return ( + + ); +}; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/card.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/card.tsx new file mode 100644 index 0000000..3f99a89 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/card.tsx @@ -0,0 +1,27 @@ +import type { JSX } from "react"; + +export function Card({ + className, + title, + children, + href, +}: { + className?: string; + title: string; + children: React.ReactNode; + href: string; +}): JSX.Element { + return ( + +

+ {title} -> +

+

{children}

+
+ ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/chart-grid.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/chart-grid.tsx new file mode 100644 index 0000000..2b29a4c --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/chart-grid.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { GridColumns, GridRows } from "@visx/grid"; +import type { scaleLinear, scaleTime } from "@visx/scale"; +import { useId } from "react"; + +type ScaleLinear = ReturnType< + typeof scaleLinear +>; +type ScaleTime = ReturnType< + typeof scaleTime +>; + +export interface ChartGridProps { + /** Width of the grid area */ + width: number; + /** Height of the grid area */ + height: number; + /** X scale (time scale) */ + xScale: ScaleTime; + /** Y scale (linear scale) */ + yScale: ScaleLinear; + /** Show horizontal grid lines. Default: true */ + showRows?: boolean; + /** Show vertical grid lines. Default: false */ + showColumns?: boolean; + /** Number of horizontal grid lines. Default: 5 */ + numTicksRows?: number; + /** Number of vertical grid lines. Default: 10 */ + numTicksColumns?: number; + /** Grid line stroke color. Default: var(--chart-grid) */ + stroke?: string; + /** Grid line stroke opacity. Default: 1 */ + strokeOpacity?: number; + /** Grid line stroke width. Default: 1 */ + strokeWidth?: number; + /** Grid line dash array. Default: "4,4" for dashed lines */ + strokeDasharray?: string; + /** Enable horizontal fade effect on grid rows. Default: true */ + fadeRows?: boolean; +} + +export function ChartGrid({ + width, + height, + xScale, + yScale, + showRows = true, + showColumns = false, + numTicksRows = 5, + numTicksColumns = 10, + stroke = "var(--chart-grid)", + strokeOpacity = 1, + strokeWidth = 1, + strokeDasharray = "4,4", + fadeRows = true, +}: ChartGridProps) { + const uniqueId = useId(); + const maskId = `chart-grid-rows-fade-${uniqueId}`; + const gradientId = `${maskId}-gradient`; + + return ( + + {/* Gradient mask for horizontal grid lines - fades at both ends */} + {showRows && fadeRows && ( + + + + + + + + + + + + )} + + {showRows && ( + + + + )} + {showColumns && ( + + )} + + ); +} + +export default ChartGrid; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/animation.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/animation.test.ts new file mode 100644 index 0000000..5286c59 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/animation.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + clipRevealTransition, + DEFAULT_CHART_ENTER_TRANSITION, +} from "../animation"; + +describe("clipRevealTransition", () => { + it("preserves explicit tween ease", () => { + const tween = { + type: "tween" as const, + duration: 0.9, + ease: [0.1, 0.2, 0.3, 0.4] as [number, number, number, number], + }; + assert.deepEqual(clipRevealTransition(tween), tween); + }); + + it("applies default ease when tween omits ease", () => { + const result = clipRevealTransition({ + type: "tween", + duration: 0.9, + }); + assert.equal(result.type, "tween"); + assert.equal(result.duration, 0.9); + assert.deepEqual(result.ease, DEFAULT_CHART_ENTER_TRANSITION.ease); + }); + + it("converts spring to tween for svg width reveal", () => { + const result = clipRevealTransition({ + type: "spring", + duration: 1.2, + bounce: 0.5, + }); + assert.equal(result.type, "tween"); + assert.equal(result.duration, 1.2); + assert.deepEqual(result.ease, DEFAULT_CHART_ENTER_TRANSITION.ease); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/bar-depth-geometry.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/bar-depth-geometry.test.ts new file mode 100644 index 0000000..4a0195e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/bar-depth-geometry.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + BAR_DEPTH_MAX_PX, + BAR_DEPTH_PERSPECTIVE_RATIO, + barDepthAndRise, + barDepthMaxDepth, +} from "../bar-depth-geometry"; + +describe("barDepthMaxDepth", () => { + it("caps at BAR_DEPTH_MAX_PX when the gap is wide", () => { + // step 100, band 90 → gap 10, gap-1=9; band*0.22=19.8 → min(19.8, 9, 7) = 7 + assert.equal(barDepthMaxDepth(100, 90), BAR_DEPTH_MAX_PX); + }); + + it("is bounded by the inter-bar gap on dense charts", () => { + // step 50, band 48 → gap 2, gap-1=1; band*0.22=10.56 → min(10.56, 1, 7) = 1 + assert.equal(barDepthMaxDepth(50, 48), 1); + }); + + it("is 0 when there is no gap between bars", () => { + assert.equal(barDepthMaxDepth(50, 50), 0); + }); +}); + +describe("barDepthAndRise", () => { + it("returns no depth for a dead-center bar (absOffset 0)", () => { + assert.deepEqual(barDepthAndRise(0, 100, 7), { + depth: 0, + perspectiveRise: 0, + }); + }); + + it("scales depth with offset, capped by maxDepth at the edge", () => { + assert.equal(barDepthAndRise(1, 100, 7).depth, 7); + assert.equal(barDepthAndRise(0.5, 100, 7).depth, 3.5); + }); + + it("caps depth by the bar height so short bars stay proportional", () => { + // naturalHeight 4 < maxDepth 7 → depth capped at 4 + assert.equal(barDepthAndRise(1, 4, 7).depth, 4); + }); + + it("derives the rise from depth via the perspective ratio", () => { + const { depth, perspectiveRise } = barDepthAndRise(0.5, 100, 7); + assert.equal(perspectiveRise, depth * BAR_DEPTH_PERSPECTIVE_RATIO); + }); + + it("clamps out-of-range inputs defensively", () => { + assert.equal(barDepthAndRise(2, 100, 7).depth, 7); // absOffset clamped to 1 + assert.equal(barDepthAndRise(-1, 100, 7).depth, 0); // clamped to 0 + assert.equal(barDepthAndRise(1, -5, 7).depth, 0); // negative height → 0 + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/chart-formatters.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/chart-formatters.test.ts new file mode 100644 index 0000000..0a2ae15 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/chart-formatters.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + hmsTimeFmt, + intFmt, + shortDateFmt, + weekdayDateFmt, +} from "../chart-formatters"; + +const sampleDates = [ + new Date(2025, 0, 5, 9, 8, 7), + new Date(2024, 11, 31, 23, 59, 59), + new Date(2026, 6, 4, 12, 0, 0), +]; + +const sampleNumbers = [0, 42, 1234, 1_234_567, -9876.5]; + +describe("chart-formatters", () => { + describe("shortDateFmt", () => { + for (const date of sampleDates) { + it(`matches toLocaleDateString for ${date.toISOString()}`, () => { + assert.equal( + shortDateFmt.format(date), + date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + ); + }); + } + }); + + describe("weekdayDateFmt", () => { + for (const date of sampleDates) { + it(`matches toLocaleDateString for ${date.toISOString()}`, () => { + assert.equal( + weekdayDateFmt.format(date), + date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }) + ); + }); + } + }); + + describe("hmsTimeFmt", () => { + for (const date of sampleDates) { + it(`matches toLocaleTimeString for ${date.toISOString()}`, () => { + assert.equal( + hmsTimeFmt.format(date), + date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + ); + }); + } + }); + + describe("intFmt", () => { + for (const value of sampleNumbers) { + it(`matches toLocaleString for ${value}`, () => { + assert.equal(intFmt(value), value.toLocaleString("en-US")); + }); + } + + it("is a reusable formatter function", () => { + const formatValue = intFmt; + assert.equal(formatValue(1000), (1000).toLocaleString("en-US")); + }); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/decimate-time-series.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/decimate-time-series.test.ts new file mode 100644 index 0000000..d7cc192 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/decimate-time-series.test.ts @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + decimateOhlcData, + decimateTimeSeries, + maxRenderPointsForWidth, +} from "../decimate-time-series"; + +describe("decimateTimeSeries", () => { + it("returns the original array when under the point budget", () => { + const data = [{ v: 1 }, { v: 2 }, { v: 3 }]; + assert.equal(decimateTimeSeries(data, 10), data); + }); + + it("always keeps the first and last points", () => { + const data = Array.from({ length: 100 }, (_, i) => ({ v: i })); + const sampled = decimateTimeSeries(data, 20, ["v"]); + assert.equal(sampled[0]?.v, 0); + assert.equal(sampled.at(-1)?.v, 99); + assert.equal(sampled.length, 20); + }); + + it("preserves spikes in the series", () => { + const data = Array.from({ length: 50 }, (_, i) => ({ + v: i === 25 ? 1000 : i, + })); + const sampled = decimateTimeSeries(data, 10, ["v"]); + assert(sampled.some((point) => point.v === 1000)); + }); +}); + +describe("decimateOhlcData", () => { + it("preserves bucket high/low extremes", () => { + const data = Array.from({ length: 100 }, (_, i) => ({ + date: new Date(i), + open: i, + high: i === 50 ? 999 : i + 5, + low: i === 50 ? 1 : i - 5, + close: i + 1, + })); + const sampled = decimateOhlcData(data, 10); + assert(sampled.some((row) => row.high === 999)); + assert(sampled.some((row) => row.low === 1)); + assert.equal(sampled.length, 10); + }); +}); + +describe("maxRenderPointsForWidth", () => { + it("returns at least 64 points", () => { + assert.equal(maxRenderPointsForWidth(10), 64); + }); + + it("scales with chart width", () => { + assert.equal(maxRenderPointsForWidth(400), 600); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/highlight-segment-bounds.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/highlight-segment-bounds.test.ts new file mode 100644 index 0000000..285f2e9 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/highlight-segment-bounds.test.ts @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { computeSegmentBounds } from "../highlight-segment-bounds"; + +// Data points at t = 0,10,20,30 ms; a linear pixel xScale (1px per ms) keeps the +// arithmetic obvious — the band is the pixel x-range one data point either side. +const data = [{ t: 0 }, { t: 10 }, { t: 20 }, { t: 30 }]; +const xAccessor = (d: Record) => new Date(d.t as number); +const xScale = (value: Date) => value.getTime(); + +describe("computeSegmentBounds", () => { + it("is inactive for empty data", () => { + assert.deepEqual( + computeSegmentBounds([], xScale, xAccessor, { index: 1 }, null), + { x: 0, width: 0, isActive: false } + ); + }); + + it("is inactive with no hover and no selection", () => { + assert.equal( + computeSegmentBounds(data, xScale, xAccessor, null, null).isActive, + false + ); + }); + + it("spans one data point either side of the hovered index", () => { + // idx 1 → [t 0, t 20] → x 0, width 20 + assert.deepEqual( + computeSegmentBounds(data, xScale, xAccessor, { index: 1 }, null), + { x: 0, width: 20, isActive: true } + ); + }); + + it("clamps the start at the first index", () => { + // idx 0 → [t 0, t 10] → x 0, width 10 + assert.deepEqual( + computeSegmentBounds(data, xScale, xAccessor, { index: 0 }, null), + { x: 0, width: 10, isActive: true } + ); + }); + + it("clamps the end at the last index", () => { + // idx 3 (last) → [t 20, t 30] → x 20, width 10 + assert.deepEqual( + computeSegmentBounds(data, xScale, xAccessor, { index: 3 }, null), + { x: 20, width: 10, isActive: true } + ); + }); + + it("uses the dragged pixel range for an active selection (takes priority)", () => { + const bounds = computeSegmentBounds( + data, + xScale, + xAccessor, + { index: 1 }, // ignored — selection wins + { active: true, startX: 5, endX: 25 } + ); + assert.deepEqual(bounds, { x: 5, width: 20, isActive: true }); + }); + + it("normalizes a reversed (right-to-left) selection drag", () => { + const bounds = computeSegmentBounds(data, xScale, xAccessor, null, { + active: true, + startX: 25, + endX: 5, + }); + assert.deepEqual(bounds, { x: 5, width: 20, isActive: true }); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/line-loading-pulse.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/line-loading-pulse.test.ts new file mode 100644 index 0000000..04402b3 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/line-loading-pulse.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { resolveLineLoadingPulseMode } from "../line-loading-pulse"; + +describe("resolveLineLoadingPulseMode", () => { + it("maps loading phases to pulse modes", () => { + assert.equal(resolveLineLoadingPulseMode("loading"), "loop"); + assert.equal(resolveLineLoadingPulseMode("exiting"), "exit"); + assert.equal(resolveLineLoadingPulseMode("revealingLoading"), "enter"); + }); + + it("returns null for non-loading phases", () => { + assert.equal(resolveLineLoadingPulseMode("ready"), null); + assert.equal(resolveLineLoadingPulseMode("revealing"), null); + assert.equal(resolveLineLoadingPulseMode("exitingReady"), null); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/loading-sweep.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/loading-sweep.test.ts new file mode 100644 index 0000000..be85a68 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/loading-sweep.test.ts @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { getSkeletonHeights } from "../loading-sweep"; + +describe("getSkeletonHeights", () => { + it("returns the requested number of heights", () => { + assert.equal(getSkeletonHeights(12).length, 12); + assert.equal(getSkeletonHeights(0).length, 0); + }); + + it("is deterministic for the same (count, seed) — SSR-safe", () => { + assert.deepEqual(getSkeletonHeights(8, 3), getSkeletonHeights(8, 3)); + }); + + it("re-rolls when the seed changes", () => { + assert.notDeepEqual(getSkeletonHeights(8, 0), getSkeletonHeights(8, 1)); + }); + + it("stays within the default [20, 80) range", () => { + for (const h of getSkeletonHeights(50, 7)) { + assert.ok(h >= 20 && h < 80, `height ${h} out of default range`); + } + }); + + it("respects a custom range", () => { + for (const h of getSkeletonHeights(50, 2, 40, 60)) { + assert.ok(h >= 40 && h < 60, `height ${h} out of [40, 60)`); + } + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/profit-loss-segments.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/profit-loss-segments.test.ts new file mode 100644 index 0000000..c8a50f5 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/profit-loss-segments.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { splitProfitLossSegments } from "../profit-loss-segments"; + +const xAccessor = (d: Record) => d.date as Date; + +describe("splitProfitLossSegments", () => { + it("returns empty array for no data", () => { + assert.deepEqual( + splitProfitLossSegments({ + data: [], + dataKey: "pnl", + xAccessor, + }), + [] + ); + }); + + it("splits at zero crossings with interpolated crossing points", () => { + const data = [ + { date: new Date("2024-01-01"), pnl: 100 }, + { date: new Date("2024-01-02"), pnl: -50 }, + { date: new Date("2024-01-03"), pnl: -20 }, + ]; + + const segments = splitProfitLossSegments({ + data, + dataKey: "pnl", + xAccessor, + }); + + assert.equal(segments.length, 2); + assert.equal(segments[0]?.isPositive, true); + assert.equal(segments[1]?.isPositive, false); + assert.equal(segments[0]?.data.at(-1)?.pnl, 0); + assert.equal(segments[1]?.data[0]?.pnl, 0); + }); + + it("keeps a single segment when values stay on one side of zero", () => { + const data = [ + { date: new Date("2024-01-01"), pnl: 10 }, + { date: new Date("2024-01-02"), pnl: 20 }, + ]; + + const segments = splitProfitLossSegments({ + data, + dataKey: "pnl", + xAccessor, + }); + + assert.equal(segments.length, 1); + assert.equal(segments[0]?.isPositive, true); + assert.equal(segments[0]?.data.length, 2); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/projection-utils.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/projection-utils.test.ts new file mode 100644 index 0000000..6afb328 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/projection-utils.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + buildHorizontalTangentBezierPath, + buildProjectionPath, +} from "../projection-utils"; + +const sourceData = Array.from({ length: 6 }, (_, index) => ({ + date: new Date(2024, 0, index + 1), + desktop: 100 + index * 10, +})); + +describe("buildProjectionPath", () => { + it("extends auto projections from the last point to the horizon", () => { + const path = buildProjectionPath({ + sourceData, + seriesKey: "desktop", + mode: "auto", + autoMethod: "linearRegression", + horizonPoints: 3, + }); + + assert.equal(path.length, 2); + assert.equal(path[0]?.value, 150); + assert.ok((path.at(-1)?.value ?? 0) > 150); + }); + + it("builds a target projection to an end value", () => { + const path = buildProjectionPath({ + sourceData, + seriesKey: "desktop", + mode: "target", + horizonPoints: 3, + endValue: 220, + }); + + assert.equal(path.length, 2); + assert.equal(path[0]?.value, 150); + assert.equal(path[1]?.value, 220); + }); +}); + +const HORIZONTAL_TANGENT_BEZIER_PATH = /^M 0,100 C 90,100 110,50 200,50$/; + +describe("buildHorizontalTangentBezierPath", () => { + it("uses horizontal control points at start and end y", () => { + const path = buildHorizontalTangentBezierPath(0, 100, 200, 50); + assert.match(path, HORIZONTAL_TANGENT_BEZIER_PATH); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/reference-area-geometry.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/reference-area-geometry.test.ts new file mode 100644 index 0000000..98a8e7b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/reference-area-geometry.test.ts @@ -0,0 +1,102 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + computeReferenceAreaRect, + resolveReferenceDataRange, +} from "../reference-area-geometry"; + +const innerWidth = 400; +const innerHeight = 200; + +const xScale = (date: Date) => date.getTime() / 10; +const yScale = (value: number) => innerHeight - value; + +function baseOptions( + overrides: Partial[0]> = {} +) { + return { + innerWidth, + innerHeight, + xScale, + yScale, + ...overrides, + }; +} + +describe("computeReferenceAreaRect", () => { + it("maps a full-width horizontal band between y1 and y2", () => { + const rect = computeReferenceAreaRect(baseOptions({ y1: 40, y2: 80 })); + assert.deepEqual(rect, { x: 0, y: 120, width: 400, height: 40 }); + }); + + it("maps a partial x-range when x1 and x2 are set", () => { + const rect = computeReferenceAreaRect( + baseOptions({ + x1: new Date(1000), + x2: new Date(2000), + y1: 50, + y2: 100, + }) + ); + assert.deepEqual(rect, { x: 100, y: 100, width: 100, height: 50 }); + }); + + it("clamps to the plot when ifOverflow is hidden", () => { + const rect = computeReferenceAreaRect( + baseOptions({ + y1: 150, + y2: 250, + ifOverflow: "hidden", + }) + ); + assert.deepEqual(rect, { x: 0, y: 0, width: 400, height: 50 }); + }); + + it("returns null when discard and partly outside the plot", () => { + const rect = computeReferenceAreaRect( + baseOptions({ + y1: 150, + y2: 250, + ifOverflow: "discard", + }) + ); + assert.equal(rect, null); + }); + + it("does not clamp when ifOverflow is visible", () => { + const rect = computeReferenceAreaRect( + baseOptions({ + y1: 150, + y2: 250, + ifOverflow: "visible", + }) + ); + assert.deepEqual(rect, { x: 0, y: -50, width: 400, height: 100 }); + }); + + it("returns null for zero plot size", () => { + assert.equal( + computeReferenceAreaRect( + baseOptions({ innerWidth: 0, innerHeight: 200, y1: 10, y2: 20 }) + ), + null + ); + }); +}); + +describe("resolveReferenceDataRange", () => { + it("returns inclusive bounds between y1 and y2", () => { + assert.deepEqual(resolveReferenceDataRange(160, 220, [0, 300]), [160, 220]); + }); + + it("extends to domain edges when y bounds are omitted", () => { + assert.deepEqual( + resolveReferenceDataRange(undefined, 220, [0, 300]), + [0, 220] + ); + assert.deepEqual( + resolveReferenceDataRange(160, undefined, [0, 300]), + [160, 300] + ); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/series-bar-layout.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/series-bar-layout.test.ts new file mode 100644 index 0000000..ad0258e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/series-bar-layout.test.ts @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + computeSeriesBarRevealClipPadding, + computeSeriesBarWidth, +} from "../series-bar-layout"; + +describe("computeSeriesBarWidth", () => { + it("caps grouped bar width to the slot", () => { + const width = computeSeriesBarWidth({ + innerWidth: 400, + dataLength: 12, + columnWidth: 400 / 11, + seriesCount: 2, + composedBarGap: 4, + }); + assert.ok(width >= 2); + assert.ok(width <= 40); + }); +}); + +describe("computeSeriesBarRevealClipPadding", () => { + it("uses half the bar width for a single stacked column", () => { + assert.equal( + computeSeriesBarRevealClipPadding({ + barWidth: 24, + seriesCount: 3, + stacked: true, + }), + 12 + ); + }); + + it("uses half the group width for grouped bars", () => { + assert.equal( + computeSeriesBarRevealClipPadding({ + barWidth: 20, + seriesCount: 2, + gap: 4, + }), + 22 + ); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/y-domain-utils.test.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/y-domain-utils.test.ts new file mode 100644 index 0000000..06dc5ce --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/y-domain-utils.test.ts @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + domainsEqual, + isReferenceAreaVisiblePhase, + mergeYDomainRecords, + niceYDomain, + shouldTweenYDomain, +} from "../y-domain-utils"; + +describe("shouldTweenYDomain", () => { + it("skips tween when both endpoints move less than 2% of span", () => { + assert.equal(shouldTweenYDomain([0, 100], [1, 101]), false); + }); + + it("tweens when min endpoint shifts enough", () => { + assert.equal(shouldTweenYDomain([0, 100], [5, 100]), true); + }); + + it("tweens when max endpoint shifts enough", () => { + assert.equal(shouldTweenYDomain([0, 100], [0, 110]), true); + }); +}); + +describe("niceYDomain", () => { + it("expands raw domain to nice tick boundaries", () => { + const [min, max] = niceYDomain([13, 87]); + assert.ok(min <= 13); + assert.ok(max >= 87); + }); +}); + +describe("mergeYDomainRecords", () => { + it("normalizes axis ids and merges maps", () => { + const merged = mergeYDomainRecords({ left: [0, 100] }, { right: [10, 50] }); + assert.deepEqual(merged.left, [0, 100]); + assert.deepEqual(merged.right, [10, 50]); + }); +}); + +describe("domainsEqual", () => { + it("returns true when axis domains match", () => { + assert.equal(domainsEqual({ left: [0, 100] }, { left: [0, 100] }), true); + }); + + it("returns false when any endpoint differs", () => { + assert.equal(domainsEqual({ left: [0, 100] }, { left: [0, 110] }), false); + }); +}); + +describe("isReferenceAreaVisiblePhase", () => { + it("shows reference areas during ready reveal phases", () => { + assert.equal(isReferenceAreaVisiblePhase("ready"), true); + assert.equal(isReferenceAreaVisiblePhase("revealing"), true); + assert.equal(isReferenceAreaVisiblePhase("gridTweenReady"), true); + }); + + it("hides reference areas during loading and exit phases", () => { + assert.equal(isReferenceAreaVisiblePhase("loading"), false); + assert.equal(isReferenceAreaVisiblePhase("exiting"), false); + assert.equal(isReferenceAreaVisiblePhase("exitingReady"), false); + assert.equal(isReferenceAreaVisiblePhase("gridTweenLoading"), false); + assert.equal(isReferenceAreaVisiblePhase("revealingLoading"), false); + }); +}); diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/animation.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/animation.ts new file mode 100644 index 0000000..0b5569b --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/animation.ts @@ -0,0 +1,36 @@ +import type { Transition } from "motion/react"; + +/** Default clip-reveal easing for cartesian charts. */ +export const DEFAULT_ANIMATION_EASING = "cubic-bezier(0.85, 0, 0.15, 1)"; + +export const DEFAULT_ANIMATION_DURATION_MS = 1100; + +/** Default enter transition — matches the original line chart reveal. */ +export const DEFAULT_CHART_ENTER_TRANSITION: Transition = { + type: "tween", + duration: DEFAULT_ANIMATION_DURATION_MS / 1000, + ease: [0.85, 0, 0.15, 1], +}; + +/** + * Clip-path width reveal must use tween — spring does not reliably animate SVG width. + */ +export function clipRevealTransition(enterTransition?: Transition): Transition { + if (enterTransition?.type === "tween") { + return { + ...enterTransition, + ease: enterTransition.ease ?? DEFAULT_CHART_ENTER_TRANSITION.ease, + }; + } + + const duration = + typeof enterTransition?.duration === "number" + ? enterTransition.duration + : DEFAULT_ANIMATION_DURATION_MS / 1000; + + return { + type: "tween", + duration, + ease: DEFAULT_CHART_ENTER_TRANSITION.ease, + }; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart-loading.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart-loading.tsx new file mode 100644 index 0000000..0e90530 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart-loading.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { curveNatural } from "@visx/curve"; +import { useMemo } from "react"; +import { Area } from "./area"; +import { AreaChart } from "./area-chart"; +import type { Margin } from "./chart-context"; +import type { LoadingStyle } from "./chart-phase"; +import { + DEFAULT_SKELETON_DATA_KEY, + DEFAULT_SKELETON_POINT_COUNT, + generateChartSkeletonData, +} from "./generate-chart-skeleton-data"; +import { Grid } from "./grid"; + +const LOADING_DATA_KEY = DEFAULT_SKELETON_DATA_KEY; +const DEFAULT_LOADING_STROKE = "var(--foreground)"; +const DEFAULT_LOADING_GRID_STROKE = + "color-mix(in oklch, var(--chart-grid) 50%, transparent)"; +const DEFAULT_LOADING_GRID_SHIMMER_STROKE = + "color-mix(in oklch, var(--foreground) 68%, transparent)"; +const DEFAULT_LOADING_STROKE_OPACITY = 0.5; + +export interface AreaChartLoadingProps { + /** Chart margins */ + margin?: Partial; + /** Stroke color for the animated loading segment. */ + stroke?: string; + /** Stroke opacity for the animated loading segment. Default: 0.5 */ + strokeOpacity?: number; + /** Grid line stroke (color and opacity via color-mix or oklch alpha). */ + gridStroke?: string; + /** Shimmer band stroke (color and opacity via color-mix or oklch alpha). */ + gridShimmerStroke?: string; + /** Animate a shimmer band across grid lines. Default: true */ + gridShimmer?: boolean; + /** Shimmer band width in pixels. Default: 140 */ + gridShimmerLength?: number; + /** Shimmer speed multiplier (higher = faster). Default: 1 */ + gridShimmerSpeed?: number; + /** Match shimmer loop to the loading line pulse (cycle + inter-loop pause). */ + gridShimmerSync?: boolean; + /** Loading animation: `"pulse"` (default traveling pulse) or `"sweep"` (a + * diagonal shimmer across the skeleton area). Default: `"pulse"`. */ + loadingStyle?: LoadingStyle; + /** Centered shimmer label text. Default: "Loading" */ + label?: string; + /** Aspect ratio as "width / height". Default: "2 / 1" */ + aspectRatio?: string; + /** Additional class name for the container */ + className?: string; +} + +export function AreaChartLoading({ + margin, + stroke = DEFAULT_LOADING_STROKE, + strokeOpacity = DEFAULT_LOADING_STROKE_OPACITY, + gridStroke = DEFAULT_LOADING_GRID_STROKE, + gridShimmerStroke = DEFAULT_LOADING_GRID_SHIMMER_STROKE, + gridShimmer = true, + gridShimmerLength, + gridShimmerSpeed, + gridShimmerSync = false, + loadingStyle = "pulse", + label = "Loading", + aspectRatio = "2 / 1", + className = "", +}: AreaChartLoadingProps) { + const data = useMemo( + () => + generateChartSkeletonData({ + dataKey: DEFAULT_SKELETON_DATA_KEY, + pointCount: DEFAULT_SKELETON_POINT_COUNT, + }), + [] + ); + + return ( + + + + + ); +} + +export default AreaChartLoading; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart.tsx new file mode 100644 index 0000000..51b871e --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { ParentSize } from "@visx/responsive"; +import type { Transition } from "motion/react"; +import { + Children, + type CSSProperties, + isValidElement, + type ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from "react"; +import { cn } from "@/lib/utils"; +import { Area, type AreaProps } from "./area"; +import type { LineConfig, Margin } from "./chart-context"; +import { ChartLoadingLabel } from "./chart-loading-label"; +import { + type ChartPhase, + type ChartStatus, + DEFAULT_CHART_STATUS, + DEFAULT_Y_DOMAIN_TWEEN_MS, + resolveRestingChartPhase, +} from "./chart-phase"; +import { PatternArea } from "./pattern-area"; +import { TimeSeriesChartInner } from "./time-series-chart-shell"; + +export interface AreaChartProps { + /** Data array - each item should have a date field and numeric values */ + data: Record[]; + /** Key in data for the x-axis (date). Default: "date" */ + xDataKey?: string; + /** Chart margins */ + margin?: Partial; + /** Animation duration in milliseconds. Default: 1100 */ + animationDuration?: number; + /** CSS easing for clip-reveal. Default: cubic-bezier(0.85, 0, 0.15, 1) */ + animationEasing?: string; + /** Motion enter transition (spring or cubic-bezier tween). */ + enterTransition?: Transition; + /** Signature of motion URL state — triggers reveal replay when it changes. */ + revealSignature?: string; + /** Aspect ratio as "width / height". Default: "2 / 1" */ + aspectRatio?: string; + /** Additional class name for the container */ + className?: string; + /** Loading vs ready — drives chart phase and loading chrome. Default: `"ready"`. */ + status?: ChartStatus; + /** Centered shimmer label while loading. */ + loadingLabel?: string; + /** Animate y-domain over this duration (ms) on status transitions. Default: 500. */ + yDomainTweenDuration?: number; + /** Animate y-domain when status or target domain changes. Default: true */ + yDomainTween?: boolean; + /** Visible x-domain for brush zoom. */ + xDomain?: [Date, Date]; + /** Full dataset length for x-scale padding when `xDomain` is set. */ + xDomainSlotCount?: number; + /** Tween y-domain when brush changes the visible x-range. Default: false */ + tweenYDomainOnXDomainChange?: boolean; + /** Inline container styles (e.g. fixed height for brush strip). */ + style?: CSSProperties; + /** Fires when the internal chart phase changes (e.g. OG capture readiness). */ + onPhaseChange?: (phase: ChartPhase) => void; + /** Child components (Area, Grid, ChartTooltip, etc.) */ + children: ReactNode; +} + +const DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 }; + +function extractAreaConfigs(children: ReactNode): LineConfig[] { + const configs: LineConfig[] = []; + + Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + + const childType = child.type as { + displayName?: string; + name?: string; + }; + const componentName = + typeof child.type === "function" + ? childType.displayName || childType.name || "" + : ""; + + const props = child.props as AreaProps | undefined; + const isPatternArea = + componentName === "PatternArea" || child.type === PatternArea; + const isAreaComponent = + componentName === "Area" || + child.type === Area || + (props && + typeof props.dataKey === "string" && + props.dataKey.length > 0 && + !isPatternArea); + + if (isAreaComponent && props?.dataKey) { + configs.push({ + dataKey: props.dataKey, + stroke: props.stroke || props.fill || "var(--chart-line-primary)", + strokeWidth: props.strokeWidth || 2, + yAxisId: props.yAxisId, + }); + } + }); + + return configs; +} + +interface ChartInnerProps { + width: number; + height: number; + data: Record[]; + xDataKey: string; + margin: Margin; + animationDuration: number; + animationEasing?: string; + enterTransition?: Transition; + revealSignature?: string; + chartStatus: ChartStatus; + loadingLabel?: string; + yDomainTweenDuration: number; + yDomainTween: boolean; + xDomain?: [Date, Date]; + xDomainSlotCount?: number; + tweenYDomainOnXDomainChange?: boolean; + children: ReactNode; + containerRef: React.RefObject; + onPhaseChange: (phase: ChartPhase) => void; +} + +function ChartInner({ + width, + height, + data, + xDataKey, + margin, + animationDuration, + animationEasing, + enterTransition, + revealSignature, + chartStatus, + loadingLabel, + yDomainTweenDuration, + yDomainTween, + xDomain, + xDomainSlotCount, + tweenYDomainOnXDomainChange, + children, + containerRef, + onPhaseChange, +}: ChartInnerProps) { + const lines = useMemo(() => extractAreaConfigs(children), [children]); + + return ( + + {children} + + ); +} + +export function AreaChart({ + data, + xDataKey = "date", + margin: marginProp, + animationDuration = 1100, + animationEasing, + enterTransition, + revealSignature, + aspectRatio = "2 / 1", + className = "", + status = DEFAULT_CHART_STATUS, + loadingLabel, + yDomainTweenDuration = DEFAULT_Y_DOMAIN_TWEEN_MS, + yDomainTween = true, + xDomain, + xDomainSlotCount, + tweenYDomainOnXDomainChange = false, + style, + onPhaseChange, + children, +}: AreaChartProps) { + const containerRef = useRef(null); + const margin = { ...DEFAULT_MARGIN, ...marginProp }; + const [chartPhase, setChartPhase] = useState(() => + resolveRestingChartPhase(status) + ); + const handlePhaseChange = useCallback( + (phase: ChartPhase) => { + setChartPhase(phase); + onPhaseChange?.(phase); + }, + [onPhaseChange] + ); + + const showLoadingLabel = Boolean( + loadingLabel?.trim() && + (chartPhase === "loading" || + chartPhase === "exiting" || + chartPhase === "gridTweenReady" || + chartPhase === "revealingLoading") + ); + + return ( +
+ + {({ width, height }) => ( + + {children} + + )} + + {showLoadingLabel ? ( + + ) : null} +
+ ); +} + +export { Area, type AreaProps } from "./area"; + +export default AreaChart; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-gradient-defs.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-gradient-defs.tsx new file mode 100644 index 0000000..cb142cb --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-gradient-defs.tsx @@ -0,0 +1,113 @@ +import { + type FadeEdges, + fadeGradientStops, + resolveFadeSides, + viewportFadeGradientAttrs, +} from "./fade-edges"; + +interface AreaGradientDefsProps { + gradientId: string; + strokeGradientId: string; + edgeMaskId: string; + edgeGradientId: string; + fill: string; + fillOpacity: number; + gradientToOpacity: number; + /** 0–1: where the bottom stop sits (1 = full-height gradient). */ + gradientSpan?: number; + resolvedStroke: string; + isPatternFill: boolean; + fadeEdges: FadeEdges; + innerWidth: number; + innerHeight: number; +} + +export function AreaGradientDefs({ + gradientId, + strokeGradientId, + edgeMaskId, + edgeGradientId, + fill, + fillOpacity, + gradientToOpacity, + gradientSpan = 1, + resolvedStroke, + isPatternFill, + fadeEdges, + innerWidth, + innerHeight, +}: AreaGradientDefsProps) { + const sides = resolveFadeSides(fadeEdges); + // Stroke gradient mirrors the area's edge fade so the line doesn't pop in + // past the faded fill. Skip emitting it when neither edge fades — the line + // can then paint a solid stroke instead of an unnecessary url(#...) ref. + const strokeStops = sides.any ? fadeGradientStops(sides) : null; + const showEdgeMask = sides.any && !isPatternFill; + const edgeStops = showEdgeMask ? fadeGradientStops(sides) : null; + const span = Math.min(1, Math.max(0.01, gradientSpan)); + const midOffset = `${span * 100}%`; + + return ( + + {isPatternFill ? null : ( + + + + {span < 1 ? ( + + ) : null} + + )} + + {strokeStops ? ( + + {strokeStops.map((stop) => ( + + ))} + + ) : null} + + {edgeStops ? ( + <> + + {edgeStops.map((stop) => ( + + ))} + + + + + + ) : null} + + ); +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area.tsx new file mode 100644 index 0000000..1439a7d --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area.tsx @@ -0,0 +1,377 @@ +"use client"; + +import { curveMonotoneX } from "@visx/curve"; +import { AreaClosed, LinePath } from "@visx/shape"; + +// CurveFactory type - simplified version compatible with visx +// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type +type CurveFactory = any; + +import { useCallback, useId, useMemo, useRef, useState } from "react"; +import { AreaGradientDefs } from "./area-gradient-defs"; +import { chartCssVars, useChartStable, useYScale } from "./chart-context"; +import type { ChartPhase, LoadingStyle } from "./chart-phase"; +import { type FadeEdges, resolveFadeSides } from "./fade-edges"; +import { + type LineLoadingPulseMode, + LineLoadingPulseStroke, + resolveLineLoadingPulseMode, +} from "./line-loading-pulse"; +import { LINE_LOADING_LOOP_PAUSE_MS } from "./line-loading-timing"; +import { LineLoadingSweep } from "./loading-sweep"; +import { + resolveDashTailBounds, + usePathStrokeMetrics, +} from "./path-stroke-utils"; +import { SeriesDashTailOverlay } from "./series-dash-tail-overlay"; +import { SeriesHighlightLayer } from "./series-highlight-layer"; +import { SeriesHoverDim } from "./series-hover-dim"; +import { SeriesMarkers } from "./series-markers"; +import type { SeriesPointMarkerStyle } from "./series-point-marker"; + +export interface AreaProps { + /** Key in data to use for y values */ + dataKey: string; + /** Y-scale group id (Recharts `yAxisId`). Default: `"left"`. */ + yAxisId?: string | number; + /** Fill color for the area gradient start. Default: var(--chart-line-primary) */ + fill?: string; + /** Fill opacity at the top of the area. Default: 0.4 */ + fillOpacity?: number; + /** Stroke color for the line. Default: same as fill */ + stroke?: string; + /** Stroke width. Default: 2 */ + strokeWidth?: number; + /** Curve function. Default: curveMonotoneX */ + curve?: CurveFactory; + /** Whether to animate the area. Default: true */ + animate?: boolean; + /** Whether to show the stroke line. Default: true */ + showLine?: boolean; + /** Whether to show highlight segment on hover. Default: true */ + showHighlight?: boolean; + /** Gradient opacity at bottom (0 = fully transparent). Default: 0 */ + gradientToOpacity?: number; + /** + * Vertical extent of the fill gradient (0–1). `1` fades across the full + * height; lower values compress the gradient toward the top. + */ + gradientSpan?: number; + /** + * Fade the area fill (and stroke) toward transparent at the chart edges. + * - `true` fades both edges, `false` disables the fade entirely. + * - `"left"` / `"right"` fades only that side — useful when the opposite + * edge butts up against another element you don't want to fade into. + * Default: false + */ + fadeEdges?: FadeEdges; + /** Render scatter-style circle markers at each data point. Default: false */ + showMarkers?: boolean; + /** Marker styling (same options as Scatter). */ + markers?: SeriesPointMarkerStyle; + /** + * Data index from which the line stroke becomes dashed (inclusive). + * Useful for projecting incomplete periods, e.g. dashed from yesterday through today. + */ + dashFromIndex?: number; + /** Dash pattern for the tail segment when `dashFromIndex` is set. Default: "6,4" */ + dashArray?: string; + /** Pulse stroke color while chart is loading. Default: var(--foreground) */ + loadingStroke?: string; + /** Pulse stroke opacity while chart is loading. Default: 0.5 */ + loadingStrokeOpacity?: number; + /** + * Show the loading pulse overlay. Default: follows chart loading phase. + * Set `false` to disable even during loading. + */ + loading?: boolean; + /** Override pulse animation mode (loop / exit / enter). */ + loadingPulseMode?: LineLoadingPulseMode; + /** + * Loading animation while the chart is in loading status: the default + * traveling `"pulse"`, or a diagonal `"sweep"` shimmer across the skeleton + * area. Default: `"pulse"`. + */ + loadingStyle?: LoadingStyle; +} + +function useAreaLoadingPulseState( + chartPhase: ChartPhase, + loading: boolean | undefined, + loadingPulseMode: LineLoadingPulseMode | undefined, + notifyLoadingPulseComplete?: () => void +) { + const phasePulseMode = resolveLineLoadingPulseMode(chartPhase); + const pulseMode = + loading === false + ? null + : (loadingPulseMode ?? (loading === true ? "loop" : phasePulseMode)); + const showLoadingPulse = pulseMode != null; + const showSeriesContent = + chartPhase === "revealing" || + chartPhase === "ready" || + chartPhase === "exitingReady"; + const [pulseEpoch, setPulseEpoch] = useState(0); + + const handleLoadingPulseComplete = useCallback(() => { + if (pulseMode === "loop") { + window.setTimeout(() => { + setPulseEpoch((epoch) => epoch + 1); + }, LINE_LOADING_LOOP_PAUSE_MS); + return; + } + notifyLoadingPulseComplete?.(); + }, [notifyLoadingPulseComplete, pulseMode]); + + return { + handleLoadingPulseComplete, + pulseMode, + pulseEpoch, + showLoadingPulse, + showSeriesContent, + }; +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors Line series layout (fill, stroke, dash, markers, pulse) +export function Area({ + dataKey, + yAxisId, + fill = chartCssVars.linePrimary, + fillOpacity = 0.4, + stroke, + strokeWidth = 2, + curve = curveMonotoneX, + animate = true, + showLine = true, + showHighlight = true, + gradientToOpacity = 0, + gradientSpan = 1, + fadeEdges = false, + showMarkers = false, + markers, + dashFromIndex, + dashArray = "6,4", + loading, + loadingStroke = chartCssVars.foreground, + loadingStrokeOpacity = 0.5, + loadingPulseMode, + loadingStyle = "pulse", +}: AreaProps) { + // Stable slice only: hover state lives inside `` and + // `` so this component (and its expensive + // child) does not re-render on cursor motion. + // The reveal-clip is now a single shared clipPath at the chart-shell + // level (`time-series-chart-shell.tsx`); we no longer render a per-area + // `` or read `revealEpoch` here. + const { + data, + renderData, + xScale, + innerHeight, + innerWidth, + xAccessor, + lines, + chartPhase, + notifyLoadingPulseComplete, + } = useChartStable(); + const yScale = useYScale(yAxisId); + const { + handleLoadingPulseComplete, + pulseMode, + pulseEpoch, + showLoadingPulse, + showSeriesContent, + } = useAreaLoadingPulseState( + chartPhase, + loading, + loadingPulseMode, + notifyLoadingPulseComplete + ); + + const seriesIndex = useMemo(() => { + const index = lines.findIndex((line) => line.dataKey === dataKey); + return index >= 0 ? index : 0; + }, [lines, dataKey]); + + const pathRef = useRef(null); + const { pathLength, pathD } = usePathStrokeMetrics(pathRef, [ + renderData, + innerWidth, + dashFromIndex, + showLine, + showSeriesContent, + showLoadingPulse, + ]); + + // Unique IDs for this area + const uniqueId = useId(); + const gradientId = `area-gradient-${dataKey}-${uniqueId}`; + const strokeGradientId = `area-stroke-gradient-${dataKey}-${uniqueId}`; + const edgeMaskId = `area-edge-mask-${dataKey}-${uniqueId}`; + const edgeGradientId = `${edgeMaskId}-gradient`; + + const isPatternFill = fill.startsWith("url("); + const showAreaFill = isPatternFill || fillOpacity > 0; + const areaFill = isPatternFill ? fill : `url(#${gradientId})`; + + // Resolved stroke color (defaults to fill; pattern URLs need a real color) + const resolvedStroke = + stroke || (isPatternFill ? chartCssVars.linePrimary : fill); + + const getY = useCallback( + (d: Record) => { + const value = d[dataKey]; + return typeof value === "number" ? (yScale(value) ?? 0) : 0; + }, + [dataKey, yScale] + ); + + const hasDashTail = resolveDashTailBounds(dashFromIndex, data.length); + // The stroke gradient is only emitted when at least one edge fades, so fall + // back to the resolved solid color otherwise — avoids an invalid url(#...). + const fadeSides = resolveFadeSides(fadeEdges); + const useViewportEdgeFade = fadeSides.any && !isPatternFill; + let strokePaint = resolvedStroke; + if (!useViewportEdgeFade && fadeSides.any) { + strokePaint = `url(#${strokeGradientId})`; + } + const highlightEnabled = + showHighlight && showLine && !showLoadingPulse && showSeriesContent; + const showSeriesStroke = showSeriesContent && showLine; + let visibleStroke = "transparent"; + if (showSeriesStroke && !hasDashTail) { + visibleStroke = strokePaint; + } + const shouldMeasurePath = showLine && (showSeriesContent || showLoadingPulse); + + const seriesLayers = ( + <> + {showSeriesContent && showAreaFill ? ( + xScale(xAccessor(d)) ?? 0} + y={getY} + yScale={yScale} + /> + ) : null} + + {shouldMeasurePath ? ( + <> + xScale(xAccessor(d)) ?? 0} + y={getY} + /> + {showSeriesStroke ? ( + + ) : null} + + ) : null} + + ); + + // Sweep style owns all loading modes (loop + the exit/enter transitions), + // drawing its own silhouette; the pulse covers the default style. + const sweepLoading = + showLoadingPulse && innerWidth > 0 && loadingStyle === "sweep"; + const pulseLoading = showLoadingPulse && innerWidth > 0 && !sweepLoading; + + return ( + <> + + + + {useViewportEdgeFade ? ( + {seriesLayers} + ) : ( + seriesLayers + )} + + + {/* Highlight segment on hover — isolated hover subscriber. */} + + + {showMarkers && showSeriesContent ? ( + + ) : null} + + {sweepLoading ? ( + + ) : null} + {pulseLoading && pathD ? ( + + ) : null} + + ); +} + +Area.displayName = "Area"; + +export default Area; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/background.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/background.tsx new file mode 100644 index 0000000..e30dbec --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/background.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { motion } from "motion/react"; +import { useId, useMemo } from "react"; +import { useChartStable } from "./chart-context"; +import { + type PatternPresetId, + type PatternPresetOptions, + renderPatternPreset, +} from "./pattern-preset"; + +export type BackgroundPatternPreset = PatternPresetId; + +export interface BackgroundProps extends PatternPresetOptions { + /** Pattern preset. `"none"` renders nothing. */ + pattern?: BackgroundPatternPreset; + /** Pattern stroke color. Default: `var(--chart-grid)` */ + color?: string; + /** Apply the pattern texture to the plot area. Default: true */ + showFill?: boolean; + /** Pattern fill opacity. Default: 1 */ + opacity?: number; + /** Fade pattern at the left and right chart edges. Default: true */ + fadeHorizontal?: boolean; + /** Fade pattern at the top and bottom chart edges. Default: true */ + fadeVertical?: boolean; + /** Horizontal fade zone as % of plot width per edge. Default: 10 */ + fadeHorizontalLength?: number; + /** Vertical fade zone as % of plot height per edge. Default: 10 */ + fadeVerticalLength?: number; +} + +const BACKGROUND_ENTER_FADE_MS = 420; + +function clampFadeLength(length: number): number { + return Math.min(45, Math.max(0, length)); +} + +function fadeMaskStops(lengthPercent: number): Array<{ + offset: string; + opacity: number; +}> { + const edge = clampFadeLength(lengthPercent); + return [ + { offset: "0%", opacity: 0 }, + { offset: `${edge}%`, opacity: 1 }, + { offset: `${100 - edge}%`, opacity: 1 }, + { offset: "100%", opacity: 0 }, + ]; +} + +/** Plot-area pattern fill for charts without a grid. Renders behind series layers. */ +export function Background({ + pattern = "diagonal", + color = "var(--chart-grid)", + scale = 1, + strokeWidth, + radius, + complement, + fill, + dotFill, + tileBackground, + showFill = true, + opacity = 1, + fadeHorizontal = true, + fadeVertical = true, + fadeHorizontalLength = 10, + fadeVerticalLength = 10, +}: BackgroundProps) { + const { innerWidth, innerHeight, isLoaded, enterTransition } = + useChartStable(); + const uniqueId = useId(); + const patternId = `chart-background-${uniqueId.replace(/:/g, "")}`; + + const hStops = useMemo( + () => fadeMaskStops(fadeHorizontalLength), + [fadeHorizontalLength] + ); + const vStops = useMemo( + () => fadeMaskStops(fadeVerticalLength), + [fadeVerticalLength] + ); + + if (pattern === "none" || !showFill || innerWidth <= 0 || innerHeight <= 0) { + return null; + } + + const patternNode = renderPatternPreset(pattern, patternId, { + color, + scale, + strokeWidth, + radius, + complement, + fill, + dotFill, + tileBackground, + }); + if (!patternNode) { + return null; + } + + const fadeMask = fadeHorizontal || fadeVertical; + const hMaskId = `chart-background-fade-h-${uniqueId.replace(/:/g, "")}`; + const hGradientId = `${hMaskId}-gradient`; + const vMaskId = `chart-background-fade-v-${uniqueId.replace(/:/g, "")}`; + const vGradientId = `${vMaskId}-gradient`; + const combinedMaskId = `chart-background-fade-${uniqueId.replace(/:/g, "")}`; + + let maskRef: string | undefined; + if (fadeHorizontal && fadeVertical) { + maskRef = `url(#${combinedMaskId})`; + } else if (fadeHorizontal) { + maskRef = `url(#${hMaskId})`; + } else if (fadeVertical) { + maskRef = `url(#${vMaskId})`; + } + + const targetOpacity = opacity; + const revealOpacity = isLoaded ? targetOpacity : 0; + const enterFade = + enterTransition && typeof enterTransition === "object" + ? enterTransition + : { duration: BACKGROUND_ENTER_FADE_MS / 1000, ease: "easeOut" as const }; + + return ( + // biome-ignore lint/a11y/noAriaHiddenOnFocusable: decorative plot-area pattern layer + + ); +} + +Background.displayName = "Background"; + +export default Background; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart-loading.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart-loading.tsx new file mode 100644 index 0000000..8258273 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart-loading.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { BarChart } from "./bar-chart"; +import type { Margin } from "./chart-context"; + +const EMPTY_DATA: Record[] = []; + +export interface BarChartLoadingProps { + /** Chart margins. */ + margin?: Partial; + /** Aspect ratio as "width / height". Default: "2 / 1" */ + aspectRatio?: string; + /** Additional class name for the container. */ + className?: string; +} + +/** + * Turnkey loading skeleton for bar charts, a thin shortcut for + * ``. Renders shimmer-swept placeholder bars while + * data is fetching; swap in a real `` once it resolves. + */ +export function BarChartLoading({ + margin, + aspectRatio = "2 / 1", + className = "", +}: BarChartLoadingProps) { + return ( + + ); +} + +export default BarChartLoading; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart.tsx new file mode 100644 index 0000000..60445d8 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart.tsx @@ -0,0 +1,706 @@ +"use client"; + +import { localPoint } from "@visx/event"; +import { ParentSize } from "@visx/responsive"; +import { scaleBand, scaleLinear } from "@visx/scale"; +import type { Transition } from "motion/react"; +import { + memo, + type ReactElement, + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { cn } from "@/lib/utils"; +import { DEFAULT_ANIMATION_EASING } from "./animation"; +import type { BarProps } from "./bar"; +import { + forEachChartChild, + isChartClipPassthrough, + isClipExcludedComponent, + isPostOverlayComponent, + isUnderlayComponent, + renderKeyedChartLayers, + resolveChartChildElement, +} from "./chart-child-passthrough"; +import { + ChartProvider, + type LineConfig, + type Margin, + type TooltipData, +} from "./chart-context"; +import { isGradientDefComponent, isPatternDefComponent } from "./chart-defs"; +import { shortDateFmt } from "./chart-formatters"; +import { + type ChartPhase, + type ChartStatus, + DEFAULT_CHART_LIFECYCLE, + resolveRestingChartPhase, +} from "./chart-phase"; +import { BarLoadingSkeleton } from "./loading-sweep"; +import { extractReferenceAreaConfigs } from "./reference-area-config"; +import { useScheduledTooltip } from "./use-scheduled-tooltip"; +import { + buildYScalesForLines, + getPrimaryYScale, + normalizeYAxisId, + wrapSingleYScale, +} from "./y-axis-scales"; + +/** Skeleton bars to show when `status="loading"` and `data` is empty. */ +const FALLBACK_LOADING_BARS = 12; + +export type BarOrientation = "vertical" | "horizontal"; + +export interface BarChartProps { + /** Data array - each item should have an x-axis key and numeric values */ + data: Record[]; + /** Key in data for the categorical axis. Default: "name" */ + xDataKey?: string; + /** Chart margins */ + margin?: Partial; + /** Animation duration in milliseconds. Default: 1100 */ + animationDuration?: number; + /** CSS easing for bar grow transitions. */ + animationEasing?: string; + /** Motion enter transition (spring or cubic-bezier tween). */ + enterTransition?: Transition; + /** Signature of motion URL state — triggers enter replay when it changes. */ + revealSignature?: string; + /** Aspect ratio as "width / height". Default: "2 / 1" */ + aspectRatio?: string; + /** Additional class name for the container */ + className?: string; + /** Gap between bar groups as a fraction of band width (0-1). Default: 0.2 */ + barGap?: number; + /** Fixed bar width in pixels. If not set, bars auto-size to fill the band. */ + barWidth?: number; + /** Bar chart orientation. Default: "vertical" */ + orientation?: BarOrientation; + /** Whether to stack bars instead of grouping them. Default: false */ + stacked?: boolean; + /** Gap between stacked bar segments in pixels. Default: 0 */ + stackGap?: number; + /** Child components (Bar, Grid, ChartTooltip, etc.). Optional — omit for a + * pure `status="loading"` skeleton. */ + children?: ReactNode; + /** Reports reveal lifecycle for OG screenshots and loading orchestration. */ + onPhaseChange?: (phase: ChartPhase) => void; + /** Fetch / display status. When `"loading"`, a shimmer skeleton replaces the + * bars (no chart data required). Default: `"ready"`. */ + status?: ChartStatus; +} + +const DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 }; + +// Extract bar configs from children synchronously +function extractBarConfigs(children: ReactNode): LineConfig[] { + const configs: LineConfig[] = []; + + forEachChartChild(children, (child) => { + const childType = child.type as { + displayName?: string; + name?: string; + __isBarDepthLayer?: boolean; + }; + // Bar-depth surface layers (BarDepthBack/Front, BarPulse) carry a + // `dataKey` to pair with a Bar but are not series themselves — skip them + // so they don't inflate the series count and shrink the real bars. + if (childType.__isBarDepthLayer) { + return; + } + const componentName = + typeof child.type === "function" + ? childType.displayName || childType.name || "" + : ""; + + const props = child.props as BarProps | undefined; + const isBarComponent = + componentName === "Bar" || + (props && typeof props.dataKey === "string" && props.dataKey.length > 0); + + if (isBarComponent && props?.dataKey) { + // Use stroke for tooltip dot color if provided, otherwise fall back to fill + // This allows gradient/pattern fills to have a solid dot color + const dotColor = + props.stroke || props.fill || "var(--chart-line-primary)"; + configs.push({ + dataKey: props.dataKey, + stroke: dotColor, + strokeWidth: 0, + yAxisId: props.yAxisId, + }); + } + }); + + return configs; +} + +interface ChartInnerProps { + width: number; + height: number; + data: Record[]; + xDataKey: string; + margin: Margin; + animationDuration: number; + animationEasing: string; + enterTransition?: Transition; + revealSignature?: string; + barGap: number; + barWidthProp?: number; + orientation: BarOrientation; + stacked: boolean; + stackGap: number; + children: ReactNode; + containerRef: React.RefObject; + onPhaseChange?: (phase: ChartPhase) => void; + status: ChartStatus; +} + +function ChartInner(props: ChartInnerProps) { + const { width, height } = props; + if (width < 10 || height < 10) { + return null; + } + return ; +} + +const ChartCore = memo(function ChartCore({ + width, + height, + data, + xDataKey, + margin, + animationDuration, + animationEasing, + enterTransition, + revealSignature = "", + barGap, + barWidthProp, + orientation, + stacked, + stackGap, + children, + containerRef, + onPhaseChange, + status, +}: ChartInnerProps) { + const { tooltipData, setTooltipData, scheduleTooltip, clearTooltip } = + useScheduledTooltip(); + const [isLoaded, setIsLoaded] = useState(false); + const [revealEpoch, setRevealEpoch] = useState(0); + const hoveredBarIndex = tooltipData?.index ?? null; + + const isHorizontal = orientation === "horizontal"; + + // Extract bar configs synchronously from children + const lines = useMemo(() => extractBarConfigs(children), [children]); + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Category accessor function - returns string for categorical scale + const categoryAccessor = useCallback( + (d: Record): string => { + const value = d[xDataKey]; + if (value instanceof Date) { + return shortDateFmt.format(value); + } + return String(value ?? ""); + }, + [xDataKey] + ); + + // For compatibility with ChartContext, provide a Date-based xAccessor + const xAccessorDate = useCallback( + (d: Record): Date => { + const value = d[xDataKey]; + if (value instanceof Date) { + return value; + } + return new Date(); + }, + [xDataKey] + ); + + // Category scale (band) - for the categorical axis + const categoryScale = useMemo(() => { + const domain = data.map((d) => categoryAccessor(d)); + const range: [number, number] = isHorizontal + ? [0, innerHeight] + : [0, innerWidth]; + return scaleBand({ + range, + domain, + padding: barGap, + }); + }, [innerWidth, innerHeight, data, categoryAccessor, barGap, isHorizontal]); + + // Band width for bars - use prop if provided, otherwise use scale's bandwidth + const bandWidth = barWidthProp ?? categoryScale.bandwidth(); + + // Compute max value considering stacking + const maxValue = useMemo(() => { + if (stacked) { + // For stacked bars, sum all values at each data point + let max = 0; + for (const d of data) { + let sum = 0; + for (const line of lines) { + const value = d[line.dataKey]; + if (typeof value === "number") { + sum += value; + } + } + if (sum > max) { + max = sum; + } + } + return max || 100; + } + // For grouped bars, find max single value + let max = 0; + for (const line of lines) { + for (const d of data) { + const value = d[line.dataKey]; + if (typeof value === "number" && value > max) { + max = value; + } + } + } + return max || 100; + }, [data, lines, stacked]); + + // Value scale (linear) - for the value axis + const valueScale = useMemo(() => { + const range = isHorizontal ? [0, innerWidth] : [innerHeight, 0]; + return scaleLinear({ + range, + domain: [0, maxValue * 1.1], + nice: true, + }); + }, [innerWidth, innerHeight, maxValue, isHorizontal]); + + const yScales = useMemo(() => { + if (isHorizontal) { + return wrapSingleYScale(valueScale); + } + return buildYScalesForLines({ + lines, + data, + innerHeight, + resolveDomain: (dataKeys) => { + let max = 0; + for (const d of data) { + for (const key of dataKeys) { + const value = d[key]; + if (typeof value === "number" && value > max) { + max = value; + } + } + } + return [0, (max || 100) * 1.1]; + }, + }); + }, [data, innerHeight, isHorizontal, lines, valueScale]); + + const primaryYScale = getPrimaryYScale(yScales, valueScale); + + // Compute stack offsets for stacked bars + const stackOffsets = useMemo(() => { + if (!stacked) { + return undefined; + } + const offsets = new Map>(); + for (let i = 0; i < data.length; i++) { + const d = data[i]; + if (!d) { + continue; + } + const pointOffsets = new Map(); + let cumulative = 0; + for (const line of lines) { + pointOffsets.set(line.dataKey, cumulative); + const value = d[line.dataKey]; + if (typeof value === "number") { + cumulative += value; + } + } + offsets.set(i, pointOffsets); + } + return offsets; + }, [data, lines, stacked]); + + // Column width for tooltip indicator + const columnWidth = useMemo(() => { + if (data.length < 1) { + return 0; + } + return isHorizontal ? innerHeight / data.length : innerWidth / data.length; + }, [innerWidth, innerHeight, data.length, isHorizontal]); + + // Pre-compute labels for ticker animation + const dateLabels = useMemo( + () => data.map((d) => categoryAccessor(d)), + [data, categoryAccessor] + ); + + // Create a fake time scale for compatibility with ChartContext + const fakeTimeScale = useMemo(() => { + const now = Date.now(); + const start = now - data.length * 24 * 60 * 60 * 1000; + const scale = { + ...categoryScale, + domain: () => [new Date(start), new Date(now)], + range: () => [0, innerWidth] as [number, number], + invert: (x: number) => new Date(start + (x / innerWidth) * (now - start)), + copy: () => scale, + }; + return scale; + }, [categoryScale, innerWidth, data.length]); + + // Animation timing — replay when motion settings change + // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature + useEffect(() => { + setRevealEpoch((n) => n + 1); + setIsLoaded(false); + // While loading, hold the skeleton (no reveal, no interaction). When + // status flips to "ready" this effect re-runs and plays the grow reveal. + if (status === "loading") { + return; + } + const timer = setTimeout(() => { + setIsLoaded(true); + }, animationDuration); + return () => clearTimeout(timer); + }, [animationDuration, revealSignature, status]); + + useEffect(() => { + onPhaseChange?.(isLoaded ? "ready" : "revealing"); + }, [isLoaded, onPhaseChange]); + + // Mouse move handler + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + const point = localPoint(event); + if (!point) { + return; + } + + const pos = isHorizontal ? point.y - margin.top : point.x - margin.left; + + // Find which band the mouse is over + const bandIndex = Math.floor(pos / columnWidth); + const clampedIndex = Math.max(0, Math.min(data.length - 1, bandIndex)); + const d = data[clampedIndex]; + + if (!d) { + return; + } + + // Calculate positions for each bar + const yPositions: Record = {}; + const xPositions: Record = {}; + const barPos = categoryScale(categoryAccessor(d)) ?? 0; + + if (isHorizontal) { + // Horizontal bars: dots at end of bar (x = value), centered vertically in band + const seriesCount = lines.length; + const groupGap = seriesCount > 1 ? 4 : 0; + const individualBarHeight = + seriesCount > 0 + ? (bandWidth - groupGap * (seriesCount - 1)) / seriesCount + : bandWidth; + + if (stacked) { + // Stacked horizontal: all bars same y, x at cumulative end + let cumulative = 0; + for (const line of lines) { + const value = d[line.dataKey]; + if (typeof value === "number") { + cumulative += value; + const axisScale = + yScales[normalizeYAxisId(line.yAxisId)] ?? valueScale; + xPositions[line.dataKey] = axisScale(cumulative) ?? 0; + yPositions[line.dataKey] = barPos + bandWidth / 2; + } + } + } else { + // Grouped horizontal: each bar at its own y position + lines.forEach((line, idx) => { + const value = d[line.dataKey]; + if (typeof value === "number") { + const axisScale = + yScales[normalizeYAxisId(line.yAxisId)] ?? valueScale; + xPositions[line.dataKey] = axisScale(value) ?? 0; + yPositions[line.dataKey] = + barPos + + idx * (individualBarHeight + groupGap) + + individualBarHeight / 2; + } + }); + } + } else if (stacked) { + // Vertical stacked bars + let cumulative = 0; + let seriesIdx = 0; + for (const line of lines) { + const value = d[line.dataKey]; + if (typeof value === "number") { + cumulative += value; + const axisScale = + yScales[normalizeYAxisId(line.yAxisId)] ?? primaryYScale; + const gapOffset = seriesIdx * stackGap; + yPositions[line.dataKey] = (axisScale(cumulative) ?? 0) - gapOffset; + seriesIdx++; + } + } + } else { + // Vertical grouped bars + const seriesCount = lines.length; + const groupGap = seriesCount > 1 ? 4 : 0; + const individualBarWidth = + seriesCount > 0 + ? (bandWidth - groupGap * (seriesCount - 1)) / seriesCount + : bandWidth; + + lines.forEach((line, idx) => { + const value = d[line.dataKey]; + if (typeof value === "number") { + const axisScale = + yScales[normalizeYAxisId(line.yAxisId)] ?? primaryYScale; + yPositions[line.dataKey] = axisScale(value) ?? 0; + xPositions[line.dataKey] = + barPos + + idx * (individualBarWidth + groupGap) + + individualBarWidth / 2; + } + }); + } + + // Tooltip position: for horizontal, position at max bar end; for vertical, center of band + let tooltipX: number; + if (isHorizontal) { + // Position tooltip at the end of the longest bar + const maxX = Math.max(...Object.values(xPositions), 0); + tooltipX = maxX; + } else { + tooltipX = barPos + bandWidth / 2; + } + + scheduleTooltip({ + point: d, + index: clampedIndex, + x: tooltipX, + yPositions, + xPositions: Object.keys(xPositions).length > 0 ? xPositions : undefined, + }); + }, + [ + categoryScale, + valueScale, + data, + lines, + margin.left, + margin.top, + categoryAccessor, + columnWidth, + bandWidth, + isHorizontal, + stacked, + stackGap, + scheduleTooltip, + yScales, + primaryYScale, + ] + ); + + const handleMouseLeave = useCallback(() => { + clearTooltip(); + }, [clearTooltip]); + + const canInteract = isLoaded; + + // Separate children into defs, pre-overlay, and post-overlay + const defsChildren: ReactElement[] = []; + const clipExcludedChildren: ReactElement[] = []; + const underlayChildren: ReactElement[] = []; + const preOverlayChildren: ReactElement[] = []; + const postOverlayChildren: ReactElement[] = []; + + forEachChartChild(children, (child) => { + const resolvedChild = resolveChartChildElement(child); + + if (isGradientDefComponent(child)) { + defsChildren.push(child); + } else if (isPatternDefComponent(child)) { + preOverlayChildren.push(child); + } else if (isPostOverlayComponent(resolvedChild)) { + postOverlayChildren.push(resolvedChild); + } else if (isClipExcludedComponent(resolvedChild)) { + clipExcludedChildren.push( + isChartClipPassthrough(child.type) ? resolvedChild : child + ); + } else if (isUnderlayComponent(resolvedChild)) { + underlayChildren.push(resolvedChild); + } else { + preOverlayChildren.push(child); + } + }); + + const referenceAreas = useMemo( + () => extractReferenceAreaConfigs(children), + [children] + ); + + const contextValue = { + ...DEFAULT_CHART_LIFECYCLE, + chartPhase: resolveRestingChartPhase(status), + chartStatus: status, + data, + renderData: data, + xScale: fakeTimeScale as unknown as ReturnType< + typeof import("@visx/scale").scaleTime + >, + yScale: isHorizontal ? valueScale : primaryYScale, + yScales, + width, + height, + innerWidth, + innerHeight, + margin, + columnWidth, + tooltipData, + setTooltipData, + containerRef, + lines, + referenceAreas, + isLoaded, + animationDuration, + animationEasing, + enterTransition, + revealEpoch, + xAccessor: xAccessorDate, + dateLabels, + // Bar-specific properties + barScale: categoryScale, + bandWidth, + hoveredBarIndex, + barXAccessor: categoryAccessor, + orientation, + stacked, + stackOffsets, + }; + + return ( + + + + ); +}); + +export function BarChart({ + data, + xDataKey = "name", + margin: marginProp, + animationDuration = 1100, + animationEasing = DEFAULT_ANIMATION_EASING, + enterTransition, + revealSignature, + aspectRatio = "2 / 1", + className = "", + barGap = 0.2, + barWidth, + orientation = "vertical", + stacked = false, + stackGap = 0, + children, + onPhaseChange, + status = "ready", +}: BarChartProps) { + const containerRef = useRef(null); + const margin = { ...DEFAULT_MARGIN, ...marginProp }; + + return ( +
+ + {({ width, height }) => ( + + {children} + + )} + +
+ ); +} + +BarChart.displayName = "BarChart"; + +export default BarChart; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth-geometry.ts b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth-geometry.ts new file mode 100644 index 0000000..91f76f7 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth-geometry.ts @@ -0,0 +1,44 @@ +/** + * Shared 3D bar-depth geometry — the single source of truth for the + * perspective math used by BOTH `` (which shrinks a + * bar's front-face top) AND the bar-depth surfaces (``'s side + + * lid). Keeping the formula here means the front face and the 3D lid can never + * drift out of alignment. + */ + +/** Hard ceiling on side-face thickness in px. Capped further by the column gap + * so depth never bleeds past the next bar's leading edge. */ +export const BAR_DEPTH_MAX_PX = 7; +/** The side parallelogram's back edge lifts by `depth * this ratio`, giving a + * subtle head-on perspective slope. */ +export const BAR_DEPTH_PERSPECTIVE_RATIO = 0.45; + +/** + * Maximum side-face depth in px for a chart, clamped so depth never spills past + * the gap between bars. `stepWidth` is d3-scaleBand's `step()` (bandwidth + + * gap); `bandWidth` is a single bar's width. Returns 0 for gapless/dense charts. + */ +export function barDepthMaxDepth(stepWidth: number, bandWidth: number): number { + const gap = Math.max(0, stepWidth - bandWidth); + return Math.min(bandWidth * 0.22, Math.max(0, gap - 1), BAR_DEPTH_MAX_PX); +} + +/** + * Per-bar side-face depth + perspective rise. + * + * - `absOffset` ∈ [0, 1]: the bar's normalized distance from the chart's + * horizontal center. 0 = dead center (no depth); 1 = chart edge (full depth). + * - `naturalHeight`: the bar's pixel height. Depth is capped by it so a short + * bar's side never reads wider than the bar is tall. + * - `maxDepth`: from `barDepthMaxDepth`. + */ +export function barDepthAndRise( + absOffset: number, + naturalHeight: number, + maxDepth: number +): { depth: number; perspectiveRise: number } { + const offset = Math.min(1, Math.max(0, absOffset)); + const cappedMaxDepth = Math.min(maxDepth, Math.max(0, naturalHeight)); + const depth = offset * cappedMaxDepth; + return { depth, perspectiveRise: depth * BAR_DEPTH_PERSPECTIVE_RATIO }; +} diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth.tsx new file mode 100644 index 0000000..fa77f71 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-depth.tsx @@ -0,0 +1,1074 @@ +"use client"; + +/** + * Bar 3D depth + glass surfaces — drop-in layers for ``. + * + * Adds a head-on perspective and a glossy glass-block sheen to the bars in + * any ``. Bars left of chart center expose their RIGHT side face; + * bars right of center expose their LEFT — the vanishing point sits at the + * chart's horizontal middle, so the row reads like vertical glass blocks + * viewed straight on. + * + * Three surfaces per bar: + * 1. Side face — a parallelogram on the depth side, lit at the front edge + * with a horizontal black gradient and shaded toward the back. + * 2. Top face — a parallelogram lid above the bar's top edge, with a + * bright catch at the front lip fading to soft white at the back. + * 3. Glass overlay — a vertical white-to-dark gradient on the bar's front + * face (sharp top reflection, transparent middle, faint bottom shadow). + * + * Usage (purely additive — no changes to ``): + * + * + * + * // before + * + * // after + * + * + * Both depth layers read geometry from `useChart()` and figure out the + * perspective themselves. The only configuration is the bar color + * (`color` / `colorAccessor`) and, for stacked bars, a `segmentsAccessor` + * supplied via ``. + * + * An optional `` paints a looping vertical sweep over a single + * "active" bar (selected by `activeIndex`) — useful for highlighting a live + * or in-progress value. Render it AFTER ``. + * + * Behavior notes: + * - Negative values are supported: a bar whose value is below zero grows + * downward from the baseline, with the lid sitting on the baseline. + * (This requires the host `` to use a value scale whose + * domain includes negatives.) + * - Zero-value bars are skipped by default; set `minBarHeight` on + * `` (and ``) to floor them to a short visible bar. + * + * Performance: + * - Three shared SVG gradients per layer instance (ID-collision-safe via + * `useId()`). + * - A per-bar `` for the side face animates from the baseline so + * the static side path is revealed bottom-up in lockstep with the bar. + * Only used where depth ≥ 0.5px. + * - At high bar density the column gap clamps depth below the 0.5px + * threshold; the back surfaces skip rendering and only the cheap glass + * rect remains. + * - `useBarDepthEntries` is memoized, so the Back/Front/Pulse layers + * compute geometry at most once per render between them. + */ + +import { motion, type Transition } from "motion/react"; +import { + createContext, + type ReactNode, + useContext, + useId, + useMemo, +} from "react"; +import { barDepthAndRise, barDepthMaxDepth } from "./bar-depth-geometry"; +import { useChartHover, useChartStable } from "./chart-context"; +import { transitionWithDelay } from "./motion-utils"; + +// ─── Constants ─────────────────────────────────────────────────────── + +/** Below this depth the back surfaces don't render — saves elements for + * dead-center bars and dense charts where perspective is sub-pixel. */ +const BAR_DEPTH_MIN_PX = 0.5; +/** Mirrors `Bar`'s default `fadedOpacity` so depth/glass dim in step with + * their bar when a sibling bar is hovered. */ +const BAR_FADED_OPACITY = 0.3; +/** Default bar color when neither `color` nor `colorAccessor` is provided. */ +const DEFAULT_BAR_COLOR = "var(--chart-line-primary)"; +/** Default opacity of the baseline "contact shadow" that grounds each bar at + * the zero line. Configurable via ``. */ +const DEFAULT_GROUND_SHADOW = 0.26; +/** Peak opacity of the white reflection catch at each bar's tip. Kept here so + * the front/side glass and the solid lid highlight all use the same value. */ +const GLASS_TIP_OPACITY = 0.2; + +// `BarPulse` — vertical sweep wave constants. The wave is a tall translucent +// rect with a centered bright band; it translates from below the bar's bottom +// edge to above the lid's back edge on a loop, clipped to the bar's full 3D +// silhouette. +/** Wave height = max(barHeight × this, PULSE_WAVE_HEIGHT_MIN_PX). 0.55 puts + * the bright band's peak in the middle of the bar at mid-travel. */ +const PULSE_WAVE_HEIGHT_RATIO = 0.55; +/** Minimum wave height in px so short bars still pulse on a steady cadence — + * the wave is taller than the bar so the bright band sweeps through cleanly + * instead of flashing for one frame. */ +const PULSE_WAVE_HEIGHT_MIN_PX = 36; +/** Seconds per sweep (bottom → above the lid). 2.4s reads as a deliberate + * heartbeat; faster feels frantic, slower feels laggy. */ +const PULSE_WAVE_DURATION_S = 2.4; +/** Peak opacity of the wave's brightest gradient stop. White at 0.85 is + * bright enough to register over saturated bar colors while the bell-curve + * stops around it keep the falloff soft so the bar's hue still dominates. */ +const PULSE_WAVE_PEAK_OPACITY = 0.85; + +const BAR_HOVER_TRANSITION = { duration: 0.15, ease: "easeOut" as const }; + +// ─── Types ─────────────────────────────────────────────────────────── + +export interface BarDepthEntry { + label: string; + /** Index in the original `data` array — used for stagger calc. NOT the + * index in this entries list. */ + dataIndex: number; + /** Original datum reference — handed back to `colorAccessor` / + * `segmentsAccessor` so consumers can color or split bars without + * bar-depth knowing the data shape. */ + datum: Record; + /** True when this bar is the active bar (`dataIndex === activeIndex`). + * Used only by ``; geometry is identical regardless. */ + isActive: boolean; + /** True when the bar's value is negative — the bar extends downward from + * the baseline. The lid still sits at the baseline; the side parallelogram + * grows downward instead of upward. */ + isNegative: boolean; + /** y of the chart's baseline (= `yScale(0)`). For positive bars this equals + * `bottomY`; for negative bars it equals `topY`. Used as the grow-from + * anchor for both the clipPath (side face) and the glass rect. */ + baselineY: number; + bandX: number; + bandWidth: number; + /** Visual top of the bar in screen Y (smaller y = higher on screen). */ + topY: number; + /** Visual bottom of the bar in screen Y. */ + bottomY: number; + barHeight: number; + /** Un-trimmed natural pixel height of the bar (= `|baselineY - yScale(value)|`). + * Segment heights are scaled against this so they line up with ``. */ + naturalHeight: number; + /** Pixels subtracted from the bar's visual top (and added to `topY`) so the + * lid's back edge lands at `yScale(value)` regardless of depth. */ + topYTrim: number; + /** Side-face thickness in px (0 = dead-center bar / dense chart). */ + depth: number; + perspectiveRise: number; + isRightOfCenter: boolean; +} + +/** One segment of a stacked bar, in stack order bottom→top. The values across + * all segments should sum to the bar's value; bar-depth uses the proportions + * to pick segment boundaries that line up with the front face's stacking. */ +export interface BarDepthSegment { + value: number; + color: string; +} + +/** Resolve a per-datum color, falling back to the shared `color` default. */ +type ColorAccessor = (datum: Record, index: number) => string; + +function resolveColor( + datum: Record, + index: number, + color: string, + colorAccessor?: ColorAccessor +): string { + return colorAccessor?.(datum, index) ?? color; +} + +/** Side-face parallelogram between two screen-Y edges, on the depth side of + * the bar (left of the bar for right-of-center bars, right for left-of). */ +function sideFacePath( + e: BarDepthEntry, + topEdge: number, + bottomEdge: number +): string { + const rise = e.perspectiveRise; + if (e.isRightOfCenter) { + const x = e.bandX; + return `M ${x} ${topEdge} L ${x - e.depth} ${topEdge - rise} L ${x - e.depth} ${bottomEdge - rise} L ${x} ${bottomEdge} Z`; + } + const x = e.bandX + e.bandWidth; + return `M ${x} ${topEdge} L ${x + e.depth} ${topEdge - rise} L ${x + e.depth} ${bottomEdge - rise} L ${x} ${bottomEdge} Z`; +} + +/** Lid (top face) parallelogram above the bar's top edge. */ +function lidFacePath(e: BarDepthEntry): string { + const rise = e.perspectiveRise; + const left = e.bandX; + const right = e.bandX + e.bandWidth; + if (e.isRightOfCenter) { + return `M ${left} ${e.topY} L ${right} ${e.topY} L ${right - e.depth} ${e.topY - rise} L ${left - e.depth} ${e.topY - rise} Z`; + } + return `M ${left} ${e.topY} L ${right} ${e.topY} L ${right + e.depth} ${e.topY - rise} L ${left + e.depth} ${e.topY - rise} Z`; +} + +/** + * Build the side-face parallelograms for a bar. Without segments it's a single + * piece in `baseColor`; with segments each gets its natural scaled height + * `(seg.value/total) * naturalHeight` and only the topmost has `topYTrim` + * removed — matching exactly how `` stacks segments, so the side seams + * line up with the front face. + */ +function buildSidePieces( + e: BarDepthEntry, + visibleSegments: BarDepthSegment[] | null, + baseColor: string +): { path: string; color: string }[] { + if (!(visibleSegments && visibleSegments.length > 0)) { + return [{ path: sideFacePath(e, e.topY, e.bottomY), color: baseColor }]; + } + const totalValue = visibleSegments.reduce((sum, s) => sum + s.value, 0); + const pieces: { path: string; color: string }[] = []; + let cursorY = e.bottomY; + for (let i = 0; i < visibleSegments.length; i++) { + const seg = visibleSegments[i]; + if (!seg) { + continue; + } + const isTopmost = i === visibleSegments.length - 1; + const segScaledHeight = (seg.value / totalValue) * e.naturalHeight; + const segHeight = isTopmost + ? Math.max(0, segScaledHeight - e.topYTrim) + : segScaledHeight; + const segBottomY = cursorY; + const segTopY = cursorY - segHeight; + cursorY = segTopY; + pieces.push({ + path: sideFacePath(e, segTopY, segBottomY), + color: seg.color, + }); + } + return pieces; +} + +// ─── Context ────────────────────────────────────────────────────────── +// `BarDepthProvider` is the single source of truth for `segmentsAccessor`, +// shared by every depth surface (``, ``, +// ``) AND by the entries hook those surfaces consume. +// +// Why context instead of per-component props: `useBarDepthEntries` computes +// `topYTrim` from the topmost segment's scaled height, so all three layers +// need the *same* `segmentsAccessor` to produce the same `topY`. A single +// provider above the chart makes it structurally impossible for them to +// disagree. +// +// `BarChart` reads its `` children with `React.Children.forEach`, which +// doesn't recurse into a provider. Wrap the provider *around* the +// ``, not inside it. + +interface BarDepthContextValue { + segmentsAccessor?: ( + datum: Record + ) => BarDepthSegment[] | null | undefined; + /** Opacity (0–1) of the baseline contact shadow. 0 removes it. */ + groundShadow?: number; + /** Minimum bar height in px — floors short/zero bars so they stay visible. + * Match the value passed to ``. */ + minBarHeight?: number; +} + +const BarDepthContext = createContext({}); + +export interface BarDepthProviderProps extends BarDepthContextValue { + children: ReactNode; +} + +/** + * Provides shared bar-depth configuration to every depth surface beneath it. + * Wrap a `` whose children include `` / + * `` / `` with this provider to: + * - split stacked bars into per-segment side faces (`segmentsAccessor`), + * - tune the baseline contact shadow (`groundShadow`, default 0.26; 0 = off), + * - floor short/zero bars so they stay visible (`minBarHeight`; pair with the + * same value on ``). + * + * Because the glass gradient is chart-wide (one Y-anchored ramp shared by the + * front face and the side glass), the provider is the single source of truth — + * so the surfaces can never disagree. + */ +export function BarDepthProvider({ + segmentsAccessor, + groundShadow, + minBarHeight, + children, +}: BarDepthProviderProps) { + // Memoize so consumers don't re-render when the config is stable. + const value = useMemo( + () => ({ segmentsAccessor, groundShadow, minBarHeight }), + [segmentsAccessor, groundShadow, minBarHeight] + ); + return ( + + {children} + + ); +} + +BarDepthProvider.displayName = "BarDepthProvider"; + +// ─── Per-bar glass gradient ────────────────────────────────────────── +// The glass is anchored to EACH BAR's own height (objectBoundingBox: 0% = the +// bar's tip, 100% = its baseline), not to the chart. So every bar — tall or +// short — gets the identical proportional gloss. (Anchoring per-chart made a +// tall bar's tip bright while a short bar's tip fell in the transparent middle +// and read as matte.) + +interface GlassGradientStop { + offset: string; + color: string; + opacity: string; +} + +/** + * Stop list for a POSITIVE bar's glass (0% = tip, 100% = baseline): a sharp + * white reflection catch at the tip falling off fast to transparent, then the + * contact shadow at the base. + */ +function buildPosBarStops(groundShadow: number): GlassGradientStop[] { + return [ + { offset: "0%", color: "white", opacity: String(GLASS_TIP_OPACITY) }, + { offset: "3%", color: "white", opacity: "0.09" }, + { offset: "8%", color: "white", opacity: "0.02" }, + { offset: "55%", color: "white", opacity: "0" }, + { offset: "100%", color: "black", opacity: String(groundShadow) }, + ]; +} + +/** + * Mirror of `buildPosBarStops` for NEGATIVE bars (which grow downward): the + * contact shadow sits at the top (the baseline) and the reflection catch at + * the bottom (the bar's tip). + */ +function buildNegBarStops(groundShadow: number): GlassGradientStop[] { + return [ + { offset: "0%", color: "black", opacity: String(groundShadow) }, + { offset: "45%", color: "white", opacity: "0" }, + { offset: "92%", color: "white", opacity: "0.02" }, + { offset: "97%", color: "white", opacity: "0.09" }, + { offset: "100%", color: "white", opacity: String(GLASS_TIP_OPACITY) }, + ]; +} + +/** + * Build both per-bar glass stop lists from the current `groundShadow`. Each + * depth surface picks `posStops` or `negStops` by the bar's direction and + * paints it objectBoundingBox, so the gloss is identical across bar heights. + */ +function useGlassStops() { + const { groundShadow = DEFAULT_GROUND_SHADOW } = useContext(BarDepthContext); + return useMemo( + () => ({ + posStops: buildPosBarStops(groundShadow), + negStops: buildNegBarStops(groundShadow), + }), + [groundShadow] + ); +} + +function readBarValue(d: Record, key: string): number { + const v = key ? d[key] : undefined; + return typeof v === "number" ? v : 0; +} + +// ─── Hooks ─────────────────────────────────────────────────────────── + +/** + * Compute per-bar 3D depth geometry from chart context. + * + * Memoized on the chart's scales + data, so calling this from sibling layers + * within the same chart is essentially free after the first settles. Returns + * one entry per renderable bar; bars with no resolvable height are excluded. + * + * Negative-value bars are kept — `isNegative` flips the entry's geometry so + * the side parallelogram and lid render in the correct direction. + * + * Exported for advanced consumers that want to render custom layers aligned + * with the same geometry (e.g. labels above each bar's top face). + */ +export function useBarDepthEntries( + dataKey: string, + activeIndex?: number +): BarDepthEntry[] { + const { + data, + barScale, + bandWidth, + yScale, + innerHeight, + innerWidth, + barXAccessor, + } = useChartStable(); + // Read shared config from ``. Outside a provider these are + // the defaults (no segmentation, no floor) — correct for simple charts. + const { segmentsAccessor, minBarHeight = 0 } = useContext(BarDepthContext); + + return useMemo(() => { + if (!(barScale && bandWidth && barXAccessor)) { + return []; + } + + const centerX = innerWidth / 2; + const zeroY = yScale(0) ?? innerHeight; + + // d3-scaleBand exposes step() (= bandwidth + gap); the shared helper caps + // depth below the gap so a side face never spills past the next bar. + const step = + (barScale as unknown as { step?: () => number }).step?.() ?? bandWidth; + const maxDepth = barDepthMaxDepth(step, bandWidth); + + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per-bar 3D geometry (direction, perspective trim, segment scaling) reads clearest as one pass + return data.flatMap((d, dataIndex): BarDepthEntry[] => { + // Stacked bars: the bar's value is the SUM of its segments (so the depth + // height matches the host BarChart's stacked total). Single bars: the + // `dataKey` column. `segmentsAccessor` comes from ``. + const visibleSegs = + segmentsAccessor?.(d)?.filter((s) => s.value > 0) ?? null; + const value = + visibleSegs && visibleSegs.length > 0 + ? visibleSegs.reduce((sum, s) => sum + s.value, 0) + : readBarValue(d, dataKey); + const isActive = activeIndex != null && dataIndex === activeIndex; + const isNegative = value < 0; + const label = barXAccessor(d); + const bandX = barScale(label) ?? 0; + const cx = bandX + bandWidth / 2; + + // `Math.abs` so negative bars produce a positive height — direction is + // captured by `isNegative`. Non-negative bars are floored to + // `minBarHeight` so short/zero values stay visible (mirrors + // ``); a floored bar skips the perspective trim so its + // tiny front face and lid stay aligned. With the default floor of 0, + // zero-value bars produce no height and are skipped. + const valuePos = yScale(value) ?? innerHeight; + const rawHeight = Math.abs(zeroY - valuePos); + const naturalHeight = isNegative + ? rawHeight + : Math.max(rawHeight, minBarHeight); + if (naturalHeight <= 0) { + return []; + } + const isFloored = !isNegative && rawHeight < naturalHeight; + + const offsetFromCenter = centerX > 0 ? (cx - centerX) / centerX : 0; + const isRightOfCenter = offsetFromCenter > 0; + const absOffset = Math.min(1, Math.abs(offsetFromCenter)); + const { depth, perspectiveRise: rawRise } = barDepthAndRise( + absOffset, + naturalHeight, + maxDepth + ); + // Perspective lift applies only to positive bars. For negative bars a + // lift would raise the lid's back edge above the baseline (a visible + // "lip" at the chart edges), so it's forced to 0 — degenerating the lid + // and flattening the side into a rectangle flush with the baseline. + const perspectiveRise = isNegative ? 0 : rawRise; + + // Perspective trim — positive value-bearing bars only. The lid's back + // edge sits `perspectiveRise` above `topY`. Without trim the silhouette + // top would vary with depth (bars further from center look taller). + // Trim shifts `topY` DOWN by `perspectiveRise` so the lid's back edge + // lands on `yScale(value)` regardless of depth. The clamp denominator + // is the topmost segment's scaled height (for stacked bars) or the whole + // bar's natural height, matching how `` stacks segments. + let trimClampReference = naturalHeight; + if (visibleSegs && visibleSegs.length > 0) { + const segTotal = visibleSegs.reduce((sum, s) => sum + s.value, 0); + const topmost = visibleSegs.at(-1); + if (segTotal > 0 && topmost) { + trimClampReference = (topmost.value / segTotal) * naturalHeight; + } + } + const isValueBar = naturalHeight > 0 && !isNegative && !isFloored; + const topYTrim = isValueBar + ? Math.min(perspectiveRise, Math.max(0, trimClampReference - 1)) + : 0; + // Direction-aware top/bottom: positive bars hang ABOVE the baseline; + // negative bars hang BELOW it. The lid always lives at `topY` so for + // negative bars it sits on the baseline. + const topY = isNegative ? zeroY : zeroY - naturalHeight + topYTrim; + const bottomY = isNegative ? zeroY + naturalHeight : zeroY; + const barHeight = bottomY - topY; + + return [ + { + label, + dataIndex, + datum: d, + isActive, + isNegative, + baselineY: zeroY, + bandX, + bandWidth, + topY, + bottomY, + barHeight, + naturalHeight, + topYTrim, + depth, + perspectiveRise, + isRightOfCenter, + }, + ]; + }); + }, [ + data, + barScale, + bandWidth, + yScale, + innerHeight, + innerWidth, + barXAccessor, + segmentsAccessor, + minBarHeight, + dataKey, + activeIndex, + ]); +} + +function useBarDepthStagger() { + const { data, animationDuration } = useChartStable(); + // Mirror `Bar`'s stagger formula so each depth surface enters in lockstep + // with its front face (`Bar` uses `animationDuration * 0.4 / count`). + const total = animationDuration || 1100; + return data.length > 1 ? (total * 0.4) / 1000 / data.length : 0; +} + +// ─── Components ─────────────────────────────────────────────────────── + +export interface BarDepthBackProps { + /** Key in data for the bar value. Match the sibling ``. */ + dataKey: string; + /** Solid color for the side + top faces. Default: var(--chart-line-primary). */ + color?: string; + /** Per-bar color override; takes precedence over `color` when provided. */ + colorAccessor?: ColorAccessor; +} + +/** + * Renders the back surfaces (side + top faces) of the 3D bar effect. + * + * Place BEFORE the `` elements in `` children so the bar's + * solid front face occludes any depth that would extend into an adjacent + * column. Skips bars at chart center (depth ≈ 0) and dense charts. + * + * For stacked bars, wrap the `` in a + * `` so the side face splits into + * per-segment parallelograms and the lid color matches the topmost segment. + */ +export function BarDepthBack({ + dataKey, + color = DEFAULT_BAR_COLOR, + colorAccessor, +}: BarDepthBackProps) { + const { isLoaded, enterTransition, revealEpoch = 0 } = useChartStable(); + const { hoveredBarIndex } = useChartHover(); + const { segmentsAccessor } = useContext(BarDepthContext); + const entries = useBarDepthEntries(dataKey); + const stagger = useBarDepthStagger(); + const { posStops: glassStops, negStops: glassNegStops } = useGlassStops(); + const idPrefix = useId().replace(/[^a-z0-9]/gi, "_"); + + // Two horizontal black gradients (one per depth direction) shade the side + // from front-edge (lit) to back-edge (in shadow). A single vertical white + // gradient on the lid: bright catch at the front lip → soft white at the + // back. The vertical glass gradient layered on top of the side adds the same + // bright-tip / dark-baseline curve as the front face for continuity. + const sideShadeRtl = `bdb-side-rtl-${idPrefix}`; + const sideShadeLtr = `bdb-side-ltr-${idPrefix}`; + const topShade = `bdb-top-${idPrefix}`; + const sideGlass = `bdb-glass-${idPrefix}`; + // Per-bar bbox variant used only for bars that extend below the baseline. + const sideGlassNeg = `bdb-glass-neg-${idPrefix}`; + + return ( + + + {/* Side shade — diagonal: lit corner at front-top, dark corner at + back-bottom. Right-of-center bars have the side on the LEFT + (gradient runs (1,0)→(0,1)); left-of-center bars mirror it. */} + + + + + + + + + {/* Lid 3D shade — mirrors the side face's directionality so the lid + never reads as a brighter "white-washed cap". Front lip at opacity + 0 (seamless with the front face), back edge picks up shadow as it + angles into perspective. */} + + + + + {/* Side glass — per-bar (objectBoundingBox): 0% = the bar's tip, + 100% = its baseline, same proportional gloss as the front face. */} + + {glassStops.map((stop) => ( + + ))} + + {/* Side glass (per-bar bbox variant for negative bars). The gradient + runs from the bar's baseline-edge (top of bbox) down to the bar's + value tip (bottom of bbox), lifting the tip glow onto each bar's + own value edge. */} + + {glassNegStops.map((stop) => ( + + ))} + + + + {entries.map((e) => { + if (e.depth < BAR_DEPTH_MIN_PX) { + return null; + } + + // Resolve stacked segments for this bar. Filter to non-zero so a + // missing segment doesn't paint an invisible 0-height piece or quietly + // become the lid color. + const rawSegments = segmentsAccessor?.(e.datum); + const visibleSegments = rawSegments?.filter((s) => s.value > 0) ?? null; + const baseColor = resolveColor( + e.datum, + e.dataIndex, + color, + colorAccessor + ); + + const sidePieces = buildSidePieces(e, visibleSegments, baseColor); + // Full-side path for the vertical glass overlay — drawn once per bar + // so the brightness curve sweeps across the entire side as one layer. + const fullSidePath = sideFacePath(e, e.topY, e.bottomY); + // Lid is one parallelogram regardless of segmentation — colored by the + // topmost visible segment so it matches the front face's top section. + const topPath = lidFacePath(e); + const topColor = visibleSegments?.at(-1)?.color ?? baseColor; + + const delay = e.dataIndex * stagger; + // Grow uses the SAME enter transition + stagger as `` so the side + // and lid rise at the exact speed of the front face. Post-load geometry + // changes get a quick settle instead. + const growTransition = isLoaded + ? { duration: 0.3, ease: "easeOut" as const } + : transitionWithDelay(enterTransition, delay); + const isFaded = + hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex; + const sideShadeId = e.isRightOfCenter ? sideShadeRtl : sideShadeLtr; + const safeId = e.label.replace(/[^a-z0-9]/gi, "_"); + const clipId = `bdb-clip-${idPrefix}-${safeId}`; + // Lid ENTRANCE only (hover-dim is owned by the wrapping group below, so + // the lid can never dim differently from the side). Positive lids + // translate up from the baseline riding the growing top edge; negative + // lids already sit at the baseline, so they fade in instead. + let lidEntrance: { + initial: false | { y?: number; opacity?: number }; + animate: { y?: number; opacity?: number }; + transition: Transition; + } = { + initial: false, + animate: { y: 0 }, + transition: { y: growTransition }, + }; + if (!isLoaded && e.isNegative) { + lidEntrance = { + initial: { y: 0, opacity: 0 }, + animate: { y: 0, opacity: 1 }, + transition: { y: growTransition, opacity: growTransition }, + }; + } else if (!isLoaded) { + lidEntrance = { + initial: { y: e.barHeight }, + animate: { y: 0 }, + transition: { y: growTransition }, + }; + } + + return ( + // Single hover-dim group for the whole back surface — side AND lid + // share this one opacity so they always fade in lockstep (mirrors + // ``, which puts grow on the inner rect and hover-dim on a + // wrapping group). Entrance animations live on the inner groups. + + {/* Side face — a clip-rect that grows from the baseline reveals the + static side path(s) bottom-up, in lockstep with the bar's grow + animation. ClipPath beats CSS scaleY because motion writes the + SVG `transform` directly. */} + + + {/* Keyed on revealEpoch AND isLoaded so it (a) replays on + every reveal and (b) snaps to final the instant `` + swaps to its static rect at isLoaded — so the side never + trails the face past the animation cutoff. */} + + + + + {/* Per-segment SOLID colors only. */} + {sidePieces.map((piece) => ( + + ))} + {/* Directional shade + Y-anchored glass painted ONCE over the + full side so both gradients stay continuous across segment + seams. Per-segment shading uses objectBoundingBox units, which + would restart the lit→shadowed ramp at every stacked boundary + and read as a notch/indent. The glass is the same global + Y-anchored curve as the front face; negatives swap to the + per-bar bbox variant so the tip catch lands on the bar's own + bottom edge. */} + + + + + {/* Top face — three paint passes: solid color → a flat tip-bright + highlight (the lid is the bar's top, so it gets the reflection + catch; it can't share the side's objectBoundingBox glass, which + would map the full ramp onto the lid's own tiny bbox) → + directional 3D shade darkening the back edge. */} + + + + + + + ); + })} + + ); +} + +BarDepthBack.displayName = "BarDepthBack"; +// Mark as a non-series layer so `BarChart` doesn't count it as a bar (it +// carries a `dataKey` to pair with a Bar). Mirrors `ChartMarkers`' +// `__isChartMarkers` flag — a static property is minification-safe, unlike +// matching on `displayName`. +(BarDepthBack as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true; + +export interface BarDepthFrontProps { + /** Key in data for the bar value. Match the sibling ``. */ + dataKey: string; +} + +/** + * Renders the front surface: a glossy glass sheen over the bar's solid fill. + * Place AFTER the `` elements in `` children so the gradient + * sits on top of the bar. + * + * Reads `segmentsAccessor` from the surrounding `` (if any) + * via `useBarDepthEntries`, so the glass rectangle's `topY` automatically + * matches the lid+side's trim. No prop wiring required. + */ +export function BarDepthFront({ dataKey }: BarDepthFrontProps) { + const { isLoaded, enterTransition, revealEpoch = 0 } = useChartStable(); + const { hoveredBarIndex } = useChartHover(); + const entries = useBarDepthEntries(dataKey); + const stagger = useBarDepthStagger(); + const { posStops: glassStops, negStops: glassNegStops } = useGlassStops(); + const idPrefix = useId().replace(/[^a-z0-9]/gi, "_"); + const glassId = `bdf-glass-${idPrefix}`; + const glassIdNeg = `bdf-glass-neg-${idPrefix}`; + + return ( + + + {/* Per-bar (objectBoundingBox) glass for POSITIVE bars: 0% = the bar's + tip, 100% = its baseline — same proportional gloss on every bar. */} + + {glassStops.map((stop) => ( + + ))} + + {/* Per-bar bbox variant for negative bars — the bright catch always + sits on the value-bearing tip no matter how deep the bar reaches. */} + + {glassNegStops.map((stop) => ( + + ))} + + + + {entries.map((e) => { + const delay = e.dataIndex * stagger; + // Same enter transition + stagger as `` so the glass rises in + // lockstep with the front face. + const baseTransition = isLoaded + ? { duration: 0.3, ease: "easeOut" as const } + : transitionWithDelay(enterTransition, delay); + const isFaded = + hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex; + + return ( + `. + initial={isLoaded ? false : { y: e.baselineY, height: 0 }} + key={`${e.label}-${revealEpoch}-${isLoaded}`} + transition={{ ...baseTransition, opacity: BAR_HOVER_TRANSITION }} + width={e.bandWidth} + x={e.bandX} + /> + ); + })} + + ); +} + +BarDepthFront.displayName = "BarDepthFront"; +(BarDepthFront as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true; + +// ─── BarPulse ───────────────────────────────────────────────────────── + +/** + * Builds the SVG path tracing the silhouette of a 3D bar — the unioned + * outline of front face + visible side parallelogram + lid parallelogram. + * Used as the clip region for `BarPulse`'s wave so the gradient only paints + * inside the bar's visible 3D shape. For dead-center bars (`depth <= 0`) the + * silhouette degenerates to just the front rectangle. + */ +function buildBarSilhouettePath(e: BarDepthEntry): string { + const { + bandX, + bandWidth, + topY, + bottomY, + depth, + perspectiveRise, + isRightOfCenter, + } = e; + if (depth <= 0) { + return [ + `M ${bandX} ${topY}`, + `L ${bandX + bandWidth} ${topY}`, + `L ${bandX + bandWidth} ${bottomY}`, + `L ${bandX} ${bottomY}`, + "Z", + ].join(" "); + } + if (isRightOfCenter) { + // Side face on LEFT — back shifted (-depth, -perspectiveRise). + return [ + `M ${bandX - depth} ${topY - perspectiveRise}`, + `L ${bandX + bandWidth - depth} ${topY - perspectiveRise}`, + `L ${bandX + bandWidth} ${topY}`, + `L ${bandX + bandWidth} ${bottomY}`, + `L ${bandX} ${bottomY}`, + `L ${bandX - depth} ${bottomY - perspectiveRise}`, + "Z", + ].join(" "); + } + // Side face on RIGHT — back shifted (+depth, -perspectiveRise). + return [ + `M ${bandX} ${topY}`, + `L ${bandX + depth} ${topY - perspectiveRise}`, + `L ${bandX + bandWidth + depth} ${topY - perspectiveRise}`, + `L ${bandX + bandWidth + depth} ${bottomY - perspectiveRise}`, + `L ${bandX + bandWidth} ${bottomY}`, + `L ${bandX} ${bottomY}`, + "Z", + ].join(" "); +} + +export interface BarPulseProps { + /** Key in data for the bar value. Match the sibling ``. */ + dataKey: string; + /** Index (in the data array) of the bar to pulse. */ + activeIndex?: number; + /** Suppress the sweep while keeping the bar's 3D + glass treatment. */ + pulsePaused?: boolean; +} + +/** + * Vertical wave that sweeps from a bar's root (baseline) toward its tip on a + * continuous loop, painted across the bar's full 3D silhouette (front face + + * lid + visible side). Reads as a heartbeat pulse flowing up the column for + * positive bars and down for negative bars — useful for marking a live or + * in-progress value without adding any fixed element. + * + * Select the bar via `activeIndex`. Set `pulsePaused` to freeze it. Place + * AFTER `` so the wave brightens on top of the glass. + * + * Performance: at most one clipped `` with a single Y translate + * per frame (no per-frame paint of gradient stops, no SVG filters). + */ +export function BarPulse({ dataKey, activeIndex, pulsePaused }: BarPulseProps) { + const { isLoaded } = useChartStable(); + const { hoveredBarIndex } = useChartHover(); + const entries = useBarDepthEntries(dataKey, activeIndex); + const idPrefix = useId().replace(/[^a-z0-9]/gi, "_"); + const activeEntries = useMemo( + () => entries.filter((e) => e.isActive), + [entries] + ); + + if (activeEntries.length === 0 || pulsePaused) { + return null; + } + // Hold the wave until bars finish growing — the silhouette clip is computed + // at the final geometry, so painting it mid-grow would reveal a wave inside + // an area the bar hasn't reached yet. + if (!isLoaded) { + return null; + } + + return ( + + + {activeEntries.flatMap((e) => { + const safeId = e.label.replace(/[^a-z0-9]/gi, "_"); + const clipId = `bar-pulse-clip-${idPrefix}-${safeId}`; + const gradId = `bar-pulse-grad-${idPrefix}-${safeId}`; + return [ + + + , + // Vertical gradient with a centered bright band shaped into a soft + // bell curve. y1=1, y2=0 makes offset 50% the rect's center, so the + // brightest pixel sits mid-rect during travel. + + + + + + + + + + + + + , + ]; + })} + + + {activeEntries.map((e) => { + const safeId = e.label.replace(/[^a-z0-9]/gi, "_"); + const clipId = `bar-pulse-clip-${idPrefix}-${safeId}`; + const gradId = `bar-pulse-grad-${idPrefix}-${safeId}`; + + const waveHeight = Math.max( + e.barHeight * PULSE_WAVE_HEIGHT_RATIO, + PULSE_WAVE_HEIGHT_MIN_PX + ); + // Travel always flows root → tip. Positive bars sweep up (root = + // bottomY); negative bars sweep down (root = topY, the baseline). The + // gradient is symmetric, so reversing the endpoints reverses direction. + const yAboveLid = e.topY - e.perspectiveRise - waveHeight; + const yBelowFloor = e.bottomY; + const yStart = e.isNegative ? yAboveLid : yBelowFloor; + const yEnd = e.isNegative ? yBelowFloor : yAboveLid; + + const isFaded = + hoveredBarIndex != null && hoveredBarIndex !== e.dataIndex; + + return ( + + + + ); + })} + + ); +} + +BarPulse.displayName = "BarPulse"; +(BarPulse as { __isBarDepthLayer?: boolean }).__isBarDepthLayer = true; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-x-axis.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-x-axis.tsx new file mode 100644 index 0000000..2fbfa5f --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-x-axis.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { motion } from "motion/react"; +import { memo, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { cn } from "@/lib/utils"; +import { useChart, useChartStable } from "./chart-context"; + +export interface BarXAxisProps { + /** Width of the date ticker box for fade calculation. Default: 50 */ + tickerHalfWidth?: number; + /** Whether to show all labels or skip some for dense data. Default: false */ + showAllLabels?: boolean; + /** Maximum number of labels to show. Default: 12 */ + maxLabels?: number; +} + +interface BarXAxisLabelProps { + label: string; + x: number; + crosshairX: number | null; + isHovering: boolean; + tickerHalfWidth: number; +} + +function BarXAxisLabel({ + label, + x, + crosshairX, + isHovering, + tickerHalfWidth, +}: BarXAxisLabelProps) { + const fadeBuffer = 20; + const fadeRadius = tickerHalfWidth + fadeBuffer; + + let opacity = 1; + if (isHovering && crosshairX !== null) { + const distance = Math.abs(x - crosshairX); + if (distance < tickerHalfWidth) { + opacity = 0; + } else if (distance < fadeRadius) { + opacity = (distance - tickerHalfWidth) / fadeBuffer; + } + } + + // Zero-width container approach for perfect centering + return ( +
+ + {label} + +
+ ); +} + +export function BarXAxis(props: BarXAxisProps) { + const { containerRef, barScale } = useChartStable(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + if (!barScale) { + return null; + } + + return ; +} + +const BarXAxisInner = memo(function BarXAxisInner({ + tickerHalfWidth = 50, + showAllLabels = false, + maxLabels = 12, + container, +}: BarXAxisProps & { container: HTMLDivElement }) { + const { margin, tooltipData, barScale, bandWidth, barXAccessor, data } = + useChart(); + + // Generate labels for each bar + const labelsToShow = useMemo(() => { + if (!(barScale && bandWidth && barXAccessor)) { + return []; + } + + const allLabels = data.map((d) => { + const label = barXAccessor(d); + const bandX = barScale(label) ?? 0; + // Center the label under the bar group + const x = bandX + bandWidth / 2 + margin.left; + return { label, x }; + }); + + // If showAllLabels is true or we have fewer than maxLabels, show all + if (showAllLabels || allLabels.length <= maxLabels) { + return allLabels; + } + + // Otherwise, skip some labels to avoid crowding + const step = Math.ceil(allLabels.length / maxLabels); + return allLabels.filter((_, i) => i % step === 0); + }, [ + barScale, + bandWidth, + barXAccessor, + data, + margin.left, + showAllLabels, + maxLabels, + ]); + + const isHovering = tooltipData !== null; + const crosshairX = tooltipData ? tooltipData.x + margin.left : null; + + return createPortal( +
+ {labelsToShow.map((item) => ( + + ))} +
, + container + ); +}); + +BarXAxis.displayName = "BarXAxis"; + +export default BarXAxis; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-y-axis.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-y-axis.tsx new file mode 100644 index 0000000..95400e5 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-y-axis.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { motion } from "motion/react"; +import { memo, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { cn } from "@/lib/utils"; +import { useChart, useChartStable } from "./chart-context"; + +export interface BarYAxisProps { + /** Whether to show all labels or skip some for dense data. Default: true */ + showAllLabels?: boolean; + /** Maximum number of labels to show. Default: 20 */ + maxLabels?: number; +} + +interface BarYAxisLabelProps { + label: string; + y: number; + bandHeight: number; + isHovered: boolean; +} + +function BarYAxisLabel({ + label, + y, + bandHeight, + isHovered, +}: BarYAxisLabelProps) { + return ( +
+ + {label} + +
+ ); +} + +export function BarYAxis(props: BarYAxisProps) { + const { containerRef, barScale } = useChartStable(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + if (!barScale) { + return null; + } + + return ; +} + +const BarYAxisInner = memo(function BarYAxisInner({ + showAllLabels = true, + maxLabels = 20, + container, +}: BarYAxisProps & { container: HTMLDivElement }) { + const { margin, barScale, bandWidth, barXAccessor, data, hoveredBarIndex } = + useChart(); + + // Generate labels for each bar + const labelsToShow = useMemo(() => { + if (!(barScale && bandWidth && barXAccessor)) { + return []; + } + + const allLabels = data.map((d, i) => { + const label = barXAccessor(d); + const bandY = barScale(label) ?? 0; + // Center the label vertically within the band + const y = bandY + margin.top; + return { label, y, bandHeight: bandWidth, index: i }; + }); + + // If showAllLabels is true or we have fewer than maxLabels, show all + if (showAllLabels || allLabels.length <= maxLabels) { + return allLabels; + } + + // Otherwise, skip some labels to avoid crowding + const step = Math.ceil(allLabels.length / maxLabels); + return allLabels.filter((_, i) => i % step === 0); + }, [ + barScale, + bandWidth, + barXAccessor, + data, + margin.top, + showAllLabels, + maxLabels, + ]); + + return createPortal( +
+ {labelsToShow.map((item) => ( + + ))} +
, + container + ); +}); + +BarYAxis.displayName = "BarYAxis"; + +export default BarYAxis; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar.tsx new file mode 100644 index 0000000..6f3d815 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar.tsx @@ -0,0 +1,466 @@ +"use client"; + +import type { scaleBand } from "@visx/scale"; +import type { Transition } from "motion/react"; +import { motion } from "motion/react"; +import { memo, useId, useMemo } from "react"; +import { barDepthAndRise, barDepthMaxDepth } from "./bar-depth-geometry"; +import { + chartCssVars, + useChart, + useChartStable, + useYScale, +} from "./chart-context"; +import { useChartLegendHover } from "./chart-legend-hover"; +import { transitionWithDelay } from "./motion-utils"; + +type ScaleBand = ReturnType< + typeof scaleBand +>; + +export type BarLineCap = "round" | "butt" | number; +export type BarAnimationType = "grow" | "fade"; + +// ── Bar-depth perspective trim ─────────────────────────────────────────── +// Uses the SHARED geometry (`bar-depth-geometry.ts`) so a +// `` front face lines up exactly with +// ``'s lid — the formula lives in one place for both. + +/** perspectiveRise for a positive bar whose visual top sits at `topY`. + * Returns 0 for a dead-center bar or a dense chart (degenerate depth). */ +function barDepthPerspectiveRise( + barScale: ScaleBand, + bandWidth: number, + barXAccessor: (d: Record) => string, + innerWidth: number, + datum: Record, + topY: number, + baselineY: number +): number { + const centerX = innerWidth / 2; + if (centerX <= 0) { + return 0; + } + const step = + (barScale as unknown as { step?: () => number }).step?.() ?? bandWidth; + const maxDepth = barDepthMaxDepth(step, bandWidth); + const bandX = barScale(barXAccessor(datum)) ?? 0; + const cx = bandX + bandWidth / 2; + const absOffset = Math.min(1, Math.abs((cx - centerX) / centerX)); + const naturalHeight = Math.abs(baselineY - topY); + return barDepthAndRise(absOffset, naturalHeight, maxDepth).perspectiveRise; +} + +export interface BarProps { + /** Key in data to use for y values */ + dataKey: string; + /** Y-scale group id for vertical bars (Recharts `yAxisId`). Default: `"left"`. */ + yAxisId?: string | number; + /** Fill color for the bar. Can be a color, gradient url, or pattern url. Default: var(--chart-line-primary) */ + fill?: string; + /** Color for tooltip dot. Use when fill is a gradient/pattern. Default: uses fill value */ + stroke?: string; + /** Line cap style for bar ends: "round", "butt", or a number for custom radius. Default: "round" */ + lineCap?: BarLineCap; + /** Whether to animate the bars. Default: true */ + animate?: boolean; + /** Animation type: "grow" (height) or "fade" (opacity + blur). Default: "grow" */ + animationType?: BarAnimationType; + /** Opacity when not hovered (when another bar is hovered). Default: 0.3 */ + fadedOpacity?: number; + /** Stagger delay between bars in seconds. Auto-calculated if not provided. */ + staggerDelay?: number; + /** Gap between stacked bars in pixels. Default: 0 */ + stackGap?: number; + /** Gap between grouped bars in pixels. Default: 4 */ + groupGap?: number; + /** Shrink each positive bar's top by its perspective rise so the front face + * lines up with ``'s lid (instead of the lid sitting above the + * front face). Pass `true` whenever the chart also renders the bar-depth 3D + * surfaces. Default: false */ + perspective?: boolean; + /** Minimum rendered bar height in px (non-stacked, vertical). Floors short or + * zero-value bars so they stay visible. Pair with the same value on + * `` when using the 3D surfaces. Default: 0 */ + minBarHeight?: number; +} + +interface BarInnerProps extends BarProps { + barScale: ScaleBand; + bandWidth: number; + barXAccessor: (d: Record) => string; +} + +interface AnimatedBarProps { + x: number; + y: number; + width: number; + height: number; + fill: string; + rx: number; + ry: number; + index: number; + isFaded: boolean; + animationType: BarAnimationType; + innerHeight: number; + fadedOpacity: number; + staggerDelay: number; + enterTransition?: Transition; + revealEpoch: number; + isHorizontal: boolean; +} + +function AnimatedBar({ + x, + y, + width, + height, + fill, + rx, + ry, + index, + isFaded, + animationType, + innerHeight, + fadedOpacity, + staggerDelay, + enterTransition, + revealEpoch, + isHorizontal, +}: AnimatedBarProps) { + const enterAnim = transitionWithDelay(enterTransition, index * staggerDelay); + + if (animationType === "fade") { + return ( + + ); + } + + const initial = isHorizontal + ? { width: 0, height, x: 0, y } + : { width, height: 0, x, y: innerHeight }; + const target = isHorizontal + ? { width, height, x: 0, y } + : { width, height, x, y }; + + return ( + + + + ); +} + +const BarInner = memo(function BarInner({ + dataKey, + yAxisId, + fill = chartCssVars.linePrimary, + lineCap = "round", + animate = true, + animationType = "grow", + fadedOpacity = 0.3, + staggerDelay, + stackGap = 0, + groupGap = 4, + perspective = false, + minBarHeight = 0, + barScale, + bandWidth, + barXAccessor, +}: BarInnerProps) { + const { + data, + yScale: chartYScale, + innerHeight, + innerWidth, + isLoaded, + hoveredBarIndex, + lines, + orientation, + stacked, + stackOffsets, + animationDuration, + enterTransition, + revealEpoch = 0, + } = useChart(); + + // Calculate stagger delay automatically if not provided + // Total animation duration is ~1200ms, with 40% for stagger spread and 60% for bar animation + const totalAnimDuration = animationDuration || 1100; + const staggerSpread = totalAnimDuration * 0.4; // 40% of time for stagger spread + const calculatedStaggerDelay = + staggerDelay ?? (data.length > 1 ? staggerSpread / 1000 / data.length : 0); + const uniqueId = useId(); + + const isHorizontal = orientation === "horizontal"; + + // Find the index of this bar series among all bar series + const { hoveredIndex: legendHoveredIndex } = useChartLegendHover(); + + const seriesIndex = useMemo(() => { + const idx = lines.findIndex((l) => l.dataKey === dataKey); + return idx >= 0 ? idx : 0; + }, [lines, dataKey]); + + const seriesConfig = lines[seriesIndex]; + const valueScale = useYScale(yAxisId ?? seriesConfig?.yAxisId); + + const isLegendDimmed = + legendHoveredIndex !== null && legendHoveredIndex !== seriesIndex; + + const seriesCount = lines.length; + const isLastSeries = seriesIndex === seriesCount - 1; + + // Calculate the width for each bar within a group (for non-stacked) + const barWidth = useMemo(() => { + if (!bandWidth || seriesCount === 0) { + return 0; + } + if (stacked) { + // Stacked bars use full band width + return bandWidth; + } + // Leave a gap between grouped bars (controlled by groupGap prop) + const effectiveGroupGap = seriesCount > 1 ? groupGap : 0; + return (bandWidth - effectiveGroupGap * (seriesCount - 1)) / seriesCount; + }, [bandWidth, seriesCount, stacked, groupGap]); + + // Calculate corner radius based on lineCap. Perspective bars force a flat + // top (radius 0) so the 3D lid from `` meets the bar with no + // gap — rounded corners would leave a wedge, so `perspective` overrides it. + const cornerRadius = useMemo(() => { + if (perspective) { + return 0; + } + if (typeof lineCap === "number") { + return lineCap; + } + if (lineCap === "round" && barWidth) { + return Math.min(barWidth / 2, 8); + } + return 0; + }, [lineCap, barWidth, perspective]); + + return ( + + {data.map((d, i) => { + const value = d[dataKey]; + if (typeof value !== "number") { + return null; + } + + const categoryValue = barXAccessor(d); + const bandPos = barScale(categoryValue) ?? 0; + + let x: number; + let y: number; + let barHeight: number; + let barW: number; + + const scale = isHorizontal ? chartYScale : valueScale; + + if (isHorizontal) { + // Horizontal bars: category on y-axis, value on x-axis + const valuePos = scale(value) ?? 0; + barW = valuePos; // Width is the value position (grows from left) + barHeight = barWidth; + + if (stacked && stackOffsets) { + const offset = stackOffsets.get(i)?.get(dataKey) ?? 0; + x = scale(offset) ?? 0; + barW = valuePos - x; + // Apply stack gap for horizontal: shift right and reduce width + const gapOffset = seriesIndex * stackGap; + x += gapOffset; + if (!isLastSeries && stackGap > 0) { + barW = Math.max(0, barW - stackGap); + } + } else { + x = 0; + // For grouped bars, offset y position + const effectiveGroupGap = seriesCount > 1 ? groupGap : 0; + y = bandPos + seriesIndex * (barWidth + effectiveGroupGap); + } + y = stacked + ? bandPos + : bandPos + + seriesIndex * (barWidth + (seriesCount > 1 ? groupGap : 0)); + } else { + // Vertical bars: category on x-axis, value on y-axis + const valuePos = scale(value) ?? 0; + barHeight = innerHeight - valuePos; + barW = barWidth; + + if (stacked && stackOffsets) { + const offset = stackOffsets.get(i)?.get(dataKey) ?? 0; + const offsetY = scale(offset) ?? innerHeight; + // Apply stack gap: shift up and reduce height + const gapOffset = seriesIndex * stackGap; + y = offsetY - barHeight - gapOffset; + // Reduce height slightly for non-last bars to create visual gap + if (!isLastSeries && stackGap > 0) { + barHeight = Math.max(0, barHeight - stackGap); + } + } else { + y = valuePos; + // For grouped bars, offset x position + const effectiveGroupGap = seriesCount > 1 ? groupGap : 0; + x = bandPos + seriesIndex * (barWidth + effectiveGroupGap); + } + x = stacked + ? bandPos + : bandPos + + seriesIndex * (barWidth + (seriesCount > 1 ? groupGap : 0)); + + // Minimum visible height — floor short/zero non-stacked bars so a + // zero-value data point still reads as a tiny bar instead of + // vanishing. Grows up from the baseline. Floored bars skip the + // perspective trim (sub-pixel on a 3px bar; keeps the front aligned + // with bar-depth, which also skips trim for floored bars). + let isFloored = false; + if ( + !stacked && + minBarHeight > 0 && + value >= 0 && + barHeight < minBarHeight + ) { + const baselineY = scale(0) ?? innerHeight; + barHeight = minBarHeight; + y = baselineY - minBarHeight; + isFloored = true; + } + + // Perspective trim — shrink the topmost positive bar's front-face + // top down by its perspective rise so it meets ``'s + // lid back edge. Stacked: only the last (topmost) series; grouped or + // single: every positive bar. Clamped to `barHeight - 1` so very + // short bars keep a positive height (matches bar-depth's clamp). + if ( + perspective && + value > 0 && + !isFloored && + (!stacked || isLastSeries) + ) { + const baselineY = scale(0) ?? innerHeight; + const rise = barDepthPerspectiveRise( + barScale, + bandWidth, + barXAccessor, + innerWidth, + d, + y, + baselineY + ); + const trim = Math.min(rise, Math.max(0, barHeight - 1)); + y += trim; + barHeight -= trim; + } + } + + const isFaded = + (hoveredBarIndex !== null && hoveredBarIndex !== i) || isLegendDimmed; + + // Use categoryValue as key since it's the unique identifier from data + const barKey = `bar-${dataKey}-${categoryValue}`; + + // Apply rounded corners: + // - For non-stacked: always apply + // - For stacked with gap: apply to all bars + // - For stacked without gap: only apply to the last series + const applyRounding = !stacked || stackGap > 0 || isLastSeries; + const effectiveRx = applyRounding ? cornerRadius : 0; + const effectiveRy = applyRounding ? cornerRadius : 0; + + if (animate && !isLoaded) { + return ( + + ); + } + + // Static bar after animation completes + return ( + + ); + })} + + ); +}); + +export function Bar(props: BarProps) { + const { barScale, bandWidth, barXAccessor } = useChartStable(); + + if (!(barScale && bandWidth && barXAccessor)) { + console.warn("Bar component must be used within a BarChart"); + return null; + } + + return ( + + ); +} + +Bar.displayName = "Bar"; + +export default Bar; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick-chart.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick-chart.tsx new file mode 100644 index 0000000..1d49a91 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick-chart.tsx @@ -0,0 +1,425 @@ +"use client"; + +import { ParentSize } from "@visx/responsive"; +import { scaleLinear, scaleTime } from "@visx/scale"; +import { bisector } from "d3-array"; +import type { Transition } from "motion/react"; +import { + Children, + isValidElement, + memo, + type ReactElement, + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { cn } from "@/lib/utils"; +import { + isClipExcludedComponent, + isPostOverlayComponent, + isUnderlayComponent, +} from "./chart-child-passthrough"; +import { ChartProvider, type LineConfig, type Margin } from "./chart-context"; +import { shortDateFmt } from "./chart-formatters"; +import { DEFAULT_CHART_LIFECYCLE } from "./chart-phase"; +import { + decimateOhlcData, + maxRenderPointsForWidth, +} from "./decimate-time-series"; +import { extractReferenceAreaConfigs } from "./reference-area-config"; +import { useChartInteraction } from "./use-chart-interaction"; +import { wrapSingleYScale } from "./y-axis-scales"; + +export interface OHLCDataPoint { + date: Date; + open: number; + high: number; + low: number; + close: number; +} + +export interface CandlestickChartProps { + /** OHLC data array */ + data: OHLCDataPoint[]; + /** Key in data for the x-axis (date). Default: "date" */ + xDataKey?: string; + /** Chart margins */ + margin?: Partial; + /** Animation duration in milliseconds. Default: 1500 */ + animationDuration?: number; + /** Motion enter transition (spring or cubic-bezier tween). */ + enterTransition?: Transition; + /** Signature of motion URL state — triggers enter replay when it changes. */ + revealSignature?: string; + /** Aspect ratio as "width / height". Default: "2 / 1" */ + aspectRatio?: string; + /** Additional class name for the container */ + className?: string; + /** Inline styles for the container (e.g. { height: 320 }) */ + style?: React.CSSProperties; + /** Gap between candles as fraction of slot width (0–1). Default: 0.2. Ignored when candleWidth is set. */ + candleGap?: number; + /** Fixed candle body width in pixels. If set, overrides candleGap. */ + candleWidth?: number; + /** When set, xScale uses this domain instead of deriving from data. Use with brush so main chart and strip share the same scale. */ + xDomain?: [Date, Date]; + /** When xDomain is set, use this as the number of slots for scale padding (e.g. full data length). */ + xDomainSlotCount?: number; + /** Child components (Candlestick, Grid, XAxis, YAxis, ChartTooltip, etc.) */ + children: ReactNode; +} + +const DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 }; + +interface ChartInnerProps { + width: number; + height: number; + data: Record[]; + xDataKey: string; + margin: Margin; + animationDuration: number; + enterTransition?: Transition; + revealSignature?: string; + candleGap: number; + candleWidthProp?: number; + xDomain?: [Date, Date]; + xDomainSlotCount?: number; + children: ReactNode; + containerRef: React.RefObject; +} + +function ChartInner(props: ChartInnerProps) { + const { width, height } = props; + if (width < 10 || height < 10) { + return null; + } + return ; +} + +const ChartCore = memo(function ChartCore({ + width, + height, + data, + xDataKey, + margin, + animationDuration, + enterTransition, + revealSignature = "", + candleGap, + candleWidthProp, + xDomain, + xDomainSlotCount, + children, + containerRef, +}: ChartInnerProps) { + const [isLoaded, setIsLoaded] = useState(false); + const [revealEpoch, setRevealEpoch] = useState(0); + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const xAccessor = useCallback( + (d: Record): Date => { + const value = d[xDataKey]; + return value instanceof Date ? value : new Date(value as string | number); + }, + [xDataKey] + ); + + const bisectDate = useMemo( + () => bisector, Date>((d) => xAccessor(d)).left, + [xAccessor] + ); + + const slotCount = + xDomain && xDomainSlotCount != null ? xDomainSlotCount : data.length; + const slotWidth = innerWidth / Math.max(slotCount, 1); + const xScale = useMemo(() => { + const minTime = xDomain + ? xDomain[0].getTime() + : Math.min(...data.map((d) => xAccessor(d).getTime())); + const maxTime = xDomain + ? xDomain[1].getTime() + : Math.max(...data.map((d) => xAccessor(d).getTime())); + const padding = slotWidth / 2; + return scaleTime({ + range: [padding, innerWidth - padding], + domain: [minTime, maxTime], + }); + }, [innerWidth, data, xAccessor, slotWidth, xDomain]); + + const yScale = useMemo(() => { + let minVal = Number.POSITIVE_INFINITY; + let maxVal = Number.NEGATIVE_INFINITY; + for (const d of data) { + const low = d.low as number | undefined; + const high = d.high as number | undefined; + if (typeof low === "number" && low < minVal) { + minVal = low; + } + if (typeof high === "number" && high > maxVal) { + maxVal = high; + } + } + if (minVal === Number.POSITIVE_INFINITY) { + minVal = 0; + } + if (maxVal === Number.NEGATIVE_INFINITY) { + maxVal = 100; + } + const padding = (maxVal - minVal) * 0.05 || 1; + return scaleLinear({ + range: [innerHeight, 0], + domain: [minVal - padding, maxVal + padding], + nice: true, + }); + }, [innerHeight, data]); + + const columnWidth = slotWidth; + const bandWidth = candleWidthProp ?? slotWidth * (1 - candleGap); + + const lines: LineConfig[] = useMemo( + () => [ + { dataKey: "close", stroke: "var(--chart-line-primary)", strokeWidth: 0 }, + ], + [] + ); + + const renderData = useMemo( + () => decimateOhlcData(data, maxRenderPointsForWidth(innerWidth)), + [data, innerWidth] + ); + + const dateLabels = useMemo( + () => data.map((d) => shortDateFmt.format(xAccessor(d))), + [data, xAccessor] + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature + useEffect(() => { + setRevealEpoch((n) => n + 1); + setIsLoaded(false); + const timer = setTimeout(() => setIsLoaded(true), animationDuration); + return () => clearTimeout(timer); + }, [animationDuration, revealSignature]); + + const { + tooltipData, + setTooltipData, + selection, + clearSelection, + interactionHandlers, + interactionStyle, + } = useChartInteraction({ + xScale, + yScale, + yScales: wrapSingleYScale(yScale), + data, + lines, + margin, + xAccessor, + bisectDate, + canInteract: isLoaded, + }); + + const hoveredCandleIndex = tooltipData?.index ?? null; + + const isDefsComponent = (child: ReactElement): boolean => { + const displayName = + (child.type as { displayName?: string })?.displayName || + (child.type as { name?: string })?.name || + ""; + return ( + displayName.includes("Gradient") || + displayName.includes("Pattern") || + displayName === "LinearGradient" || + displayName === "RadialGradient" || + displayName === "Lines" || + displayName === "PatternLines" + ); + }; + + const defsChildren: ReactElement[] = []; + const clipExcludedChildren: ReactElement[] = []; + const underlayChildren: ReactElement[] = []; + const preOverlayChildren: ReactElement[] = []; + const postOverlayChildren: ReactElement[] = []; + Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + if (isDefsComponent(child)) { + defsChildren.push(child); + } else if (isPostOverlayComponent(child)) { + postOverlayChildren.push(child); + } else if (isClipExcludedComponent(child)) { + clipExcludedChildren.push(child); + } else if (isUnderlayComponent(child)) { + underlayChildren.push(child); + } else { + preOverlayChildren.push(child); + } + }); + + const referenceAreas = useMemo( + () => extractReferenceAreaConfigs(children), + [children] + ); + + const yScales = useMemo(() => wrapSingleYScale(yScale), [yScale]); + + const contextValue = useMemo( + () => ({ + ...DEFAULT_CHART_LIFECYCLE, + data, + renderData, + xScale, + yScale, + yScales, + width, + height, + innerWidth, + innerHeight, + margin, + columnWidth, + tooltipData, + setTooltipData, + containerRef, + lines, + referenceAreas, + isLoaded, + animationDuration, + enterTransition, + revealEpoch, + xAccessor, + dateLabels, + selection: selection ?? null, + clearSelection, + bandWidth, + hoveredCandleIndex, + }), + [ + data, + renderData, + xScale, + yScale, + yScales, + width, + height, + innerWidth, + innerHeight, + margin, + columnWidth, + tooltipData, + setTooltipData, + containerRef, + lines, + referenceAreas, + isLoaded, + animationDuration, + enterTransition, + revealEpoch, + xAccessor, + dateLabels, + selection, + clearSelection, + bandWidth, + hoveredCandleIndex, + ] + ); + + return ( + + + + ); +}); + +export function CandlestickChart({ + data, + xDataKey = "date", + margin: marginProp, + animationDuration = 1100, + enterTransition, + revealSignature, + aspectRatio = "2 / 1", + className = "", + style, + candleGap = 0.2, + candleWidth, + xDomain, + xDomainSlotCount, + children, +}: CandlestickChartProps) { + const containerRef = useRef(null); + const margin = { ...DEFAULT_MARGIN, ...marginProp }; + const dataAsRecords = data as unknown as Record[]; + + return ( +
+ + {({ width, height }) => ( + + {children} + + )} + +
+ ); +} + +CandlestickChart.displayName = "CandlestickChart"; + +export default CandlestickChart; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick.tsx new file mode 100644 index 0000000..af6ec86 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/candlestick.tsx @@ -0,0 +1,439 @@ +"use client"; + +import type { Transition } from "motion/react"; +import { motion } from "motion/react"; +import { memo, useMemo } from "react"; +import { useChart } from "./chart-context"; +import { useChartLegendHover } from "./chart-legend-hover"; +import { transitionWithDelay } from "./motion-utils"; + +const DEFAULT_POSITIVE = "url(#candlestick-positive)"; +const DEFAULT_NEGATIVE = "url(#candlestick-negative)"; + +const SOLID_POSITIVE = "var(--color-emerald-500)"; +const SOLID_NEGATIVE = "var(--color-red-500)"; +const WICK_WIDTH = 1.5; + +export interface CandlestickProps { + /** Whether to animate the candlesticks. Default: true */ + animate?: boolean; + /** Fill for positive (close >= open) candles. Color or url(#gradient). Default: emerald */ + positiveFill?: string; + /** Fill for negative candles. Color or url(#gradient). Default: red */ + negativeFill?: string; + /** Optional pattern URL for body only (e.g. url(#pattern)). When set, body is drawn solid first, then pattern overlaid and masked to the body rect. */ + bodyPatternPositive?: string; + /** Optional pattern URL for negative candle body. */ + bodyPatternNegative?: string; + /** Inner border width on the body (drawn inside so it does not expand the shape). Default: 0 (off). */ + insideStrokeWidth?: number; + /** Opacity when another candle is hovered. Default: 0.3 */ + fadedOpacity?: number; + /** Dim non-hovered candles on hover. Default: true */ + showHoverFade?: boolean; +} + +interface CandleGeometry { + time: number; + centerX: number; + bodyTop: number; + bodyHeight: number; + bodyLeft: number; + candleWidth: number; + wickTop: number; + wickHeight: number; + wickLeft: number; + bodySolidFill: string; + wickFill: string; + bodyPattern?: string; + insideStrokeWidth: number; + isPositive: boolean; +} + +function getSolidColor(isPositive: boolean): string { + return isPositive ? SOLID_POSITIVE : SOLID_NEGATIVE; +} + +function computeGeometries( + renderData: Record[], + xScale: (value: Date) => number | undefined, + yScale: (value: number) => number | undefined, + xAccessor: (d: Record) => Date, + candleWidth: number, + positiveFill: string, + negativeFill: string, + bodyPatternPositive: string | undefined, + bodyPatternNegative: string | undefined, + insideStrokeWidth: number +): CandleGeometry[] { + return renderData.map((d) => { + const date = xAccessor(d); + const open = d.open as number; + const high = d.high as number; + const low = d.low as number; + const close = d.close as number; + const centerX = xScale(date) ?? 0; + const yHigh = yScale(high) ?? 0; + const yLow = yScale(low) ?? 0; + const yOpen = yScale(open) ?? 0; + const yClose = yScale(close) ?? 0; + const bodyTop = Math.min(yOpen, yClose); + const bodyHeight = Math.abs(yClose - yOpen) || 1; + const bodyLeft = centerX - candleWidth / 2; + const wickTop = Math.min(yHigh, yLow); + const wickHeight = Math.abs(yLow - yHigh) || 1; + const isPositive = close >= open; + const fill = isPositive ? positiveFill : negativeFill; + const bodyPattern = isPositive ? bodyPatternPositive : bodyPatternNegative; + const hasPatternOverlay = Boolean(bodyPattern); + const bodySolidFill = hasPatternOverlay ? getSolidColor(isPositive) : fill; + + return { + time: date.getTime(), + centerX, + bodyTop, + bodyHeight, + bodyLeft, + candleWidth, + wickTop, + wickHeight, + wickLeft: centerX - WICK_WIDTH / 2, + bodySolidFill, + wickFill: hasPatternOverlay ? bodySolidFill : fill, + bodyPattern: hasPatternOverlay ? bodyPattern : undefined, + insideStrokeWidth, + isPositive, + }; + }); +} + +function geometryDimOpacity( + geometry: CandleGeometry, + fadedOpacity: number, + legendHoveredIndex: number | null, + hoveredTime: number | null +): number { + if (legendHoveredIndex !== null) { + const dimFromLegend = + (legendHoveredIndex === 0 && !geometry.isPositive) || + (legendHoveredIndex === 1 && geometry.isPositive); + return dimFromLegend ? fadedOpacity : 1; + } + if (hoveredTime !== null && geometry.time !== hoveredTime) { + return fadedOpacity; + } + return 1; +} + +const CandlestickBody = memo(function CandlestickBody({ + geometry, +}: { + geometry: CandleGeometry; +}) { + const { + wickLeft, + wickTop, + wickHeight, + wickFill, + bodyLeft, + bodyTop, + bodyHeight, + candleWidth, + bodySolidFill, + bodyPattern, + insideStrokeWidth, + } = geometry; + + return ( + <> + + + {bodyPattern ? ( + + ) : null} + {insideStrokeWidth > 0 ? ( + + ) : null} + + ); +}); + +const CandlestickBodies = memo(function CandlestickBodies({ + geometries, + fadedOpacity, + legendHoveredIndex, + hoveredTime, +}: { + geometries: CandleGeometry[]; + fadedOpacity: number; + legendHoveredIndex: number | null; + hoveredTime: number | null; +}) { + return ( + <> + {geometries.map((geometry) => ( + + + + ))} + + ); +}); + +interface AnimatedCandleProps { + geometry: CandleGeometry; + delay: number; + enterTransition: Transition; + revealEpoch: number; +} + +function AnimatedCandle({ + geometry, + delay, + enterTransition, + revealEpoch, +}: AnimatedCandleProps) { + const t = transitionWithDelay(enterTransition, delay); + const bodyOrigin = `${geometry.centerX}px ${geometry.bodyTop + geometry.bodyHeight / 2}px`; + const wickCenterY = geometry.wickTop + geometry.wickHeight / 2; + + return ( + + + + {geometry.bodyPattern ? ( + + ) : null} + + ); +} + +export function Candlestick({ + animate = true, + positiveFill = DEFAULT_POSITIVE, + negativeFill = DEFAULT_NEGATIVE, + bodyPatternPositive, + bodyPatternNegative, + insideStrokeWidth = 0, + fadedOpacity = 0.3, + showHoverFade = true, +}: CandlestickProps) { + const { + data, + xScale, + yScale, + xAccessor, + animationDuration, + enterTransition, + revealEpoch = 0, + isLoaded, + bandWidth, + columnWidth, + hoveredCandleIndex, + } = useChart(); + const { hoveredIndex: legendHoveredIndex } = useChartLegendHover(); + + const candleWidth = Math.min(bandWidth ?? columnWidth * 0.8, columnWidth); + + const geometries = useMemo( + () => + computeGeometries( + data, + xScale, + yScale, + xAccessor, + candleWidth, + positiveFill, + negativeFill, + bodyPatternPositive, + bodyPatternNegative, + insideStrokeWidth + ), + [ + data, + xScale, + yScale, + xAccessor, + candleWidth, + positiveFill, + negativeFill, + bodyPatternPositive, + bodyPatternNegative, + insideStrokeWidth, + ] + ); + + const hoveredTime = useMemo(() => { + if (hoveredCandleIndex == null) { + return null; + } + const point = data[hoveredCandleIndex]; + return point ? xAccessor(point).getTime() : null; + }, [hoveredCandleIndex, data, xAccessor]); + + const highlightGeometry = useMemo(() => { + if (hoveredCandleIndex == null) { + return null; + } + const point = data[hoveredCandleIndex]; + if (!point) { + return null; + } + return ( + computeGeometries( + [point], + xScale, + yScale, + xAccessor, + candleWidth, + positiveFill, + negativeFill, + bodyPatternPositive, + bodyPatternNegative, + insideStrokeWidth + )[0] ?? null + ); + }, [ + hoveredCandleIndex, + data, + xScale, + yScale, + xAccessor, + candleWidth, + positiveFill, + negativeFill, + bodyPatternPositive, + bodyPatternNegative, + insideStrokeWidth, + ]); + + const defaultEnter: Transition = { + type: "spring", + duration: 0.8, + bounce: 0.15, + }; + const enter = enterTransition ?? defaultEnter; + const staggerDelayMs = + data.length > 0 ? (animationDuration * 0.6) / data.length : 0; + + if (animate && !isLoaded) { + return ( + + {geometries.map((geometry, index) => ( + + ))} + + ); + } + + return ( + + + {highlightGeometry ? ( + + + + ) : null} + + ); +} + +Candlestick.displayName = "Candlestick"; + +export default Candlestick; diff --git a/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/chart-brush-handle.tsx b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/chart-brush-handle.tsx new file mode 100644 index 0000000..12e29e4 --- /dev/null +++ b/seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/chart-brush-handle.tsx @@ -0,0 +1,116 @@ +"use client"; + +import type { BrushHandleRenderProps } from "@visx/brush/lib/BrushHandle"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import type { ChartBrushOverlayHost } from "./chart-brush-track-overlay"; +import { useChartStable } from "./chart-context"; + +/** Matches shadcn `ResizableHandle` withHandle pill (`h-6 w-1 rounded-lg`). */ +const HANDLE_HEIGHT_PX = 24; +const HANDLE_WIDTH_PX = 4; + +/** + * Invisible hit target for visx `BrushHandle` drag interaction. + * Visible pills render in `ChartBrushHandleOverlay` above the blur layer. + */ +function brushHandleCursor(className: string) { + return className.includes("left") || className.includes("right") + ? "ew-resize" + : "ns-resize"; +} + +export function renderChartBrushHandle({ + x, + y, + width, + height, + className, +}: BrushHandleRenderProps) { + return ( + + ); +} + +export interface ChartBrushHandleOverlayProps { + innerWidth: number; + innerHeight: number; + /** Pixel x of the selection start within the plot area. */ + selectionX0: number; + /** Pixel x of the selection end within the plot area. */ + selectionX1: number; +} + +/** Renders brush resize pills above the outside blur portal (`z-[2]`). */ +export function ChartBrushHandleOverlayContent({ + containerRef, + margin, + innerWidth, + innerHeight, + selectionX0, + selectionX1, +}: ChartBrushHandleOverlayProps & ChartBrushOverlayHost) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + const x0 = Math.max(0, Math.min(selectionX0, selectionX1, innerWidth)); + const x1 = Math.max( + x0, + Math.min(Math.max(selectionX0, selectionX1), innerWidth) + ); + const edges = x0 === x1 ? [x0] : [x0, x1]; + + const plotLeft = margin.left; + const plotTop = margin.top; + const handleTop = plotTop + (innerHeight - HANDLE_HEIGHT_PX) / 2; + + return createPortal( +