[TEST] Facelift on Vibe 3 - #3466
Conversation
Port the facelift style foundation onto the version3 base:
- add .facelift-{light,dark,black}-app-theme themes
- add --surface-{primary,positive,negative,warning}-color to the four
existing themes, plus --info-color-selected/-hover so `info` and the
deprecated `neutral` element colors resolve on non-facelift themes too
- add --border-radius-full (99px) and allow it in the stylelint rule
- register the facelift themes in generate-colors THEME_PATTERNS and
regenerate tokens.json
- remap MapStateSelected(Hover)Color onto the new --surface-* vars, add
`info` and deprecate `neutral`
Component styles that need no markup change: - ButtonGroup: active segment uses primary fill, disabled uses token colors - Menu: 8px inner padding, min-width instead of fixed width, overflow visible - BaseMenuItem: full-width rounded rows, radius moved to the row itself - MenuItemIcon: fixed 20x20 icon slot; MenuItem label renders as small Label - MenuItemSubMenu / MenuButton: dialog padding "none" so the menu owns padding - Label: kindFill switches to --surface-* fills with colored text - Checkbox / DialogContentContainer / ToastLink: token and radius touch-ups
- root uses `gap: var(--icon-gap)` instead of per-icon margins, so the .leftIcon/.rightIcon classes and their className plumbing are dropped - per-size --icon-gap / --padding-inline, with .hasLeftIcon/.hasRightIcon trimming one --space-2 to optically balance the icon side bearing - medium padding-inline 16px -> 12px, large 24px -> 16px - loader gets a fixed 20px height; textPlaceholder mirrors the flex gap - secondary active state drops its primary border Deviation from the v4 facelift branch: it deleted .marginRight/.marginLeft while Button.tsx still applied them, silently turning two public props into no-ops. Both classes are kept here.
- input radius 4px -> 8px (--border-radius-medium) - medium height 40px -> 36px with text2, large 48px -> 40px - new `iconPosition="left"` prop: pins the primary icon to the inline start, keeps it fully opaque, and reserves 36px of input padding for it Focus/active already uses --primary-color on version3, so the facelift's "TextField focus primary" change is a no-op here.
Selection is no longer a filled row background - the row keeps the plain hover treatment and a trailing Check icon marks the selected item. Adds .selectedCheck, tightens the row radius to 6px, and lets .endElement lay out multiple children.
Replaces the per-tab scaleX(0/1) bottom border with a single 2px indicator rendered once on TabList and slid into place. Position and width are measured from the active tab's rect in a useLayoutEffect, re-measured via ResizeObserver, and snap without animation on first paint or when prefers-reduced-motion is set. Tabs themselves become borderless pills: secondary text by default, primary when active, 8px-radius hover background, and shorter list heights (36/32/40px for medium/small/large).
- normal/positive/negative/warning all render on --primary-background-color
with --primary-text-color; the type is carried by a 24px round icon badge
instead of a full-bleed colored surface
- portals to document.body so the toast stacks above app chrome; new
`inline` prop renders in place for gallery/static previews
- message truncates with ellipsis and gets a Tooltip once it overflows
- close button and action buttons drop to size="xs" and follow the type
(fixed-light on dark, primary otherwise)
- icon glyph is 16px with its color set inline, to beat app-level
`svg[class*="icon_"] { color: var(--icon-color) }` rules
- fully rounded pill (--border-radius-full) and a new `variant` prop: `default` | `readOnly` | `filterable` (primary-themed toggle with default/hover/pressed states, driven by the new `pressed` prop) - new `size` prop: `medium` (24px) | `small` (20px, text3, 14px icons) - padding tightens to 4px on sides that carry an icon or avatar - the close control is absolutely positioned and only revealed on hover/focus-within; the resting width is locked in a useLayoutEffect so the hover padding truncates the label instead of growing the chip - new Chips.types.ts (ChipsVariant, ChipsSize), re-exported from index Chips.colors / Chips.avatarTypes static props are kept, unlike the v4 facelift branch which dropped them.
Ports the facelift's new SegmentedControl: xs/small/medium/large sizes, optional per-option icon and tooltip, roving-tabindex keyboard navigation. Registered in the component barrel and in ComponentDefaultTestId / ComponentVibeId. Icon uses the v3 iconSize prop and useMergeRef comes from the local hook rather than @vibe/shared.
New tests: - BaseItem: check icon appears when selected, not when selected + readOnly - Chips: readOnly ignores onClick and hides close; filterable hides close, fires onClick, and sets aria-pressed - TextField: iconPosition="left" marks the wrapper and icon container, and is ignored when there is no icon Toast.snapshot.test.tsx mocks createPortal as a passthrough, since react-test-renderer cannot render portals. Button.test.jsx asserts the new root-level hasLeftIcon class instead of the removed per-icon leftIcon class. Refreshed snapshots follow from the facelift styling: Chips, Label, Toast, TabList, TabsContext, MenuItem, Dropdown, SplitButton, Button, plus the component-exports snapshot gaining SegmentedControl.
Brings apps/kitchen-sink over from the v4 facelift branch and repoints it at Vibe 3. It sits outside the yarn workspace, so it cannot affect the published packages. - repin both package.json and published-vibe/package.json to the v3 line (@vibe/core 3.88.6 etc.) and swap @vibe/style 4.0.0 for monday-ui-style 0.26.2, which is how the style package is published on v3 - vite configs alias monday-ui-style instead of @vibe/style; Current points at the workspace build (facelift themes), Original at the published copy - main.tsx imports monday-ui-style/dist/index.min.css explicitly: @vibe/core declares `sideEffects: ["*.scss","*.css",...]`, so its style-imports.ts is treeshaken and the theme CSS never reached the bundle. Previously the facelift tokens only existed as a hand-written polyfill in src/styles.css - Dropdown usages move to @vibe/core/next; on v3 the plain @vibe/core Dropdown is still the legacy react-select one - Icon props follow the v3 API: size -> iconSize (31 sites), label -> iconLabel - drop `as const` on the ButtonGroup options (v3 wants a mutable array), remove `baseUrl` (removed in TS 7), and drop a stray Arduino log.txt Verified: both `npm run build:current` and `npm run build:original` succeed, with the two runtimes correctly isolated (facelift indigo present in Current, absent in Original).
kitchen-sink resolved @types/react 18.3.31 (csstype 3.2.3) while the workspace sources it aliases resolve the root's 18.2.69 (csstype 3.1.3). Two csstype copies make React.CSSProperties structurally incompatible, which broke SubIcon assignability for every @mondaydotcomorg/icons glyph (27 errors across src/screens). Pin both to the workspace versions. tsc --noEmit now reports 0 errors.
- prettier collapses the Button icon ternaries onto one line - Toast.snapshot.test.tsx uses `import type * as ReactDOM` instead of an `import()` type annotation, which v3's consistent-type-imports forbids `yarn lint` now passes for all 18 projects.
Not needed: Vite does not treeshake in dev, so `npm run dev` already loads @vibe/core's style-imports.ts and, through the workspace alias, the facelift themes. kitchen-sink carries its own token override in src/styles.css, the same pattern mf-topbar uses with src/styles/themes/warmth.css. Only `vite build` output is affected (the sideEffects field lets the theme CSS be treeshaken there); the dev panes are unaffected.
ResizeObserver.observe() always fires one callback immediately, and the observer effect depended on updateSelectedIndicator, so it re-subscribed on every tab change. One frame into the glide that callback ran the non-animated path, which sets `transition: none` and forces a reflow - snapping the indicator to its target and killing the animation. - remember the last applied left/width and make a non-animated update a no-op when the geometry has not actually changed, so the observer's initial callback cannot interrupt an in-flight transition - hold the updater in a ref so the observer stays subscribed across tab changes instead of tearing down and re-observing each time
Was --primary-text-color, copied from the facelift branch. Matches v3's previous tab underline, which used var(--primary-color) via .tabWrapper:after. kitchen-sink's injected Original-pane indicator follows, so both panes agree.
Adds six semantic surfaces and their paired text colours, per the Facelift "surface colors" spec (Figma 630:106200): --surface-primary / -positive / -negative / -warning / -neutral / -info --text-on-surface-* for each The Figma layers are the base colour at 60% alpha, so the surfaces are built with color-mix() over each theme's existing token rather than hard-coded hexes. Overriding e.g. --warning-color-selected now flows into --surface-warning automatically, and one value composites correctly over light, dark and black. Verified against the spec's flattened values: primary #e0efff, warning #fdf3c7 and neutral #f1f2f5 land exactly, positive is within 1/255, negative within 6/255 (the designer's flatten wasn't precisely 60% of that base). Wiring: - Colors.ts maps the six semantics 1:1 onto the surfaces (primary no longer borrows --ui-background-color, neutral is a real semantic rather than an alias of info) and gains getOnSurfaceTextColor() - Chips pairs its JS-driven fill with the matching text colour; content colors have no pairing and keep the default text colour - Label kindFill uses the surface + on-surface text for every semantic - AttentionBox (legacy and next) fills switch to the surfaces; their copy stays --primary-text-color, as the design keeps it neutral - the tokens are documented in colorsMap and listed in kitchen-sink's panel Facelift's primary surface is indigo (#d4daff at 60% → #e5e9ff). --surface-info stays the informational blue in every theme, which is the point of having it once primary moves to indigo. Dark themes follow their own bright base semantics for on-surface text, since the spec only covers light. kitchen-sink no longer polyfills the surfaces - the package owns them now. Its Original-pane simulation keeps working via var() fallbacks to the pre-facelift tokens.
The close control was pinned to --primary-text-color, so it stayed dark while the chip's text moved to --text-on-surface-* for semantic colors. It now uses currentColor and inherits like the leading/trailing icons. The override is spelled `.icon.close` (three classes) on purpose: the close control is an IconButton, and its own .kindTertiary.colorOnPrimaryColor rule sets --text-color-on-primary at two-class specificity, which would otherwise win or lose on stylesheet order and could render a white icon on a pale surface. AttentionBox icons (legacy and next) likewise switch to currentColor so they track the box's text rather than pinning --primary-text-color.
Root cause, replacing my earlier specificity workaround:
Text renders its own colour class - the label came out as
`typography primary ...`, i.e. --primary-text-color pinned on the element.
A declaration on the element always beats inheritance, so the label ignored
the chip's inline --text-on-surface-* while the icons, being currentColor,
picked it up. Label and icons therefore disagreed. No amount of specificity
on the chip's own rules could fix that, because the competing declaration was
on a different element.
- the label passes color="inherit", so it follows the chip's colour
- the close IconButton drops color="on-primary-color", a leftover from when
chips had saturated fills; it forced --text-color-on-primary (white) at
two-class specificity, tying with `.chips .icon` and leaving the outcome to
stylesheet order. Without it the button only declares
`.kindTertiary { color: --primary-text-color }` at one class, which
`.chips .icon { color: currentColor }` beats outright
- reverted the `.icon.close` three-class escalation, no longer needed
Core: 158 files / 1476 tests pass, stylelint unchanged at 100 pre-existing.
Updated rule: each semantic surface is its own colour at 12%, rather than 60% of the *-color-selected token. --surface-primary: color-mix(in srgb, var(--primary-color) 12%, transparent); --surface-positive: color-mix(in srgb, var(--positive-color) 12%, transparent); --surface-negative: color-mix(in srgb, var(--negative-color) 12%, transparent); --surface-warning: color-mix(in srgb, var(--warning-color) 12%, transparent); --surface-neutral: color-mix(in srgb, var(--primary-text-color) 12%, transparent); --surface-info: color-mix(in srgb, var(--info-color) 12%, transparent); The formula is now byte-identical in all seven themes: each theme's own base colour flows through, so facelift's indigo --primary-color yields an indigo surface with no per-theme literal. The #d4daff hex the previous rule needed is gone. Adds --info-color (#0073ea) to every theme. v3 only had --info-color-selected, and `info` needs a base of its own so it can stay blue when the brand primary moves to indigo. Two derivations are mine, not from the spec, and worth a designer check: - neutral has no semantic colour, so it derives from --primary-text-color, which flips correctly between light and dark themes - --info-color is the classic monday blue
Chips resolved its fill in JS and wrote it to `style`, which is why consumers needed !important to recolour a chip: nothing but an important author declaration outranks an inline style. The fill was inline because hover was modelled in React state (isHovered + useSetFocus), so the value changed per render. Semantic colors now use the same `color-*` class pattern Label already has, reading --surface-* and --text-on-surface-*. The ~40 content colors can't each have a class, so they pass their palette in as a --chips-surface custom property; either way `background-color` itself stays in CSS, so hover is a plain :hover/:focus-within rule. That lets the JS hover machinery go entirely - isHovered, both mouse handlers and the useSetFocus call existed only to recompute the background, so this also drops a re-render on every hover. Content colors deliberately get no --chips-surface-hover: getElementColor returns early for them and ignores the hover palette, because no --color-*-selected-hover token exists. The previous JS hover branch produced the resting colour for them too, so behaviour is unchanged. A new test pins this down. kitchen-sink's chips-theme-* !important fills are deleted - the gallery already passes `color`, so the component handles it. Two of them had also drifted: -primary forced --ui-background-color and -info forced --surface-primary.
Triggers .github/workflows/prerelease.yml so the v3 microfrontends can install the facelift. Versions derive from this branch's package.json files, so this publishes @vibe/core@3.89.0-alpha-<sha> and monday-ui-style@0.27.0-alpha-<sha> rather than the 4.6.0-alpha line the v4 branch produces.
PR Summary by QodoPort Facelift themes and components to Vibe v3
AI Description
Diagram
High-Level Assessment
Files changed (227)
|
Code Review by Qodo
1. SCSS module imports external font
|
| @@ -0,0 +1,242 @@ | |||
| @import url("https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Syne:wght@600;700&display=swap"); | |||
There was a problem hiding this comment.
1. Scss module imports external font 📘 Rule violation ⚙ Maintainability
VibeAppBoardView.module.scss adds an @import statement, which is explicitly forbidden in CSS Module files. This also introduces a runtime dependency on an external stylesheet.
Agent Prompt
## Issue description
`VibeAppBoardView.module.scss` imports Google Fonts despite the prohibition on imports in `.module.scss` files.
## Issue Context
Load or declare the required fonts outside the component CSS Module, while retaining CSS Modules and design tokens for component styling.
## Fix Focus Areas
- apps/kitchen-sink/src/screens/components/VibeAppBoardView.module.scss[1-1]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const actual = await vi.importActual<typeof ReactDOM>("react-dom"); | ||
| return { | ||
| ...actual, | ||
| createPortal: (node: React.ReactNode) => node |
There was a problem hiding this comment.
2. Toast tests bypass portal behavior 📘 Rule violation ☼ Reliability
The updated Toast snapshot test replaces createPortal with an identity function instead of testing portal and DOM-dependent behavior. It therefore does not validate the newly changed rendering, truncation, tooltip, test-ID, or relevant accessibility behavior in a real DOM.
Agent Prompt
## Issue description
The Toast test bypasses portal rendering and continues to rely on snapshots, leaving the changed DOM behavior unverified.
## Issue Context
Use Vitest and Testing Library with a real DOM to exercise portal versus inline rendering, overflow-triggered tooltip behavior, established Toast test IDs, and relevant accessibility attributes.
## Fix Focus Areas
- packages/core/src/components/Toast/__tests__/Toast.snapshot.test.tsx[7-14]
- packages/core/src/components/Toast/Toast.tsx[172-204]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| {toastElement} | ||
| </CSSTransition>, | ||
| document.body |
There was a problem hiding this comment.
3. Toast escapes scoped themes 🐞 Bug ≡ Correctness
Portaling Toast to document.body moves it outside the wrapper where ThemeProvider scopes custom theme variables. Toasts under nested or custom providers therefore render using the body's theme rather than their provider's configured colors and tokens.
Agent Prompt
## Issue description
Toast portals to `document.body`, escaping scoped `ThemeProvider` styles and rendering with the wrong theme.
## Issue Context
`ThemeProvider` applies generated theme classes to a wrapper div, while Toast depends on inherited CSS variables such as `--primary-background-color`.
## Fix Focus Areas
- packages/core/src/components/Toast/Toast.tsx[244-260]
- packages/core/src/components/ThemeProvider/ThemeProvider.tsx[101-107]
- packages/core/src/components/Toast/Toast.module.scss[3-35]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Portal to body so Toast always stacks above app chrome (search, sticky headers). | ||
| return createPortal( | ||
| <CSSTransition | ||
| in={open} | ||
| nodeRef={nodeRef} | ||
| classNames={{ enterActive: styles.enterActive, exitActive: styles.exitActive }} | ||
| timeout={400} | ||
| unmountOnExit | ||
| > | ||
| {toastElement} | ||
| </CSSTransition>, | ||
| document.body |
There was a problem hiding this comment.
4. Toast crashes server rendering 🐞 Bug ☼ Reliability
Every non-inline Toast now reads document.body synchronously during render. Rendering Toast during SSR or prerendering throws ReferenceError: document is not defined, whereas the previous implementation did not access browser globals during render.
Agent Prompt
## Issue description
Non-inline Toast accesses `document.body` during render and crashes in document-less environments.
## Issue Context
Other portaled components in this package use `isClient()` before creating their portals.
## Fix Focus Areas
- packages/core/src/components/Toast/Toast.tsx[244-260]
- packages/core/src/components/LegacyModal/LegacyModal.tsx[208-208]
- packages/core/src/components/next/Dialog/Dialog.tsx[527-536]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| try { | ||
| return JSON.parse(serializedState) as AppState; | ||
| } catch { |
There was a problem hiding this comment.
5. Transferred state crashes startup 🐞 Bug ☼ Reliability
readTransferredVibeState casts arbitrary query-string JSON directly to AppState, and the
provider prefers it over validated persisted defaults. A URL such as ?vibeKitchenSinkState={}
crashes initial rendering when theme code dereferences the missing tokenOverrides.colors.
Agent Prompt
## Issue description
Malformed or partial transferred state from the URL can crash the application during its first render.
## Issue Context
The query parameter is externally controllable and is consumed before the effect that removes it. Persisted state already has a validation/defaulting path that transferred state should reuse.
## Fix Focus Areas
- apps/kitchen-sink/src/lib/vibeSource.ts[15-24]
- apps/kitchen-sink/src/context/KitchenSinkContext.tsx[53-67]
- apps/kitchen-sink/src/lib/storage.ts[1-131]
- apps/kitchen-sink/src/lib/cssVarOverrides.ts[6-21]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| --surface-primary: color-mix(in srgb, var(--primary-color) 12%, transparent); | ||
| --surface-positive: color-mix(in srgb, var(--positive-color) 12%, transparent); | ||
| --surface-negative: color-mix(in srgb, var(--negative-color) 12%, transparent); | ||
| --surface-warning: color-mix(in srgb, var(--warning-color) 12%, transparent); |
There was a problem hiding this comment.
6. Semantic tokens omitted 🐞 Bug ≡ Correctness
The new --surface-* and --text-on-surface-* variables are silently omitted from generated tokens.json because the generator only accepts property names containing color. The generated token artifact is therefore inconsistent with the facelift theme CSS and the public core color map.
Agent Prompt
## Issue description
The token generator drops the newly introduced semantic surface and on-surface color variables.
## Issue Context
The variables do not contain the substring `color`, but they are advertised as supported color tokens by the core color map.
## Fix Focus Areas
- packages/style/scripts/generate-colors.ts[72-113]
- packages/style/src/themes/facelift-light-theme.scss[11-22]
- packages/style/src/themes/facelift-dark-theme.scss[11-22]
- packages/style/src/themes/facelift-black-theme.scss[11-22]
- packages/core/src/utils/colors-vars-map.ts[84-99]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| aria-checked={isSelected} | ||
| aria-disabled={isDisabled || undefined} | ||
| tabIndex={isSelected || (selectedIndex === -1 && index === 0) ? 0 : -1} | ||
| onClick={() => onSelect(index)} |
There was a problem hiding this comment.
7. Disabled segment remains focusable 🐞 Bug ≡ Correctness
SegmentedControl assigns tabIndex={0} to a selected segment, or to the first segment when no value
matches, without excluding disabled options. A disabled selected/first option therefore remains in
the keyboard tab order even though selection logic treats it as unavailable.
Agent Prompt
## Issue description
Disabled SegmentedControl options can remain keyboard-focusable through the roving tabindex implementation.
## Issue Context
The selected index can identify a disabled option, and the no-selection fallback always targets index zero regardless of its disabled state.
## Fix Focus Areas
- packages/core/src/components/SegmentedControl/SegmentedControl.tsx[53-63]
- packages/core/src/components/SegmentedControl/SegmentedControl.tsx[101-118]
- packages/core/src/components/SegmentedControl/SegmentedControl.tsx[198-216]
- packages/core/src/components/SegmentedControl/__tests__/SegmentedControl.test.tsx[1-128]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <IconButton | ||
| icon={Workspace} | ||
| kind={listView ? "tertiary" : "secondary"} | ||
| size="xs" | ||
| aria-label="Grid view" | ||
| aria-pressed={!listView} | ||
| onClick={() => setListView(false)} |
There was a problem hiding this comment.
8. Grid view never renders 🐞 Bug ≡ Correctness
The Manage Agents “Grid view” button changes listView to false, but listView only controls the two toggle buttons' appearance. The page unconditionally renders the same table and rows, so activating Grid view produces no corresponding view change.
Agent Prompt
## Issue description
The Manage Agents view toggle advertises a grid layout but never changes the rendered agents layout.
## Issue Context
The existing state only changes button styling and ARIA state; the table is unconditional.
## Fix Focus Areas
- apps/kitchen-sink/src/screens/components/ManageAgentsPage.tsx[131-133]
- apps/kitchen-sink/src/screens/components/ManageAgentsPage.tsx[172-208]
- apps/kitchen-sink/src/screens/components/ManageAgentsPage.module.scss[1-248]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| setTimeout(() => { | ||
| setNewMessageIds(new Set()); | ||
| }, 5000); |
There was a problem hiding this comment.
9. Older timer clears new messages 🐞 Bug ≡ Correctness
Each Sidekick send schedules a timeout that clears the entire newMessageIds set. If another message is sent within five seconds, the earlier timeout removes the newer message's animation state prematurely and the timers are not cancelled on unmount.
Agent Prompt
## Issue description
An older Sidekick send timeout clears animation state belonging to messages sent later.
## Issue Context
Every timer replaces the complete set rather than removing only the IDs created by that send, and timer handles are not cleaned up.
## Fix Focus Areas
- apps/kitchen-sink/src/screens/components/SidekickChatPage.tsx[331-350]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const handleLastSegmentAnimationEnd = useCallback(() => { | ||
| if (animationEndRef.current && isComplete) { | ||
| animationEndRef.current(); | ||
| } |
There was a problem hiding this comment.
10. Completion callback fires twice 🐞 Bug ≡ Correctness
Fade-mode ResponseStream invokes onComplete once when text processing finishes and again when the final segment's CSS animation ends. The Sidekick consumer consequently receives two completion notifications for one response and performs redundant state updates.
Agent Prompt
## Issue description
ResponseStream calls its completion callback twice for a single fade-mode stream.
## Issue Context
`markComplete` already invokes the callback; the final animation handler invokes the same callback independently.
## Fix Focus Areas
- apps/kitchen-sink/src/screens/components/ResponseStream.tsx[140-145]
- apps/kitchen-sink/src/screens/components/ResponseStream.tsx[320-327]
- apps/kitchen-sink/src/screens/components/ResponseStream.tsx[352-381]
- apps/kitchen-sink/src/screens/components/SidekickChatPage.tsx[170-181]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
A new prerelease version of this PR has been published! 🎉 |
--primary-color #2b06e5 -> #3543fc --brand-color #2b06e5 -> #3543fc (follows primary) --positive-color #00854d -> #166d30 --negative-color #cd0026 (was #d83a52) --warning-color unchanged, marked UNTOUCHED in the design --primary-text-color unchanged; the swatch is bound to the existing token Applied to the three facelift themes only; the standard light/dark/black/hacker themes keep the monday palette. All six surfaces follow automatically, since they are color-mix of these at 12% - no surface token needed editing. Flattened on white the facelift surfaces are now primary #e7e8ff, positive #e3ede6, negative #f9e0e5, warning #fff9e0. --text-on-surface-primary was the literal #3543fc, which is now exactly --primary-color, so it derives from it instead of duplicating the hex.
Palette experiments belong in the consumer override, not the design system: mf-topbar's src/styles/themes/warmth.css can retune several themes at once without republishing Vibe. So --primary-color, --brand-color, --positive-color and --negative-color go back to their previous facelift values, and tokens.json is byte-identical to before the palette commit. Kept, because it is structure rather than a colour choice: --text-on-surface-primary stays var(--primary-color) instead of a duplicated hex, so it follows whatever primary the theme or an override sets. The surface work itself is unaffected: --surface-* are color-mix of these bases at 12%, so they track the reverted values automatically.
The palette lives here rather than in the design system, so several themes can be retuned without republishing Vibe - the same pattern as mf-topbar's src/styles/themes/warmth.css. styles.css sets only the base colours for the three facelift theme classes (Figma 684:115861 PRIMARIES: #3543fc / #166d30 / #cd0026; warning and --primary-text-color untouched). Everything derived follows on its own: --surface-* are color-mix of these at 12%, and --text-on-surface-primary is var(--primary-color). The selectors are qualified with `body` deliberately. AppThemeShell puts the theme class on <body>, and the package defines .facelift-*-app-theme at the same one-class specificity; in dev nothing is treeshaken and the package CSS is injected after styles.css, so an unqualified selector would lose the tie on source order. screens/styles/globals.css carries the same primaries for the Glaze screens.
…mth.css
Replaces the primaries-only block with a port of
mf-topbar/src/styles/themes/warmth.css, so the kitchen sink and mf-topbar
render the same palette instead of two hand-maintained subsets. Verified
token-for-token: 64 light, 39 dark, 39 black, identical names and values.
Only the selectors differ. mf-topbar puts `.warmth` on <html> and leans on the
host's light/dark/black class on <body>; the kitchen sink's AppThemeShell puts
`facelift-<mode>-app-theme` on <body>, so:
:root.warmth .light-app-theme, :root.warmth .default-app-theme
-> body.facelift-light-app-theme
They stay qualified with `body` because the style package defines
`.facelift-*-app-theme` at the same one-class specificity, and in dev its CSS is
injected after this file, so an unqualified selector would lose on source order.
Dropped from the port: topbar's `div[class*='useLocalDevMf']` rule, which hides
its own local-dev badge.
Also removed the `--ui-background-hover-color: #dfdbd8` one-off from styles.css;
warmth.css sets that token (#e8e6e6 for light) and now owns it.
This brings surfaces, borders, icons and radius across, not just the primaries.
Anything derived still follows on its own: the six --surface-* are color-mix of
these bases at 12%, and --text-on-surface-primary is var(--primary-color).
Aligns the theme sources with mf-topbar's warmth.css, which is where this
palette was tuned:
* Semantic surfaces are mixed against --primary-background-color rather
than `transparent`, at 16% (neutral stays 12%). Transparent surfaces
let whatever sits behind a Chip or AttentionBox bleed through, so the
same token read differently depending on its container.
* --text-on-surface-* reference the semantic tokens (--positive-color,
--negative-color, --warning-color, --info-color, --link-color) instead
of hardcoded hexes, so they follow the theme. Primary uses
--link-color: in the dark themes --primary-color is too dark to read
against its own surface.
* The dark and black palettes are lifted a step (dark
--primary-background-color #1a1a19 -> #2a2a28), with the kitchen-sink
theme overrides moved in lockstep.
These sources had drifted behind their own generated artifact:
packages/style/src/files/tokens.json was already committed with these
values, so `generate-colors` is a no-op on this commit. This makes the
sources match what the tokens have been describing.
Two changes, applied across light, dark, black, hacker and the three facelift themes rather than only the facelift ones, so a non-facelift theme does not get a surface treatment that disagrees with the rest. - Surfaces drop from 12%/16% to 8% and mix over --primary-background-color instead of `transparent`. A translucent surface picks up whatever sits behind it, so nesting one inside another compounded the tint; opaque surfaces render the same wherever they land. - --secondary-background-color aligns with --allgrey-background-color in the dark and black themes (#404660 -> #363a50, #424240 -> #2e2e2c), which were reading as a third distinct grey next to the new surfaces. The surface comments still claimed 12% and "keeps them derived" without saying what they now mix over — updated in all six themes. tokens.json regenerated from the sources (verified idempotent). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
A new prerelease version of this PR has been published! 🎉 |
Add a unified Home screens page with mode-specific stroke palettes, and port the pointer-following composer border animation across chat pages. Co-authored-by: Cursor <cursoragent@cursor.com>
Tighten Home mode subtitles so the greeting lives only in the title. Co-authored-by: Cursor <cursoragent@cursor.com>
…ase] Co-authored-by: Cursor <cursoragent@cursor.com>
|
A new prerelease version of this PR has been published! 🎉 |
Co-authored-by: Cursor <cursoragent@cursor.com>
Scope of change — what this PR does and does not touch
This PR only adds commits to the new branch
facelift/v3and proposes merging them intoversion3later. Nothing else has been modified:version3(base)e0158f583. This PR is a draft; nothing is merged into it.master(Vibe 4)56a5ab194.version3-facelift-exploration(the existing v4 facelift branch)8cfcbd7e8. Not rebased, not force-pushed, not deleted. Its4.6.0-alpha-*prereleases keep working formf-topbar.facelift/v3(this branch)version3. The only branch written to.No force-push, no history rewrite, no branch deletion. The prerelease publishes new package versions on the 3.x line and does not overwrite or retag anything already published.
Why this branch exists
The facelift work was originally cut from
version3so the cluster's microfrontends could install it. Commit84c410d8dthen mergedorigin/master(v4) into it, which flipped@vibe/coreto 4.5.3 — so prereleases published as4.6.0-alpha-*. Onlymf-topbarcould install those; mf-header (^3.88.3), mf-leftpane (3.81.1), mf-workspace-page (^3.75.0), mf-ai-editor (^3.83.1) and mf-ai-app-builder (^3.83.4) could not. That merge also left the branch a hybrid:TextField.tsx/TabList.tsxkept v3 source whileMenuButton.tsx/Dropdown/took v4.Merging
version3back into it could not undo that, so this re-applies the facelift onto a cleanversion3base. Prereleases from here are@vibe/core@3.89.0-alpha-*andmonday-ui-style@0.27.0-alpha-*.What's in it
Semantic surfaces — six tokens plus paired text colours, added to all seven themes:
color-mixkeeps them derived, so overriding a base token flows through and the block is byte-identical in every theme — facelift's indigo--primary-coloryields an indigo surface with no per-theme literal. Also adds--info-color, which v3 lacked.Components — facelift themes (
.facelift-{light,dark,black}-app-theme) and--border-radius-full; Tabs sliding indicator; Toast white-surface redesign with semantic icon badge; Chips pill radius plusvariant/size; BaseItem check-icon selection; TextFieldiconPosition; Button icon gap and per-size padding; Label / AttentionBox / Menu restyle; new SegmentedControl.Chips fill moved from an inline style into CSS — semantic colours use the same
color-*class pattern Label already had; the ~40 content colours pass their palette in as a custom property. This removes the need for!importantdownstream and letisHovered/useSetFocusgo, dropping a re-render on every hover.apps/kitchen-sink — ported and repinned to the v3 line. It sits outside the yarn workspace, so it cannot affect the published packages.
Additive vs. behaviour-changing
Additive (new tokens, new props, new component). Two changes are visible to existing v3 consumers and are intentional:
Colors.tsmaps the semantics onto the new surfaces, soLabel/Chipscolor="primary"andcolor="neutral"shift appearance.Toastnow portals todocument.body; the newinlineprop opts out.Verification
yarn build— 20/20 projectsyarn testcannot finish onversion3:packages/iconsandpackages/codemoddeclare"test": "vitest"withoutrun, so they sit in watch mode. Pre-existing onversion3; those two were run withvitest run.yarn lint— 18/18build:currentandbuild:originalboth pass, with the two runtimes correctly isolatedOpen questions for a designer
--text-on-surface-*follow each theme's bright base semantics; the Figma section only specifies light--surface-neutralderives from--primary-text-color(no neutral semantic colour exists) and--info-coloris the classic monday blue — both are my choice, not the spec--surface-*will not appear intokens.json: the generator only collects property names containingcolorand dropsvar()values, whichcolor-mixis🤖 Generated with Claude Code