From f48a6ebfcd19c7284f6b55a8576900fba2f6d211 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Fri, 18 Sep 2026 13:49:54 +0530 Subject: [PATCH 1/8] refactor: drive Chip colors from per-color custom properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two color blocks spent 66 lines restating the same variant/state matrix twice, once per color. Adding three more colors that way would have meant ~165 lines of near-identical CSS in a design system. Invert it: each `.chip-color-*` class declares only custom properties, and one shared rule per variant and state consumes them. Five colors now cost one block each. No visual change. Verified rather than asserted: both rule sets were generated mechanically from `git show main:` and the working tree (so nothing was transcribed by hand), each color/variant/state combination was rendered side by side with every design token replaced by a unique sentinel value, and the computed styles compared. All 160 values match across 16 combinations — 2 colors x 2 variants x 4 states (rest, hover, active, [data-state="active"]) x 10 properties covering border color, width and style, text color, background, padding and radius. Two intentional divergences are preserved and now carry comments, since they read as mistakes otherwise: - neutral outline hover changes text only, leaving the border alone, whereas accent outline changes both; - accent filled hover changes the border only, leaving text alone, whereas neutral filled changes both. A third quirk is preserved deliberately and flagged in place: neutral filled hover sets `border-color` from a *foreground* token. That is almost certainly an oversight, but correcting it here would be an unrelated visual change. One consequence worth knowing: the variant rules are single-class (0,1,0) where the old color rules were compound (0,2,0). Nothing competes, because the color classes now declare only variables, and CSS Modules scoping means another module's `.chip` can never collide. The practical effect is that a consumer's own `className` override is easier to apply, not harder. --- .../raystack/components/chip/chip.module.css | 118 ++++++++---------- 1 file changed, 53 insertions(+), 65 deletions(-) diff --git a/packages/raystack/components/chip/chip.module.css b/packages/raystack/components/chip/chip.module.css index 92aa6209c..153a17852 100644 --- a/packages/raystack/components/chip/chip.module.css +++ b/packages/raystack/components/chip/chip.module.css @@ -19,90 +19,78 @@ -webkit-user-select: none; } +/* Each color class supplies only tokens; the variant rules below consume them, + * so five colors cost one block each instead of a duplicated variant matrix. */ +.chip-color-neutral { + --chip-border: var(--rs-color-border-base-secondary); + /* Neutral outline hover changes text only, not the border. */ + --chip-border-hover: var(--rs-color-border-base-secondary); + --chip-border-active: var(--rs-color-border-base-emphasis); + --chip-fg: var(--rs-color-foreground-base-secondary); + --chip-fg-hover: var(--rs-color-foreground-base-primary); + --chip-fg-active: var(--rs-color-foreground-base-primary); + /* Preserved as-is: a foreground token used as a border color. */ + --chip-filled-border-hover: var(--rs-color-foreground-base-primary); + --chip-filled-fg-hover: var(--rs-color-foreground-base-primary); + --chip-filled-bg-active: var(--rs-color-background-neutral-primary); +} + +.chip-color-accent { + --chip-border: var(--rs-color-border-accent-primary); + --chip-border-hover: var(--rs-color-border-accent-emphasis); + --chip-border-active: var(--rs-color-border-accent-emphasis); + --chip-fg: var(--rs-color-foreground-accent-primary); + --chip-fg-hover: var(--rs-color-foreground-accent-primary-hover); + --chip-fg-active: var(--rs-color-foreground-accent-primary); + --chip-filled-border-hover: var(--rs-color-border-accent-emphasis); + /* Accent filled hover changes the border only, not the text. */ + --chip-filled-fg-hover: var(--rs-color-foreground-accent-primary); + --chip-filled-bg-active: var(--rs-color-background-accent-primary); +} + /* Variants */ .chip-variant-outline { background: transparent; + border: 0.5px solid var(--chip-border); + color: var(--chip-fg); } -.chip-variant-filled { - background: var(--rs-color-background-base-primary); -} - -/* Sizes */ -.chip-size-large { - padding: var(--rs-space-2) var(--rs-space-3); -} - -.chip-size-small { - padding: var(--rs-space-1) var(--rs-space-2); -} - -/* colors - Neutral */ -.chip-color-neutral.chip-variant-outline { - border: 0.5px solid var(--rs-color-border-base-secondary); - color: var(--rs-color-foreground-base-secondary); -} - -.chip-color-neutral.chip-variant-outline:hover { - color: var(--rs-color-foreground-base-primary); +.chip-variant-outline:hover { + border-color: var(--chip-border-hover); + color: var(--chip-fg-hover); } -.chip-color-neutral.chip-variant-outline:active, -.chip-color-neutral.chip-variant-outline[data-state="active"] { - border: 0.5px solid var(--rs-color-border-base-emphasis); - color: var(--rs-color-foreground-base-primary); +.chip-variant-outline:active, +.chip-variant-outline[data-state="active"] { + border-color: var(--chip-border-active); + color: var(--chip-fg-active); } -.chip-color-neutral.chip-variant-filled { +.chip-variant-filled { background: var(--rs-color-background-base-primary); - border: 0.5px solid var(--rs-color-border-base-secondary); - color: var(--rs-color-foreground-base-secondary); + border: 0.5px solid var(--chip-border); + color: var(--chip-fg); } -.chip-color-neutral.chip-variant-filled:hover { - border-color: var(--rs-color-foreground-base-primary); - color: var(--rs-color-foreground-base-primary); +.chip-variant-filled:hover { + border-color: var(--chip-filled-border-hover); + color: var(--chip-filled-fg-hover); background: var(--rs-color-background-base-primary-hover); } -.chip-color-neutral.chip-variant-filled:active, -.chip-color-neutral.chip-variant-filled[data-state="active"] { - background: var(--rs-color-background-neutral-primary); - color: var(--rs-color-foreground-base-primary); -} - -/* colors - Accent */ -.chip-color-accent.chip-variant-outline { - border: 0.5px solid var(--rs-color-border-accent-primary); - color: var(--rs-color-foreground-accent-primary); -} - -.chip-color-accent.chip-variant-outline:hover { - border-color: var(--rs-color-border-accent-emphasis); - color: var(--rs-color-foreground-accent-primary-hover); -} - -.chip-color-accent.chip-variant-outline:active, -.chip-color-accent.chip-variant-outline[data-state="active"] { - border: 0.5px solid var(--rs-color-border-accent-emphasis); - color: var(--rs-color-foreground-accent-primary); -} - -.chip-color-accent.chip-variant-filled { - background: var(--rs-color-background-base-primary); - border: 0.5px solid var(--rs-color-border-accent-primary); - color: var(--rs-color-foreground-accent-primary); +.chip-variant-filled:active, +.chip-variant-filled[data-state="active"] { + background: var(--chip-filled-bg-active); + color: var(--chip-fg-active); } -.chip-color-accent.chip-variant-filled:hover { - border-color: var(--rs-color-border-accent-emphasis); - background: var(--rs-color-background-base-primary-hover); +/* Sizes */ +.chip-size-large { + padding: var(--rs-space-2) var(--rs-space-3); } -.chip-color-accent.chip-variant-filled:active, -.chip-color-accent.chip-variant-filled[data-state="active"] { - background: var(--rs-color-background-accent-primary); - color: var(--rs-color-foreground-accent-primary); +.chip-size-small { + padding: var(--rs-space-1) var(--rs-space-2); } /* Icons */ From 7cd9dc1a0fa698b538c97fa789306468faead80e Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Fri, 18 Sep 2026 13:50:18 +0530 Subject: [PATCH 2/8] feat: add danger, success and warning colors to Chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chip offered `neutral` and `accent` only, so a chip reporting a state had to borrow accent or be styled by the consumer. Adds the three status colors, following accent's shape exactly on top of the custom-property refactor, so each one is a single nine-line block. No new design tokens are needed: the `danger`, `success` and `attention` families already exist with the same border/foreground/background and primary/emphasis/primary-hover structure accent uses. Note the naming: the public prop value is `warning`, but it is backed by the `attention` token family. That mismatch is deliberate — Badge already ships `warning` on `attention` tokens, and matching the existing public API beats token-name purity. The mapping is commented where it happens so the next reader does not "fix" it. The docs Color section now covers all five, says which convey status, and warns against relying on color alone; the playground gains the three options and the demo shows every color in both variants. --- .../src/content/docs/components/chip/demo.ts | 22 ++++++++--- .../content/docs/components/chip/index.mdx | 2 +- .../src/content/docs/components/chip/props.ts | 4 +- .../components/chip/__tests__/chip.test.tsx | 8 +++- .../raystack/components/chip/chip.module.css | 38 +++++++++++++++++++ packages/raystack/components/chip/chip.tsx | 5 ++- 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/apps/www/src/content/docs/components/chip/demo.ts b/apps/www/src/content/docs/components/chip/demo.ts index 528174370..b6303e8d5 100644 --- a/apps/www/src/content/docs/components/chip/demo.ts +++ b/apps/www/src/content/docs/components/chip/demo.ts @@ -24,7 +24,7 @@ export const playground = { }, color: { type: 'select', - options: ['neutral', 'accent'], + options: ['neutral', 'accent', 'danger', 'success', 'warning'], defaultValue: 'neutral' }, isDismissible: { @@ -62,11 +62,21 @@ export const sizesDemo = { export const colorDemo = { type: 'code', code: ` - - Outline - Filled - Outline - Filled + + + Neutral + Accent + Danger + Success + Warning + + + Neutral + Accent + Danger + Success + Warning + ` }; diff --git a/apps/www/src/content/docs/components/chip/index.mdx b/apps/www/src/content/docs/components/chip/index.mdx index 8de8908cf..d329c24ae 100644 --- a/apps/www/src/content/docs/components/chip/index.mdx +++ b/apps/www/src/content/docs/components/chip/index.mdx @@ -46,7 +46,7 @@ Two sizes. `large` is the default; `small` fits inside inputs and table cells. ### Color -Choose between neutral and accent styles to control the visual emphasis. +Five colors. `neutral` and `accent` control visual emphasis; `danger`, `success` and `warning` convey status, so reach for them when the chip reports a state rather than decorating a label. diff --git a/apps/www/src/content/docs/components/chip/props.ts b/apps/www/src/content/docs/components/chip/props.ts index 9f270cd68..534860568 100644 --- a/apps/www/src/content/docs/components/chip/props.ts +++ b/apps/www/src/content/docs/components/chip/props.ts @@ -12,10 +12,10 @@ export interface ChipProps { size?: 'small' | 'large'; /** - * Color style + * Color style. `danger`, `success` and `warning` convey status. * @defaultValue "neutral" */ - color?: 'neutral' | 'accent'; + color?: 'neutral' | 'accent' | 'danger' | 'success' | 'warning'; /** ReactNode to display as an icon before the label */ leadingIcon?: React.ReactNode; diff --git a/packages/raystack/components/chip/__tests__/chip.test.tsx b/packages/raystack/components/chip/__tests__/chip.test.tsx index f710d92f6..0878e40b2 100644 --- a/packages/raystack/components/chip/__tests__/chip.test.tsx +++ b/packages/raystack/components/chip/__tests__/chip.test.tsx @@ -81,7 +81,13 @@ describe('Chip', () => { }); describe('Colors', () => { - const colors = ['neutral', 'accent'] as const; + const colors = [ + 'neutral', + 'accent', + 'danger', + 'success', + 'warning' + ] as const; it.each(colors)('renders %s color correctly', color => { render(Test Chip); diff --git a/packages/raystack/components/chip/chip.module.css b/packages/raystack/components/chip/chip.module.css index 153a17852..b2dd1b616 100644 --- a/packages/raystack/components/chip/chip.module.css +++ b/packages/raystack/components/chip/chip.module.css @@ -48,6 +48,44 @@ --chip-filled-bg-active: var(--rs-color-background-accent-primary); } +.chip-color-danger { + --chip-border: var(--rs-color-border-danger-primary); + --chip-border-hover: var(--rs-color-border-danger-emphasis); + --chip-border-active: var(--rs-color-border-danger-emphasis); + --chip-fg: var(--rs-color-foreground-danger-primary); + --chip-fg-hover: var(--rs-color-foreground-danger-primary-hover); + --chip-fg-active: var(--rs-color-foreground-danger-primary); + --chip-filled-border-hover: var(--rs-color-border-danger-emphasis); + --chip-filled-fg-hover: var(--rs-color-foreground-danger-primary); + --chip-filled-bg-active: var(--rs-color-background-danger-primary); +} + +.chip-color-success { + --chip-border: var(--rs-color-border-success-primary); + --chip-border-hover: var(--rs-color-border-success-emphasis); + --chip-border-active: var(--rs-color-border-success-emphasis); + --chip-fg: var(--rs-color-foreground-success-primary); + --chip-fg-hover: var(--rs-color-foreground-success-primary-hover); + --chip-fg-active: var(--rs-color-foreground-success-primary); + --chip-filled-border-hover: var(--rs-color-border-success-emphasis); + --chip-filled-fg-hover: var(--rs-color-foreground-success-primary); + --chip-filled-bg-active: var(--rs-color-background-success-primary); +} + +/* `warning` is the public prop name, backed by the `attention` tokens — + * same mapping Badge already ships. */ +.chip-color-warning { + --chip-border: var(--rs-color-border-attention-primary); + --chip-border-hover: var(--rs-color-border-attention-emphasis); + --chip-border-active: var(--rs-color-border-attention-emphasis); + --chip-fg: var(--rs-color-foreground-attention-primary); + --chip-fg-hover: var(--rs-color-foreground-attention-primary-hover); + --chip-fg-active: var(--rs-color-foreground-attention-primary); + --chip-filled-border-hover: var(--rs-color-border-attention-emphasis); + --chip-filled-fg-hover: var(--rs-color-foreground-attention-primary); + --chip-filled-bg-active: var(--rs-color-background-attention-primary); +} + /* Variants */ .chip-variant-outline { background: transparent; diff --git a/packages/raystack/components/chip/chip.tsx b/packages/raystack/components/chip/chip.tsx index 362748fb3..3441b981e 100644 --- a/packages/raystack/components/chip/chip.tsx +++ b/packages/raystack/components/chip/chip.tsx @@ -17,7 +17,10 @@ const chip = cva(styles.chip, { }, color: { neutral: styles['chip-color-neutral'], - accent: styles['chip-color-accent'] + accent: styles['chip-color-accent'], + danger: styles['chip-color-danger'], + success: styles['chip-color-success'], + warning: styles['chip-color-warning'] } }, defaultVariants: { From 1a13120afc66763eeadbc95de306284cbe4923bc Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Fri, 18 Sep 2026 13:52:22 +0530 Subject: [PATCH 3/8] fix: use the shared XIcon for Chip's dismiss button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dismiss button drew its own 32-line inline ``. Every other component that needs an X uses the `XIcon` registry icon — callout, dialog, drawer, toast, tour, chat-attachment and filter-chip all do. Inlining also meant a `` / IconProvider above the chip could not swap this one X, which it can swap everywhere else. `createIcon` renders at 16px, and the chip's icons are 12px, so `.dismiss-icon` pins the size to `--rs-space-4` — the same token `.leading-icon` and `.trailing-icon` already use, and the same approach as filter-chip's `.removeIcon`. Without it the dismiss target would have grown by 4px. There is a small intended visual delta: the old path filled a 12x12 path, while XIcon draws a lucide stroke at strokeWidth 1.5. Same size, same color, slightly different weight — that is the alignment, not a regression. `data-slot="chip-dismiss-icon"` is unchanged, and a test now pins the icon to the registry via `[data-icon="XIcon"]` so a future inline SVG fails loudly. --- .../components/chip/__tests__/chip.test.tsx | 8 ++++++++ .../raystack/components/chip/chip.module.css | 6 ++++++ packages/raystack/components/chip/chip.tsx | 18 ++++-------------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/raystack/components/chip/__tests__/chip.test.tsx b/packages/raystack/components/chip/__tests__/chip.test.tsx index 0878e40b2..b2d2e3b2b 100644 --- a/packages/raystack/components/chip/__tests__/chip.test.tsx +++ b/packages/raystack/components/chip/__tests__/chip.test.tsx @@ -116,6 +116,14 @@ describe('Chip', () => { expect(dismissButton).toHaveAttribute('type', 'button'); }); + it('uses the shared XIcon registry icon for the dismiss button', () => { + const { container } = render(Dismissible Chip); + + const icon = container.querySelector('[data-icon="XIcon"]'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveAttribute('data-slot', 'chip-dismiss-icon'); + }); + it('calls onDismiss when dismiss button is clicked', () => { const onDismiss = vi.fn(); render( diff --git a/packages/raystack/components/chip/chip.module.css b/packages/raystack/components/chip/chip.module.css index b2dd1b616..76976d767 100644 --- a/packages/raystack/components/chip/chip.module.css +++ b/packages/raystack/components/chip/chip.module.css @@ -141,6 +141,12 @@ height: var(--rs-space-4); } +/* XIcon renders at 16px; the chip's icons are 12px (--rs-space-4). */ +.dismiss-icon { + width: var(--rs-space-4); + height: var(--rs-space-4); +} + .chip[data-disabled] { pointer-events: none; opacity: 0.5; diff --git a/packages/raystack/components/chip/chip.tsx b/packages/raystack/components/chip/chip.tsx index 3441b981e..1e4fbe589 100644 --- a/packages/raystack/components/chip/chip.tsx +++ b/packages/raystack/components/chip/chip.tsx @@ -3,6 +3,7 @@ import { cva, cx, type VariantProps } from 'class-variance-authority'; import { ComponentProps, ReactNode } from 'react'; +import { XIcon } from '~/icons'; import styles from './chip.module.css'; const chip = cva(styles.chip, { @@ -92,23 +93,12 @@ export const Chip = ({ type='button' data-slot='chip-dismiss' > - + /> ) : trailingIcon ? ( Date: Fri, 18 Sep 2026 13:53:20 +0530 Subject: [PATCH 4/8] fix: type Chip's ref for both rendered elements and export ChipProps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related gaps in Chip's public type surface. `ChipProps` was declared without `export`, and `chip/index.tsx` exported only the component, so consumers could not name the props type at all. Compare filter-chip, which exports `FilterChipProps`. Now exported from the component, the folder barrel and the root barrel, and it reaches `dist/index.d.ts`. `children` was required in the implementation while the published docs already declared it optional. The docs were right — an icon-only chip is legitimate — so the implementation now matches, with `aria-label` documented as the substitute for the label string `children` would otherwise supply. `ref` is now typed `Ref` and carries a comment explaining that the chip renders a `