Conversation
The light theme is flat by design: surfaces get no elevation, and
controls carry none in either theme. Tailwind inlines shadow values at
build time — only the color is a variable, and --tw-shadow-color is
registered inherits: false — so there is no single place to switch them
off per theme. Add elevation-xs, elevation-sm and elevation-panel
utilities that read a variable instead: flat in light, the values the
raw Tailwind utilities used in dark, so nothing shifts there.
Three rules now hold:
controls no shadow in either theme. Buttons, inputs, switches, and
select triggers are flat; segmented controls that used
shadow-sm as their only selected-state signal get
ring-1 ring-border, which reads in both themes.
surfaces elevation-*. Cards, form panels, toolbars, icon chips.
floating untouched. Popovers, dropdowns, dialogs, sheets and the
slide-over detail panels overlay the page and need the
separation in both themes.
The utilities compose through --tw-shadow rather than setting box-shadow
directly, because ring-* builds its outline from the same box-shadow
list; setting the property outright made elevation-* win on cascade
order and erased the ring on every card in light mode.
Also replaces the two sidebar menu outlines drawn as
shadow-[0_0_0_1px_hsl(var(--sidebar-border))] with ring utilities. Those
resolved to hsl(oklch(...)) after the oklch token migration, which is
invalid, so the outline they intended has not been rendering.
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
…-server card Signed-off-by: Anna Effort <anna.effort@ibm.com>
…aceholders Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
Contributor
Author
|
Note from work while building the pending-invitations chip for IBM/mcp-context-forge#5536... components for that all inherit shadow-xs from the outline variant, so each needed a shadow-none at the call site. That can be removed when this work is merged. |
a-effort
added a commit
that referenced
this pull request
Sep 11, 2026
Measured against the Figma frame: 8px padding on all four sides with a content-driven height, 4px corners, and an explicit foreground on the label, which was inheriting its colour. The icon square is 17.5px rather than the drawn 20px. At 20px the chip stands 38px tall, and the Settings tab row reserves 32px for its toolbar contents, so the drawn size pushes the whole row taller. The three outline buttons opt out of the variant's shadow-xs, which reads as an unwanted shadow in light mode. Removing it at the variant belongs to #35, so these join the dead overrides that PR already sweeps up. Signed-off-by: Anna Effort <anna.effort@ibm.com>
vishu-bh
pushed a commit
that referenced
this pull request
Oct 1, 2026
* Add a team icon background token The teams table and the members dialog each hardcoded bg-yellow-500. The invitations dialog needs the same square, so give it a token named for the domain rather than shipping a third copy. Value is #ffd200 from the design, not yellow-500's #eab308, so the existing squares change shade slightly. No .dark override, for the reason the other icon-bg tokens already state. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the invitations API module Transport for the invitee's own invitations, for #5536. Two of the three routes do not exist yet and are the subject of #6010, which also leaves the inbox path undecided. Both unknowns are confined to this module, so settling them is a one-line change here. TeamInvitation re-exports the generated TeamInvitationResponse rather than restating it, so regenerating the client turns any drift between what this assumed and what the backend ships into a compile error. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the invitations message bundle Its own bundle rather than keys in teams.json, since the components consuming it sit outside components/teams and a later surface elsewhere in the app should not reach into the teams bundle. The invitation sentence is one message with named placeholders so translators control word order, and the count uses ICU plural. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the pending invitation row One invitation as a sentence plus either its two actions or the outcome that replaced them. Accepted and declined are deliberately asymmetric, per node 5966:29865: accepted gets a check and full contrast, declined is muted with no icon. Roles map to display names with a fallback to the raw string, so an unrecognised role stays readable. Renders a div rather than an li, so a surface showing one invitation inline is not forced into list markup. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the pending invitations dialog Presentational: it takes the list and callbacks and owns no fetching. Once nothing is left to act on it holds the confirmation for 4000ms and closes itself, reusing sonner's default duration so the app has one dwell rather than two. Pointer entry cancels the timer outright rather than pausing it, since a timer that resumes on pointer exit closes at an unpredictable moment. Radix's usual pause-while-focused signal is unusable because the dialog traps focus. Scroll is scoped to the list rather than the dialog. DialogContent's own overflow puts the close button inside the scroll container, so past roughly seven invitations the X scrolls out of reach. Resolving a row unmounts the focused button, so focus moves to the next actionable row or to the close button, which is what the new data-slot on the primitive is for. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the pending invitations data hook Resolutions are committed after the server confirms, not optimistically: both actions are irreversible and the dialog stays open, so a rollback would visibly flip a row back. Named Data to keep it distinct from the context consumer of the same idea. An accidental second instance holds its own resolutions, which is the divergent count the provider exists to prevent, so the two should not be one import typo apart. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the pending invitations provider One provider, one dialog, N triggers. Two mounted copies of a self-contained component would hold separate resolutions, so resolving in one would leave the other's count stale with nothing logged. Mounted in AppShell, which an app-wide fetch would not justify on its own. The fetch is gated on a mounted consumer instead, so a page with no trigger issues no request. That gating is also what lets this land ahead of #6010 with no feature flag: nothing calls the missing routes until a trigger is rendered. Focus return is handled here rather than by a DialogTrigger, which would force every trigger into one dialog's subtree. The element that opened the dialog is commonly gone by close, since resolving the last invitation unmounts every trigger. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Add the pending invitations chip The Settings > Teams trigger, hidden at zero pending. Not yet rendered anywhere. Mounting it registers the consumer that starts the fetch, so the Teams page wiring waits on #6010. Takes the height and padding of the Create team button it will sit beside rather than the design's standalone 36px chip. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Apply design review to the invitations chip Measured against the Figma frame: 8px padding on all four sides with a content-driven height, 4px corners, and an explicit foreground on the label, which was inheriting its colour. The icon square is 17.5px rather than the drawn 20px. At 20px the chip stands 38px tall, and the Settings tab row reserves 32px for its toolbar contents, so the drawn size pushes the whole row taller. The three outline buttons opt out of the variant's shadow-xs, which reads as an unwanted shadow in light mode. Removing it at the variant belongs to #35, so these join the dead overrides that PR already sweeps up. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Mount the invitations chip on the Teams page The step this feature could not land earlier. Mounting the chip is what registers a consumer, which is what enables the fetch, so this is the only change that calls the routes IBM/mcp-context-forge#6010 has yet to add. Everything before it merges inert. Teams reacts to an accepted invitation through a counter in context rather than a callback prop, so a later surface elsewhere in the app can react independently of whichever one rendered the trigger. e2e/invitations.spec.ts stubs the three routes through the api-mock fixture, one test per visual state. Under E2E_REAL_API=true it becomes the integration test, once e2e/seed/seed.ts can seed a pending invitation for the test user. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Trim the invitations comments Cut the build-time narrative: which routes did not exist yet, which alternatives were weighed, why a structure was chosen over another. That belongs in the PR and the issues, and goes stale in the code. What is left says what a reader needs to work with it: mount one hook instance, the row is not an <li>, the dwell never resumes, focus return is manual because the opener is usually gone. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Apply design review to the invitations dialog Corners at 12px. The sentence and the action row lose their 16px inset, so both sit flush with the header against the dialog's own padding. Roles render lowercase, matching teams.json, with the English article inside the value: "an owner", "a member". pt-BR and es-ES carry no article, since "como proprietário" is right and "como um proprietário" is not. The resolved row keeps the height of the buttons it replaces, so accepting or declining no longer shifts the invitation below it, and the accepted check uses the success token. The list no longer scrolls. scrollbar-gutter-stable goes on DialogContent, which is a scroll container through the primitive; the property does not inherit, so it needs its own. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Cover the Teams page invitations wiring The chip renders when invitations are pending and not otherwise, and the teams list refetches when acceptedCount increments. The refetch is what puts a newly joined team in the table, and it had no coverage at all. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Drop a scratch test committed by mistake It was debug instrumentation for the HeaderProfileMenu scroll lock, with no assertions, and it failed both lint and format:check. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Apply review fixes to the pending invitations UI The dwell now cancels on pointer movement or a key press during it, rather than on pointer entry: resolving the last invitation means clicking inside the dialog, so entry alone cancelled the dwell for every mouse user. The accept button's accessible name now contains its visible label in all three locales, so voice control can address it. The load error gets a localised headline over the sanitised detail, the pattern the Teams page already uses. sanitizeError only returns English. The chip moves out of the has-teams branch, so someone invited to a first team can still reach the invitation. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Drop expired invitations and announce each resolution Expired invitations are filtered as they arrive. The inbox filters on is_active, which stays true past the expiry date, so they would otherwise be counted and offered a Join that cannot succeed. The dialog no longer stays open with nothing in it: a refetch that comes back empty closes it, and open() no-ops at a count of zero. An error keeps it open so the retry stays reachable. The sr-only region now names the row in flight and each outcome as it lands, rather than only reporting once everything is resolved. The toolbar no longer renders empty scaffolding, and the chip drops its trailing margin when it has no siblings. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Fix three e2e assertions that had never been executed A role=status count of zero can never hold: HeaderQuickNav mounts a permanent announcer on every page. An open modal puts the chip in an aria-hidden subtree, so the role query has to opt into it. Outcome text now also appears in the announcer, so the row assertions are scoped to the row. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Address Copilot review on the invitations dialog A refetch answered after the user acted no longer publishes: it would drop the row they just resolved along with its confirmation. The dialog also stays open while a request is in flight, so its outcome can be shown. The sr-only summary counts what is still pending rather than the original total, which it was calling pending after rows resolved. The post-accept teams refresh catches its rejection. useQuery.execute rethrows after recording the error, so the bare call leaked an unhandled rejection; the other refresh paths in the page already catch. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Move the invitations badge into the header Settings lists the Teams tab only for is_admin and redirects everyone else off /app/settings/teams, so the badge was unreachable by the invitees it is for. The header renders on every authenticated page for every role, and the provider already wraps it. The badge itself is unchanged. The Teams toolbar goes back to what it was, and createTeamRef goes with it, since it existed only as the badge's focus fallback. The e2e suite missed this because its default user is an admin, so the new case signs in as a non-admin. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Restyle the invitations badge for the header Drops the box from 38px to 28px: the h-auto override is gone, so size="sm" applies, with 6px of vertical padding and a 16px icon square to match. The half-pixel sizes go with it. Team icon chips move to the brand cyan, which --server-icon-bg already carries. That recolours the Teams table and the manage-members dialog too, both of which moved onto this token earlier in the branch. The count names the entity, since the badge is now app-wide rather than on the Teams page. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Refresh the inbox on focus and announce every transition The list was fetched once per session: the mount effect is the only automatic load, and the trigger hides at a count of zero, so an empty first response left no way to discover a later invitation. It now refetches when the tab regains focus, if the list is more than a minute old. Expiry is rechecked against expires_at rather than trusting the server's is_expired from load time. The announcer took one in-flight entry and one newly resolved id, so a second concurrent action was never announced, the first outcome was suppressed while the second ran, and batched resolutions lost one. It now covers every row. Invitation fixtures move to a relative expiry, which the date check would otherwise have made stale on 2026-09-17. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Address review feedback on the invitations UI Pause the staleness refresh while the dialog is open, so a list landing behind it cannot drop a committed resolution and its confirmation. Skip busy rows when moving focus after a row resolves: their buttons are disabled, so focusing one leaves focus on the body. Seed the post-accept team refetch on mount, in Teams and now also in TeamSwitcher, which was never refreshed at all. Move the TeamForm icon onto the team token the rest of the page uses. Signed-off-by: Anna Effort <anna.effort@ibm.com> * Clear the loading flag on unregister and accumulate announcements Signed-off-by: Anna Effort <anna.effort@ibm.com> * fix: Drop lapsed invitations on a timer and recheck expiry on accept The expiry sweep runs while the dialog is closed, so a tab that is never blurred stops reporting lapsed invitations as pending. It refilters state rather than refetching. Accept rechecks expiry at the click, covering the open dialog where the sweep is paused. Decline stays unguarded. Signed-off-by: Anna Effort <anna.effort@ibm.com> * fix: Stop the expiry-click refetch clobbering a sibling confirmation Accept on a lapsed row showed the toast and refetched. The server drops an invitation already accepted in the same dialog, so the fresh list took that row and its confirmation with it, emptying the dialog and triggering the auto-close. The toast alone is the response now. Signed-off-by: Anna Effort <anna.effort@ibm.com> * fix: Hide the invitations retry once every row is resolved Signed-off-by: Anna Effort <anna.effort@ibm.com> * chore: refresh openapi.json to API v1.0.11+aac1bf Signed-off-by: Anna Effort <anna.effort@ibm.com> --------- Signed-off-by: Anna Effort <anna.effort@ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #34. Light theme is flat by design: surfaces get no elevation, controls carry none in either theme, and floating layers keep theirs.
Why a token
Tailwind inlines shadow values at build time — only the color is a variable, and
--tw-shadow-coloris registeredinherits: false— so there is no single place to switch shadows off per theme.src/index.cssgains four utilities that read a variable instead:elevation-xsshadow-xselevation-smshadow-smelevation-panelelevation-controlDark mode keeps the exact values the raw utilities used, so nothing shifts there.
They compose through
--tw-shadowrather than settingbox-shadowdirectly. This matters:ring-*builds its outline from the samebox-shadowlist, and.elevation-*is emitted after.ring-1, so setting the property outright erased the ring on every card in light mode.Three rules
elevation-*: cards, form panels, toolbars, icon chips.elevation-controlis the deliberate exception. Flattening the selected chip of a segmented control loses what makes it read as selected, so it stays raised in both themes. All 13 segmented controls in the app now share it — previously they split across two shadow levels and a dark-fill variant.Also here
bg-white text-blacksubmit button that was invisible against the light card, and dropped five one-off hex literals (#141414,#2b2b2f,#55555c,#252529,#343438).neutral-400/neutral-500totext-muted-foreground— more legible in both themes. ~12 overrides remain in the tools/resources/prompts forms; see the note below.variant="ghost"carriesaria-expanded:bg-muted, so Optional configuration and Advanced settings filled whenever expanded.tool-add-icontokens like its siblings, instead of a hardcoded gray.shadow-[0_0_0_1px_hsl(var(--sidebar-border))]resolves tohsl(oklch(...))after the token migration, which is invalid — that outline has not been rendering. Replaced withring-*.Testing
2841 unit tests pass;
tsc --noEmit,eslint, andprettier --checkclean. One test needed updating:input.test.tsxasserted the input carriesshadow-xs.The utilities were verified against the emitted CSS — light resolves to
0 0 #0000, dark to the original Tailwind values byte-for-byte, and all four variant forms (aria-pressed:,peer-checked:,data-[state=active]:,data-[state=checked]:) compile.This is a visual change and the tests do not cover it. Both regressions caught during development — the ring clobbering above, and a card that went white in dark mode — were found by eye, not by CI. Worth a pass through both themes before merge.
Not included
--card(which currently equals--mainexactly, sobg-cardgives no separation from the page). Parked onform-card-fill; needs rework.ui/inputandui/textareadisagree on placeholder colour — the token vs. hardcoded neutrals, running opposite to each other. ~12 per-form overrides work around it. Fixing it intextarea.tsxis one line but touches every textarea.shadow-noneoverrides, left over from cancelling shadows the primitives no longer have.