Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/7397-remove-duplicate-chart-primitives.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 8 additions & 9 deletions packages/components/shadcn-components.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
customComponents: Record<string, { movedToPlugin?: string }>;
};

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');
});
});
Loading
Loading