Skip to content

refactor: split oversized modules into focused ones - #45

Merged
AlexKempen merged 56 commits into
certfrom
claude/codebase-restructure-features-snex55
Aug 24, 2026
Merged

refactor: split oversized modules into focused ones#45
AlexKempen merged 56 commits into
certfrom
claude/codebase-restructure-features-snex55

Conversation

@AlexKempen

Copy link
Copy Markdown
Collaborator

Breaks up shared/types.ts, shared/api-models.ts, backend/app.ts,
backend/auth.ts and frontend/queries.ts along their seams so the
following move to a feature layout is a pure relocation.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H

claude added 16 commits August 20, 2026 18:21
Breaks up shared/types.ts, shared/api-models.ts, backend/app.ts,
backend/auth.ts and frontend/queries.ts along their seams so the
following move to a feature layout is a pure relocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Replaces src/shared with ownership-based placement: the backend owns
the contract (DTOs, domain enums, configuration models) and the
frontend imports it, so there is no third top-level bucket.

backend/  app.ts assembles Hono; db/ holds the client and schema; lib/
holds the Onshape client and request plumbing; features/{auth,users,
library,configurations,thumbnails,build-checker,favorites,search} each
own their routes, storage and models.

frontend/ features/<feature>/{queries.ts,components/} with cross-cutting
helpers in lib/ and shared UI in components/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
The frontend reaches the backend's DTOs and domain models through
@backend/* instead of counting ../ levels; imports within a side stay
relative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Every module that turns an Onshape response into what we store now lives
in features/load: the parse-* modules (document contents, configurations,
configuration records, vendors, fasten info), the per-group and
per-insertable loaders, and the Workflows and job tracker that drive them.

Two boundaries were untangled to make that a clean feature:

- ThumbnailWorkflow moved to features/thumbnails, and the render/upload
  helpers moved out of its routes into store.ts. load and thumbnails
  previously imported each other in a cycle; the dependency is now
  one-way from thumbnails to load.
- parse-fasten mixed parsing with insert-time query building.
  getFastenQuery moved to features/library/insertables/fasten-query.ts,
  leaving parse-fasten to parsing alone.

configurations keeps the domain the frontend shares (models,
canonicalization, combinations, input parser); its Onshape parsers moved
to load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
auth/ had three files that all answered 'is this caller allowed' without a
line between them. Split by role instead:

- session.ts    the session cookie and its KV records
- onshape-oauth.ts  the handshake only
- caller.ts     resolving who is calling, with the KV memoization, plus
                productionCaller (was services.ts)
- guards.ts     both gates, which were one-per-file in sign-in.ts and
                access-control.ts
- routes.ts     the OAuth redirects and /access-data

/access-data reports access level and sign-in state, so it moves from
users to auth. What is left of users is the caller's stored preferences,
so it becomes settings/.

app.ts was a composition root plus the /init handler plus error handling.
/init is now features/entry (its test came along, as routes.test.ts
rather than app.test.ts), the error handler is lib/errors.ts, and the
caller binding is lib/context.ts, leaving app.ts to mount and nothing else.

AppServices is renamed Caller: it is the request's caller, not a service
registry. not-signed-in.test.ts is split into the modules it covers,
guards.test.ts and routes.test.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Matches the feature that serves it and what it actually updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
configurations.id is a 1:1 FK to insertables.id, so LibraryOut's
configurationId was always just the insertable's own id: a boolean
wearing an id costume. Every consumer either truthiness-tested it or
passed it straight back as the id.

InsertableOut now carries isConfigurable, and callers fetch by
insertable id. The route param follows suit
(/api/configuration/:insertableId), and the configuration query keys
gain insertableConfigurationQueryMatchKey for the prefix match
insert-menu was spelling inline.

Adds tests pinning the behavior this rests on: the library response
marks configurability but carries no parameters or records, which stay
in D1 until an insertable actually needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
An element's part number, name and material were kept as the first entry
of configurations.records — the probe of its default configuration. Every
probed insertable therefore carried a configurations row, including ones
with no parameters to configure, and "is it configurable?" had to test the
parameter count rather than the row's existence.

That part data is not a configuration of the element, it is the element,
so it moves to insertables.part_data. configurations is left holding only
configuration data: a row exists exactly when there are parameters, and
isConfigurable is now just whether the row is there.

ConfigurationRecord becomes PartData plus the configuration that produced
it, and toRecords() recomposes the full list — the element's own data as
the record an unset configuration falls back to, then one per indexed
configuration — for search and the configuration endpoint. Both paths are
now covered by tests that fail without it.

The migration backfills part_data from records[0] (always the default
probe; see toResult), strips it from records, and drops the rows left
with no parameters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
The library reloads from Onshape, so the column starts null and
repopulates on the next load rather than being carried over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
…dions

Icons: all 34 Tabler icons map to a Phosphor equivalent, each name
checked against the installed package rather than guessed. A filled
heart becomes weight="fill" and Tabler's title prop becomes Phosphor's
alt, which renders the same <title> element.

notifications.tsx imported ReactNode from @tabler/icons-react, which
does not export it; it now comes from react.

Home page: the library and search sections were two near-identical
Accordion.Item blocks, with their open state read from a hand-built
array and written back through a branch on whether a search was active.
Both are now one list of sections, each carrying where its own open
state lives, so the value and onChange plumbing stops branching.

The accordion divider was styled onto the panel content, so a collapsed
section had no line under it. It moves to the control, which is rendered
either way; content keeps the closing line when open. Verified in the
running app.

Also deletes the beta-complete page, which nothing linked to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
useAccessData falls back to a signed-out placeholder while access-data is
in flight, so the navbar rendered the sign-in button on every load and
then removed it once the response said the caller was already signed in.
ResolvedAccessData now reports isLoaded, and the button waits for it.
Anything gated on signedIn being true was already safe — it renders
nothing until the data arrives; only the signed-out branch could flash.

The toast's action button sat in a wrap="nowrap" row with no flex-shrink
of its own, so a long message shrank it and clipped the label — 31px of
the 47px "Sign in" needed. It no longer shrinks, and the message wraps
instead.

Both verified in the running app: the button never appears across a
delayed access-data response, and the label now measures 47/47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Mantine's Notifications containerWidth defaults to 440px and never
grows, so a message with an action button wrapped to two lines at every
window size - measured identically at 800px and 1400px wide. Setting it
to max-content sizes the toast to its content and lets Mantine clamp it
to the viewport when there is genuinely no room.

Measured across widths: 360 -> 328px/3 lines, 420 -> 388px/2 lines,
900 and 1400 -> 514px/1 line, none overflowing the viewport.

flexShrink on the action button stays: it is what keeps the label whole
in the narrow case where the row still has to wrap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
isLoaded was re-deriving what TanStack already reports. useAccessData now
passes through isPending, which also fixes a failure mode the derived
version had: on an errored access-data, data stays undefined, so the old
flag would have hidden the sign-in button forever instead of offering it.

useSaveSettings had the same placeholder problem in the other negative
gate - a snapshot reading signed-out would silently persist to
localStorage for a user who has a server-side row. It now resolves access
data when the mutation runs, so it cannot act on a placeholder.

Verified against a delayed access-data: no button while signed in,
button once a signed-out answer lands, button offered after the request
fails outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Editing now requires a session as well as the access level. Access level
alone let a signed-out caller through wherever it is granted without one
(a dev ACCESS_LEVEL_OVERRIDE), and answered 403 rather than 401 for
everyone else. Both cases are now covered by tests.

Routes: every body and query is a zod schema, and every entity id comes
from a route-params helper. That adds a favoriteRoute/getFavoriteParam
pair and moves three paths onto the existing convention -
/favorite/:favoriteId, /default-configuration/favorite/:favoriteId,
/configuration/insertable/:insertableId, /reload-group-thumbnail/group/
:groupId - with the frontend using the matching to*Path helpers. The
previously-unchecked c.req.json<T>() casts on favorites, groups,
insertables and settings were assertions, not validation.

cache.ts: the cache-version check was a one-key zod schema guarding a
"versioned: false" option nothing ever passed. Both are gone.

ThumbnailWorkflow reads the microversion from the insertable it already
resolves, rather than carrying one that may be stale by the time it runs.

R2 thumbnail metadata is a typed ThumbnailMetadata rather than a bare
Record, so an element's own thumbnail is tagged with the empty canonical
configuration it represents instead of omitting the field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
@AlexKempen
AlexKempen force-pushed the claude/codebase-restructure-features-snex55 branch from cf0e650 to 287815b Compare August 21, 2026 13:41
claude and others added 13 commits August 21, 2026 14:14
dto.ts is contract.ts - the backend owns the contract, and DTO was an
acronym that named nothing. JobStatus moves out of the library's contract
into the load feature that produces it.

libraryId is stored per user but was missing from Settings, so
DEFAULT_LIBRARY_ID lived off in library-id.ts as a second default. It is
now part of Settings and DEFAULT_SETTINGS, and SettingsUpdate is just
Partial<Settings>.

Favorite writes are all scoped to their owner. Reordering and setting a
default configuration were keyed on the favorite id alone, so one user
could rewrite another's. Scoping the WHERE rather than reading the row
first means no extra query.

PartData is PartMetadata: absent values are undefined rather than null,
and ConfigurationRecord documents that it is the same fields plus the
configuration that produced them. Each probe now records whether it
resolved to an open composite, and toResult derives instability by
comparing against the element's own probe, so no record carries a flag
about a comparison it cannot see.

The two configuration-count issues now name their limits:
CONFIGURATION_LIMIT_EXCEEDED and MANUAL_INDEXING_REQUIRED, matching
IndexingBand.EXCEEDED and .MANUAL.

addBuildIssue always returns a new array, with a test for it.
checkIndexedPartNumber takes probes, not probed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Every failure now answers with { kind, message }, where kind says what
the client should do with the message rather than leaving it to guess:

- handled  the message is written for the user; show it
- notice   the same, for an outcome that is not a failure
- internal the message is for the logs; the caller shows its own wording

The frontend switches on kind alone, so it handles an error it has never
heard of. Callers keep supplying the wording for their own context, which
is what internal errors fall back to.

That replaces three inconsistent paths: a hand-rolled body two group
routes returned, HTTPException messages that reached the user or did not
depending on the route, and zValidator's own 400 body - which never
reached the error handler at all, since it answers rather than throws.
Routes now use a validate() wrapper that throws instead.

Onshape failures get a mapping of their own, so what we are willing to
repeat is one function rather than a decision per call site: a 401/403
says to try signing in again, a rate limit keeps its retry-after, and
anything else stays generic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
An unknown library id now 404s instead of quietly redirecting to
FRCDesignLib, which hid the bad url. That surfaced a latent crash: the
root renders around the not-found, so it saw an unvalidated libraryId
and handed MantineProvider an undefined primaryColor. isLibraryId moves
next to the other library path helpers and both callers use it.

Mantine has no relative-time helper (only @mantine/charts and
@mantine/dates components, neither of which we depend on), but the
platform does. formatRelativeTime is now Intl.RelativeTimeFormat, which
produces the same strings, localized, and adds "yesterday".

ApiErrorBody is a discriminated union keyed on kind, following the same
BuildIssueOf pattern already used for build issues, so a kind carries
exactly its own fields. retryAfterSeconds now belongs to a rate-limited
error rather than sitting optional on every error. The notice kind is
gone: nothing ever produced one, and it was not distinguishable from
handled.

showToast updates a live toast rather than hiding and re-showing it, so
a loading toast becoming a success one stays in place.

Standalone, getColorTheme defaulted the system scheme to light, ignoring
the OS. It now takes the scheme explicitly and the root supplies
Mantine's useColorScheme when Onshape has not put one on the url.

IconColor and HeartIconColor are gone - the latter was a duplicate of
IconColor.RED. Phosphor icons render currentColor, so passing them
through Box lets Mantine colors work directly. PrimaryColor and
HEADER_CONTROL_COLOR stay: one is the dynamic per-library primary, the
other is deliberately a hex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
IconSize mixed magnitudes with one context name (CONTROL). Usage says the
split is real rather than accidental: TINY, SMALL and MEDIUM are general
sizes for an icon in a line of content (55 of 64 uses, across buttons,
menus, toasts and rows), while the three largest each have exactly one
placement. So the three general ones keep magnitude names and the rest
say where they go: CONTROL stays, LARGE becomes SECTION and HUGE becomes
PAGE. The doc comments now describe where each is actually used rather
than where it was first meant to be.

No size keeps its name with a different value, so nothing silently
re-points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
utils.ts held five unrelated things. Each moves to where it is used:
getQueryUpdater and patchQuery become lib/query-cache.ts, useIsHome
joins the other library route helpers, and capitalize and
handleBooleanChange move into their single callers. The re-export of
query-params went to api-client alone, which now imports it directly, so
nothing is left and the file is gone.

The Fast Refresh warnings were real, and the cause is the opposite of
what it looks like: each menu file exports only its opener function
while defining components privately. React Refresh can only swap a
module whose exports are all components, so an edit invalidated the
module and propagated to every importer - editing a menu remounted its
callers instead of preserving their state. The openers move to sibling
modules and the component files export their components, which silences
the warning. Verified by driving HMR in a browser: the invalidate
messages are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Give the cancel/restore toast an id keyed on the insertable, so opening
and cancelling the same one repeatedly refreshes a single toast instead
of stacking up a column of them. showInfoToast now takes an options
object rather than a bare id, since it also needs autoClose.

Mount hono/logger on the app. Only the Worker routes run it (assets are
served ahead of the Worker by run_worker_first), and console.log reaches
Workers Logs in production because observability is already enabled.

Add a one-time tip after inserting an element's default configuration
from the insert menu, pointing out that a right-click would have done
the same thing. The condition is the canonical configuration being
empty rather than "unchanged since the menu opened": right-click quick
insert on a card passes no configuration, so a search hit's
configuration is not what the tip would have gotten the user.
InsertableCard rendered its menu without a configuration, so right-click
quick insert on a search hit inserted the element defaults while the
card's own thumbnail and the insert menu both showed the hit's
configuration. Pass it through, as FavoriteCard already does for a
favorite's default configuration.

With that fixed, the quick-insert tip can use the condition it should
have: the configuration is still the one the menu opened with, compared
canonically so an untouched menu counts as unchanged whether or not it
was opened with a configuration.
The navbar was one colored strip holding the brand, a library dropdown,
search and settings. It is now two rows: library tabs with the brand and
settings alongside, over a full-width search input and its vendor filter.
Switching libraries is a tab click rather than a two-step menu, and the
placeholder names the library being searched.

The chrome goes neutral with it. The header is no longer painted with the
primary color, so its controls drop the white override they needed to
stay legible, and every library now shares one theme with the brand green
as an accent on controls and the active tab, in place of a green, orange
and blue that changed the whole app per library. The root no longer needs
to resolve a library id to build a theme.

Tab labels leave no room for a name plus its status, so the name and the
status are separate: the tabs show the short label, and the tooltip,
search placeholder and accordion title spell out the rest.
The navbar refactor collapsed the three libraries onto one green theme.
Restore the per-library primary color — green, orange, blue — now as an
accent on controls and the active tab rather than a painted header.

getLibraryColor falls back instead of returning undefined for an unknown
library, so the root can theme a url naming one that does not exist; that
is what forced the id check the root used to carry, and the 404 for such
a url still comes from the library route.
Put the brand's colored tile back under the book, now filled with the
library's accent instead of painting the whole header, and darken the tab
row a step so it reads apart from the search row below it.

Move Beta and Deprecated out of the name and into badges in the library
title, the one place with room to spell them out. The name is plain
everywhere else, so the tab tooltip and search placeholder no longer
carry a parenthetical.

Also spell MKCad the way its tab does, and swap the library section's
single book for a shelf of them, which reads as a library rather than a
document.
Give the insert menu chrome around its configuration: the header and a
new footer holding the favorite and insert controls both sit on the
surface the navbar's tab row uses, which moves up to style-constants now
that two places want it.

The favorite icon shrank from 24px to 16px on hover, because a hovered
non-favorite swaps a bare Phosphor icon for one wrapped in Box, and Box
builds its own style over the font-size that Phosphor's size prop sets.
Size the wrapped icons with fz so the value survives.

Also let the status badges take the library's color rather than one per
status, and shrink the settings gear so the top row breathes.
Extract openAppModal with AppModalBody and AppModalFooter, so a modal's
header and footer sit on the same surface with the same tight padding and
its body spans the full width between them. The insert, favorite,
settings and add-group menus all wear it now; add-group's action moves
into the footer alongside the others.

Name libraries in full on their tabs, which the tooltips existed to
supply, so both the tooltips and the short labels go. Three full names
outgrow a panel under about 540px, so the list scrolls rather than
reflowing the navbar into two rows.

Also drop Mantine's active class, whose 1px translate pushed every button
down on click.
claude and others added 27 commits August 22, 2026 05:42
Drop the default radius from Mantine's md to sm, so buttons, inputs and
cards read crisper in a dense panel.

Draw a border around a modal's card and bring the body's padding in to
match the header and footer, which now share one inset: the preview card
lines up with the favorite below it and the title above.

Also hide the tab list's scrollbar, which reserved a 1px track in the top
bar even with nothing to scroll.
The card carried its own margin from before the modal body had padding,
so the two stacked and pushed the preview 25px off the modal's edge. Drop
the margin and tighten the card's own padding, leaving the preview inset
once, in line with the header and footer.
AppTitle fixes the weight, size, gap and truncation for a heading, with
slots for a leading icon, a quieter second line, and trailing content.
MenuTitle builds on it for the modal headers, replacing the near-identical
title each of the insert and favorite menus carried.

Apply it to the headings that had each drifted their own way: accordion
sections, which had no weight of their own, the group header, which set
its size and weight inline, and the warning alerts, which now lead with an
icon like everything else. The favorites accordion icon also grows to the
size the two beside it already used.
The boxes were already centred to the pixel; what reads as misalignment is
that text centres on its line box while the eye centres it on the cap
height, about a pixel higher, leaving the icon low.

CSS text-box would trim the line box to the caps and fix this outright,
but it clips descenders under the overflow that truncation needs, and it
makes row heights depend on their content. So title icons take the pixel
back with a transform instead, which leaves layout untouched, from one
constant used by AppTitle and the accordion's own icon slot.
Lead a part's details with its name rather than its number, in both the
menu titles and the search cards. Even up the insert button, whose label
side was half again as wide as its icon side, and use one gap inside a
modal body so the preview sits as far from the parameters as they sit
from the footer. In light mode the chrome moves from gray-0 to gray-2,
which was too close to white to read as a separate surface.

Cache job status privately for two seconds, so switching libraries and
back does not refetch it, and widen the query's reuse window to cover a
remount. Two seconds stays under the poll's three, so a poll is never
answered with what it already had.

Modals now take focus on a sentinel rather than the close button, which
had been left circled whenever one opened. Focus still enters the dialog,
so Tab stays scoped to it.
The nudge wrapper was a plain block, so the icon inside it went back to
sitting on the text baseline — several pixels low rather than the one the
nudge takes back. Centre it instead, which blockifies the icon: the back
arrow was 4.4px out and is now half a pixel, in line with the accordion's
own icons.

Set a section header's height rather than leaving it to the tallest child,
which was the label in an accordion and the menu button in a group header,
and give the group header the accordion's inset so their icons share a
column. Both are 48px with icons at 16px now, from 48/52 at 16/12.

Also encode the configuration a document url carries, focus search on
mount, and revert the job status caching — it is live state, as its test
already said.
Each parameter row carried its own top margin, so the first one added to
the gap the modal body already leaves: 24px above the parameters against
12px below. Space them from the stack instead, and the two match.

The button's icon side read wider because the plus is inset inside its own
box, so an equal padding leaves an unequal gap. Trim that side: the ink now
sits 12px from the top, left, and 12.8px from the right.

The group header left its divider to the row below it while an accordion
control carries its own, making the group's band a pixel taller. Give it
the border, so both are 48px.
The footer padded 6px above and below a button already inset 12 from the
side, so the button sat closer to the divider than to the edge. Pad it
evenly instead. The header keeps the tighter vertical, where a title line
needs less room than a button, so the constant is now named for it.

Revert the hand-tuned padding on the insert button itself, which was
chasing the same complaint in the wrong place; Mantine's own is fine.
The header took a hand-written 6px vertical padding and a shrunk close
button to stay tight, which left a title with no second line cramped. Use
the same inset as the body and footer, with the close button at its own
size, and the constant that existed only for that padding goes away.

Drop two declarations that turned out to do nothing: `white-space: nowrap`
on a tab, which a non-wrapping list already gives, and `padding-block: 0`
on an accordion control, whose default is already zero. The label's own
padding-block is what had to go, and stays.

Trim the comments that had grown past the two lines the project asks for.
Restore minHeight: 0, so a Mantine minimum no longer overrides the inset
the header shares with the body and footer: 60px down to 53 for a title
with no second line.
Resolve a part's page once, when its record is built, into a url on the
record: a description that is already a url wins, since it names the exact
product, over one derived from the vendor and part number. Only McMaster
and WCP have derivable urls, so getVendorPartUrl answers for those and
nothing for the rest.

The menu subtitle shows it: the part number becomes a link with a copy
button beside it, and stays plain text when there is no page to point at.
A title's second line is laid out as a row now, so it can hold them.

Also list Custom last among the vendors, being the absence of one.
Onshape's own vendor field is often unset, which left WCP parts with no
link. Fall back to the insertable's vendors, the same signal the filters
use, when they name exactly one — several cannot say which a record is.
Also read that field written as a full name, not only as a code.

A part with no parameters mounts no ConfigurationWrapper, so nothing
reported its record and the insert menu's title had no part number at all.
Query it directly in that case, from a hook the wrapper now shares.
Onshape's vendor field is empty on every part, so records carried none and
no part number could be linked. Parse one per record while the library
loads, from the options its configuration selected and from the part's own
name, through the same reader the element's vendors already use.

A selection names the vendor more precisely than the part does, so it is
read first, and an absent value counts as the parameter's default — which
is what the element's own probe, configured with nothing, resolves to.
Read before canonicalizing, which drops a selection that is the default.
A modal title's line-height is 1, and the second line inherited it, so it
had no leading under its baseline while the first has plenty above its
caps — the block read low in a header padded evenly. Set a line-height on
that row: the caps now sit 16.9px from the top and the last baseline
17.4px from the bottom, against 16.9 and 15.0 before.
It sat outside the anchor, so it took the row's dimmed color while the
link took the accent, and read as disabled next to it. The link and the
copy button carry the affordance on their own.
Neither has a per-part url, so point at their product search instead, with
the part number escaped as a query value rather than pasted into a path.

The launch icon goes back beside the part number, inside the anchor so it
takes the link's color rather than the row's dimmed one. The anchor lays
its two children out in a row, which centres the icon on the text instead
of dropping it onto the baseline.

The copy button now appears only when there is no page to open, and copies
the part number, which is what there is left to search with.
Drop the admin reload-thumbnail option and the two routes behind it. The
load is the only path now, so widen its polling: cap the doubling at two
minutes and raise the limit, turning seventeen minutes of increasingly
coarse waits into half an hour that still checks every two. Insertables
load in parallel, so a long tail holds nothing else up.

Read parts raises NO_PARTS itself rather than reporting a flag the caller
turns into one, and is named for what it reads rather than for the one
thing it used to derive.

Also search AndyMark, which has no per-part url either.
Store the caller's Onshape user id on the session record instead of a
parallel `user-id:` KV entry, so a sign-in keeps one key and a token
refresh carries the resolved id forward.

Drop a part number that only repeats the part name: the library uses
that for generic parts with no real number, and it identifies nothing,
so it should not be shown, searched, or linked to a vendor.

Ellipsize the part numbers that remain — some run to 80 characters —
by letting the modal title shrink and giving the number the room the
part name leaves rather than sharing the overflow between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Some parts carry "N/A" where a real part number would go. Like a part
number that only repeats the name, it identifies nothing, so drop it
alongside those rather than rendering the placeholder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
This reverts commit 54906a6. An "N/A" part number is entered by an
admin deliberately, so it should be shown as written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
A record was only picked when its part number or name matched the whole
query contiguously, so any multi-word query fell through to the default
configuration: "maxspline 24t" names a configuration that no record
reads as. Score by query term instead, keeping a whole-query match above
any number of loose terms so a part number typed in full still wins, and
pick the better-scoring of the part number and name rather than always
preferring the number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
`TTB-0016-5/32` was read as the mixed number 16 5/32, so it indexed as
`TTB 16.16` and a `TTB-0016` query underlined nothing after the prefix.
A leading zero marks a part number segment rather than a quantity, so
split the two and canonicalize each: `TTB 16 0.16`. Leading zeros are
spelling rather than value, so either way of writing a segment now finds
the part, and a numeric match underlines the zeros it landed behind.

The filter callout gains an icon, drops the library accent for a plain
informational blue, and uses a neutral button so it stops competing with
the results it reports on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
An xs ActionIcon is 18px against the subtitle's 16.8px text line, so the
row grew whenever the part number was copyable rather than linked and
the title above it moved. Size the button to the line instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
The part number names its own vendor most precisely, since a part
configurable across several carries a generic vendor while each
configuration's number still says who sells that one — so read the
vendor off the number first, falling back to the tagging for the numbers
that carry no prefix.

Search rows now link the number to that vendor page, and the part name
gives up every character before the number loses one. The insert menu
drops the part name entirely: the number is what identifies what gets
inserted.

No assembly configures its part properties today, so probing every
combination only rediscovers what the default already says; assemblies
now read the default alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
"N/A" is what an admin writes where a generic part has no real number,
so it identifies nothing and is worth no row of its own. Filter it at
the two places a part number is shown, alongside the existing rule for
one that only repeats the name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Comments: cut the two dozen that ran past the two lines AGENTS.md allows,
and the ones that opened by restating a signature before saying anything.

Tests: one shared `configurationRecord` fixture replaces four local
`record` builders that had drifted to four different signatures, and
`paramsWithConfigs` and `quantityParam` stop being redefined beside the
copies in `__test_utils__`. Cases that only varied by an argument become
tables — `evaluateCondition` was rebuilding the same condition object in
each of eleven tests, and `evaluateExpression` repeated one shape
twenty-two times. Helpers absorb the argument lists that were noise at
the call site: search's three trailing `undefined, undefined, true`, and
the six-argument probe in the configuration-record tests.

Same coverage, 400 fewer lines of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
@AlexKempen
AlexKempen merged commit 9f5de9d into cert Aug 24, 2026
1 check passed
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.

2 participants