Skip to content

feat(layout,app-shell,i18n): the runtime sidebar menu is not user-reorderable; the Pinned section keeps the user's order (objectui#12059) - #12066

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-12059-sidebar-no-reorder
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-12059-sidebar-no-reorder

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12059
Clause-②: yes

The runtime sidebar no longer offers drag-to-reorder on the app's menu; the Pinned section is the one place a user orders entries. This reverses objectui#11626, which had extended menu drag-to-reorder to grouped sidebars; that card's pins move here (its two test files are deleted and replaced, its pending changeset is corrected, see below).

Maintainer's ruling (card body, verbatim): 「右侧浏览器,左侧菜单在用户界面不应该显示拖动按钮,拖动设计是studio的工作。」 Scope addition 6081436048: 「补到 12059 上,按置顶区来做」, 「暂时不考虑编辑模式」.

What changed

Runtime sidebar (UnifiedSidebar, app-shell). No drag grip and no menu reorder on any viewport: it stops passing enableReorder / onReorder, and useNavOrder is removed, so the objectui-nav-order-APP key is neither read nor written. A key left from before changes nothing (pinned below). Clearing it was allowed, not required; it is not cleared. The menu renders in the app's authored order.

Pinned section (the scope addition).

  • A pinned row is dragged by the row itself. No grip at rest; the row's link gets cursor: grab, and a 2px insertion line marks where the row will land while dragging.
  • Keyboard: with the pinned row's link focused, dnd-kit's KeyboardSensor with sortableKeyboardCoordinates: Space picks up, arrows move, Space drops, Escape cancels. Enter is deliberately NOT a start key (dnd-kit binds it by default): it still follows the link. No extra buttons, no extra focus stop: the link stays a link (dnd-kit's role="button" / tabIndex are not applied) and carries aria-describedby to dnd-kit's instructions; the live region announces pick-up, move and drop.
  • A pointer drag carries the row under the pointer. A keyboard drag leaves it in place, dimmed, so it never covers the row it is over (measured in Chromium, below).
  • The click a pointer drag ends with is swallowed. Measured in Chromium: without the guard, a drag released over the row's own external link navigated the page away (dnd-kit stops that click's propagation, so the router's Link never sees it, but nothing prevents its default).
  • Persistence: the order is the order of the type: 'nav' favorites in the UserDataAdapter-backed favorites list (nav:NAVID), so it syncs exactly as the pins do. No server-side change: createObjectStackUserStateAdapter stores the whole list as one JSON value in sys_user_preference and hands it back as stored.
  • Order rules: a new pin joins the end (a content favorite still goes first, as before); an unpin removes the entry and leaves the others in order; a re-pin joins the end again. A reorder fills the slots the named pins already hold, so a pin another app's sidebar draws, and every content favorite, keep their places. At the 20 nav-pin cap a new pin still joins the end and the earliest-pinned one rolls off (the documented "older entries roll off within their own bucket" keeps holding).
  • No edit mode, no hide/show, no reset.

Published surface (Clause-②: yes)

  • @object-ui/layout — NavigationRendererProps: removed enableReorder, onReorder; added pinnedOrder?: string[], onPinnedReorder?: (pinnedIds: string[]) => void. AppSchemaRendererProps: removed enableReorder, onReorder (a pass-through only; no in-repo caller passes them, only its own test did). Retired under the zero-pull rule; RETIRED comments mark both places.
  • @object-ui/app-shell — useFavorites() return gains reorderNavPins(navIds); useNavPins() return gains reorderPins(orderedIds), and its pinnedIds is in the user's order. ⚠️ These two return members are surface the claim's file surface implied (the order "persists with the pins" in useNavPins.ts / the favorites store) but did not enumerate, and FavoritesProvider.tsx is a file beyond the claim's list; both are named in the report for the seat to amend the claim.
  • @object-ui/i18n — console.nav.dragToReorder (the menu grip's label, zh 「拖动以重新排序」) removed from all ten packs: its only reader was the retired grip. view.dragToReorder is a different key and stays.
  • Changeset .changeset/12059-sidebar-no-reorder.md: minor for layout, app-shell and i18n, with the breaking notes.

Mechanism readings (dispatch zone 2)

  1. Favorites store order — the store is a FavoriteItem[]; pinnedNavIds is built by walking it, so its iteration order is the array order. The write is commit → saveFavorites (localStorage) + the debounced adapter.save(items), which the ObjectStack adapter upserts as the whole list in one value. No new field on the server record.
  2. Where the Pinned section renders — inside NavigationRenderer itself (its favoritesSection, labelled console.nav.favorites), from collectPinnedItems over the tree. No separate component; it now goes through the new PinnedNavigationList when onPinnedReorder is passed.
  3. Callers of enableReorder / onReorder — UnifiedSidebar (app arm) and AppSchemaRenderer (pass-through). Studio's navigation editor does reorder, and not through NavigationRenderer: AppNavCanvas (the Studio app designer's nav canvas) reorders the root list with native HTML5 drag-and-drop (draggable + onDrop); plugin-designer also has NavigationDesigner (a drag-and-drop tree builder) and AppCreationWizard (up/down buttons). No Studio edit.
  4. objectui-nav-order-* — read and written only by UnifiedSidebar's useNavOrder, now removed. The one other tracked spelling is the auth test sessionUserChangePurge-5664.test.tsx, which uses objectui-nav-order-crm as a stand-in for an un-namespaced key in a generic purge; it reads nothing from the sidebar and is untouched.
  5. 「拖动以重新排序」 — console.nav.dragToReorder; pinned rows do not reuse it (no grip, the link keeps its own name), so it became dead and left all ten packs.

Mobile: UnifiedSidebar passes enablePinning={!isMobile} (unchanged, pin/unpin is out of scope), so a phone draws no Pinned section at all and there is nothing to reorder there; no touch sensor was added.

Tests

  • packages/layout/src/__tests__/NavigationRenderer.pinnedOrder-12059.test.tsx (new, 11): no menu row carries a grip, aria-roledescription="sortable", the drag instructions or the grab cursor (grouped and group-free); the four retired props are pinned absent by @ts-expect-error; pinnedOrder orders the section (control: none ⇒ menu order); a pinned row is its own handle (one link + its pin button, no grip; control: no onPinnedReorder ⇒ plain row); keyboard reorder down and up through the real KeyboardSensor with the insertion line on the right edge of the right row and the dragged row held in place; Escape cancels and Enter never picks up (lit control: Space does); a pointer drag released over its own link does not let the browser follow it (control: a plain click does reach the window unprevented).
  • packages/app-shell/src/layout/__tests__/UnifiedSidebar.pinnedOrder-12059.test.tsx (new, 4; real NavigationRenderer, real useNavPins over the real FavoritesProvider, an adapter standing in for the server row): no grip in the real sidebar; a stored objectui-nav-order-crm value (every level reversed) changes nothing; a keyboard reorder is saved through adapter.save with the unnamed pin and the starred record in place, and a fresh mount with nothing local draws it back from the adapter; a new pin goes to the end, an unpin leaves the order, a re-pin goes to the end.
  • packages/app-shell/src/context/__tests__/FavoritesProvider.navPinOrder-12059.test.tsx (new, 4): append vs prepend, the slot-filling reorder and its persistence, an unknown id and a no-op order, the cap.
  • Moved/retired: NavigationRenderer.groupedReorder-11626.test.tsx and UnifiedSidebar.groupedNavOrder-11626.test.tsx deleted (they pinned the reversed behaviour); NavigationRenderer.listNesting-11690.test.tsx keeps its axe pin over the new sortable pinned list instead of the menu reorder; the AppSchemaRenderer.test.tsx reorder case is removed; a stale bullet in AppSchemaRenderer.mobileTabOrder-11395.test.tsx's header is updated.

Reverse verification (predictions written before each run; every restore proven by blob hash = HEAD and an empty git diff HEAD)

run mutation predicted observed
RV1 the four source files back to 2a48bd40 UnifiedSidebar test 4/4 red 4 failed (grip found; stored order applied; keyboard did nothing; pins in tree order)
RV2 NavigationRenderer.tsx back to 2a48bd40 layout test 6 red / 5 green 6 failed / 5 passed, the predicted six
RV2b NavigationRenderer.tsx + AppSchemaRenderer.tsx back to base, layout type-check TS2578 on the four retired-prop lines TS2578 x4 on exactly those lines (plus the expected unknown-prop errors)
RV3 drop the after-drag click guard 1 red (pointer-drag click) 1 failed / 10 passed; first attempt refused by ablation-replace (replacement text was a substring of the anchor, so its count could not rise; nothing ran), redone with a distinct marker
RV3b same, in Chromium the drag-to-self on the external link navigates away navigated to the external URL; restored: stays put, plain click navigates (control)
RV4 dnd-kit default keyboard codes 1 red (Enter) 1 failed / 10 passed
RV5 no insertion line 2 red (keyboard down / up) 2 failed / 9 passed
RV6 nav pin prepended again 3 red across two files 3 failed / 5 passed
RV8 cap evicts the first in order 1 red (cap) 1 failed / 3 passed
RV9 keyboard drag not held in place 1 red (keyboard down) 1 failed / 10 passed

All test imports resolve @object-ui/layout through the vitest alias to src, so no dist rebuild sits between a mutation and its run.

Browser (Chromium via Playwright, a temporary dev-only console page, never committed; server stopped by PID)

At rest: 0 grips, 0 aria-roledescription, 0 insertion lines; pinned link cursor: grab, menu link cursor: pointer with no aria-describedby. Keyboard: Space / ArrowDown drew one 2px line after the next row, the live region read the move, Space dropped, focus stayed on the moved link. Pointer: dragging the last pinned row to the top reordered it, no navigation, no reload. A drag released on its own internal link: no navigation; on its own external link: the page stayed (control: a plain click on that link navigated). No page errors.

First-load bytes

pnpm check:eager-closure on a console built at HEAD: exit 0, "Console eager closure is 3167.8 KB gzipped" (3,243,862 B; ceiling 3,281,467 B, ruling 6070754914). The same build over the base sources: 3,244,066 B. Delta −204 B gzipped (−1,604 B raw).

Gates (final reading at 79d36886a)

  • pnpm --filter @object-ui/layout type-check, pnpm --filter @object-ui/app-shell type-check, pnpm --filter @object-ui/i18n type-check: exit 0 (each script name echoed).
  • pnpm exec vitest run packages/layout/ + the app-shell sidebar / pins / favorites suites (src/layout, src/context, src/hooks and every other test naming UnifiedSidebar / useNavPins / FavoritesProvider / useFavorites, console included): 182 files, 1512 tests passed. pnpm exec vitest run packages/i18n/: 82 files, 1316 passed / 13 skipped. The full app-shell suite is CI's.
  • pnpm exec eslint on the 21 touched TS files: 0 errors, 41 warnings, none on an added line.
  • Exit 0: check:control-bytes, check:test-path-roots, check:changeset-claims (one pending changeset names packages/app-shell/README.md; read, still true), check:pending-changeset-literals, check:new-line-citations (0 new), check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity, check:readme-exports, check:doc-fences, check:doc-types, check:doc-snippets, check:phantom-deps, check:unused-deps, check:eager-locale-catalogues, check:icon-record-names, check:unreferenced-sources, check:self-import, check-changeset-presence, check-changeset-no-major, check-changeset-overwrite (report-only; reports the correction below), check-doc-links, check:eager-closure.
  • check-governed-queue-guard --test over the diff: NOT GOVERNED.

Docs

content/docs/guide/user-state-persistence.md (reference rows, the pinned-order rules, an "Ordering the sidebar" paragraph) and packages/app-shell/README.md (user-scoped state). No guide page described dragging menu entries.

Acceptance notes

  • The objectui#11626 changeset is still pending in the same release window. Its body would have published that grouped-menu drag-to-reorder works and that the reorder props keep their types. Its prose is corrected (front matter untouched) to say it was retired before release; check-changeset-overwrite reports that modification, as designed.
  • Existing pins change order once. Pins made before this change were prepended, so their stored order is newest first; they now draw in that order instead of menu order, until the user drags them. Stated in the changeset.
  • Announcements are dnd-kit's defaults (English, entry ids), as the retired menu sort's were. Localizing them needs new pack keys, which the claim does not allow.
  • No aria-roledescription on pinned links, on purpose: it would replace "link" in what a screen reader announces. The rows are described by dnd-kit's instructions instead.
  • Pre-existing doc drift, not touched: the user-state guide's "Cross-device sync" line says a pin shows up on mobile, but the sidebar draws no Pinned section on mobile; its useFavorites() reference row also omits refreshLabel.
  • Downstream (triage 6082337150): platform-checklist: retire the drag-reorder step and acceptance clause of platform-core.shell-nav-personalization — end users do not reorder the runtime sidebar (maintainer ruling 2026-10-09) objectstack#22487 must land no later than the objectstack console pin bump that carries this merge; the seat names the merge commit there after landing.

Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8


Generated by Claude Code

claude added 7 commits October 9, 2026 14:55
… pinned section keeps the user's order (objectui#12059)

The runtime sidebar drew a drag grip on every menu row and kept a private
per-user menu order in localStorage. Arranging an app's navigation is
Studio's job, so the menu reorder is retired and personal ordering moves
to the pinned section, persisted with the pins in the favorites store.

- layout: NavigationRenderer retires enableReorder / onReorder and every
  menu sortable path; adds pinnedOrder and onPinnedReorder. A pinned row
  is dragged by the row itself (no grip, cursor-grab, insertion line),
  and from the keyboard through dnd-kit's KeyboardSensor (Space / arrows
  / Space, Escape cancels). AppSchemaRenderer stops forwarding the
  retired props.
- app-shell: UnifiedSidebar drops useNavOrder (objectui-nav-order-APP is
  no longer read or written) and passes the pinned order. The favorites
  store appends a new nav pin and gains reorderNavPins; useNavPins gains
  reorderPins.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…sidebar's wiring (objectui#12059)

The sidebar draws no menu grip and ignores a stored objectui-nav-order
key; a keyboard reorder of pinned rows is saved through the favorites
adapter and the next load draws it; a new pin joins the end, an unpin
leaves the order. The store pins the slot-filling reorder and the cap.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#12059)

The sidebar menu grip it labelled is gone, and a pinned row is dragged by
its own link with no grip, so nothing asks for the key. Removed from all
ten packs; view.dragToReorder is a different key and stays.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…tui#12059)

The user-state guide and the app-shell README describe the pinned order
stored with the pins, reorderNavPins / reorderPins, and that the app's
menu is not reorderable in the console. The changeset declares minor for
layout, app-shell and i18n with the breaking notes.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…-built MouseEvent (objectui#12059)

object-ui/no-synthetic-event-trigger refuses dispatching a constructed
MouseEvent; the instrument now reads fireEvent.click's answer and whether
the click reached the window.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…r is retired before release (objectui#12059)

Its body would have published that drag-to-reorder works on a grouped
sidebar menu and that the reorder props keep their types; objectui#12059
retires both in the same release window. Prose only; the front matter is
untouched.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#12059)

Measured in Chromium: a keyboard drag moved the dragged row onto the row
it was over, so the two labels overlapped. A pointer drag still carries
the row under the pointer; a keyboard drag dims it in place and the
insertion line alone shows where it lands.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/11806-login-server-unreachable.md

  • names packages/app-shell/README.md → packages/app-shell/README.md — edited by this change

    packages/app-shell/README.md documents the two new states in its sign-up table. Its useSignUpOffer recipe for hosts now asks decideSignUpOffer only once the read has answered, and answers unreachable for a failed read.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 2a48bd408 (merge-base with origin/main): 25 file(s) changed outside .changeset/, read against 277 pending declaration(s) that publish a body (282 pending in total). · run

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3167.8 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-CacyI0t8.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) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.83KB 141.44KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.42KB 68.20KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 248.82KB 65.56KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.62KB 69.16KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.10KB 30.28KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.79KB 23.21KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 79d36886a50b36afecbf659b4586bbffddd585a0
Local-runs: none

Inputs, and nothing else: card #12059 (body and all six comments: the amendment link 6081363126, the scope addition 6081436048, triage 6082337150, the claim 6082900231, the dev report 6084290443, the claim amendment 6084322873); PR #12066 (body, its 27-file list, and the net main...79d36886a diff, 3006 lines, read whole); the 43 check-runs on the head: 40 success, 3 skipped (the coverage shard, coverage, dependabot), 0 failed, 0 in progress. Record written 2026-10-09T16:11Z.

① Derived judgments

Each change the diff implies, judged against the ruling (card body), the scope addition, the claim and its amendment.

Public surface

  1. @object-ui/layout NavigationRendererProps: enableReorder and onReorder removed; pinnedOrder (string ids) and onPinnedReorder (called with the drawn ids in their new order) added; a RETIRED comment holds the place. Right: the claim's zero-pull retire parameter applies, since after the diff no in-repo caller passes them (UnifiedSidebar and AppSchemaRenderer both stop); the amendment names the added pair; the new layout test pins all four retired props absent with @ts-expect-error (Type Check success).
  2. @object-ui/layout AppSchemaRendererProps and its InternalSidebar: the pass-through enableReorder / onReorder removed, RETIRED comment. Right: amendment 6084322873; declared-but-unhonoured otherwise.
  3. @object-ui/app-shell: useFavorites() gains reorderNavPins(navIds) (the no-provider fallback gains a no-op too); useNavPins() gains reorderPins(orderedIds), and its pinnedIds is now in stored order because pinnedNavIds is built by walking favorites in array order. Right: amendment answer A; the claim's persistence clause needs a write member on the store useNavPins reads.
  4. @object-ui/i18n: console.nav.dragToReorder leaves all ten packs (one line each). Right: its one reader, the grip's aria-label in NavDragGrip, is deleted in the same diff; view.dragToReorder is untouched; the claim lets a dead key leave with the i18n gates (Lint and the Test shards are success).
  5. Nothing else is published: the diff touches .changeset/, content/docs/guide/, packages/app-shell, packages/i18n, packages/layout only; nothing under packages/components/src/ui/, nothing in Studio, nothing in objectstack. Right: the claim's not-on-it list holds.

Runtime sidebar (the ruling)
6. UnifiedSidebar stops passing enableReorder / onReorder; useNavOrder and its helpers (applyLevelOrder, applyGroupOrders, groupChildren, sameLevel, handleReorder, handleNavReorder) are deleted, so objectui-nav-order-APP is neither read nor written; a stale key is ignored, not cleared. Right: card scope 1 and 2 (clearing is "may", not "must"); pinned by UnifiedSidebar.pinnedOrder-12059, where a fully reversed stored order changes nothing.
7. NavigationRenderer: NavDragGrip, SortableNavigationItem, SortableNavigationList, GroupReorderContext, moveWithinLevel, withGroupChildren, drawsNavItem and the GripVertical import are gone; the grouped and the group-free arm both draw plain SidebarMenu rows. No menu row carries a grip, aria-roledescription, aria-describedby or cursor-grab. Right: card scope 1 on every viewport and card test 5, pinned in NavigationRenderer.pinnedOrder-12059 for grouped and group-free menus.
8. objectui#11626's two pin files are deleted and the 11626 changeset's prose rewritten to say the grouped reorder was retired before release, front matter untouched. Right: the amendment accepts the correction; Changeset Overwrite Report (report-only) and Changeset Claim Re-read are success.

Pinned section (the scope addition)
9. Sortable only when onPinnedReorder is passed: PinnedNavigationList (DndContext id="nav-pinned", PointerSensor at 5px, KeyboardSensor with sortableKeyboardCoordinates and the codes start/end Space, cancel Escape). The row's own link, or an action entry's button, is the activator: setActivatorNodeRef, both sensors' listeners, aria-describedby to dnd-kit's instructions, cursor-grab. dnd-kit's role="button", tabIndex and aria-roledescription are deliberately not applied, so the link stays a link and the row keeps one focus stop. Right: "dragging the entry itself", "nothing marks it at rest", cursor: grab, keyboard "without a visible handle" with the sortable announcements, "no extra buttons". Leaving aria-roledescription off the pinned links is a reading of the card, not a departure: the card forbids the sortable grip and asks for announcements, and the live region still gives them (the test asserts it names the moved row).
10. Enter is not a start key (dnd-kit binds it by default), so Enter still follows the link. Right: pinned by test; the row would otherwise lose its one activation key.
11. holdRowsStill strategy plus a DropIndicator line (data-drop-indicator before/after on the target row's edge); a keyboard drag is held in place and dimmed, a pointer drag follows the pointer. Right: "an insertion line while dragging".
12. swallowClickAfterPointerDrag: a 50ms window-capture preventDefault on click after a pointer drag ends or cancels, never after a keyboard drag. Right and necessary: dnd-kit's own 50ms guard stops propagation at document capture, so React handlers never run, but it does not prevent the default, and the release over the row's own link would be a full navigation. The test's control shows a plain click still reaches the window unprevented. The window is as long as dnd-kit's own.
13. orderPinnedItems: the named ids first in the given order, unnamed pinned entries after in menu order, duplicates collapsed; no pinnedOrder means menu order. Right: one app's sidebar may draw only some of the user's pins.
14. A drop reports arrayMove(ids, from, to) over the ids the section draws. With a search narrowing the section that is a subset, and withNavPinOrder fills only the named pins' own slots, so a partial order stays coherent. Right.

Persistence (the claim's stop condition)
15. The order is the order of the type: 'nav' entries in the one stored FavoriteItem list: no new field, no server change, so the claim's stop did not trigger. Right: the sidebar test saves through a stand-in adapter.save(items) and a fresh mount with nothing local reads the order back from it.
16. withAddedFavorite: a nav pin is appended (a content favorite still leads); at the cap the earliest-pinned by favoritedAt among the existing pins rolls off before capByBucket runs, so the new pin is never the one evicted. withNavPinOrder: slot-filling, unknown ids ignored, and a no-op returns the same array so nothing is committed; reorderNavPins commits only on change. Right: "a newly pinned entry goes to the end, and unpinning removes it from the order"; pinned in FavoritesProvider.navPinOrder-12059 and through the real pin button in the sidebar test (pin, unpin, re-pin).
17. One-time visible effect: pins made before this change were prepended, so a user with existing pins sees them newest-first until they drag. Right, and the changeset says so.

Mobile and touch
18. enablePinning={!isMobile} is unchanged, so a phone draws no Pinned section and there is nothing to order; no touch sensor was added. Right under card item 4 and the claim's touch parameter (read-only on mobile is allowed). Observation, not a defect: on a touch device at desktop width the row-as-activator PointerSensor applies with no touch-action set; a browser pan fires pointercancel, which cancels the drag, so touch there is effectively read-only as well.

Tests and docs
19. Three new suites (11 + 4 + 4) cover card test 5 and the scope addition's three test bullets with the real dnd-kit sensors; listNesting-11690 keeps its axe pin over the one sortable list left; the AppSchemaRenderer reorder case and a stale mobileTabOrder-11395 header line are updated. Test, its 8 shards, dist pins, Lint, Type Check, Build and E2E: success.
20. user-state-persistence.md and app-shell/README.md describe the order rules and the two new members (Doc Snippet, Doc Component, Internal Docs Link and README Export checks: success); the Studio question (card scope 3) is answered in the PR body: AppNavCanvas reorders with native HTML5 drag-and-drop, no Studio edit, and the diff touches no Studio file. Right.

Nothing in the diff is judged wrong.

② Semver level

.changeset/12059-sidebar-no-reorder.md: @object-ui/layout: minor, @object-ui/app-shell: minor, @object-ui/i18n: minor, with a breaking note on layout (two props retired on each of two prop types) and on i18n (a pack key removed), and the app-shell note naming the two added return members and the one-time order effect. That is exactly what the diff publishes (judgments 1 to 4), and those three are the only published packages touched (judgment 5). The claim's grade, minor with a breaking note where a prop leaves and never major, is met; Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are success. The 11626 changeset keeps its front matter, so no release declaration is lost.

The PR body's second line reads Clause-②: yes, as the claim and the amendment require, and the changeset repeats it with the surface enumerated: two props off and two on NavigationRendererProps, two off AppSchemaRendererProps, reorderNavPins, reorderPins, the pack key. That enumeration is the full set the diff carries; nothing published is left unnamed.

③ Boundary flags

Dev flags (report 6084290443), each answered or escalated:

  • open_questions[0], name FavoritesProvider.tsx, AppSchemaRenderer.tsx and the three surface members in the claim: answered, amendment 6084322873 took A, and the diff matches it member for member.
  • Deviation, the PR opened before the amendment: answered, the seat accepted and recorded it in the amendment.
  • Deviation, the 11626 changeset's prose corrected: answered, accepted in the amendment; front matter unchanged (judgment 8).
  • Deviation, the commit-trailer form follows objectui's AGENTS.md rather than the harness reminder: no contract matter, the repo's own rule governs, and commits are outside this record's inputs.
  • Deviation, two temporary probe files under apps/console, never committed: answered by the diff, the 27-file list holds nothing under apps/.
  • out_of_scope_findings[0], carrier none: the user-state guide's "Cross-device sync" line says a pin shows on mobile, but no Pinned section is drawn there, and its useFavorites() reference row still omits refreshLabel. Pre-existing drift, rightly left alone here. Escalated to the seat: file a follow-up card or let it lie.
  • out_of_scope_findings[1], carrier none: dnd-kit's default announcements read entry ids in English. Status quo of the retired menu sort, and localizing them is a new-key stop under the claim, so rightly not done here. Escalated to the seat: a follow-up card if announcements should name labels in the user's language.
  • Studio editor (card scope 3): answered in the PR body; no Studio edit in the diff.
  • Mobile (card item 4) and touch: answered (judgment 18); the observation there needs no action unless the seat wants press-and-hold on desktop-width touch devices.
  • Persistence stop condition: did not trigger (judgment 15).
  • i18n: no new key, one dead key removed with the gates: within the claim.
  • Downstream (triage 6082337150, the claim): platform-checklist: retire the drag-reorder step and acceptance clause of platform-core.shell-nav-personalization — end users do not reorder the runtime sidebar (maintainer ruling 2026-10-09) objectstack#22487 must land no later than the console pin bump that carries this merge, and this PR's merge commit is to be named there after landing. For the seat, after merge.
  • The PR is still a draft; readying it is the seat's.

Implemented-by: claude/issue-12059-sidebar-no-reorder
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants