+
+
+
+ {/* Unset falls back to `currentColor` in CSS, which is what the
+ icons themselves inherit β so the swatch stays honest. */}
+
+ {color || 'Inherit'}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/www/src/components/icongallery/index.ts b/apps/www/src/components/icongallery/index.ts
new file mode 100644
index 000000000..00e8b87eb
--- /dev/null
+++ b/apps/www/src/components/icongallery/index.ts
@@ -0,0 +1 @@
+export { IconGallery } from './icongallery';
diff --git a/apps/www/src/components/mdx/mdx-components.tsx b/apps/www/src/components/mdx/mdx-components.tsx
index f69a6589e..9e42d213f 100644
--- a/apps/www/src/components/mdx/mdx-components.tsx
+++ b/apps/www/src/components/mdx/mdx-components.tsx
@@ -11,6 +11,7 @@ import type {
TableHTMLAttributes
} from 'react';
import Demo from '../demo';
+import { IconGallery } from '../icongallery';
import { TokenTable } from '../tokentable';
import { TypeTable } from '../typetable';
import { Code } from './code';
@@ -122,6 +123,7 @@ const mdxComponents = {
TokenTable: (props: ComponentPropsWithoutRef) => (
),
+ IconGallery,
Callout: (props: ComponentPropsWithoutRef) => (
=0.500.0 <0.600.0`. lucide is a 0.x package, so
+a wide range would let your installed version decide how Apsara looks.
+
+
+ `@radix-ui/react-icons` is no longer a dependency of Apsara. If your own code
+ imports from it, keep it in your own `dependencies`.
+
+
+## 2. Check the icons that changed shape
+
+Eight icons inside Apsara's own components draw a different shape than before.
+Everything else is the same glyph in a different drawing style.
+
+| Where | Before (radix) | After (lucide) | What changed |
+| --- | --- | --- | --- |
+| `Sidebar` collapse | `TriangleDownIcon` | `ChevronDownIcon` | A solid triangle becomes a chevron. lucide has no solid caret |
+| `Menu` and `ContextMenu` submenu marker | in-house `TriangleRightIcon` | `ChevronRightIcon` | A solid triangle becomes a chevron |
+| `ChatPanel` expand | `SizeIcon` | `ExpandIcon` | A different glyph |
+| `ChatPanel` minimize | `MinusIcon` | `ShrinkIcon` | A dash becomes the matched pair of `ExpandIcon` |
+| `PromptInput` stop | `StopIcon` | `SquareIcon` | Solid becomes stroke |
+| `DataTable` sort ascending | `TextAlignTopIcon` | `ArrowUpNarrowWideIcon` | A different glyph |
+| `DataTable` and `DataView` sort descending | `TextAlignBottomIcon` | `ArrowDownWideNarrowIcon` | A different glyph |
+| `@raystack/apsara/icons` β `ShoppingBagFilledIcon` | in-house solid SVG | `ShoppingBagIcon` | Solid becomes stroke |
+
+Two more are worth a look, though the glyph is nearly the same:
+
+- `DatePicker` and `RangePicker` now use `CalendarDaysIcon` rather than a plain
+ calendar, so the glyph has day marks inside it.
+- `Search` clear and `Toast` error now use `CircleXIcon` in place of
+ `CrossCircledIcon`.
+
+## 3. Expect a 1px size change in some places
+
+Every Apsara icon now renders at **16Γ16** with `strokeWidth={1.5}`, which draws
+the 1px stroke of the design because lucide's viewBox is 24 units wide. The radix
+icons were intrinsically 15Γ15.
+
+- A call site that set no size grows from 15px to 16px.
+- A call site that set a CSS class is unaffected β CSS beats an SVG presentation
+ attribute.
+- A call site that set `width`/`height` explicitly is unaffected β your props are
+ applied after Apsara's base values.
+
+## 4. If you want the radix appearance back
+
+Register the radix icons at ``. Apsara ships no radix preset, so copy this
+map into your app:
+
+```tsx
+'use client';
+
+import {
+ ArrowDownIcon,
+ ArrowUpIcon,
+ CalendarIcon,
+ CheckCircledIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ CopyIcon,
+ Cross1Icon,
+ CrossCircledIcon,
+ DotsHorizontalIcon,
+ ExclamationTriangleIcon,
+ FileTextIcon,
+ InfoCircledIcon,
+ MagnifyingGlassIcon,
+ MinusIcon,
+ MixerHorizontalIcon,
+ MoonIcon,
+ PlusIcon,
+ SizeIcon,
+ StopIcon,
+ SunIcon,
+ TableIcon,
+ TextAlignBottomIcon,
+ TextAlignTopIcon
+} from '@radix-ui/react-icons';
+import { Theme, type IconOverrides } from '@raystack/apsara';
+
+export const radixIcons: IconOverrides = {
+ XIcon: Cross1Icon,
+ CircleXIcon: CrossCircledIcon,
+ CircleCheckIcon: CheckCircledIcon,
+ TriangleAlertIcon: ExclamationTriangleIcon,
+ InfoIcon: InfoCircledIcon,
+ CheckIcon: CheckIcon,
+ CopyIcon: CopyIcon,
+ SearchIcon: MagnifyingGlassIcon,
+ ChevronDownIcon: ChevronDownIcon,
+ ChevronLeftIcon: ChevronLeftIcon,
+ ChevronRightIcon: ChevronRightIcon,
+ ArrowUpIcon: ArrowUpIcon,
+ ArrowDownIcon: ArrowDownIcon,
+ PlusIcon: PlusIcon,
+ MinusIcon: MinusIcon,
+ EllipsisIcon: DotsHorizontalIcon,
+ ExpandIcon: SizeIcon,
+ ShrinkIcon: MinusIcon,
+ SquareIcon: StopIcon,
+ CalendarDaysIcon: CalendarIcon,
+ FileTextIcon: FileTextIcon,
+ TableIcon: TableIcon,
+ SlidersHorizontalIcon: MixerHorizontalIcon,
+ ArrowUpNarrowWideIcon: TextAlignTopIcon,
+ ArrowDownWideNarrowIcon: TextAlignBottomIcon,
+ SunIcon: SunIcon,
+ MoonIcon: MoonIcon
+};
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+```
+
+Two icons cannot be restored from radix, because radix has no equivalent:
+
+- `ListFilterIcon` β the old `FilterIcon` was an in-house SVG. The lucide shape is
+ very close.
+- The `Sidebar` collapse caret. The map above sets `ChevronDownIcon` to the radix
+ chevron, which every other call site also used. Restoring the solid triangle in
+ the sidebar alone would also change `Accordion`, `Select`, `Combobox`, and
+ `Breadcrumb`, because one name draws one shape everywhere.
+
+You do not have to take the whole map. A partial map changes only the icons it
+names.
+
+## 5. Register from a client component
+
+An icon map is an object of functions, and a function cannot cross the boundary
+from a React Server Component to a Client Component. If your `` sits
+directly in a server layout today, move it into a `providers.tsx` file marked
+`'use client'`, as shown above.
+
+This constraint is new. It applies to any runtime icon registry, not just this
+design.
+
+## 6. Rename nine icons from `@raystack/apsara/icons`
+
+The `./icons` subpath itself keeps working, and is still the entry point to reach
+when you want icons without the component library. What changes is the names it
+exports: it used to export the raw in-house SVG assets, and it now exports
+registry wrappers under their registry keys. Nine of the old names are gone.
+
+
+ These nine imports fail to resolve. Rename them.
+
+
+| Removed name | Use instead | Appearance |
+| --- | --- | --- |
+| `BellSlashIcon` | `BellOffIcon` | Similar |
+| `BuildingsFilledIcon` | `Building2Icon` | Solid becomes stroke |
+| `CoinIcon` | `CoinsIcon` | Similar |
+| `FilterIcon` | `ListFilterIcon` | Similar |
+| `OrganizationIcon` | `Building2Icon` | Similar |
+| `ResetIcon` | `RotateCcwIcon` | Similar |
+| `ShoppingBagFilledIcon` | `ShoppingBagIcon` | Solid becomes stroke |
+| `SidebarIcon` | `PanelLeftIcon` | Similar |
+| `TriangleRightIcon` | `ChevronRightIcon` | Solid triangle becomes a chevron |
+
+`BuildingsFilledIcon` and `OrganizationIcon` both become `Building2Icon`, so they
+are now one component and one override.
+
+Five names are unchanged, because they were already registry keys: `BellIcon`
+(lucide `Bell`), and the four in-house SVGs lucide cannot draw β `CoPilotIcon`,
+`CoinColoredIcon`, `CheckCircleFilledIcon`, `CrossCircleFilledIcon`.
+
+Every other name from the subpath is one of the 243 registry icons, and the same
+component as the one the package root exports.
+
+## Next
+
+See [Icons βΊ Usage](/docs/icons/usage) for the naming rules and the override API,
+and [All icons](/docs/icons/all-icons) for the full set.
diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts
index f5101ec04..256eebb25 100644
--- a/apps/www/src/content/docs/components/breadcrumb/demo.ts
+++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts
@@ -152,9 +152,9 @@ export const iconsDemo = {
}>Home
- }>Documents
+ }>Documents
- }>Settings
+ }>Settings
`
},
{
@@ -163,20 +163,20 @@ export const iconsDemo = {
}>Home
- }>Documents
+ }>Documents
- }>Settings
+ }>Settings
`
},
{
name: 'Both Icons',
code: `
- } trailingIcon={}>Home
+ } trailingIcon={}>Home
- } trailingIcon={}>Documents
+ } trailingIcon={}>Documents
- } trailingIcon={}>Settings
+ } trailingIcon={}>Settings
`
},
{
@@ -185,9 +185,9 @@ export const iconsDemo = {
}/>
- }/>
+ }/>
- }/>
+ }/>
`
}
]
diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts
index 2c60ec0f0..2456e9a85 100644
--- a/apps/www/src/content/docs/components/command/demo.ts
+++ b/apps/www/src/content/docs/components/command/demo.ts
@@ -121,10 +121,10 @@ export const withIconsDemo = {
No results found.} onClick={() => setOpen(false)}>Home
} onClick={() => setOpen(false)}>Notifications
- } onClick={() => setOpen(false)}>Filters
- } onClick={() => setOpen(false)}>Organization
- } onClick={() => setOpen(false)}>Orders
- } onClick={() => setOpen(false)}>Layout
+ } onClick={() => setOpen(false)}>Filters
+ } onClick={() => setOpen(false)}>Organization
+ } onClick={() => setOpen(false)}>Orders
+ } onClick={() => setOpen(false)}>Layout
`)
};
diff --git a/apps/www/src/content/docs/components/datatable/index.mdx b/apps/www/src/content/docs/components/datatable/index.mdx
index 4ec6f746f..24b50725d 100644
--- a/apps/www/src/content/docs/components/datatable/index.mdx
+++ b/apps/www/src/content/docs/components/datatable/index.mdx
@@ -377,7 +377,7 @@ Zero state is shown when no data has been fetched initially (no filters or searc
```tsx
import { DataTable, EmptyState } from "@raystack/apsara";
-import { OrganizationIcon } from "@raystack/apsara/icons";
+import { Building2Icon } from "@raystack/apsara/icons";
}
+ icon={}
heading="No users yet"
subHeading="Get started by creating your first user."
/>
@@ -402,7 +402,7 @@ Empty state is shown when initial data exists but no results match after applyin
```tsx
import { DataTable, EmptyState } from "@raystack/apsara";
-import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons";
+import { Building2Icon, ListFilterIcon } from "@raystack/apsara/icons";
}
+ icon={}
heading="No users yet"
subHeading="Get started by creating your first user."
/>
}
emptyState={
}
+ icon={}
heading="No users found"
subHeading="We couldn't find any matches for that keyword or filter. Try alternative terms or check for typos."
/>
@@ -441,12 +441,12 @@ You can provide custom React components for both states:
```tsx
import { DataTable, EmptyState, Flex, Text, Button } from "@raystack/apsara";
-import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons";
+import { Building2Icon, ListFilterIcon } from "@raystack/apsara/icons";
-
+
No data available
@@ -460,7 +460,7 @@ import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons";
}
emptyState={
}
+ icon={}
heading="No matches found"
subHeading="Try adjusting your filters or search query."
/>
diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts
index f434643bb..6a125a6b1 100644
--- a/apps/www/src/content/docs/components/sidebar/demo.ts
+++ b/apps/www/src/content/docs/components/sidebar/demo.ts
@@ -31,34 +31,34 @@ export const preview = {
- }>
+ }>
Overview
- }>
+ }>
Preview
} active>
Dashboard
- }>
+ }>
Analytics
- }>
+ }>
Settings
- }>
+ }>
Reports
- }>
+ }>
Activities
- }>
+ }>
Help & Support
@@ -82,14 +82,14 @@ export const positionDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -107,14 +107,14 @@ export const positionDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -139,10 +139,10 @@ export const variantDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
+ }>Analytics
`)
@@ -160,10 +160,10 @@ export const variantDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
+ }>Analytics
`)
@@ -181,10 +181,10 @@ export const variantDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
+ }>Analytics
`)
@@ -208,14 +208,14 @@ export const stateDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -232,14 +232,14 @@ export const stateDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -256,14 +256,14 @@ export const stateDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -280,14 +280,14 @@ export const stateDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`)
@@ -311,14 +311,14 @@ export const tooltipDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
- }>Settings
+ }>Analytics
+ }>Settings
- }>Help
+ }>Help
`),
@@ -337,10 +337,10 @@ export const collapsibleDemo = {
- }>Overview
+ }>Overview
} active>Dashboard
- }>Analytics
+ }>Analytics
`),
@@ -362,9 +362,9 @@ export const hideTooltipDemo = {
}>Overview
} active>Dashboard
- }>Settings
+ }>Settings
- }>
+ }>
Help
@@ -384,19 +384,19 @@ export const collapsibleGroupDemo = {
- }>
+ }>
Overview
- }>
+ }>
Reports
- }>
+ }>
Activities
- }>
- }>
+ }>
+ }>
Settings
@@ -423,7 +423,7 @@ export const controlledGroupDemo = {
- }>
+ }>
Overview
- }>
+ }>
Reports
- }>
+ }>
Activities
@@ -470,16 +470,16 @@ export const groupIconDemo = {
- }>
+ }>
} active>
Dashboard
- }>
+ }>
Analytics
- }>
- }>
+ }>
+ }>
Reports
}>
@@ -506,18 +506,18 @@ export const moreDemo = {
} active>
Dashboard
- }>
+ }>
Analytics
- }>
+ }>
Reports
}>
Activities
- } disabled>
+ } disabled>
Notifications
@@ -525,10 +525,10 @@ export const moreDemo = {
- }>
+ }>
Preferences
- }>
+ }>
Documentation
diff --git a/apps/www/src/content/docs/icons/all-icons/index.mdx b/apps/www/src/content/docs/icons/all-icons/index.mdx
new file mode 100644
index 000000000..7496dc55a
--- /dev/null
+++ b/apps/www/src/content/docs/icons/all-icons/index.mdx
@@ -0,0 +1,7 @@
+---
+title: All icons
+description: Browse every icon Apsara ships, and preview any size, stroke width, and colour.
+source: packages/raystack/icons/icon-map.json
+---
+
+
diff --git a/apps/www/src/content/docs/icons/meta.json b/apps/www/src/content/docs/icons/meta.json
new file mode 100644
index 000000000..d6b8b518c
--- /dev/null
+++ b/apps/www/src/content/docs/icons/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Icons",
+ "pages": ["usage", "all-icons"]
+}
diff --git a/apps/www/src/content/docs/icons/usage/demo.ts b/apps/www/src/content/docs/icons/usage/demo.ts
new file mode 100644
index 000000000..a39078fa0
--- /dev/null
+++ b/apps/www/src/content/docs/icons/usage/demo.ts
@@ -0,0 +1,126 @@
+'use client';
+
+export const basicDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+
+`
+};
+
+export const sizeDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+`
+};
+
+export const colorDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+`
+};
+
+export const overrideDemo = {
+ type: 'code',
+ code: `// A double chevron stands in for every ChevronDownIcon below.
+const MyChevron = props => (
+
+);
+
+render(
+
+
+
+ default
+
+
+
+
+
+ overridden
+
+
+
+)`
+};
+
+export const iconPropsDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+};
+
+export const nestedDemo = {
+ type: 'code',
+ code: `const Square = props => (
+
+);
+
+const Circle = props => (
+
+);
+
+render(
+
+
+
+
+
+
+
+ {/* Names only XIcon, so CheckIcon keeps the outer Circle. */}
+
+
+
+
+
+
+
+
+)`
+};
diff --git a/apps/www/src/content/docs/icons/usage/index.mdx b/apps/www/src/content/docs/icons/usage/index.mdx
new file mode 100644
index 000000000..3f12b0b5c
--- /dev/null
+++ b/apps/www/src/content/docs/icons/usage/index.mdx
@@ -0,0 +1,263 @@
+---
+title: Usage
+description: How to import an Apsara icon, size and colour it, and replace any icon with your own component.
+source: packages/raystack/icons
+---
+
+import {
+ basicDemo,
+ sizeDemo,
+ colorDemo,
+ overrideDemo,
+ iconPropsDemo,
+ nestedDemo,
+} from "./demo.ts";
+
+Apsara ships 243 icons. 239 are drawn by [lucide](https://lucide.dev), and 4 are
+in-house SVGs that lucide cannot supply. Every icon is exported under a stable
+name, and every icon can be replaced with your own component.
+
+
+
+Browse the set on [All icons](/docs/icons/all-icons).
+
+## Anatomy
+
+Import an icon by name from `@raystack/apsara/icons`:
+
+```tsx
+import { SearchIcon, ChevronDownIcon } from '@raystack/apsara/icons'
+
+
+```
+
+Each icon is its own module, so you pay only for the icons you import. A build
+that shows three icons ships three icons, not all 243.
+
+The package root exports the same components, so
+`import { SearchIcon } from '@raystack/apsara'` works too β it is the natural
+choice in a file that already imports Apsara components. See
+[which path to import from](#which-path-to-import-from) for the difference.
+
+## Naming
+
+An icon name is the lucide name in Pascal case with the suffix `Icon`. So lucide
+`circle-x` is `CircleXIcon`, and `list-filter` is `ListFilterIcon`.
+
+A name identifies a **shape**, not a role. Two roles that use one shape therefore
+share one name. Inside Apsara this happens once: `CircleXIcon` draws both the
+error icon of the `Toast` and the clear button of the `Search` field, so an
+override of `CircleXIcon` changes both.
+
+## Base props
+
+Every icon renders with `width={16} height={16} strokeWidth={1.5}`. The rendered
+stroke is `strokeWidth Γ size Γ· 24`, because lucide draws in a 24-unit viewBox.
+So the default draws the 1px stroke of the design at 16px.
+
+If you change the size and want to keep a 1px stroke, scale the stroke with it β
+`strokeWidth={24 / size}`.
+
+All three are standard SVG attributes, so any icon library accepts them. A
+library that draws solid shapes simply ignores `stroke-width`.
+
+### Size
+
+Pass `width` and `height` to change the size. A CSS class or `style` also wins,
+because CSS beats an SVG presentation attribute.
+
+
+
+Do not pass the lucide `size` or `absoluteStrokeWidth` props. They are specific
+to lucide, so they break as soon as an icon is overridden. `absoluteStrokeWidth`
+does nothing here in any case, because Apsara sets `width`/`height` rather than
+`size`.
+
+### Colour
+
+An icon inherits `currentColor`, so set `color` on the icon or on an ancestor.
+
+
+
+### The `data-icon` attribute
+
+Every icon renders `data-icon=""`. Use it to style a single icon from CSS
+without re-rendering anything, and to select an icon in a test:
+
+```css
+[data-icon='ChevronDownIcon'] {
+ color: var(--rs-color-foreground-base-tertiary);
+}
+```
+
+```tsx
+expect(document.querySelector('[data-icon="XIcon"]')).toBeInTheDocument();
+```
+
+## Replacing an icon
+
+Give `` a map of icon name to your own component. Apsara then uses your
+component everywhere that icon appears β inside its own components too.
+
+
+
+```tsx
+import { Theme } from '@raystack/apsara'
+import { X, ChevronDown } from 'lucide-react'
+
+const icons = { XIcon: X, ChevronDownIcon: ChevronDown }
+
+
+
+
+```
+
+The map is partial: an icon you do not name keeps its default. You never have to
+supply a complete set.
+
+
+ You may pass the map as an inline object literal. Apsara compares its contents,
+ so a new literal on every render costs nothing.
+
+
+### Props for every icon
+
+`iconProps` applies props to every icon below the provider. The props at the call
+site still win.
+
+
+
+Prefer `data-icon` and CSS when a style rule is enough β `iconProps` flows through
+React and re-renders the icons, and CSS does not.
+
+### Nesting
+
+A nested `` layers on the one above it, one name at a time. So a subtree
+can change one icon and keep everything else it inherited.
+
+
+
+## API Reference
+
+### Theme props
+
+
+
+### IconProvider
+
+`` mounts this for you. Use it directly only if you want icon overrides
+without a theme scope.
+
+
+
+### Types
+
+
+
+`IconComponent` is `ComponentType`, and `IconOverrides` is
+`Partial>`. `IconName` is the union of all 243
+names, so a typo in an override map is a type error.
+
+```tsx
+import type {
+ IconComponent,
+ IconName,
+ IconOverrides,
+ IconProps
+} from '@raystack/apsara';
+```
+
+## Server components
+
+The icons are client components, and an icon map is an object of functions. A
+function cannot cross the boundary from a Server Component to a Client
+Component, so register the overrides from a client component:
+
+```tsx
+// app/providers.tsx
+'use client';
+
+import { Theme } from '@raystack/apsara';
+import { X } from 'lucide-react';
+
+const icons = { XIcon: X };
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+```
+
+```tsx
+// app/layout.tsx (Server Component)
+import { Providers } from './providers';
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+Resolution is a pure function of the context and the props β no `localStorage`,
+no `window`, no effect β so the server markup and the client markup are
+identical. There is no hydration mismatch and no flash of the wrong icon.
+
+## Which path to import from
+
+Both entry points export the same 243 icons, as the same components. What differs
+is how much work a bundler has to do to strip the rest of Apsara.
+
+```tsx
+// The same component, either way.
+import { SearchIcon } from '@raystack/apsara/icons';
+import { SearchIcon } from '@raystack/apsara';
+```
+
+`@raystack/apsara/icons` reaches no component module at all, so one icon costs one
+icon whatever the bundler does. The package root re-exports every component
+alongside the icons; Apsara sets `"sideEffects": false` so a bundler that honours
+that flag strips them and the two paths come out identical, but a bundler that
+does not honour it keeps them all. Bundling a single icon against the published
+files gives:
+
+| Import from | Bundler honours `sideEffects` | Bundler does not |
+| --- | --- | --- |
+| `@raystack/apsara/icons` | 2 modules, 1.5 kB | 2 modules, 1.5 kB |
+| `@raystack/apsara` | 2 modules, 1.5 kB | 332 modules, 541 kB |
+
+Sizes are unminified. So the subpath is the safer default: identical in the good
+case, and far cheaper in the bad one. Use the root in files that already import
+components, where a second import line buys nothing.
+
+CommonJS cannot tree-shake at all, so there the difference is unconditional:
+`require('@raystack/apsara')` loads 332 modules and every component, while
+`require('@raystack/apsara/icons')` loads 244 modules and no component.
+
+### What changed in this release
+
+The subpath used to export raw SVG assets, which meant `BellIcon` from `./icons`
+and `BellIcon` from the package root were two different components. It now
+re-exports the registry wrappers, so both give the same overridable component.
+
+Every name it exports is now a registry key, so nine of the old in-house names are
+gone. Rename them:
+
+| Removed name | Use instead |
+| --- | --- |
+| `BellSlashIcon` | `BellOffIcon` |
+| `BuildingsFilledIcon` | `Building2Icon` |
+| `CoinIcon` | `CoinsIcon` |
+| `FilterIcon` | `ListFilterIcon` |
+| `OrganizationIcon` | `Building2Icon` |
+| `ResetIcon` | `RotateCcwIcon` |
+| `ShoppingBagFilledIcon` | `ShoppingBagIcon` |
+| `SidebarIcon` | `PanelLeftIcon` |
+| `TriangleRightIcon` | `ChevronRightIcon` |
+
+The other five in-house names are registry keys already, so they keep working
+unchanged: `BellIcon`, `CoPilotIcon`, `CoinColoredIcon`, `CheckCircleFilledIcon`,
+and `CrossCircleFilledIcon`.
diff --git a/apps/www/src/content/docs/icons/usage/props.ts b/apps/www/src/content/docs/icons/usage/props.ts
new file mode 100644
index 000000000..24ab83822
--- /dev/null
+++ b/apps/www/src/content/docs/icons/usage/props.ts
@@ -0,0 +1,55 @@
+import type { ComponentType, ReactNode, SVGProps } from 'react';
+
+/**
+ * The props of every Apsara icon.
+ *
+ * `children` is excluded on purpose: an icon draws a fixed shape. Excluding it
+ * is also what makes a real icon library assignable to `IconComponent` β
+ * `@radix-ui/react-icons`, for one, declares `children?: undefined`.
+ */
+export type IconProps = Omit, 'children'>;
+
+/** Any component that can stand in for an Apsara icon. */
+export type IconComponent = ComponentType;
+
+/**
+ * A partial map of icon name to replacement component. Every key is optional,
+ * so a map that names one icon changes that icon only.
+ */
+export type IconOverrides = Partial>;
+
+export interface IconProviderProps {
+ /**
+ * Icon components that replace the Apsara defaults, keyed by icon name β for
+ * example `{ XIcon: MyX }`. A partial map changes only the icons it names, and
+ * a nested provider layers on the one above it, per name.
+ */
+ icons?: IconOverrides;
+
+ /**
+ * Props applied to every icon below this provider β for example
+ * `{ strokeWidth: 2 }`. These beat the Apsara base values and lose to the
+ * props at the call site.
+ */
+ props?: IconProps;
+
+ children: ReactNode;
+}
+
+export interface ThemeIconProps {
+ /**
+ * Icon components that replace the Apsara defaults, keyed by icon name.
+ * Apsara mounts the icon provider only when this or `iconProps` is set.
+ *
+ * The map holds functions, so a React Server Component cannot pass it β set it
+ * from a client component.
+ */
+ icons?: IconOverrides;
+
+ /**
+ * Props applied to every Apsara icon. The props at the call site still win.
+ * Prefer the `data-icon` attribute and CSS where a style rule is enough,
+ * because CSS re-renders nothing.
+ */
+ iconProps?: IconProps;
+}
diff --git a/apps/www/src/content/docs/meta.json b/apps/www/src/content/docs/meta.json
index ed1d989d7..019e38db7 100644
--- a/apps/www/src/content/docs/meta.json
+++ b/apps/www/src/content/docs/meta.json
@@ -1,3 +1,3 @@
{
- "pages": ["(overview)", "theme", "ai-elements", "components"]
+ "pages": ["(overview)", "theme", "icons", "ai-elements", "components"]
}
diff --git a/packages/raystack/CHANGELOG.md b/packages/raystack/CHANGELOG.md
index 238c21663..dc134238b 100644
--- a/packages/raystack/CHANGELOG.md
+++ b/packages/raystack/CHANGELOG.md
@@ -1,5 +1,91 @@
# @raystack/apsara
+## 0.50.0
+
+### Icon registry β lucide replaces @radix-ui/react-icons (BREAKING)
+
+Apsara refers to every icon by a stable name held in one map,
+`packages/raystack/icons/icon-map.json`. Changing that map and running
+`pnpm build:icon-registry` changes the icon library for the whole design
+system β no call site and no consumer code changes. Consumers can also
+replace any icon with their own component, once, at ``.
+
+The default library moves from `@radix-ui/react-icons` to lucide in the
+same release, so some icons look different. See the
+[migration guide](https://apsara.raystack.io/docs/migrating-to-lucide-icons).
+
+#### Breaking changes
+
+- **`lucide-react` is a new peer dependency**, range
+ `>=0.500.0 <0.600.0`. Install it. The range is narrow because lucide is
+ a 0.x package and a wide range would let the consumer's version decide
+ how Apsara looks.
+- **`@radix-ui/react-icons` is no longer a dependency.** It remains a
+ `devDependency` because `scripts/generate-icons-code-connect.js` needs
+ it for `pnpm figma:sync`.
+- **Eight icons draw a different shape.** The sidebar collapse caret and
+ the submenu marker change from a solid triangle to a chevron (lucide has
+ no solid caret); `ChatPanel` expand/minimize become the matched
+ `ExpandIcon`/`ShrinkIcon` pair; the `PromptInput` stop button and
+ `ShoppingBagFilledIcon` go from solid to stroke; and the `DataTable` sort
+ arrows change glyph. `DatePicker` gains day marks inside its calendar
+ glyph.
+- **Icons render at 16Γ16** with `strokeWidth={1.5}`, where the radix
+ icons were intrinsically 15Γ15. A call site that sets a CSS class or an
+ explicit `width`/`height` is unaffected. `1.5` is 1 rendered pixel: the
+ prop counts units of lucide's 24-unit viewBox, so the stroke on screen
+ is `strokeWidth Γ width / 24`.
+- **Icon overrides must be registered from a client component.** An icon
+ map is an object of functions, and a function cannot cross the boundary
+ from a React Server Component. A `` that sits directly in a
+ server layout must move into a `providers.tsx` marked `'use client'`.
+- **The 10 in-house SVG assets that lucide can replace are deleted.**
+ Five of their names are registry keys and still resolve; the other nine
+ must be renamed β see the subpath note below.
+
+#### New features
+
+- **``** replaces any icon by name. The map is partial,
+ so an override of one icon leaves the rest alone, and a nested ``
+ layers on the one above it per name.
+- **``** applies props to every icon. The props at
+ the call site still win.
+- **`data-icon=""`** on every icon, so CSS can style one icon
+ without a re-render and a test can select it.
+- **243 icons**, from `@raystack/apsara/icons` and from the package root β
+ 239 lucide, 4 in-house SVGs that lucide cannot draw (`CoPilotIcon`,
+ `CheckCircleFilledIcon`, `CrossCircleFilledIcon`, `CoinColoredIcon`).
+ Each icon is its own module, so a build that shows three icons ships
+ three icons rather than all 243. A bundle test enforces this. Prefer the
+ subpath: it reaches no component module, so one icon costs one icon even
+ when a bundler does not honour `"sideEffects": false`.
+- **New public types**: `IconName` (the union of all 243 names, so a typo
+ in an override map is a type error), `IconOverrides`, `IconComponent`,
+ `IconProps`, `IconProviderProps`, and `IconProvider` itself.
+- **`pnpm check:icon-map`** asserts that every lucide name in the map is a
+ real export and that every in-house `asset` names a file that exists, so
+ moving the peer range cannot silently break an icon name.
+
+#### The `@raystack/apsara/icons` subpath
+
+- **The subpath stays supported, and is now the icons-only entry point.**
+ It exports the same 243 icons as the package root, as the same
+ overridable registry wrappers, and reaches no component module β which
+ matters for CJS, where the root barrel's `.cjs` eagerly requires every
+ component.
+- **Nine in-house icon names are removed** (breaking). The subpath used to
+ export the raw SVG assets; it now exports registry keys, so rename:
+ `BellSlashIcon` β `BellOffIcon`, `BuildingsFilledIcon` β
+ `Building2Icon`, `CoinIcon` β `CoinsIcon`, `FilterIcon` β
+ `ListFilterIcon`, `OrganizationIcon` β `Building2Icon`, `ResetIcon` β
+ `RotateCcwIcon`, `ShoppingBagFilledIcon` β `ShoppingBagIcon`,
+ `SidebarIcon` β `PanelLeftIcon`, `TriangleRightIcon` β
+ `ChevronRightIcon`. `BuildingsFilledIcon` and `OrganizationIcon` both
+ become `Building2Icon`, so they are now one component. Five names are
+ unchanged because they were already registry keys: `BellIcon`,
+ `CoPilotIcon`, `CoinColoredIcon`, `CheckCircleFilledIcon`,
+ `CrossCircleFilledIcon`.
+
## 0.49.0
### Calendar / DatePicker / RangePicker improvements (PR #819)
diff --git a/packages/raystack/components/accordion/accordion-trigger.tsx b/packages/raystack/components/accordion/accordion-trigger.tsx
index c52735e4d..e3e033ef4 100644
--- a/packages/raystack/components/accordion/accordion-trigger.tsx
+++ b/packages/raystack/components/accordion/accordion-trigger.tsx
@@ -1,8 +1,8 @@
'use client';
import { Accordion as AccordionPrimitive } from '@base-ui/react';
-import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
+import { ChevronDownIcon } from '~/icons/generated';
import styles from './accordion.module.css';
export const AccordionTrigger = ({
diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
index 2f8d0b925..196ffa5e1 100644
--- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
+++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx
@@ -1,9 +1,9 @@
'use client';
import { mergeProps, useRender } from '@base-ui/react';
-import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import React, { ComponentProps, ReactNode } from 'react';
+import { ChevronDownIcon } from '~/icons/generated';
import { Menu } from '../menu';
import styles from './breadcrumb.module.css';
diff --git a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx
index e599b9404..ec684d2bb 100644
--- a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx
+++ b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx
@@ -1,8 +1,8 @@
'use client';
-import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ComponentProps } from 'react';
+import { ChevronRightIcon, EllipsisIcon } from '~/icons/generated';
import styles from './breadcrumb.module.css';
export interface BreadcrumbEllipsisProps extends ComponentProps<'span'> {}
@@ -10,7 +10,7 @@ export interface BreadcrumbEllipsisProps extends ComponentProps<'span'> {}
export const BreadcrumbEllipsis = ({
ref,
className,
- children = ,
+ children = ,
...props
}: BreadcrumbEllipsisProps) => {
return (
diff --git a/packages/raystack/components/calendar/calendar.tsx b/packages/raystack/components/calendar/calendar.tsx
index 68612b046..f163e26f9 100644
--- a/packages/raystack/components/calendar/calendar.tsx
+++ b/packages/raystack/components/calendar/calendar.tsx
@@ -1,12 +1,12 @@
'use client';
-import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import { cva, cx } from 'class-variance-authority';
import dayjs from 'dayjs';
import timezonePlugin from 'dayjs/plugin/timezone';
import utcPlugin from 'dayjs/plugin/utc';
import { ChangeEvent, ReactNode, useEffect, useRef, useState } from 'react';
import { DayPicker, DayPickerProps, DropdownProps } from 'react-day-picker';
+import { ChevronLeftIcon, ChevronRightIcon } from '~/icons/generated';
import { IconButton } from '../icon-button';
import { Select } from '../select';
diff --git a/packages/raystack/components/calendar/date-picker.tsx b/packages/raystack/components/calendar/date-picker.tsx
index 99d8a391d..a8096a2d0 100644
--- a/packages/raystack/components/calendar/date-picker.tsx
+++ b/packages/raystack/components/calendar/date-picker.tsx
@@ -1,6 +1,5 @@
'use client';
-import { CalendarIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -8,6 +7,7 @@ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import { useEffect, useId, useRef, useState } from 'react';
import { PropsBase } from 'react-day-picker';
+import { CalendarDaysIcon } from '~/icons/generated';
import { Input } from '../input';
import { InputProps } from '../input/input';
import { Popover } from '../popover';
@@ -223,7 +223,7 @@ export function DatePicker({
placeholder='Select date'
aria-invalid={!!error}
className={styles.datePickerInput}
- trailingIcon={showCalendarIcon ? : undefined}
+ trailingIcon={showCalendarIcon ? : undefined}
{...inputProps}
aria-describedby={describedBy}
ref={popover.inputRef}
diff --git a/packages/raystack/components/calendar/range-picker.tsx b/packages/raystack/components/calendar/range-picker.tsx
index f97a5db91..9072f5960 100644
--- a/packages/raystack/components/calendar/range-picker.tsx
+++ b/packages/raystack/components/calendar/range-picker.tsx
@@ -1,10 +1,10 @@
'use client';
-import { CalendarIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DateRange, PropsBase } from 'react-day-picker';
+import { CalendarDaysIcon } from '~/icons/generated';
import { Flex } from '../flex';
import { Input } from '../input';
import { InputProps } from '../input/input';
@@ -219,7 +219,7 @@ export function RangePicker({
: undefined}
+ trailingIcon={showCalendarIcon ? : undefined}
className={styles.datePickerInput}
{...startInputProps}
value={startDate}
@@ -232,7 +232,7 @@ export function RangePicker({
: undefined}
+ trailingIcon={showCalendarIcon ? : undefined}
className={styles.datePickerInput}
{...endInputProps}
value={endDate}
diff --git a/packages/raystack/components/callout/callout.tsx b/packages/raystack/components/callout/callout.tsx
index 9e6d4c422..522a048f1 100644
--- a/packages/raystack/components/callout/callout.tsx
+++ b/packages/raystack/components/callout/callout.tsx
@@ -1,6 +1,5 @@
'use client';
-import { Cross1Icon, InfoCircledIcon } from '@radix-ui/react-icons';
import { cva, type VariantProps } from 'class-variance-authority';
import {
type ComponentProps,
@@ -9,6 +8,7 @@ import {
useEffect,
useState
} from 'react';
+import { InfoIcon, XIcon } from '~/icons/generated';
import { IconButton } from '../icon-button';
import styles from './callout.module.css';
@@ -65,7 +65,7 @@ export function Callout({
action,
dismissible,
onDismiss,
- icon = ,
+ icon = ,
...props
}: CalloutProps) {
// Dismissal is controlled when `onDismiss` is given; otherwise fall back to
@@ -114,7 +114,7 @@ export function Callout({
onClick={handleDismiss}
aria-label='Dismiss message'
>
-
+
)}
diff --git a/packages/raystack/components/chat-panel/chat-panel-parts.tsx b/packages/raystack/components/chat-panel/chat-panel-parts.tsx
index fb9a056f1..8e6ce1202 100644
--- a/packages/raystack/components/chat-panel/chat-panel-parts.tsx
+++ b/packages/raystack/components/chat-panel/chat-panel-parts.tsx
@@ -1,9 +1,9 @@
'use client';
import { useMergedRefs } from '@base-ui/utils/useMergedRefs';
-import { MinusIcon, SizeIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ComponentProps, MouseEvent, PointerEvent, ReactNode } from 'react';
+import { ExpandIcon, ShrinkIcon } from '~/icons/generated';
import { IconButton, type IconButtonProps } from '../icon-button/icon-button';
import styles from './chat-panel.module.css';
import { useChatPanelContext } from './chat-panel-context';
@@ -90,7 +90,7 @@ export function ChatPanelMinimizeTrigger({
onClick={handleClick}
{...props}
>
- {children ?? }
+ {children ?? }
);
}
@@ -138,7 +138,7 @@ export function ChatPanelExpandTrigger({
onClick={handleClick}
{...props}
>
- {resolvedChildren ?? }
+ {resolvedChildren ?? }
);
}
diff --git a/packages/raystack/components/chat-panel/chat-panel-trigger.tsx b/packages/raystack/components/chat-panel/chat-panel-trigger.tsx
index 422feb7de..61b262819 100644
--- a/packages/raystack/components/chat-panel/chat-panel-trigger.tsx
+++ b/packages/raystack/components/chat-panel/chat-panel-trigger.tsx
@@ -9,7 +9,7 @@ import {
useEffect,
useRef
} from 'react';
-import { ReactComponent as CoPilotIcon } from '../../icons/assets/co-pilot.svg';
+import { CoPilotIcon } from '~/icons/generated';
import styles from './chat-panel.module.css';
import { useChatPanelContext } from './chat-panel-context';
diff --git a/packages/raystack/components/chat/chat-attachment.tsx b/packages/raystack/components/chat/chat-attachment.tsx
index 1165b10b9..49b7dbc00 100644
--- a/packages/raystack/components/chat/chat-attachment.tsx
+++ b/packages/raystack/components/chat/chat-attachment.tsx
@@ -1,8 +1,8 @@
'use client';
-import { Cross2Icon, FileTextIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ComponentProps, ReactNode } from 'react';
+import { FileTextIcon, XIcon } from '~/icons/generated';
import { IconButton } from '../icon-button';
import { Spinner } from '../spinner';
import styles from './chat.module.css';
@@ -77,7 +77,7 @@ export function ChatAttachment({
className={styles['attachment-remove']}
onClick={onRemove}
>
-
+
)}
diff --git a/packages/raystack/components/chat/chat-messages.tsx b/packages/raystack/components/chat/chat-messages.tsx
index 339d85755..5a45077e4 100644
--- a/packages/raystack/components/chat/chat-messages.tsx
+++ b/packages/raystack/components/chat/chat-messages.tsx
@@ -1,7 +1,6 @@
'use client';
import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area';
-import { ArrowDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import {
ComponentProps,
@@ -16,6 +15,7 @@ import {
useRef,
useState
} from 'react';
+import { ArrowDownIcon } from '~/icons/generated';
import { ScrollAreaScrollbar } from '../scroll-area/scroll-area-scrollbar';
import { usePrefersReducedMotion } from '../tour/use-prefers-reduced-motion';
import styles from './chat.module.css';
diff --git a/packages/raystack/components/code-block/__tests__/code-block.test.tsx b/packages/raystack/components/code-block/__tests__/code-block.test.tsx
index 87f139f38..5c3963689 100644
--- a/packages/raystack/components/code-block/__tests__/code-block.test.tsx
+++ b/packages/raystack/components/code-block/__tests__/code-block.test.tsx
@@ -12,14 +12,6 @@ vi.mock('~/hooks/useCopyToClipboard', () => ({
})
}));
-// Mock icon components used inside CopyButton to avoid invalid element errors
-vi.mock('@radix-ui/react-icons', () => ({
- CopyIcon: () => null,
- CheckIcon: () => null,
- ChevronDownIcon: () => null,
- CaretSortIcon: () => null
-}));
-
const JAVASCRIPT_CODE = `function hello() {
console.log('Hello, world!');
}`;
diff --git a/packages/raystack/components/combobox/combobox-input.tsx b/packages/raystack/components/combobox/combobox-input.tsx
index b7a68bf49..a17c4ecd8 100644
--- a/packages/raystack/components/combobox/combobox-input.tsx
+++ b/packages/raystack/components/combobox/combobox-input.tsx
@@ -1,8 +1,8 @@
'use client';
import { Combobox as ComboboxPrimitive } from '@base-ui/react';
-import { ChevronDownIcon } from '@radix-ui/react-icons';
import { type ComponentProps } from 'react';
+import { ChevronDownIcon } from '~/icons/generated';
import { Input } from '../input';
import { useComboboxContext } from './combobox-root';
diff --git a/packages/raystack/components/context-menu/context-menu-trigger.tsx b/packages/raystack/components/context-menu/context-menu-trigger.tsx
index 34752c872..4285c7f62 100644
--- a/packages/raystack/components/context-menu/context-menu-trigger.tsx
+++ b/packages/raystack/components/context-menu/context-menu-trigger.tsx
@@ -4,7 +4,7 @@ import {
Autocomplete as AutocompletePrimitive,
ContextMenu as ContextMenuPrimitive
} from '@base-ui/react';
-import { TriangleRightIcon } from '~/icons';
+import { ChevronRightIcon } from '~/icons/generated';
import { Cell, CellBaseProps } from '../menu/cell';
import { useMenuContext } from '../menu/menu-root';
import { getMatch } from '../menu/utils';
@@ -33,7 +33,7 @@ export interface ContextMenuSubTriggerProps
export const ContextMenuSubTrigger = ({
children,
value,
- trailingIcon = ,
+ trailingIcon = ,
leadingIcon,
...props
}: ContextMenuSubTriggerProps) => {
diff --git a/packages/raystack/components/copy-button/copy-button.tsx b/packages/raystack/components/copy-button/copy-button.tsx
index 062d20df0..b7fc402c1 100644
--- a/packages/raystack/components/copy-button/copy-button.tsx
+++ b/packages/raystack/components/copy-button/copy-button.tsx
@@ -1,8 +1,8 @@
'use client';
-import { CheckIcon, CopyIcon } from '@radix-ui/react-icons';
import { useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
+import { CheckIcon, CopyIcon } from '~/icons/generated';
import { IconButton, IconButtonProps } from '../icon-button/icon-button';
import styles from './copy-button.module.css';
diff --git a/packages/raystack/components/data-table/components/content.tsx b/packages/raystack/components/data-table/components/content.tsx
index 86d2ac505..31dfb30ad 100644
--- a/packages/raystack/components/data-table/components/content.tsx
+++ b/packages/raystack/components/data-table/components/content.tsx
@@ -1,10 +1,10 @@
'use client';
-import { Cross2Icon, TableIcon } from '@radix-ui/react-icons';
import type { HeaderGroup, Row } from '@tanstack/react-table';
import { flexRender } from '@tanstack/react-table';
import { cx } from 'class-variance-authority';
import { useCallback, useEffect, useRef } from 'react';
+import { TableIcon, XIcon } from '~/icons/generated';
import { Badge } from '../../badge';
import { Button } from '../../button';
@@ -338,7 +338,7 @@ export function Content({
variant='text'
color='neutral'
size='small'
- trailingIcon={}
+ trailingIcon={}
onClick={handleClearFilters}
>
Clear Filters
diff --git a/packages/raystack/components/data-table/components/display-settings.tsx b/packages/raystack/components/data-table/components/display-settings.tsx
index 8bdff1270..924680084 100644
--- a/packages/raystack/components/data-table/components/display-settings.tsx
+++ b/packages/raystack/components/data-table/components/display-settings.tsx
@@ -1,9 +1,8 @@
'use client';
-import { MixerHorizontalIcon } from '@radix-ui/react-icons';
-
import { cx } from 'class-variance-authority';
import { isValidElement, ReactNode } from 'react';
+import { SlidersHorizontalIcon } from '~/icons/generated';
import { Button } from '../../button';
import { Flex } from '../../flex';
import { Popover } from '../../popover';
@@ -29,7 +28,7 @@ export function DisplaySettings({
variant='outline'
color='neutral'
size='small'
- leadingIcon={}
+ leadingIcon={}
>
Display
diff --git a/packages/raystack/components/data-table/components/filters.tsx b/packages/raystack/components/data-table/components/filters.tsx
index cc319ea2e..3fd51b406 100644
--- a/packages/raystack/components/data-table/components/filters.tsx
+++ b/packages/raystack/components/data-table/components/filters.tsx
@@ -2,7 +2,7 @@
import { cx } from 'class-variance-authority';
import { isValidElement, ReactNode, useMemo } from 'react';
-import { FilterIcon } from '~/icons';
+import { ListFilterIcon } from '~/icons/generated';
import { FilterOperatorTypes, FilterType } from '~/types/filters';
import { Button } from '../../button';
import { FilterChip, type FilterChipValue } from '../../filter-chip';
@@ -51,7 +51,7 @@ function AddFilter({
else if (appliedFiltersSet.size > 0)
return (
-
+
);
else
@@ -59,7 +59,7 @@ function AddFilter({
}
+ leadingIcon={}
color='neutral'
className={className}
>
diff --git a/packages/raystack/components/data-table/components/ordering.tsx b/packages/raystack/components/data-table/components/ordering.tsx
index ea28a3135..b5d4fff84 100644
--- a/packages/raystack/components/data-table/components/ordering.tsx
+++ b/packages/raystack/components/data-table/components/ordering.tsx
@@ -1,7 +1,9 @@
'use client';
-import { TextAlignBottomIcon, TextAlignTopIcon } from '@radix-ui/react-icons';
-
+import {
+ ArrowDownWideNarrowIcon,
+ ArrowUpNarrowWideIcon
+} from '~/icons/generated';
import { Flex } from '../../flex';
import { IconButton } from '../../icon-button';
import { Select } from '../../select';
@@ -74,9 +76,11 @@ export function Ordering({ columnList, onChange, value }: OrderingProps) {
disabled={columnList.length === 0}
>
{currentOrder === SortOrders.ASC ? (
-
+
) : (
-
)}
diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx
index 851c1cc0a..d9a3c6322 100644
--- a/packages/raystack/components/data-table/components/virtualized-content.tsx
+++ b/packages/raystack/components/data-table/components/virtualized-content.tsx
@@ -1,12 +1,12 @@
'use client';
-import { TableIcon } from '@radix-ui/react-icons';
import type { HeaderGroup, Row } from '@tanstack/react-table';
import { flexRender } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { cx } from 'class-variance-authority';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import tableStyles from '~/components/table/table.module.css';
+import { TableIcon } from '~/icons/generated';
import { Badge } from '../../badge';
import { EmptyState } from '../../empty-state';
import { Flex } from '../../flex';
@@ -381,10 +381,7 @@ export function VirtualizedContent({
{showLoaderRows && (
-
+
)}
);
diff --git a/packages/raystack/components/data-view/__tests__/data-view.test.tsx b/packages/raystack/components/data-view/__tests__/data-view.test.tsx
index 011df6145..13e9428b3 100644
--- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx
+++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx
@@ -2,13 +2,6 @@ import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
-// SVG icons are inlined via @svgr/rollup at build time. In Vitest they resolve
-// to undefined, so stub the `~/icons` module with no-op components.
-vi.mock('~/icons', () => ({
- FilterIcon: () => null,
- __esModule: true
-}));
-
// biome-ignore lint/suspicious/noShadowRestrictedNames: legitimate export name
import { DataView } from '../data-view';
import type {
diff --git a/packages/raystack/components/data-view/__tests__/timeline.test.tsx b/packages/raystack/components/data-view/__tests__/timeline.test.tsx
index cdf089356..eadfaf44d 100644
--- a/packages/raystack/components/data-view/__tests__/timeline.test.tsx
+++ b/packages/raystack/components/data-view/__tests__/timeline.test.tsx
@@ -3,13 +3,6 @@ import userEvent from '@testing-library/user-event';
import dayjs from 'dayjs';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
-// SVG icons are inlined via @svgr/rollup at build time. In Vitest they resolve
-// to undefined, so stub the `~/icons` module with no-op components.
-vi.mock('~/icons', () => ({
- FilterIcon: () => null,
- __esModule: true
-}));
-
// biome-ignore lint/suspicious/noShadowRestrictedNames: legitimate export name
import { DataView } from '../data-view';
import type {
diff --git a/packages/raystack/components/data-view/components/clear-filters.tsx b/packages/raystack/components/data-view/components/clear-filters.tsx
index 2a8b61ae1..ee276a7df 100644
--- a/packages/raystack/components/data-view/components/clear-filters.tsx
+++ b/packages/raystack/components/data-view/components/clear-filters.tsx
@@ -1,8 +1,8 @@
'use client';
-import { Cross2Icon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { useCallback } from 'react';
+import { XIcon } from '~/icons/generated';
import { Button } from '../../button';
import { Flex } from '../../flex';
import styles from '../data-view.module.css';
@@ -86,7 +86,7 @@ export function FilterSummary({ className }: DataViewClearFiltersProps) {
variant='text'
color='neutral'
size='small'
- trailingIcon={}
+ trailingIcon={}
onClick={handleClearFilters}
>
Clear Filters
diff --git a/packages/raystack/components/data-view/components/display-controls.tsx b/packages/raystack/components/data-view/components/display-controls.tsx
index 643919343..5dad27489 100644
--- a/packages/raystack/components/data-view/components/display-controls.tsx
+++ b/packages/raystack/components/data-view/components/display-controls.tsx
@@ -1,7 +1,7 @@
'use client';
-import { MixerHorizontalIcon } from '@radix-ui/react-icons';
import { isValidElement, ReactNode } from 'react';
+import { SlidersHorizontalIcon } from '~/icons/generated';
import { Button } from '../../button';
import { Flex } from '../../flex';
@@ -35,7 +35,7 @@ export function DisplayControls({
variant='outline'
color='neutral'
size='small'
- leadingIcon={}
+ leadingIcon={}
>
Display
diff --git a/packages/raystack/components/data-view/components/filters.tsx b/packages/raystack/components/data-view/components/filters.tsx
index 9d8ac2fd2..33f8cfb63 100644
--- a/packages/raystack/components/data-view/components/filters.tsx
+++ b/packages/raystack/components/data-view/components/filters.tsx
@@ -2,7 +2,7 @@
import { cx } from 'class-variance-authority';
import { isValidElement, ReactNode, useMemo } from 'react';
-import { FilterIcon } from '~/icons';
+import { ListFilterIcon } from '~/icons/generated';
import { FilterOperatorTypes, FilterType } from '~/types/filters';
import { Button } from '../../button';
import { FilterChip } from '../../filter-chip';
@@ -45,7 +45,7 @@ function AddFilter({
if (appliedFiltersSet.size > 0) {
return (
-
+
);
}
@@ -53,7 +53,7 @@ function AddFilter({
}
+ leadingIcon={}
color='neutral'
>
Filter
diff --git a/packages/raystack/components/data-view/components/ordering.tsx b/packages/raystack/components/data-view/components/ordering.tsx
index 850954f13..9d0f00633 100644
--- a/packages/raystack/components/data-view/components/ordering.tsx
+++ b/packages/raystack/components/data-view/components/ordering.tsx
@@ -1,7 +1,6 @@
'use client';
-import { TextAlignBottomIcon } from '@radix-ui/react-icons';
-
+import { ArrowDownWideNarrowIcon } from '~/icons/generated';
import { Flex } from '../../flex';
import { IconButton } from '../../icon-button';
import { Select } from '../../select';
@@ -68,7 +67,7 @@ export function Ordering({ columnList, onChange, value }: OrderingProps) {
size={4}
disabled={columnList.length === 0}
>
-
diff --git a/packages/raystack/components/dialog/dialog-misc.tsx b/packages/raystack/components/dialog/dialog-misc.tsx
index bbff676b7..1f26cb15a 100644
--- a/packages/raystack/components/dialog/dialog-misc.tsx
+++ b/packages/raystack/components/dialog/dialog-misc.tsx
@@ -1,9 +1,9 @@
'use client';
import { Dialog as DialogPrimitive } from '@base-ui/react';
-import { Cross1Icon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { type ComponentProps } from 'react';
+import { XIcon } from '~/icons/generated';
import { Flex } from '../flex';
import { IconButton } from '../icon-button';
import styles from './dialog.module.css';
@@ -63,7 +63,7 @@ export function CloseButton(props: DialogPrimitive.Close.Props) {
render={}
{...props}
>
-
+
);
}
diff --git a/packages/raystack/components/drawer/drawer-content.tsx b/packages/raystack/components/drawer/drawer-content.tsx
index 95d2a386c..e59342976 100644
--- a/packages/raystack/components/drawer/drawer-content.tsx
+++ b/packages/raystack/components/drawer/drawer-content.tsx
@@ -1,9 +1,9 @@
'use client';
import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer';
-import { Cross1Icon } from '@radix-ui/react-icons';
import { cva, cx, type VariantProps } from 'class-variance-authority';
import { ReactNode } from 'react';
+import { XIcon } from '~/icons/generated';
import { IconButton } from '../icon-button';
import styles from './drawer.module.css';
@@ -63,7 +63,7 @@ export function DrawerContent({
aria-label={closeLabel}
render={}
>
-
+
)}
diff --git a/packages/raystack/components/filter-chip/filter-chip.tsx b/packages/raystack/components/filter-chip/filter-chip.tsx
index d4c9d9ef4..726d118b1 100644
--- a/packages/raystack/components/filter-chip/filter-chip.tsx
+++ b/packages/raystack/components/filter-chip/filter-chip.tsx
@@ -1,9 +1,9 @@
'use client';
-import { Cross1Icon } from '@radix-ui/react-icons';
import { cva, VariantProps } from 'class-variance-authority';
import dayjs from 'dayjs';
import { ComponentProps, ReactElement, useCallback, useState } from 'react';
+import { XIcon } from '~/icons/generated';
import {
FilterOperation,
FilterOperator,
@@ -238,7 +238,7 @@ export const FilterChip = ({
aria-label={`Remove ${label} filter`}
onClick={onRemove}
>
-
+
)}
diff --git a/packages/raystack/components/menu/menu-trigger.tsx b/packages/raystack/components/menu/menu-trigger.tsx
index 056f74d6e..5dee0db07 100644
--- a/packages/raystack/components/menu/menu-trigger.tsx
+++ b/packages/raystack/components/menu/menu-trigger.tsx
@@ -3,7 +3,7 @@
import { Autocomplete as AutocompletePrimitive } from '@base-ui/react';
import { Menu as MenuPrimitive } from '@base-ui/react/menu';
import { cx } from 'class-variance-authority';
-import { TriangleRightIcon } from '~/icons';
+import { ChevronRightIcon } from '~/icons/generated';
import { Button } from '../button';
import { useMenubarContext } from '../menubar/menubar';
import { Cell, CellBaseProps } from './cell';
@@ -56,7 +56,7 @@ export interface MenuSubTriggerProps
export function MenuSubTrigger({
children,
value,
- trailingIcon = ,
+ trailingIcon = ,
leadingIcon,
...props
}: MenuSubTriggerProps) {
diff --git a/packages/raystack/components/number-field/number-field.tsx b/packages/raystack/components/number-field/number-field.tsx
index 67e99592c..3f8483df0 100644
--- a/packages/raystack/components/number-field/number-field.tsx
+++ b/packages/raystack/components/number-field/number-field.tsx
@@ -1,9 +1,9 @@
'use client';
import { NumberField as NumberFieldPrimitive } from '@base-ui/react';
-import { MinusIcon, PlusIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ComponentProps, createContext, useContext, useId } from 'react';
+import { MinusIcon, PlusIcon } from '~/icons/generated';
import { useFieldContext } from '../field';
import { Label } from '../label';
import styles from './number-field.module.css';
diff --git a/packages/raystack/components/prompt-input/prompt-input-submit.tsx b/packages/raystack/components/prompt-input/prompt-input-submit.tsx
index 5bfadd10e..dcc1782fe 100644
--- a/packages/raystack/components/prompt-input/prompt-input-submit.tsx
+++ b/packages/raystack/components/prompt-input/prompt-input-submit.tsx
@@ -1,8 +1,8 @@
'use client';
-import { ArrowUpIcon, StopIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ComponentProps, MouseEvent } from 'react';
+import { ArrowUpIcon, SquareIcon } from '~/icons/generated';
import { Spinner } from '../spinner';
import styles from './prompt-input.module.css';
import { usePromptInputContext } from './prompt-input-context';
@@ -43,7 +43,7 @@ export function PromptInputSubmit({
(context.status === 'submitted' ? (
) : context.status === 'streaming' ? (
-
+
) : (
))}
diff --git a/packages/raystack/components/reasoning/reasoning.tsx b/packages/raystack/components/reasoning/reasoning.tsx
index 0f8c8c4d7..3961005f1 100644
--- a/packages/raystack/components/reasoning/reasoning.tsx
+++ b/packages/raystack/components/reasoning/reasoning.tsx
@@ -1,7 +1,6 @@
'use client';
import { useControlled } from '@base-ui/utils/useControlled';
-import { ChevronRightIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import {
ComponentProps,
@@ -12,6 +11,7 @@ import {
useMemo,
useRef
} from 'react';
+import { ChevronRightIcon } from '~/icons/generated';
import { Collapsible } from '../collapsible';
import styles from './reasoning.module.css';
diff --git a/packages/raystack/components/search/search.tsx b/packages/raystack/components/search/search.tsx
index 807ca7f1b..ca7c13184 100644
--- a/packages/raystack/components/search/search.tsx
+++ b/packages/raystack/components/search/search.tsx
@@ -1,6 +1,6 @@
'use client';
-import { CrossCircledIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
+import { CircleXIcon, SearchIcon } from '~/icons/generated';
import { IconButton } from '../icon-button';
import { Input } from '../input';
import { InputProps } from '../input/input';
@@ -38,7 +38,7 @@ export function Search({
aria-label='Clear search'
className={styles.clearButton}
>
-
+
) : undefined;
@@ -46,7 +46,7 @@ export function Search({
return (
}
+ leadingIcon={}
trailingIcon={trailingIconWithClear}
placeholder={placeholder}
disabled={disabled}
diff --git a/packages/raystack/components/select/select-trigger.tsx b/packages/raystack/components/select/select-trigger.tsx
index 6c75fbcaa..c4138b8c9 100644
--- a/packages/raystack/components/select/select-trigger.tsx
+++ b/packages/raystack/components/select/select-trigger.tsx
@@ -4,9 +4,9 @@ import {
Combobox as ComboboxPrimitive,
Select as SelectPrimitive
} from '@base-ui/react';
-import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cva, VariantProps } from 'class-variance-authority';
import { ComponentProps, SVGAttributes } from 'react';
+import { ChevronDownIcon } from '~/icons/generated';
import { Flex } from '../flex';
import styles from './select.module.css';
import { useSelectContext } from './select-root';
diff --git a/packages/raystack/components/sidebar/sidebar-misc.tsx b/packages/raystack/components/sidebar/sidebar-misc.tsx
index f3a32036f..38981e47f 100644
--- a/packages/raystack/components/sidebar/sidebar-misc.tsx
+++ b/packages/raystack/components/sidebar/sidebar-misc.tsx
@@ -1,7 +1,6 @@
'use client';
import { Accordion as AccordionPrimitive } from '@base-ui/react';
-import { TriangleDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import {
ComponentProps,
@@ -10,6 +9,7 @@ import {
useContext,
useState
} from 'react';
+import { ChevronDownIcon } from '~/icons/generated';
import { Flex } from '../flex';
import styles from './sidebar.module.css';
import { SidebarLeadingVisual } from './sidebar-leading-visual';
@@ -165,7 +165,7 @@ export function SidebarNavigationGroup({
>
{label}
-
diff --git a/packages/raystack/components/sidebar/sidebar-more.tsx b/packages/raystack/components/sidebar/sidebar-more.tsx
index 22a238fb1..d03d60233 100644
--- a/packages/raystack/components/sidebar/sidebar-more.tsx
+++ b/packages/raystack/components/sidebar/sidebar-more.tsx
@@ -1,8 +1,8 @@
'use client';
-import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { ReactNode, useContext } from 'react';
+import { EllipsisIcon } from '~/icons/generated';
import { Menu } from '../menu';
import { Tooltip } from '../tooltip';
import styles from './sidebar.module.css';
@@ -31,9 +31,7 @@ export function SidebarMore({
const { isCollapsed, position, hideCollapsedItemTooltip } =
useContext(SidebarContext);
if (!children) return null;
- const triggerIcon = leadingIcon ?? (
-
- );
+ const triggerIcon = leadingIcon ?? ;
const triggerContent = (