Skip to content

fix(layout,app-shell): drag-to-reorder works within each level of a grouped sidebar (objectui#11626) - #11656

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-11626-grouped-nav-reorder
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-11626-grouped-nav-reorder

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11626

Clause-②: no

What changes

Triage's direction on the card (5986793209): the grouped arm of NavigationRenderer gets the group-free arm's sortable path, scoped to each group, and the sidebar's order store keeps an order per group key. A move into another group stays out of scope, because which group an entry sits in is app structure, not a personal order.

  • @object-ui/layout NavigationRenderer. With enableReorder on, each group's children are a sortable list with a DndContext of their own, and so is each run of top-level entries between two groups. No list is a drop target for another. A move is reported through the existing onReorder(reorderedItems): the top-level list, with the moved group's children reordered and carrying their positions as order. A grouped menu offers no grip while searchQuery narrows it.
  • @object-ui/app-shell useNavOrder. A group's order is stored under the group's id, beside __root__ in objectui-nav-order-APP. The id is a spec SnakeCaseIdentifierSchema (^[a-z][a-z0-9_]*$), so it holds across reloads and locales and can never be __root__; the label is translated, so it is not the key. The store finds the moved level by diffing the report against the tree it drew, so only that level is written, and every stored level is applied on load. A group-free app's __root__ record is byte-identical.

The resumed review found two holes the grouped arm would have inherited from the group-free arm. Both were measured before they were fixed:

  • Authored order. The renderer sorts every level by order, and the store applied a saved order by array position only. So on an app that authors order, a drag was stored and then sorted straight back. Measured in both arms on the resumed head: the store was written, and the menu was still drawn in the app's order. Now a level with a saved order carries its saved positions as order, and a level with no saved order is passed on untouched. The moved-group detection compares order as well as ids, because under authored order a move can land on the listed id sequence.
  • Dead tab stops. dnd-kit's sortable attributes (role="button", tabIndex=0) sat on the row wrapper while its listeners sat on the grip. So every row was a focusable "button" from which no key could start a drag. Measured on the resumed head: 7 of 7 row wrappers were focusable, and 0 of 7 grips. Spreading that wrapper to every grouped menu would have added one dead tab stop per entry on every stock app. The grip is now the drag activator (setActivatorNodeRef), with attributes and listeners together on it. Live, a keyboard reorder now works.

No export, prop, type member, callback parameter or i18n key is added. NavigationRendererProps.enableReorder and onReorder keep their types; their doc comments now state the within-level scope and what onReorder receives. No README or guide page describes nav reorder (searched packages/layout, packages/app-shell, content/docs and docs), so no doc drifts.

Live: a console built from this branch, against an objectstack showcase backend

The backend is the stock showcase (objectstack dev --seed-admin --fresh), run from an objectstack worktree detached at ba575886, which is 14 commits behind objectstack main at the time of the run. Two consoles were served from source with vite against that one backend: main 0baf86f and this branch beef430. The browser was Playwright Chromium, and the page was /apps/showcase_app/showcase_task. The list endpoint /api/v1/meta/app serves every group with expanded: false (the spec default), so each group was opened by clicking its label before it was read.

reading main 0baf86f this branch beef430
grips, every group as served 0 2 (the top-level run)
grips with Workspace and Data Model open 0 18 (2 + 12 + 4), all tabindex=0; no non-grip element carries the sortable description
mouse drag "Settings" to the top of Workspace no grip to drag drawn Settings, My Work, Approvals, New Project (Wizard); stored {"grp_workspace":["nav_settings","nav_my_work","nav_review_queue","nav_new_project_wizard"]}
reload store never written same order drawn, same record
keyboard: focus the Approvals grip, Space, ArrowDown, Space n/a focus lands on the grip; Approvals moves down one, and the record follows
drag "Page Authoring" above "Capability Map" n/a top-level run drawn Page Authoring, Capability Map; __root__ written beside grp_workspace
drag "My Work" onto "Projects" in Data Model n/a My Work stays in Workspace (it lands at the end of its own list); Data Model unchanged; no grp_data key
reload after all of the above n/a every stored level drawn; 0 page errors

Reverse verification

Each mutation went through the anchor-checked ablation tool, with a restore armed on EXIT, INT and TERM. Each restore was proven: blob equals HEAD, and git diff HEAD is empty. Every run is against the two pinned files, and the mutations ran on committed head 6eb56eb. Later commits changed no line a mutation touched; the grip refactor in beef430 is covered by the A4 pins, which pass at beef430.

mutation expected observed
A1: the grouped sortable path off (groupedReorder = false) red 16 of 22 failed: every grouped case. The group-free and search cases stayed green
A2: no order stamping in the store the authored-order cases red 3 of 22 failed: exactly those three
A3: moved-group detection by ids only the case where the move lands on the listed sequence goes red 1 of 22 failed: exactly that case
A4: sortable attributes back on the row wrapper the activator pins red 2 of 22 failed: the grouped and group-free activator pins

Tests and gates

gate head result
pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 cf8b3d3 exit 0
pnpm --filter @object-ui/layout build beef430 exit 0
pnpm exec vitest run packages/layout/ beef430 exit 0, 38 files and 344 tests passed
the two pinned files beef430 exit 0, 22 of 22 passed
pnpm exec vitest run packages/app-shell/ cf8b3d3 exit 0, 1005 files passed and 1 skipped, 9985 tests passed and 9 skipped
pnpm --filter @object-ui/layout type-check (its test tsconfig includes src/**/*.test.tsx) beef430 exit 0
pnpm --filter @object-ui/app-shell type-check (same include) beef430 exit 0
pnpm --filter @object-ui/layout lint beef430 exit 0: 0 errors, 35 warnings. NavigationRenderer.tsx carries 22, against 23 on main through the same spelling; the new test file carries none
pnpm --filter @object-ui/app-shell lint e42f5d5 exit 0, 0 errors. UnifiedSidebar.tsx carries 10 warnings on main and 10 at head through the same spelling: the same rules, shifted by the insertion
check:new-line-citations, check:control-bytes, check:i18n-keys, check:i18n-dead-keys, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check-changeset-presence.mjs, check-changeset-no-major.mjs beef430 all exit 0
check-governed-queue-guard.mjs --test over the five changed paths beef430 NOT GOVERNED

Two readings were taken on an earlier head:

  • The app-shell suite (cf8b3d3). Between cf8b3d3 and beef430 only NavigationRenderer.tsx changed: one comment line, and the grip destructuring its handle, which changes no behaviour. packages/app-shell is byte-identical, and the pinned app-shell file, which drives the grip through the real sidebar, was re-run at beef430.
  • The app-shell lint (e42f5d5). eslint.config.js enables no type-aware linting, so a verdict is per file, and every app-shell file is byte-identical to beef430.

Not run locally, and declared to CI: check:eager-closure, which reads a full console production build.

Acceptance notes

  • Keys: a group's order lives under its id. Two groups that shared an id would share an order; the spec describes the id as unique, and this change does not police that.
  • Not a finding: on /apps/showcase_app/... no sidebar row lights and the active group loads closed, on main as on this branch. The console's own hrefs for this app use the route segment com.example.showcase, so the name-segment URL is a non-canonical door. The order store is keyed by the app name either way.
  • Comment drift, left as found: the home-arm comment in UnifiedSidebar.tsx says enablePinning and enableReorder "both persist under useNavOrder". Pins persist in the favorites store (useNavPins, type: 'nav' favorites), not in objectui-nav-order-APP.
  • Polish, left as found: the grip sits in the row's left padding against the entry icon. These are the group-free arm's classes, unchanged.

Resumed run

This branch resumes a run lost to a container restart. It continues from head 75c86fe, adding commits only: no rebase, amend or force-push. The lost run's commits were reviewed as written. The direction, the per-group store, the __root__ key, the cross-group refusal and the search switch-off were kept. Five commits were added on top: 6eb56eb (the two holes above, with their pins), c7a5d7b (changeset), cf8b3d3 (a merge of origin/main at 0baf86f), e42f5d5 (a comment the activator change had made false) and beef430 (the grip destructures its handle, which removes three refs-during-render lint warnings the first spelling had added). Nothing the lost run measured is reused: every reading above was taken in this run.

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


Generated by Claude Code

claude added 11 commits October 5, 2026 04:59
…rouped sidebar (objectui#11626)

The sortable path existed only in NavigationRenderer's group-free arm, so on
every grouped menu (every stock app) `enableReorder` drew no grip. Each
group's children, and each run of top-level entries between two groups, is
now a sortable list of its own; nothing moves into or out of a group. The
move is reported through the existing `onReorder(reorderedItems)`: the
top-level list with the moved group's children reordered.

The console's nav-order store keeps an order per group `id` beside
`__root__`, writes only the level that moved, and applies every level on
read. The group-free arm and its `__root__` record are unchanged.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…`__root__` record (objectui#11626)

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

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

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…`order`, and the grip is the drag activator (objectui#11626)

Review of the resumed branch found two holes the grouped arm inherited from
the group-free one, both measured before this change:

- The renderer sorts every level by `order`. `useNavOrder` applied a saved
  order by array position only, so where an app authors `order` a drag was
  stored and sorted straight back (group and `__root__` alike). A level with a
  saved order now carries its positions as `order`; a level without one is
  untouched. The moved-group detection compares `order` as well as ids, since
  a move under authored `order` can land on the listed id sequence.
- dnd-kit's `attributes` (role=button, tabIndex=0) sat on the row wrapper and
  its `listeners` on the grip, so every row was a focusable "button" no key
  could drag from. The grip is now the activator (`setActivatorNodeRef`), with
  attributes and listeners together. Spreading the old wrapper to every
  grouped menu would have added one dead tab stop per entry on every stock app.

The stored `__root__` record of a group-free app is byte-identical.

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

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

Since the grip became the drag activator, a row wrapper carries no tabIndex;
what an empty one would leave behind is a drop target.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…o refs-during-render lint warning (objectui#11626)

`NavDragGrip` read `handle.ref`, `handle.attributes` and `handle.listeners`
during render; the react-hooks refs rule took the whole handle for a ref and
flagged all three reads (main 23 warnings in NavigationRenderer.tsx, the
branch 25). The handle is destructured in the parameter list and its setter
is named `activator`; the file now lints at 22, one fewer than main.

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

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3318.1 KB 3330.4 KB
Main entry chunk (gzip) 152.1 KB 350 KB
Entry file index-BbnSI9lr.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
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) 40.70KB 10.94KB
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) 574.98KB 137.97KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
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) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 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.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 143.58KB 38.81KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 247.21KB 65.03KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.92KB 64.87KB
plugin-kanban (index.js) 50.06KB 15.74KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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.08KB 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.25KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT: PR objectui#11656, head beef430. It lands when every check on this head is green

domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-05T11:01Z. Reviewed against GitHub and origin/main, not against the report's prose (report on objectui#11626).

  • Shape.

    • The PR is a draft against main, and its assignee is os-steve. Its first line is Fixes #11626, the only line with a closing keyword next to an issue number. Clause-②: no is on its own line.
    • The diff is 5 files, +1000/−69, inside the claim's surface. No path is governed. GitHub reads it as mergeable, and the queue rebuilds it on main.
  • The resumption. The first run was lost to the container restart. This run kept its six pushed commits and re-pointed one assertion. It added fixes on top and re-took every reading. Nothing pushed was rewritten.

  • The ruling, as written.

    • Each group's children, and each run of top-level entries between groups, is its own sortable list. A move stays within its level, and a cross-group move is refused (no shared drop target).
    • The move is reported through the unchanged onReorder(reorderedItems), as the top-level list with that group's children reordered.
    • useNavOrder stores the level under the group's spec id, beside __root__, and applies every stored level on load. The id is SnakeCaseIdentifierSchema, never __root__, and never a translated label.
    • A narrowed (searched) grouped menu offers no grip.
  • The fence held. NavigationRendererProps.enableReorder and onReorder keep their types: only their doc comments change. The row component that took dragListeners (now dragHandle) is NavigationItemRenderer, which NavigationRenderer.tsx does not export, and the two new interfaces are module-local.

  • Zone 2 item 4: falsified, and the change is accepted. A group-free app's stored __root__ record is byte-identical (pinned). The flat arm's behaviour does change in two places, both shipped defects in code this card had to share:

    • (a) dnd-kit's sortable attributes moved from the row wrapper to the grip. Before, every row was a focusable "button" no key could start a drag from. Now the one focusable element per row is the activator, and keyboard reorder works live.
    • (b) On an app that authors order, a saved order used to be sorted straight back by the renderer. Grouped reorder needs this fixed to meet the Done-when, and the flat arm shares the same applyOrder.

    The changeset states both.

  • Tests.

    • The pinned pair has 22 cases. Ablations: grouped reorder off turns 16 red; no order stamping turns 3 red; detection by ids only turns 1 red; attributes back on the wrapper turns 2 red.
    • A pre-fix probe on the resumed head shows both flat-arm defects.
    • packages/layout/ and packages/app-shell/ are green.
  • Live (showcase, 8 groups). On main: 0 grips, and the store is never written. On this head:

    • grips render on open groups;
    • a mouse drag within Workspace is drawn and stored under grp_workspace, and survives a reload;
    • a keyboard move works;
    • a top-level drag writes __root__;
    • a drag aimed at another group keeps the entry in its own group.
  • Changeset, sentence by sentence. It is @object-ui/layout and @object-ui/app-shell, both patch. The per-level lists, the refusal, the onReorder payload and order stamping, the search switch-off, the grip-as-activator paragraph, the store keys, the authored-order paragraph and the Clause-②: no line all match the diff.

Minor, not blocking: two merge commits (417e392 from the lost run, cf8b3d3) carry no trailer pair. They are pushed and are not rewritten. The squash commit is built from the PR.

Left as noted, not filed:

  • The UnifiedSidebar.tsx home-arm comment says pins persist under useNavOrder, but they live in the favorites store.
  • The grip sits in the row's left padding.

Landing: on all-green checks on this head, ready, then auto-merge into the merge queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 11:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 11:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit b88937b Oct 5, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11626-grouped-nav-reorder branch October 5, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console(nav): drag-to-reorder is wired only for group-free navigation — every stock app's sidebar is grouped, so enableReorder never renders a grip

2 participants