diff --git a/.changeset/7397-remove-duplicate-chart-primitives.md b/.changeset/7397-remove-duplicate-chart-primitives.md new file mode 100644 index 0000000000..27dee2d1c6 --- /dev/null +++ b/.changeset/7397-remove-duplicate-chart-primitives.md @@ -0,0 +1,50 @@ +--- +'@object-ui/components': minor +--- + +BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`, +`ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, +`ChartStyle` and the `ChartConfig` type — are removed. `@object-ui/plugin-charts` +is the single implementation (objectui#7397, maintainer ruling 2026-09-04). + +**Migration: import the chart primitives from `@object-ui/plugin-charts`.** + +(The bump is `minor` by this repo's release model — objectui's major is pinned to +the `@objectstack` family major, and its own breaking changes ship as `minor` with +the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This +paragraph is that spelling-out: the break below is real and consumer-visible.) + +- **What breaks, by specifier**: `import { ChartContainer, ChartTooltip, + ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } from + '@object-ui/components'` and `import type { ChartConfig } from + '@object-ui/components'` no longer resolve — TS2305 at build time, `undefined` at + runtime. They were reachable through two `export *` hops (`src/index.ts` → + `./ui` → `./chart`), so this is a real removal from the published surface, not a + tidy-up of dead code. +- **Not affected**: `ChartSkeleton` — the chart-area loading placeholder in + `src/custom/view-skeleton.tsx` — is a different symbol and stays exported. + `@objectstack/spec/ui` still owns the authored-chart `ChartConfig`; only the + per-series style map published from this package is gone. `@object-ui/plugin-charts` + calls its own map `ChartContainerConfig`, so the two names no longer collide. +- **Why the copy had to go rather than be fixed in place**: it duplicated + `packages/plugin-charts/src/ChartContainerImpl.tsx` and carried the + label-resolution hole objectui#7248 had already fixed there. `ChartLegendContent` + resolves a label as `config[nameKey || item.dataKey || 'value']` while rendering + the colour swatch unconditionally, so a legend entry whose config lookup misses + paints an anonymous coloured dot — on a scatter that reads as a data point drawn + outside the plot area, which is exactly how objectui#7248 was reported. Two copies + of one primitive is how a fixed bug returns; consumers importing from + `@object-ui/components` were getting the unfixed one. +- **Why not re-export the plugin's copy from here instead**: `@object-ui/plugin-charts` + depends on `@object-ui/components` (`workspace:*`), so the dependency direction + forbids it. +- **Consumer census**: zero in-repo importers, measured with a lit control — no file + under `apps/**`, `examples/**` or `packages/**` imported any of these names from + `@object-ui/components`. `plugin-charts` reaches its own copy by relative path. + The `hotcrm` and `cloud` repositories could not be read from the seat that made + this change (HTTP 403), so no claim is made about them. + +`packages/components/shadcn-components.json` records `chart` under +`customComponents` with `movedToPlugin: "@object-ui/plugin-charts"`, which is what +keeps `pnpm shadcn:update-all` from re-fetching the primitive and silently undoing +this. diff --git a/packages/components/shadcn-components.json b/packages/components/shadcn-components.json index 5003a6ac5e..178702ec4c 100644 --- a/packages/components/shadcn-components.json +++ b/packages/components/shadcn-components.json @@ -93,15 +93,6 @@ "button" ] }, - "chart": { - "source": "https://ui.shadcn.com/r/styles/default/chart.json", - "dependencies": [ - "recharts", - "lucide-react" - ], - "registryDependencies": [], - "localEdits": "Three local edits. (1) The container is `block … w-full` rather than upstream’s `flex justify-center`: a flex box gives ResponsiveContainer no measurable width, so charts render axes but no marks (#1634, commit 9b163173b). (2) Tooltip/Legend props are hand-written instead of derived from Recharts’ own types, which did not type-check here (2e11aa72f). (3) Tailwind v4 syntax: upstream still ships v3’s `[--var]` arbitrary values, which on Tailwind 4.x compile to a bare custom-property name instead of `var(--var)` — invalid CSS that browsers drop. Applied repo-wide in 925051db6." - }, "checkbox": { "source": "https://ui.shadcn.com/r/styles/default/checkbox.json", "dependencies": [ @@ -376,6 +367,14 @@ ], "movedToPlugin": "@object-ui/plugin-calendar-view" }, + "chart": { + "description": "Excluded from the sync - the chart primitives (ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle) were removed from @object-ui/components and live in @object-ui/plugin-charts (ChartContainerImpl.tsx), which is the single implementation. It began as a Shadcn component and is listed here, never under `components`, so `pnpm shadcn:update-all` cannot resurrect src/ui/chart.tsx - a second copy is how the objectui#7248 legend bug would come back (objectui#7397).", + "dependencies": [ + "recharts", + "lucide-react" + ], + "movedToPlugin": "@object-ui/plugin-charts" + }, "chatbot": { "description": "Custom ObjectUI component - Chatbot interface (moved to @object-ui/plugin-chatbot)", "dependencies": [ diff --git a/packages/components/src/__tests__/chart-primitives-removed-7397.test.ts b/packages/components/src/__tests__/chart-primitives-removed-7397.test.ts new file mode 100644 index 0000000000..d439e0cff8 --- /dev/null +++ b/packages/components/src/__tests__/chart-primitives-removed-7397.test.ts @@ -0,0 +1,167 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The chart primitives are gone from `@object-ui/components`, and may not come + * back (objectui#7397, maintainer ruling of 2026-09-04, batch #28, option (a)). + * + * ## What was actually wrong + * + * `packages/components/src/ui/chart.tsx` was a second copy of the primitives in + * `packages/plugin-charts/src/ChartContainerImpl.tsx`, and it carried the + * label-resolution hole that objectui#7248 fixed in the plugin-charts one: the + * legend swatch renders unconditionally while the label comes only from a config + * hit, so an entry whose lookup misses paints an anonymous coloured dot. On a + * scatter that reads as a data point drawn outside the plot area, which is what + * objectui#7248 was reported as. + * + * The card that found it recorded the copy as "not re-exported from the + * `packages/components` barrel" and therefore "currently unreachable". Both were + * false: `src/index.ts` re-exported `./ui`, `ui/index.ts` re-exported `./chart`, + * and `chart.tsx` ended in a trailing `export { ... }` block. Two `export *` hops, + * so every external consumer importing `ChartLegendContent` from + * `@object-ui/components` got the UNFIXED copy. The card's probe had been + * `^export (const|function|type|interface) ChartLegend...`, which returns 0 + * against a trailing export block -- a zero produced by the wrong query shape, + * with no control to reveal it. Hence the lit control in the first test below: + * this file's own zeros have to be readings. + * + * ## Why a pin and not just the deletion + * + * "Two copies of one primitive is how a fixed bug returns" is the ruling's + * reason, so the guard has to outlive the diff that satisfies it. There are two + * ways the file comes back, and they need different guards: + * + * - **by sync.** `scripts/shadcn-sync.js` iterates `manifest.components`, so + * `pnpm shadcn:update-all` would re-fetch `chart` and rewrite the file. The + * manifest record is the guard, and the third test holds it: `chart` sits + * under `customComponents` (never fetched) carrying `movedToPlugin`, the + * vocabulary this manifest already uses for `calendar-view`, `chatbot` and + * `timeline`. + * - **by hand.** Nothing stops someone re-adding `ui/chart.tsx` and a barrel + * line. The first two tests are that guard. + * + * Deleting this file is deleting the ruling. If the primitives are ever meant to + * return to `@object-ui/components`, that is a new decision on objectui#7397 and + * this file is where it gets recorded. + */ + +import { describe, it, expect } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +import * as components from '../index'; + +/** `packages/components` -- two levels up from `src/__tests__`. */ +const PKG_DIR = resolve(__dirname, '../..'); + +/** + * The six value exports plus the `ChartConfig` type alias that `ui/chart.tsx` + * published. `ChartConfig` is named here because the deletion removed it from + * the surface too; `@objectstack/spec/ui` owns the authored-chart `ChartConfig`, + * and `@object-ui/plugin-charts` calls the per-series style map + * `ChartContainerConfig` precisely so the two never collide again. + */ +const REMOVED = [ + 'ChartContainer', + 'ChartTooltip', + 'ChartTooltipContent', + 'ChartLegend', + 'ChartLegendContent', + 'ChartStyle', + 'ChartConfig', +] as const; + +describe('@object-ui/components no longer publishes the chart primitives (objectui#7397)', () => { + it('exports none of them from the package barrel', () => { + const surface = Object.keys(components); + + // The lit control. A zero below is only a reading if this fires: the whole + // defect on objectui#7397 was a zero produced against a surface nobody had + // proved was being read at all. + expect( + surface.length, + 'nothing parsed out of the @object-ui/components barrel -- the assertion below would prove nothing', + ).toBeGreaterThan(100); + expect( + surface, + 'the control name is missing, so this file is not reading the real package surface', + ).toContain('Button'); + expect( + surface, + 'ChartSkeleton is the loading placeholder in src/custom/view-skeleton.tsx -- a DIFFERENT symbol that legitimately stays; if it vanished, this file is measuring the wrong module', + ).toContain('ChartSkeleton'); + + expect( + REMOVED.filter((name) => surface.includes(name)), + [ + '@object-ui/components exports a chart primitive again. The maintainer ruling on', + 'objectui#7397 (2026-09-04, batch #28, option (a)) removed these: a second copy of', + 'ChartLegendContent is how the objectui#7248 legend bug returns, and the copy that', + 'lived here was the unfixed one.', + '', + '@object-ui/plugin-charts (ChartContainerImpl.tsx) is the single implementation.', + 'components cannot re-export it either -- plugin-charts depends on', + '@object-ui/components (workspace:*), so the dependency direction forbids it.', + ].join('\n'), + ).toEqual([]); + }); + + it('and the synced primitive file itself is gone, with no barrel line pointing at it', () => { + expect( + existsSync(join(PKG_DIR, 'src/ui/chart.tsx')), + 'packages/components/src/ui/chart.tsx is back. It is the duplicate objectui#7397 removed.', + ).toBe(false); + + const uiBarrel = readFileSync(join(PKG_DIR, 'src/ui/index.ts'), 'utf8'); + + // Floor: prove the barrel really parsed before reading a zero out of it. + expect( + uiBarrel.split('\n').filter((line) => line.startsWith('export * from')).length, + 'no re-export lines parsed out of src/ui/index.ts -- the assertion below would prove nothing', + ).toBeGreaterThan(10); + + expect( + uiBarrel.includes("from './chart'"), + "src/ui/index.ts re-exports './chart' again -- see the header on objectui#7397.", + ).toBe(false); + }); + + it('and the shadcn manifest records the exclusion, so a re-sync cannot resurrect it', () => { + const manifest = JSON.parse( + readFileSync(join(PKG_DIR, 'shadcn-components.json'), 'utf8'), + ) as { + components: Record; + customComponents: Record; + }; + + expect( + Object.keys(manifest.components).length, + 'no `components` entries parsed out of shadcn-components.json -- the assertions below would prove nothing', + ).toBeGreaterThan(1); + + expect( + Object.keys(manifest.components), + [ + '`chart` is back under `components` in shadcn-components.json. That object is what', + 'scripts/shadcn-sync.js iterates, so `pnpm shadcn:update-all` would re-fetch the', + 'primitive and rewrite src/ui/chart.tsx -- undoing objectui#7397 silently, with no', + 'diff anybody reviewed.', + ].join('\n'), + ).not.toContain('chart'); + + expect( + manifest.customComponents.chart?.movedToPlugin, + [ + 'shadcn-components.json no longer records `chart` as moved to a plugin package.', + 'That record is the ONLY thing standing between `pnpm shadcn:update-all` and a', + 'resurrected duplicate: entries under `customComponents` are never fetched.', + ].join('\n'), + ).toBe('@object-ui/plugin-charts'); + }); +}); diff --git a/packages/components/src/ui/chart.tsx b/packages/components/src/ui/chart.tsx deleted file mode 100644 index c05d3b2d51..0000000000 --- a/packages/components/src/ui/chart.tsx +++ /dev/null @@ -1,384 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -"use client" - -import * as React from "react" -import * as RechartsPrimitive from "recharts" - -import { cn } from "../lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -} - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a ") - } - - return context -} - -const ChartContainer = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] - } ->(({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) -}) -ChartContainer.displayName = "Chart" - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( -