Skip to content

refactor(web): finish the component reorganisation — theme, skeletons, shell, modals - #260

Merged
houko merged 6 commits into
p3/layeringfrom
p3/layering-2
Sep 19, 2026
Merged

houko merged 6 commits into
p3/layeringfrom
p3/layering-2

Conversation

@houko

@houko houko commented Sep 18, 2026

Copy link
Copy Markdown
Member

Description

Stacked on p3/layering (#258) — review that first.

Four relocations inside apps/web/core/components, finishing the reorganisation #258 started. 156 changed lines across 120 files, and git records 39 renames, so this reads as a move rather than a rewrite.

  • core/components/core/theme/ (6 files) → core/components/appearance/, which is feature-named and already holds the public entry point.
  • core/components/ui/ → core/components/skeletons/, and the two files in it that are not skeletons evicted to labels/ and empty-state/.
  • The three app-shell primitives (page-title, content-wrapper, app-header) → core/components/shell/. 98 import lines, most of them from app/.
  • core/components/core/modals/ dispersed into the eight owning features.

core/components/core goes from 42 files to 25. The other 25 are deliberately untouched — emptying that directory is not this PR's job.

Type of Change

  • Code refactoring

Screenshots and Media (if applicable)

None. Every file moved byte-identical; only import specifiers changed.

Test Scenarios

  • pnpm turbo run build check:types check:lint check:format --force — 59/59, no cache, exit 0.
  • git diff -M --find-renames=90% --stat → 159 files, 156 insertions / 156 deletions, 39 renames. A move-only diff: no line is both added and removed within a file.
  • The base branch turned unicorn/filename-case on as an error, so every moved path had to satisfy it; check:lint is the proof.

References

Two deliberate interactions with #252 (chore/remove-dead-code-and-assets), both of which a reviewer needs to know about:

  1. The ui/ → skeletons/ rename moves 20 of 24 files, not all 24. The other 4 are files chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates #252 deletes. Moving them would create 4 rename-vs-delete conflicts whose wrong resolution resurrects dead code — the exact risk worth avoiding. Verified before choosing this: all 4 have zero references anywhere in apps or packages, and none imports a sibling inside ui/, so nothing dangles. The consequence is that core/components/ui does not disappear until chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates #252 lands.
  2. Step 10's import rewrite necessarily touches 2 files chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates #252 deletes (issues/issue-detail/relation-select.tsx, onboarding/profile-setup.tsx). They were rewritten rather than skipped, because skipping would leave this branch failing tsc. That produces 2 modify/delete conflicts when chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates #252 merges, both resolved by taking the deletion. Unlike case 1, there was no option that avoided a conflict.

Corrections to the plan, all measured:

Not done: the remaining 25 files in core/components/core still need homes, and apps/web/helpers/ is still a fourth root alongside app/ and core/ — the only structural oddity left in the path map.

…appearance

Theme lived in two directories: core/components/appearance held the public entry point (theme-switcher.tsx, the only file the barrel re-exports and the only one settings reaches) while the six pieces it composes sat under the unnamed core/components/core bucket. A reader arriving at the switcher had to leave the feature directory to find the selector it renders.

appearance/ is the home rather than a new top-level theme/ because it is feature-named and already owns the entry point. The four files reached only through relative ./ imports from custom-theme-selector.tsx move with it untouched, so the entire cross-directory cost is the two specifiers in theme-switcher.tsx, which become sibling-relative in line with how the rest of this directory already refers to its own files.
core/components/ui holds 24 files, 22 of which are skeleton loaders under loader/. The two exceptions are a label-count chip and an empty-state layout, and both have an obvious owning directory already: IssueLabelsList belongs with the other label components and EmptySpace/EmptySpaceItem with the other empty states. Moving them first means the directory that gets renamed next contains nothing but loaders, so the new name needs no qualification.

Neither file is added to core/components/labels/index.ts: that barrel is hand-maintained and already omits several of its neighbours, so the single consumer keeps importing by path.
The path lied twice over. apps/web imports the @pace/ui package on nearly every screen, so "@/components/ui" reads as a local copy of the design system when in fact every file under it is a loading skeleton. Naming the directory for what it contains removes the collision and tells a reader where skeletons live without opening the folder.

Four files stay behind in core/components/ui because #252 (chore/remove-dead-code-and-assets) deletes them: markdown-to-component.tsx, profile-empty-state.tsx, loader/notification-loader.tsx and loader/pages-loader.tsx. All four are unreferenced anywhere in the repo, and none of them imports a sibling, so leaving them in place breaks nothing and keeps this rename from colliding with that deletion. core/components/ui disappears when #252 lands.
…s/shell

PageTitle, ContentWrapper and AppHeader are the three highest-traffic residents of core/components/core, and unlike the rest of that bucket they share an obvious identity: they are the chrome every route renders around its content, consumed almost entirely by app/ layouts and pages. Ninety-eight of the directory's import lines point at these three files, so naming them is where the bucket's cost actually is.

shell rather than app-shell keeps the import short, and rather than core/layouts because that directory already exists with a different meaning: it holds three route-level layout components, not primitives those layouts compose.

The remaining 33 files in core/components/core are deliberately untouched. They have no single destination between them and each one wants a judgement call about its owning feature; emptying the directory is not what this change is for.
…eatures

Every one of the eight files in this directory has an unambiguous owner, so the grouping by "is a modal" bought nothing: it only guaranteed that anyone working on work items, the workspace logo or the profile avatar had to look somewhere other than the feature directory. Fifteen import lines total, and the two relative imports inside the group (bulk-delete-issues-modal to its item, existing-issues-list-modal to its empty state) survive untouched because both pairs land in issues/.

The two destinations that needed a judgement call: gpt-assistant-popover.tsx goes to editor/ rather than issues/ because it is a generic AI-completion popover over a RichTextEditor with no work-item concept in it, and its sole consumer imports it as an editor affordance. user-image-upload-modal.tsx goes to profile/ rather than account/ because it edits the profile avatar; account/ in this codebase holds authentication and account-lifecycle concerns, which is also why change-email-modal.tsx goes there.

core/modals/ (the unrelated top-level directory) is not involved, and the remaining 25 files of core/components/core are left alone.
@github-actions

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 79 / 100 (Needs work) · 1 fixed · vs p3/layering

1 warning

core/components/issues/select/base.tsx

  • ⚠️ L153 Interaction on static element no-static-element-interactions

Reviewed by React Doctor for commit 3caf357. See inline comments for fixes.

@houko
houko merged commit f5146fe into p3/layering Sep 19, 2026
4 checks passed
@houko
houko deleted the p3/layering-2 branch September 19, 2026 12:24
houko added a commit that referenced this pull request Sep 19, 2026
…, shell, modals (#260)

* refactor(web): merge core/components/core/theme into core/components/appearance

Theme lived in two directories: core/components/appearance held the public entry point (theme-switcher.tsx, the only file the barrel re-exports and the only one settings reaches) while the six pieces it composes sat under the unnamed core/components/core bucket. A reader arriving at the switcher had to leave the feature directory to find the selector it renders.

appearance/ is the home rather than a new top-level theme/ because it is feature-named and already owns the entry point. The four files reached only through relative ./ imports from custom-theme-selector.tsx move with it untouched, so the entire cross-directory cost is the two specifiers in theme-switcher.tsx, which become sibling-relative in line with how the rest of this directory already refers to its own files.

* refactor(web): move the two non-loaders out of core/components/ui

core/components/ui holds 24 files, 22 of which are skeleton loaders under loader/. The two exceptions are a label-count chip and an empty-state layout, and both have an obvious owning directory already: IssueLabelsList belongs with the other label components and EmptySpace/EmptySpaceItem with the other empty states. Moving them first means the directory that gets renamed next contains nothing but loaders, so the new name needs no qualification.

Neither file is added to core/components/labels/index.ts: that barrel is hand-maintained and already omits several of its neighbours, so the single consumer keeps importing by path.

* refactor(web): rename core/components/ui to core/components/skeletons

The path lied twice over. apps/web imports the @pace/ui package on nearly every screen, so "@/components/ui" reads as a local copy of the design system when in fact every file under it is a loading skeleton. Naming the directory for what it contains removes the collision and tells a reader where skeletons live without opening the folder.

Four files stay behind in core/components/ui because #252 (chore/remove-dead-code-and-assets) deletes them: markdown-to-component.tsx, profile-empty-state.tsx, loader/notification-loader.tsx and loader/pages-loader.tsx. All four are unreferenced anywhere in the repo, and none of them imports a sibling, so leaving them in place breaks nothing and keeps this rename from colliding with that deletion. core/components/ui disappears when #252 lands.

* refactor(web): extract the route-shell primitives into core/components/shell

PageTitle, ContentWrapper and AppHeader are the three highest-traffic residents of core/components/core, and unlike the rest of that bucket they share an obvious identity: they are the chrome every route renders around its content, consumed almost entirely by app/ layouts and pages. Ninety-eight of the directory's import lines point at these three files, so naming them is where the bucket's cost actually is.

shell rather than app-shell keeps the import short, and rather than core/layouts because that directory already exists with a different meaning: it holds three route-level layout components, not primitives those layouts compose.

The remaining 33 files in core/components/core are deliberately untouched. They have no single destination between them and each one wants a judgement call about its owning feature; emptying the directory is not what this change is for.

* refactor(web): disperse core/components/core/modals into the owning features

Every one of the eight files in this directory has an unambiguous owner, so the grouping by "is a modal" bought nothing: it only guaranteed that anyone working on work items, the workspace logo or the profile avatar had to look somewhere other than the feature directory. Fifteen import lines total, and the two relative imports inside the group (bulk-delete-issues-modal to its item, existing-issues-list-modal to its empty state) survive untouched because both pairs land in issues/.

The two destinations that needed a judgement call: gpt-assistant-popover.tsx goes to editor/ rather than issues/ because it is a generic AI-completion popover over a RichTextEditor with no work-item concept in it, and its sole consumer imports it as an editor affordance. user-image-upload-modal.tsx goes to profile/ rather than account/ because it edits the profile avatar; account/ in this codebase holds authentication and account-lifecycle concerns, which is also why change-email-modal.tsx goes there.

core/modals/ (the unrelated top-level directory) is not involved, and the remaining 25 files of core/components/core are left alone.

* chore: bump the workspace version, which check-version requires of every PR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant