Skip to content

Commit 4c2a465

Browse files
os-zhuangclaude
andauthored
docs(components): record why the eight small Shadcn divergences exist, and split --check into documented vs undocumented (#3054)
Triaged the eight components diverging from upstream by 1–2 lines, on the theory that small divergences would be stale cruft worth dropping to widen the safe-to-sync set. That theory was wrong. **All eight are load-bearing; none can be synced away.** Six are the Tailwind v4 arbitrary-value migration (`popover`, `tooltip`, `hover-card`, `menubar`, `dropdown-menu`, `context-menu`). Upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to a bare `transform-origin: --radix-…` instead of `var(--radix-…)` — invalid CSS that browsers drop, leaving the open/close animation scaling from the default origin rather than the one Radix computes. Verified by compiling both forms against the installed tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051d, with zero v3-form occurrences left. `form` adds `Record<\`data-${string}\`, string | undefined>` to FormItem so callers can attach a stable `data-testid` to the field wrapper — the ADR-0054 testability contract again, same family as `command`'s `contentProps`. `sonner` re-exports `toast` so consumers take it from @object-ui/components rather than depending on `sonner` directly; plugin-form and plugin-kanban import it that way. So the deliverable inverts. Nothing gets synced; instead each reason is recorded in a new `localEdits` field on the manifest entry, and the script surfaces it — printed in full when `--update` refuses, and reduced to a `[documented]` / `[UNDOCUMENTED]` marker in `--check`, which now splits the Modified bucket: documented (8): context-menu, dropdown-menu, form, hover-card, menubar, popover, sonner, tooltip UNDOCUMENTED (9): badge, calendar, chart, command, select, sheet, sidebar, slider, table That split is the point. Divergence with a recorded reason is a decision; divergence without one is an open question, and until someone writes it down there is no way to tell a deliberate fix from stale drift — which is exactly the ambiguity that let `resizable` rot for a major version. Manifest only; no component source changes. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 19e9fa0 commit 4c2a465

2 files changed

Lines changed: 40 additions & 11 deletions

File tree

packages/components/shadcn-components.json

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@
127127
"dependencies": [
128128
"radix-ui"
129129
],
130-
"registryDependencies": []
130+
"registryDependencies": [],
131+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-context-menu-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Same for `max-h-(--radix-context-menu-content-available-height)`. Applied repo-wide in 925051db6; do not revert piecemeal."
131132
},
132133
"dialog": {
133134
"source": "https://ui.shadcn.com/r/styles/default/dialog.json",
@@ -148,7 +149,8 @@
148149
"dependencies": [
149150
"radix-ui"
150151
],
151-
"registryDependencies": []
152+
"registryDependencies": [],
153+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-dropdown-menu-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051db6; do not revert piecemeal."
152154
},
153155
"form": {
154156
"source": "https://ui.shadcn.com/r/styles/default/form.json",
@@ -160,14 +162,16 @@
160162
"registryDependencies": [
161163
"button",
162164
"label"
163-
]
165+
],
166+
"localEdits": "FormItem accepts `Record<`data-${string}`, string | undefined>` on top of upstream's `React.HTMLAttributes<HTMLDivElement>`, so callers can attach a stable `data-testid` to the field wrapper (ADR-0054 testability contract). Upstream has no equivalent; syncing removes it and the locators go with it."
164167
},
165168
"hover-card": {
166169
"source": "https://ui.shadcn.com/r/styles/default/hover-card.json",
167170
"dependencies": [
168171
"radix-ui"
169172
],
170-
"registryDependencies": []
173+
"registryDependencies": [],
174+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-hover-card-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051db6; do not revert piecemeal."
171175
},
172176
"input": {
173177
"source": "https://ui.shadcn.com/r/styles/default/input.json",
@@ -193,7 +197,8 @@
193197
"dependencies": [
194198
"radix-ui"
195199
],
196-
"registryDependencies": []
200+
"registryDependencies": [],
201+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-menubar-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051db6; do not revert piecemeal."
197202
},
198203
"navigation-menu": {
199204
"source": "https://ui.shadcn.com/r/styles/default/navigation-menu.json",
@@ -214,7 +219,8 @@
214219
"dependencies": [
215220
"radix-ui"
216221
],
217-
"registryDependencies": []
222+
"registryDependencies": [],
223+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-popover-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051db6; do not revert piecemeal."
218224
},
219225
"progress": {
220226
"source": "https://ui.shadcn.com/r/styles/default/progress.json",
@@ -289,7 +295,8 @@
289295
"sonner",
290296
"next-themes"
291297
],
292-
"registryDependencies": []
298+
"registryDependencies": [],
299+
"localEdits": "Re-exports `toast` from the module (`import { Toaster as Sonner, toast }` / `export { Toaster, toast }`) so consumers get it from @object-ui/components rather than depending on `sonner` directly — plugin-form, plugin-kanban and others import it that way. Upstream exports only `Toaster`; syncing breaks those imports."
293300
},
294301
"switch": {
295302
"source": "https://ui.shadcn.com/r/styles/default/switch.json",
@@ -341,7 +348,8 @@
341348
"dependencies": [
342349
"radix-ui"
343350
],
344-
"registryDependencies": []
351+
"registryDependencies": [],
352+
"localEdits": "Tailwind v4 syntax. Local uses `origin-(--radix-tooltip-content-transform-origin)`; upstream still ships v3's `origin-[--radix-…]`, which on Tailwind 4.x compiles to the bare `transform-origin: --radix-…` rather than `var(--radix-…)` — invalid CSS that browsers drop, so the open/close animation would scale from the default origin instead of the one Radix computes. Verified against tailwindcss 4.3.3: the paren form emits `var()`, the bracket form does not. Applied repo-wide in 925051db6; do not revert piecemeal."
345353
}
346354
},
347355
"customComponents": {

scripts/shadcn-sync.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ async function checkComponent(name, manifest) {
257257
status = 'modified';
258258
message = `${localOnly.length} local line(s) upstream lacks — --update would refuse`;
259259
if (upstreamOnly.length > 0) message += `, ${upstreamOnly.length} upstream line(s) pending`;
260+
// Divergence with a recorded reason is a decision; divergence without
261+
// one is an open question. Surfacing the split makes the untriaged
262+
// components a visible to-do instead of undifferentiated noise.
263+
message += componentInfo.localEdits ? ' [documented]' : ' [UNDOCUMENTED]';
260264
} else if (upstreamOnly.length > 0) {
261265
status = 'outdated';
262266
message = `${upstreamOnly.length} upstream line(s) to pick up — safe to sync`;
@@ -272,6 +276,7 @@ async function checkComponent(name, manifest) {
272276
shadcnLines,
273277
localOnly: localOnly.length,
274278
upstreamOnly: upstreamOnly.length,
279+
documented: Boolean(componentInfo.localEdits),
275280
message,
276281
};
277282
} catch (fetchError) {
@@ -349,10 +354,20 @@ async function checkAllComponents() {
349354
}
350355

351356
if (results.modified.length > 0) {
357+
const undocumented = results.modified.filter((r) => !r.documented);
358+
const documented = results.modified.filter((r) => r.documented);
359+
352360
log('\nThese carry local edits a sync would delete:', 'yellow');
353-
log(` ${results.modified.map((r) => `${r.name}(${r.localOnly})`).join(', ')}`, 'dim');
354-
log(' Port the edits onto the new upstream version by hand, or --force to', 'dim');
355-
log(' take upstream as-is. `--diff <name>` shows both sides.', 'dim');
361+
if (documented.length > 0) {
362+
log(` documented (${documented.length}): ${documented.map((r) => `${r.name}(${r.localOnly})`).join(', ')}`, 'dim');
363+
log(' → reason recorded in `localEdits` in shadcn-components.json', 'dim');
364+
}
365+
if (undocumented.length > 0) {
366+
log(` UNDOCUMENTED (${undocumented.length}): ${undocumented.map((r) => `${r.name}(${r.localOnly})`).join(', ')}`, 'yellow');
367+
log(' → nobody has written down why these diverge. Until someone does,', 'dim');
368+
log(' there is no way to tell a deliberate fix from stale drift.', 'dim');
369+
log(' Triage with `--diff <name>`, then add a `localEdits` note.', 'dim');
370+
}
356371
}
357372

358373
return results;
@@ -429,6 +444,12 @@ async function updateComponent(name, manifest, options = {}) {
429444
log(`✗ Refusing to overwrite ${name}.tsx — ${lost.length} local line(s) upstream does not have:`, 'red');
430445
lost.slice(0, 8).forEach((l) => log(` ${l.length > 96 ? l.slice(0, 96) + '…' : l}`, 'dim'));
431446
if (lost.length > 8) log(` … and ${lost.length - 8} more`, 'dim');
447+
if (componentInfo.localEdits) {
448+
// Someone already worked out why this diverges. Say so here rather
449+
// than making the next person re-derive it from a wall of diff.
450+
log(` Why it diverges (localEdits in shadcn-components.json):`, 'cyan');
451+
log(` ${componentInfo.localEdits}`, 'dim');
452+
}
432453
log(` Overwriting deletes these. Port them onto the new upstream version by hand,`, 'yellow');
433454
log(` or re-run with --force if they are genuinely obsolete.`, 'yellow');
434455
return 'skipped';

0 commit comments

Comments
 (0)