Skip to content

feat(components): give alert-dialog's confirm button a destructive variant (objectui#8978) - #9199

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-8978-alert-dialog-destructive-confirm
Sep 11, 2026
Merged

feat(components): give alert-dialog's confirm button a destructive variant (objectui#8978)#9199
os-tesla merged 2 commits into
mainfrom
claude/issue-8978-alert-dialog-destructive-confirm

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8978

alert-dialog can paint the red destructive confirm. AlertDialogSchema declares
actionVariant?: 'default' | 'destructive', the renderer reads it onto the confirm
button, and a DOM pin with a firing control holds it there.


The shape, and the argument against the tombstone

R2 required this to be argued rather than assumed, so here it is in full.

The spelling is actionVariant, ⛔ not confirmVariant. The retired key stays
retired: a document that reds on confirmVariant today still reds on it, with the
same invalid_type code. Re-adding a published spelling that refuses an author
today and accepts one tomorrow is precisely
「协议不应该改来改去啊,否则元数据应用怎么办」 (maintainer, 2026-09-10). What did move
is one published string — that tombstone's MESSAGE, from "it has NO surviving
spelling … a separate question that needs its own card" to naming actionVariant.
This card is that card, so the message was stale the moment it landed here.

actionVariant is not a new dialect. This node already calls that button action
in three places — actionText, onAction, AlertDialogAction — and the same
retirement pointed confirmLabel at actionText for exactly that reason. Using
confirm* for the variant while confirm* for the label is tombstoned would have
published two prefixes for one button.

The value domain is two, ⛔ not ButtonSchema.variant's six — by measurement.
See M1/M5 below. Declaring a value this node cannot actually render would be the
confirmVariant disease one level down, at the value instead of the key, and the
pin is built so that widening the union without widening the mechanism reds.


M1 — which variants does buttonVariants admit, and is destructive among them

Printed from packages/components/src/ui/button.tsx, not assumed. Six:
default destructive outline secondary ghost link, plus a size axis,
with defaultVariants: { variant: 'default', size: 'default' }. destructive
emits bg-destructive text-destructive-foreground hover:bg-destructive/90; the
default the confirm button is pinned to emits
bg-primary text-primary-foreground hover:bg-primary/90. ⇒ yes, destructive
is among them, and buttonVariants() === buttonVariants({ variant: undefined })
byte-for-byte (measured), which is what makes the wiring free of blast radius.

⭐ The pin never types those class names in. It computes the expected token set
from buttonVariants at run time, so an upstream rename follows instead of going
stale, and an ORACLE control fails if the two variants ever stop differing.

M2 — blast radius of AlertDialogAction's default

The default is NOT changed, so the radius is zero — and that is a DOM reading,
not an argument.
Every AlertDialogAction call site in the tree, enumerated by
git grep: apps/console/.../ApprovalsInboxPage.tsx (4),
packages/app-shell/.../InvitationsPage.tsx, MembersPage.tsx, SettingsPage.tsx
(2), views/ActionConfirmDialog.tsx, packages/components/.../NotificationAlerts.tsx
(2), packages/plugin-detail/src/RelatedList.tsx,
packages/plugin-form/src/DrawerForm.tsx, ModalForm.tsx,
packages/plugin-gantt/src/ObjectGantt.tsx, and the SDUI renderer. Every one of
them passes no variant. Under this change the renderer passes className=undefined
when the key is unauthored, and cn(buttonVariants(), undefined) is byte-identical
to cn(buttonVariants()) — asserted as the pin's UNTOUCHED control, which
compares two whole rendered dialogs.

⚠️ A finding the card did not anticipate: the card's suggested route — edit
packages/components/src/ui/alert-dialog.tsx to pass a variant — is forbidden by
AGENTS.md Commandment #7
, a No-Touch zone whose files are regenerated by
pnpm shadcn:update, and alert-dialog is one of the tracked components
(pnpm shadcn:list). The sanctioned escape hatch is a declared patch in
scripts/shadcn-local-patches.mjs, whose own header says to keep the payload out
of src/ui/. So the variant is applied as a className OVERRIDE from the renderer —
which is not an invention: NotificationAlerts.tsx already expresses a footer
variant on this very button the same way, with a comment saying why.

M3 — the "authored it three times and had to drop it" catalog sites

Found. Commit ed5fee9be (PR #7962) converted four fixtures under
examples/schema-catalog/src/schemas/components-overlay-alert-dialog/ off a
phantom actions array, and its own message says "the per-button variant is
DROPPED". The dropped confirm-button variants:

fixture confirm label dropped variant
basic-alert-dialog Continue destructive
confirmation-dialog Save default
custom-actions Yes, Continue default
destructive-action Delete destructive

⚠️ The honest count is two, not three, and I am not going to round it up. The
reading that yields exactly three is a different denominator: the dropped variants
the primitives could not reproduce for free — two destructive confirms plus the
ghost cancel in confirmation-dialog (the two outline cancels and the two
default confirms are what the primitives already emit). That is a plausible
reconstruction of the ruling's sentence, ⛔ not a fact I measured it against. The
two destructive confirms are restored here; the ghost cancel is not, and would
need a cancelVariant key that nothing has asked for — noted below, not filed.

M4 — the card's anchors, re-measured by TEXT

The card read 2c208d5bb; this branch is based on fe9e0d054. Located by text,
never by line number, and all three are still exactly as the card describes:
import { buttonVariants } from "./button", the confirm button's
className={cn(buttonVariants(), className)} with no argument, the cancel
button's buttonVariants({ variant: "outline" }), and the renderer's
{schema.actionText && …AlertDialogAction onClick={schema.onAction}…}. The
premise holds; only the line numbers moved.

M5 — does an authorable key need to be added at all

Yes, and the sweep is what says so. The repo has a strong idiom for authoring a
button variant — a key spelled variant, typed at buttonVariants' domain, on the
node that owns the button: ButtonSchema.variant in packages/types/src/form.ts
read at renderers/form/button.tsx, plus the same spelling on the action:*
family, badge, alert, toggle-group and more (18 registrations declare a
variant input; 20-odd renderers read schema.variant).

⛔ But no existing idiom reaches this button. AlertDialogSchema has no variant
key and no footer slot; the footer is generated from two strings, and the
content slot renders above the footer and is not an AlertDialogAction. The
fixtures in M3 prove the point from the author's side: they reached for
type: 'button' nodes with variant — the idiom — and the renderer carried none
of it. So the idiom governs the spelling and value domain, not whether a key is
needed.

And then the value domain, measured through the real renderer rather than inherited
from the idiom:

value override lands declared
default clean
destructive clean
secondary clean ⛔ nothing asked for it; widening later is non-breaking, narrowing is not
outline leaks text-primary-foreground
ghost leaks bg-primary text-primary-foreground
link leaks bg-primary hover:bg-primary/90

An override can only displace a baked-in class that shares its tailwind-merge
group, and those three set no background and/or no text colour, so the primitive's
own survives underneath. The MECHANISM- legs of the pin render each of the three
through the renderer and assert the leftovers are still on the node — so this table
is re-derived on every run, not recorded.


The pin, and the ablation matrix

packages/components/src/__tests__/alert-dialog-action-variant-8978.test.tsx
renders through the real SchemaRenderer and the real registry and reads the
confirm button's class attribute. ⛔ No leg asserts that a prop was passed, that a
component received a value, or that a source file contains a spelling.

Ablation: the wiring line in the renderer replaced by the pre-fix
{schema.actionText && …AlertDialogAction onClick=…}, proven on disk by grep
before and after, restored by git checkout HEAD -- and proven restored by an
empty git diff HEAD.

Ran twice, because the FIRST run caught a bad assertion of my own.

leg wired ablated (run 1) ablated (run 2, after the repair)
WIRED / ORACLE / INSTRUMENT / UNTOUCHED (4 controls) PASS PASS PASS
LEG 1 — destructive confirm carries the destructive tokens PASS FAIL FAIL
LEG 1b — and loses the tokens they displace PASS FAIL FAIL
LEG 2 — default confirm carries neither, and is unchanged PASS PASS PASS
LEG 3 — cancel untouched, confirm moved PASS FAIL FAIL
MECHANISM+ default PASS PASS PASS
MECHANISM+ destructive PASS FAIL FAIL
MECHANISM- outline / ghost / link PASS ⚠️ PASS FAIL ×3
authoring-face closure (4 legs) PASS PASS PASS
totals 17 passed 4 failed / 13 passed 7 failed / 10 passed

The MECHANISM- row is the finding. With the wiring deleted those three
legs stayed GREEN, because each asserted only that the primitive's baked-in
classes were still on the node — and "still there" is trivially true of a
renderer that applies nothing at all. That is an assertion that cannot fail
through the very ablation that proves the feature, i.e. exactly the shape
confirmVariant got away with for months. ⛔ Not noted and left: each leg now
asserts ARRIVAL first (the tokens the variant itself emits ARE on the node) and
only then the loss, and all three red under ablation. Second commit on this
branch.

The legs that still stay green under ablation are the ones that do not name the
wiring: the four controls, the default path (unchanged by construction — which
is leg 2's whole job), and the authoring-face legs, which are schema facts.

On-disk proof, both runs: HEAD blob hash printed, anchor grep -c 1 before /
0 after, post-mutation blob hash differs from HEAD's, restore by
git checkout HEAD -- <absolute path> from an EXIT INT TERM trap, restoration
proven by an empty git diff HEAD, not by an exit code.

Acceptance notes

Found and deliberately NOT fixed, none of them filed:

  • noted, not filed — the ghost cancel variant. confirmation-dialog.json
    authored variant: 'ghost' on its cancel button before PR fix(examples): author the four alert-dialog fixtures in the dialect the renderer reads (objectui#7693) #7962 stripped it.
    AlertDialogCancel hard-codes outline, and there is no cancelVariant key.
    This card is the destructive confirm; a cancel variant is a different
    capability with no ruling behind it, and the same override channel would carry
    neither ghost nor link anyway. Handler if it is ever wanted: whoever widens
    the override mechanism.
  • noted, not filedNotificationAlerts.tsx's ALERT_ACTION_CLASSES. A
    hand-written map of override classes for this exact button, including an explicit
    bg-transparent to neutralise the baked-in default — i.e. the repo already knew
    about the leak measured in M5. It is internal React props, not authorable
    metadata, so it is not in any filing category; it is cited here because it is the
    precedent the renderer's approach follows. Handler: whoever widens the union.
  • noted, not filed — the card's :15/:115/:128 line citations. Drifted, as
    the card itself warned. Nothing to fix; recorded so M4's re-measurement is not
    read as a correction to the card.

Verification

All at 4a617ceb8.

  • pnpm --filter @object-ui/types test177 files, 3460 tests, all passed
  • pnpm --filter @object-ui/components test265 files, 2430 tests, all passed
  • vitest run examples/schema-catalog/31 files, 2151 tests, all passed
    (the two edited fixtures render through the real registry there)
  • pnpm --filter @object-ui/types --filter @object-ui/components run type-check
    Done on both, tsconfig.test.json included, which is what compiles the
    @ts-expect-error and Equal legs
  • pnpm --filter @object-ui/types --filter @object-ui/components run lint
    0 errors on both (279 / 953 pre-existing warnings), which is the whole
    population turbo run lint covers for the two packages this diff touches. The
    narrowing is measurable: type-aware linting is NOT enabled (the flat config
    extends tseslint.configs.recommended, and languageOptions carries only
    ecmaVersion + globals — no project, no projectService), so this diff
    cannot move a verdict on a file it does not contain.
  • Gates run green: changeset:check · check:changeset-claims ·
    check-changeset-presence · check:doc-types · check:doc-fences ·
    check:doc-example-ids · check:new-line-citations · check:handler-key-reads ·
    check:control-bytes · check:side-effects-array · check:unreferenced-sources ·
    check:spec-symbols · check:element-data-source-declaration ·
    check:prompt-keys · check:vi-mock-specifiers
  • ⚠️ NOT MEASURED, left to CI, all three for the same reason — they need build
    artefacts a whole-tree build produces and the container's foreground cap does
    not allow: check:sdui-registration-pins (exit 2, "No console build to weigh
    at apps/console/dist/assets — this is exit 2, not a pass"),
    check:doc-snippets and check:doc-examples (both exit 2 on the same
    prerequisite). ⛔ None of the three is recorded as green. The cheap sibling of
    the first, check:side-effects-array, IS green, and this diff adds no module
    and no registration — only an inputs entry on one that already existed.
  • pnpm shadcn:check not run: it is a weekly scheduled workflow needing the
    network, and this diff deliberately touches no file under
    packages/components/src/ui/.

Two existing pins were re-derived rather than deleted, both on the other side of a
question they had answered honestly at the time: the confirmVariant refusal
message leg (it asserted the message names NO survivor; it now asserts the message
names actionVariant, while the sentence ruling the two LABEL keys out is kept and
still asserted), and the read-dialect pin's comment that said the block names no
substitute.


Generated by Claude Code

…variant (objectui#8978)

`AlertDialogSchema` declares `actionVariant?: 'default' | 'destructive'` and the
renderer reads it onto the confirm button, so the red destructive confirm
decision batch #70 granted on 2026-09-07 is finally authorable.

⛔ NOT under the spelling that ruling named. `confirmVariant` is retired with a
tombstone (objectui#7963) because it was measured inert — it reached no DOM node
at all — and re-adding a published spelling that reds today would be exactly the
churn the maintainer flagged on 2026-09-10. It still reds, with the same code;
only its MESSAGE moved, from "no replacement" to naming `actionVariant`.
`actionVariant` is the `action*` dialect this node already uses for that button
(`actionText`, `onAction`, `AlertDialogAction`), and the dialect the retirement
itself pointed `confirmLabel` at.

Two values and not `ButtonSchema.variant`'s six, by measurement rather than by
deference: `packages/components/src/ui/**` is a No-Touch zone (AGENTS.md #7) and
`AlertDialogAction` bakes in `cn(buttonVariants(), className)` with no variant
prop, so the variant arrives as a className OVERRIDE. An override displaces only
a baked-in class in its own tailwind-merge group, and rendered through the real
renderer `outline` / `ghost` / `link` leave the primitive's background and/or
text colour showing underneath while `default` / `destructive` land clean.
Declaring a value the node cannot render is the retired key's disease one level
down.

The pin reads the confirm button's own `class` off the rendered DOM, never that
a prop was passed, with the expected tokens computed FROM `buttonVariants`
instead of typed in. Two legs so a probe on the wrong node cannot read clean —
the destructive dialog carries the tokens and loses the ones they displace, the
default dialog carries neither — plus an `UNTOUCHED` control proving a document
without the key renders a byte-identical dialog, which is the blast-radius
answer for every `AlertDialogAction` call site in the tree.

The two schema-catalog fixtures whose confirm button authored
`variant: "destructive"` before PR #7962 stripped it get it back.

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-Authored-By: Claude <noreply@anthropic.com>
@os-tesla os-tesla added domain:ui objectui ui stream: fix lands on the published library or apps — objectui execution seat needs:contract-review labels Sep 11, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.1 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-Cr4hVW6b.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.18KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.95KB 65.34KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…eted (objectui#8978)

Found by the ablation, ⛔ not by review. With the renderer's `actionVariant`
wiring removed, the three `MECHANISM-` legs (`outline` / `ghost` / `link`) stayed
GREEN: each asserted only that the primitive's baked-in classes are still on the
node, and "still there" is trivially true of a renderer that applies nothing at
all. An assertion that cannot fail through the very ablation that proves the
feature is the exact shape the retired key got away with for months, so it is
fixed rather than noted: each leg now asserts ARRIVAL first — the tokens the
variant itself emits ARE on the node — and only then the loss.

Ablation matrix after the fix: 7 failed | 10 passed, against 4 failed | 13 passed
before it. Every leg that names the wiring reds; the four controls, the
default-path legs and the authoring-face legs stay green, which is what they are
for.

Also widens the two class-oracle helpers to the full variant union so
`MECHANISM+` can ask about `default`, whose displaced set is correctly empty
(`tsc -p packages/components/tsconfig.test.json` caught the narrow parameter).

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.0 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-Cu_XhSZf.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.18KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.95KB 65.34KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-tesla
os-tesla marked this pull request as ready for review September 11, 2026 17:33
@os-tesla
os-tesla added this pull request to the merge queue Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

PM review — ACCEPT, contract review PASS, armed SQUASH

Read against git merge-base origin/main <head> = fe9e0d054: 10 files, +523/−32. CI 36/36, 0 pending, 0 failing. --pair 9199 → both carriers agree; needs:contract-review cleared from both.

R2 — the tombstone holds, verified at source. confirmVariant still reds with the same invalid_type code; the only published string that moved is the tombstone's message, which used to say the key had "NO surviving spelling … a separate question that needs its own card" — and this card is that card, so the sentence was stale on arrival. ⭐ The refusal pin was re-derived, not deleted: it now asserts the message names the remedy, keeps the cancelText/actionText ruling-out clause it always had, and gains a CONTROL proving actionVariant is a narrow enum rather than .passthrough() admitting anything — which is the difference between a reading and a vacuous .success.

R5 — the brief's own suggested route was wrong, and the seat caught it. My dispatch inherited the card's suggestion to edit packages/components/src/ui/alert-dialog.tsx. That file is a No-Touch zone: AGENTS.md Commandment #7, verified at :107"upstream 3rd-party files overwritten by sync scripts — never edit their logic/styles". Confirmed the diff touches zero files under that path. Routing the variant as a className override from the renderer, with NotificationAlerts.tsx as the in-repo precedent, is the right call and it was reached by measurement rather than by following the brief.

Three things I want on the record, because each is a habit worth keeping:

  1. The ablation caught the seat's own bad assertion. Run 1 read 4 failed / 13 passed and the three MECHANISM- legs stayed green while the wiring was deleted — they asserted only that the primitive's baked-in classes were still present, which is trivially true of a renderer that applies nothing. Fixed rather than noted (arrival asserted first, then the loss), and run 2 reads 7 failed / 10 passed. That is precisely the shape confirmVariant got away with for months, caught by the instrument built to catch it.
  2. M3 refused to round two up to three. The ruling's recorded reason says the catalog "authored it three times"; the measurement finds two dropped destructive confirms. The seat offers a plausible reconstruction of the third and labels it explicitly as a reconstruction, ⛔ not a measurement. Inheriting the ruling's number would have been easier and wrong.
  3. The value domain is two because two is what the channel carries, measured through the real renderer: outline / ghost / link leak the primitive's own background or text colour because an override can only displace a class sharing its tailwind-merge group. Declaring a value the node cannot render would be the same disease one level down — and the MECHANISM- legs re-derive that table on every run, so widening the union without widening the mechanism reds.

Contract review: additive minor on @object-ui/types + @object-ui/components; no default changes, no narrowing; an unauthored document renders byte-identically, asserted by the UNTOUCHED control comparing two whole rendered dialogs rather than argued. The three NOT MEASURED gates are declared as exit-2 prerequisites and ⛔ none is recorded as green — correct.


Generated by Claude Code

Merged via the queue into main with commit 93fc0e7 Sep 11, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8978-alert-dialog-destructive-confirm branch September 11, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:ui objectui ui stream: fix lands on the published library or apps — objectui execution seat examples package: components package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

capability(components): the destructive (red) confirm button on alert-dialog — batch #70 wanted it, confirmVariant was never a vehicle for it

2 participants