Skip to content

Commit 29ca519

Browse files
rohankokane-devrohankokanerzp-slash[bot]
authored
fix: blade theme and neutral button (#3910)
* fix: updates storybook theme & adds the button black variant * chore: interactive story * fix: focus ring transition * fix: change color to neutral * chore: adds changeset * fix: button neutral tokens * chore: apply requested changes via autonomous agent Co-authored-by: admin <admin> --------- Co-authored-by: rohankokane <rohan.kokane+oss@razorpay.com> Co-authored-by: rzp-slash[bot] <swe-agent@razorpay.com>
1 parent 4dd93e1 commit 29ca519

11 files changed

Lines changed: 812 additions & 33 deletions

File tree

.agents/skills/review-pr/scripts/post-review.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ function archiveUiScreenshots(reviewJson, repoArg, prNum) {
221221
return cdnMap;
222222
}
223223

224-
const screenshotCdnMap = archiveUiScreenshots(reviewJson, repo, prNumber);
224+
let screenshotCdnMap = {};
225+
try {
226+
screenshotCdnMap = archiveUiScreenshots(reviewJson, repo, prNumber);
227+
} catch (e) {
228+
console.warn('Screenshot archiving failed — continuing without CDN screenshots:', e.message);
229+
}
225230

226231
// ---------------------------------------------------------------------------
227232
// Build payload
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@razorpay/blade-core': minor
3+
'@razorpay/blade-svelte': minor
4+
---
5+
6+
feat: add `black` color variant to Button
7+
8+
fix: focus-ring transition and offset flash on inputs
9+
10+
fix: use `bladeTheme` as default Storybook theme in blade-svelte

packages/blade-core/src/styles/Button/button.module.css

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,30 @@
129129
--btn-accent-border-highlighted: var(--interactive-border-negative-highlighted);
130130
}
131131

132+
/*
133+
* Neutral reuses the filled pattern. `--btn-accent-bevel` overrides the top/bottom
134+
* white bevel with the higher-contrast faded-highlighted token the design specifies
135+
* for a dark surface.
136+
*/
137+
.color-neutral {
138+
--btn-accent-bg-default: var(--interactive-background-neutral-default);
139+
--btn-accent-bg-highlighted: var(--interactive-background-neutral-highlighted);
140+
--btn-accent-bg-disabled: var(--interactive-background-neutral-disabled);
141+
--btn-accent-border-default: var(--interactive-border-neutral-default);
142+
--btn-accent-border-highlighted: var(--interactive-border-neutral-highlighted);
143+
--btn-accent-bevel: var(--interactive-border-static-white-faded-highlighted);
144+
}
145+
132146
/* ===== Filled button (primary variant) — shared across accent colors ===== */
133147
.color-primary.primary,
134148
.color-positive.primary,
135-
.color-negative.primary {
149+
.color-negative.primary,
150+
.color-neutral.primary {
136151
background-color: var(--btn-accent-bg-default);
137152
box-shadow: inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
138153
inset 0 0 0 0.5px var(--btn-accent-border-default),
139-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
140-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
154+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
155+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
141156

142157
&::before {
143158
content: '';
@@ -161,9 +176,8 @@
161176
background-color: var(--btn-accent-bg-highlighted);
162177
box-shadow: inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
163178
inset 0 0 0 0.5px var(--btn-accent-border-highlighted),
164-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
165-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
166-
179+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
180+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
167181
&::before {
168182
opacity: 0;
169183
}
@@ -173,9 +187,8 @@
173187
background-color: var(--btn-accent-bg-highlighted);
174188
box-shadow: inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
175189
inset 0 0 0 0.5px var(--btn-accent-border-highlighted),
176-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
177-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
178-
190+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
191+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
179192
&::before {
180193
opacity: 0;
181194
}
@@ -187,9 +200,8 @@
187200
box-shadow: 0px 0px 0px 4px var(--surface-border-primary-muted),
188201
inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
189202
inset 0 0 0 0.5px var(--btn-accent-border-highlighted),
190-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
191-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
192-
203+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
204+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
193205
&::before {
194206
opacity: 0;
195207
}
@@ -209,7 +221,9 @@
209221
.color-primary.secondary,
210222
.color-primary.tertiary,
211223
.color-positive.secondary,
212-
.color-negative.secondary {
224+
.color-negative.secondary,
225+
.color-neutral.secondary,
226+
.color-neutral.tertiary {
213227
background-color: var(--surface-background-gray-intense);
214228
box-shadow: inset 0 -1px 0.5px 0 var(--interactive-border-static-black-faded-highlighted),
215229
inset 0 0 0 1px var(--interactive-border-gray-default),
@@ -515,9 +529,8 @@
515529
background-color: var(--btn-accent-bg-default);
516530
box-shadow: inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
517531
inset 0 0 0 0.5px var(--btn-accent-border-default),
518-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
519-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
520-
532+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
533+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
521534
&::before {
522535
opacity: 1;
523536
}
@@ -558,8 +571,8 @@
558571
pointer-events: none;
559572
box-shadow: inset 0 -1.5px 0 0 var(--btn-accent-border-highlighted),
560573
inset 0 0 0 0.5px var(--btn-accent-border-default),
561-
inset 0 1.5px 0 0 var(--interactive-border-static-white-faded),
562-
inset 0 -2px 0 0 var(--interactive-border-static-white-faded);
574+
inset 0 1.5px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded)),
575+
inset 0 -2px 0 0 var(--btn-accent-bevel, var(--interactive-border-static-white-faded));
563576
}
564577

565578
/*

packages/blade-core/src/styles/Button/button.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ export function getButtonBackgroundColorToken({
7676
}
7777
}
7878

79+
if (color === 'neutral') {
80+
if (variant === 'primary') {
81+
return `interactive.${property}.staticBlack.${_state}`;
82+
}
83+
// secondary/tertiary render the same as the default (gray) buttons
84+
if (variant === 'secondary') {
85+
return isBorder ? 'interactive.border.primary.default' : 'surface.background.gray.intense';
86+
}
87+
if (variant === 'tertiary') {
88+
return _state === 'disabled'
89+
? 'interactive.background.staticWhite.ghost'
90+
: 'surface.background.gray.intense';
91+
}
92+
}
93+
7994
if (color === 'transparent') {
8095
if (variant !== 'tertiary') {
8196
throw new Error(
@@ -175,6 +190,19 @@ export function getButtonTextColorToken({
175190
}
176191
}
177192

193+
if (color === 'neutral') {
194+
if (variant === 'primary') {
195+
return `interactive.${property}.staticWhite.${state === 'disabled' ? 'disabled' : 'normal'}`;
196+
}
197+
// secondary/tertiary render the same as the default (gray) buttons
198+
if (variant === 'secondary') {
199+
return `interactive.${property}.gray.${state === 'disabled' ? 'disabled' : 'normal'}`;
200+
}
201+
if (variant === 'tertiary') {
202+
return `interactive.${property}.gray.${stateSuffix}`;
203+
}
204+
}
205+
178206
if (color === 'transparent') {
179207
if (variant !== 'tertiary') {
180208
throw new Error(

packages/blade-core/src/styles/Input/baseInput.module.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@
5555

5656
.focus-ring-wrapper:focus-within {
5757
outline: 4px solid var(--surface-border-primary-muted);
58-
outline-offset: 0px;
59-
transition-property: outline;
58+
outline-offset: 1px;
59+
/* Transition only outline-width: transitioning the full `outline` shorthand
60+
* also animates outline-color from the resting `currentcolor` (dark) to the
61+
* muted ring color, which flashes a solid dark outline on focus before it
62+
* settles. Animating width alone grows the ring in cleanly (mirrors React). */
63+
transition-property: outline-width;
6064
transition-duration: var(--duration-xgentle);
6165
transition-timing-function: var(--easing-emphasized);
6266
}

packages/blade-core/src/tokens/theme.css

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
/* ===== THEME TOKENS - LIGHT MODE ===== */
146146

147147
/* Surface Colors */
148-
--surface-background-gray-subtle: hsla(200, 10%, 94%, 1);
148+
--surface-background-gray-subtle: hsla(0, 0%, 97%, 1);
149149
--surface-background-gray-moderate: hsla(0, 0%, 97%, 1);
150150
--surface-background-gray-intense: hsla(0, 0%, 100%, 1);
151151
--surface-background-primary-faint: transparent;
@@ -261,9 +261,9 @@
261261
--interactive-background-information-disabled: hsla(200, 100%, 41%, 0.09);
262262
--interactive-background-information-faded: hsla(200, 100%, 41%, 0.09);
263263
--interactive-background-information-faded-highlighted: hsla(200, 100%, 41%, 0.18);
264-
--interactive-background-neutral-default: hsla(205, 10%, 24%, 1);
265-
--interactive-background-neutral-highlighted: hsla(200, 10%, 18%, 1);
266-
--interactive-background-neutral-disabled: hsla(206, 10%, 29%, 0.18);
264+
--interactive-background-neutral-default: hsla(0, 0%, 0%, 1);
265+
--interactive-background-neutral-highlighted: hsla(0, 0%, 0%, 0.88);
266+
--interactive-background-neutral-disabled: hsla(0, 0%, 0%, 0.18);
267267
--interactive-background-neutral-faded: hsla(206, 10%, 29%, 0.12);
268268
--interactive-background-neutral-faded-highlighted: hsla(206, 10%, 29%, 0.18);
269269
--interactive-background-gray-default: hsla(206, 10%, 29%, 0.06);
@@ -305,8 +305,8 @@
305305
--interactive-border-information-highlighted: hsla(200, 100%, 33%, 1);
306306
--interactive-border-information-disabled: hsla(200, 100%, 41%, 0.18);
307307
--interactive-border-information-faded: hsla(200, 100%, 41%, 0.18);
308-
--interactive-border-neutral-default: hsla(200, 11%, 11%, 1);
309-
--interactive-border-neutral-highlighted: hsla(0, 0%, 2%, 1);
308+
--interactive-border-neutral-default: hsla(0, 0%, 0%, 1);
309+
--interactive-border-neutral-highlighted: hsla(0, 0%, 0%, 0.88);
310310
--interactive-border-neutral-disabled: hsla(203, 8%, 80%, 1);
311311
--interactive-border-neutral-faded: hsla(206, 10%, 29%, 0.12);
312312
--interactive-border-gray-default: hsla(204, 8%, 88%, 1);
@@ -359,6 +359,10 @@
359359
--interactive-text-on-primary-subtle: hsla(0, 0%, 100%, 0.8);
360360
--interactive-text-on-primary-muted: hsla(0, 0%, 100%, 0.64);
361361
--interactive-text-on-primary-disabled: hsla(0, 0%, 100%, 0.32);
362+
--interactive-text-on-neutral-normal: hsla(0, 0%, 100%, 1);
363+
--interactive-text-on-neutral-subtle: hsla(0, 0%, 100%, 0.8);
364+
--interactive-text-on-neutral-muted: hsla(0, 0%, 100%, 0.64);
365+
--interactive-text-on-neutral-disabled: hsla(0, 0%, 100%, 0.32);
362366
--interactive-text-static-white-normal: hsla(0, 0%, 100%, 1);
363367
--interactive-text-static-white-subtle: hsla(0, 0%, 100%, 0.8);
364368
--interactive-text-static-white-muted: hsla(0, 0%, 100%, 0.64);
@@ -399,6 +403,10 @@
399403
--interactive-icon-on-primary-subtle: hsla(0, 0%, 100%, 0.8);
400404
--interactive-icon-on-primary-muted: hsla(0, 0%, 100%, 0.64);
401405
--interactive-icon-on-primary-disabled: hsla(0, 0%, 100%, 0.32);
406+
--interactive-icon-on-neutral-normal: hsla(0, 0%, 100%, 1);
407+
--interactive-icon-on-neutral-subtle: hsla(0, 0%, 100%, 0.8);
408+
--interactive-icon-on-neutral-muted: hsla(0, 0%, 100%, 0.64);
409+
--interactive-icon-on-neutral-disabled: hsla(0, 0%, 100%, 0.32);
402410
--interactive-icon-static-white-normal: hsla(0, 0%, 100%, 1);
403411
--interactive-icon-static-white-subtle: hsla(0, 0%, 100%, 0.8);
404412
--interactive-icon-static-white-muted: hsla(0, 0%, 100%, 0.64);

packages/blade-svelte/.storybook/BladeThemeDecorator.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Reads colorScheme / brandColor from Storybook globals via props.
55
*/
66
import type { Snippet } from 'svelte';
7-
import { bladeNeutralTheme, createTheme } from '@razorpay/blade-core/tokens';
7+
import { bladeTheme, createTheme } from '@razorpay/blade-core/tokens';
88
import type { ColorSchemeNamesInput, ThemeTokens } from '@razorpay/blade-core/tokens';
99
import BladeProvider from '../src/components/BladeProvider/BladeProvider.svelte';
1010
@@ -24,17 +24,17 @@
2424
const themeTokens = $derived.by((): ThemeTokens => {
2525
if (brandColor) {
2626
try {
27-
return createTheme({ brandColor, baseTheme: bladeNeutralTheme }).theme;
27+
return createTheme({ brandColor, baseTheme: bladeTheme }).theme;
2828
} catch (error) {
2929
console.warn(
3030
'[BladeThemeDecorator]: Invalid brandColor, falling back to default theme',
3131
brandColor,
3232
error,
3333
);
34-
return bladeNeutralTheme;
34+
return bladeTheme;
3535
}
3636
}
37-
return bladeNeutralTheme;
37+
return bladeTheme;
3838
});
3939
</script>
4040

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script module lang="ts">
2+
import { defineMeta } from '@storybook/addon-svelte-csf';
3+
import InteractiveTokensPlayground from './InteractiveTokensPlayground.svelte';
4+
5+
const { Story } = defineMeta({
6+
title: 'Playground/Interactive Tokens',
7+
component: InteractiveTokensPlayground,
8+
tags: ['autodocs'],
9+
parameters: {
10+
layout: 'fullscreen',
11+
docs: {
12+
description: {
13+
component:
14+
"Every Blade Svelte component that consumes `interactive.*` color tokens, rendered together. Pick a brand color to run it through Blade's real `createTheme` pipeline and feed `BladeProvider` — exactly like a themed app. The full `interactive.primary.*` state scale is derived by `createTheme` (positive / negative / notice / neutral / gray / static* are fixed Blade tokens with no app-facing override).",
15+
},
16+
},
17+
},
18+
});
19+
</script>
20+
21+
<!-- Self-contained playground: owns its own BladeProvider, so the global decorator is skipped. -->
22+
<Story
23+
name="Playground"
24+
asChild
25+
parameters={{
26+
controls: { disable: true },
27+
skipBladeProviderDecorator: true,
28+
}}
29+
>
30+
<InteractiveTokensPlayground />
31+
</Story>

0 commit comments

Comments
 (0)