diff --git a/.github/workflows/release-rc.yaml b/.github/workflows/release-rc.yaml index ba779afff..774c535ed 100644 --- a/.github/workflows/release-rc.yaml +++ b/.github/workflows/release-rc.yaml @@ -42,6 +42,9 @@ jobs: - name: Install Dependencies πŸ”§ run: pnpm install + - name: Check the icon map + run: pnpm --filter=@raystack/apsara check:icon-map + - name: Build Step πŸ”§ env: CI: "" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 07ae75b5e..d62cc4483 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -42,6 +42,9 @@ jobs: - name: Install Dependencies πŸ”§ run: pnpm install + - name: Check the icon map + run: pnpm --filter=@raystack/apsara check:icon-map + - name: Build Step πŸ”§ env: CI: "" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 218d40db6..e5a72eb0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,6 +32,12 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check the icon map + run: pnpm --filter=@raystack/apsara check:icon-map + + - name: Generate the icon registry + run: pnpm --filter=@raystack/apsara build:icon-registry + - name: Run tests run: pnpm --filter=@raystack/apsara test diff --git a/.gitignore b/.gitignore index 11abb209e..90267296e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,9 @@ yarn-error.log* # generated Code Connect batch file (created by figma:generate-icons at publish time) packages/raystack/figma/icons.figma.batch.json + +# generated icon registry β€” one wrapper module per key of icons/icon-map.json. +# Written by `build:icon-registry`, which runs on `pnpm install` (the `prepare` +# script of @raystack/apsara) and again before build, dev and test. Edit +# icon-map.json, never these files. +packages/raystack/icons/generated/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f51f2f845..44c01ae66 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -192,8 +192,12 @@ pnpm lint # Clean build artifacts pnpm clean -# Build icon components -pnpm build:icons +# Generate the icon registry from icons/icon-map.json into icons/generated/ +# (not in git β€” `pnpm install` runs this for you, and so do build, dev and test) +pnpm build:icon-registry + +# Check that every icon-map.json entry resolves to a real lucide export +pnpm check:icon-map ``` ## Testing diff --git a/apps/www/src/app/examples/datatable-virtual/page.tsx b/apps/www/src/app/examples/datatable-virtual/page.tsx index 737e98b67..5989b4a5b 100644 --- a/apps/www/src/app/examples/datatable-virtual/page.tsx +++ b/apps/www/src/app/examples/datatable-virtual/page.tsx @@ -12,7 +12,11 @@ import { Sidebar, Text } from '@raystack/apsara'; -import { BellIcon, FilterIcon, SidebarIcon } from '@raystack/apsara/icons'; +import { + BellIcon, + ListFilterIcon, + PanelLeftIcon +} from '@raystack/apsara/icons'; import { useCallback, useMemo, useState } from 'react'; const sampleData = [ @@ -514,13 +518,13 @@ const Page = () => { } + leadingIcon={} > DataTable – Virtualized } + leadingIcon={} > DataTable – Content @@ -553,7 +557,11 @@ const Page = () => { size='small' style={{ width: '200px' }} /> - @@ -586,7 +594,7 @@ const Page = () => { rowHeight={44.5} emptyState={ } + icon={} heading='No results' variant='empty1' subHeading='Try adjusting your filters or search.' diff --git a/apps/www/src/app/examples/datatable/page.tsx b/apps/www/src/app/examples/datatable/page.tsx index c8a2d9b6e..785e329b7 100644 --- a/apps/www/src/app/examples/datatable/page.tsx +++ b/apps/www/src/app/examples/datatable/page.tsx @@ -12,7 +12,11 @@ import { Sidebar, Text } from '@raystack/apsara'; -import { BellIcon, FilterIcon, SidebarIcon } from '@raystack/apsara/icons'; +import { + BellIcon, + ListFilterIcon, + PanelLeftIcon +} from '@raystack/apsara/icons'; import { useState } from 'react'; const sampleData = [ @@ -527,11 +531,11 @@ const Page = () => { } + leadingIcon={} > DataTable - }> + }> Reports Activities @@ -567,7 +571,11 @@ const Page = () => { size='small' style={{ width: '200px' }} /> - @@ -594,7 +602,7 @@ const Page = () => { } + icon={} heading='No results' variant='empty1' subHeading='Try adjusting your filters or search.' diff --git a/apps/www/src/app/examples/dataview/page.tsx b/apps/www/src/app/examples/dataview/page.tsx index 32c79b21c..dbb2535cc 100644 --- a/apps/www/src/app/examples/dataview/page.tsx +++ b/apps/www/src/app/examples/dataview/page.tsx @@ -19,7 +19,11 @@ import { Sidebar, Text } from '@raystack/apsara'; -import { BellIcon, FilterIcon, SidebarIcon } from '@raystack/apsara/icons'; +import { + BellIcon, + ListFilterIcon, + PanelLeftIcon +} from '@raystack/apsara/icons'; type ProfileCell = { row: { original: Profile } }; @@ -502,7 +506,7 @@ const Page = () => { } + leadingIcon={} active > DataView @@ -596,7 +600,7 @@ const Page = () => { {/* Empty/zero state lifted out of renderers β€” single sibling reads context */} } + icon={} heading='No matching people' variant='empty1' subHeading='Try adjusting your filters or search.' @@ -604,7 +608,7 @@ const Page = () => { } + icon={} heading='No people yet' variant='empty1' subHeading='Add your first teammate to get started.' diff --git a/apps/www/src/app/examples/icons/page.tsx b/apps/www/src/app/examples/icons/page.tsx new file mode 100644 index 000000000..b47f71b3d --- /dev/null +++ b/apps/www/src/app/examples/icons/page.tsx @@ -0,0 +1,613 @@ +'use client'; + +/** + * A/B review page for the icon registry. + * + * The left column of every pair renders inside ``, + * which restores the old @radix-ui/react-icons appearance. The right column + * uses the lucide defaults that Apsara now ships. So this page is both the + * review tool and a live test of the override mechanism. + * + * The `radixIcons` map below lives in this file only β€” the package ships no + * radix preset. It is also the map that the migration guide gives to a consumer + * that wants to keep the old appearance. + * + * DELETE THIS PAGE after the release. + */ + +import { + ArrowDownIcon as RadixArrowDown, + ArrowUpIcon as RadixArrowUp, + CalendarIcon as RadixCalendar, + CheckIcon as RadixCheck, + CheckCircledIcon as RadixCheckCircled, + ChevronDownIcon as RadixChevronDown, + ChevronLeftIcon as RadixChevronLeft, + ChevronRightIcon as RadixChevronRight, + CopyIcon as RadixCopy, + Cross1Icon as RadixCross1, + CrossCircledIcon as RadixCrossCircled, + DotsHorizontalIcon as RadixDotsHorizontal, + ExclamationTriangleIcon as RadixExclamationTriangle, + FileTextIcon as RadixFileText, + InfoCircledIcon as RadixInfoCircled, + MagnifyingGlassIcon as RadixMagnifyingGlass, + MinusIcon as RadixMinus, + MixerHorizontalIcon as RadixMixerHorizontal, + MoonIcon as RadixMoon, + PlusIcon as RadixPlus, + SizeIcon as RadixSize, + StopIcon as RadixStop, + SunIcon as RadixSun, + TableIcon as RadixTable, + TextAlignBottomIcon as RadixTextAlignBottom, + TextAlignTopIcon as RadixTextAlignTop, + TriangleDownIcon as RadixTriangleDown +} from '@radix-ui/react-icons'; +import { + Accordion, + ArrowDownIcon, + ArrowDownWideNarrowIcon, + ArrowUpIcon, + ArrowUpNarrowWideIcon, + Breadcrumb, + Button, + CalendarDaysIcon, + Callout, + CheckIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + CircleCheckIcon, + CircleXIcon, + CoPilotIcon, + CopyIcon, + EllipsisIcon, + ExpandIcon, + FileTextIcon, + Flex, + type IconOverrides, + InfoIcon, + ListFilterIcon, + Menu, + MinusIcon, + MoonIcon, + NumberField, + PlusIcon, + Search, + SearchIcon, + Select, + ShrinkIcon, + SlidersHorizontalIcon, + SquareIcon, + SunIcon, + TableIcon, + Text, + Theme, + TriangleAlertIcon, + XIcon +} from '@raystack/apsara'; + +/** The map that restores the radix appearance. 27 of the 29 internal keys. */ +const radixIcons: IconOverrides = { + XIcon: RadixCross1, + CircleXIcon: RadixCrossCircled, + CircleCheckIcon: RadixCheckCircled, + TriangleAlertIcon: RadixExclamationTriangle, + InfoIcon: RadixInfoCircled, + CheckIcon: RadixCheck, + CopyIcon: RadixCopy, + SearchIcon: RadixMagnifyingGlass, + ChevronDownIcon: RadixChevronDown, + ChevronLeftIcon: RadixChevronLeft, + ChevronRightIcon: RadixChevronRight, + ArrowUpIcon: RadixArrowUp, + ArrowDownIcon: RadixArrowDown, + PlusIcon: RadixPlus, + MinusIcon: RadixMinus, + EllipsisIcon: RadixDotsHorizontal, + ExpandIcon: RadixSize, + ShrinkIcon: RadixMinus, + SquareIcon: RadixStop, + CalendarDaysIcon: RadixCalendar, + FileTextIcon: RadixFileText, + TableIcon: RadixTable, + SlidersHorizontalIcon: RadixMixerHorizontal, + ArrowUpNarrowWideIcon: RadixTextAlignTop, + ArrowDownWideNarrowIcon: RadixTextAlignBottom, + SunIcon: RadixSun, + MoonIcon: RadixMoon +}; + +/** + * The sidebar used the solid `TriangleDownIcon`, so its "before" is not the + * radix `ChevronDownIcon` that other call sites used. Kept separate so the map + * above stays a straight per-key preset. + */ +const sidebarBefore: IconOverrides = { ChevronDownIcon: RadixTriangleDown }; + +type Row = { + key: string; + Icon: React.ComponentType>; + replaces: string; + changes?: string; + /** The "before" is not in `radixIcons` β€” see the note in each row. */ + before?: IconOverrides; +}; + +const ROWS: Row[] = [ + { key: 'XIcon', Icon: XIcon, replaces: 'Cross1Icon, Cross2Icon' }, + { + key: 'CircleXIcon', + Icon: CircleXIcon, + replaces: 'CrossCircledIcon', + changes: 'Shared key: the toast error and the search clear' + }, + { + key: 'CircleCheckIcon', + Icon: CircleCheckIcon, + replaces: 'CheckCircledIcon' + }, + { + key: 'TriangleAlertIcon', + Icon: TriangleAlertIcon, + replaces: 'ExclamationTriangleIcon' + }, + { key: 'InfoIcon', Icon: InfoIcon, replaces: 'InfoCircledIcon' }, + { key: 'CheckIcon', Icon: CheckIcon, replaces: 'CheckIcon' }, + { key: 'CopyIcon', Icon: CopyIcon, replaces: 'CopyIcon' }, + { key: 'SearchIcon', Icon: SearchIcon, replaces: 'MagnifyingGlassIcon' }, + { + key: 'ChevronDownIcon', + Icon: ChevronDownIcon, + replaces: 'ChevronDownIcon' + }, + { + key: 'ChevronDownIcon (sidebar)', + Icon: ChevronDownIcon, + replaces: 'TriangleDownIcon', + changes: 'A solid triangle becomes a chevron. lucide has no solid caret', + before: sidebarBefore + }, + { + key: 'ChevronLeftIcon', + Icon: ChevronLeftIcon, + replaces: 'ChevronLeftIcon' + }, + { + key: 'ChevronRightIcon', + Icon: ChevronRightIcon, + replaces: 'ChevronRightIcon, in-house TriangleRightIcon', + changes: 'The submenu marker changes: a solid triangle becomes a chevron' + }, + { key: 'ArrowUpIcon', Icon: ArrowUpIcon, replaces: 'ArrowUpIcon' }, + { key: 'ArrowDownIcon', Icon: ArrowDownIcon, replaces: 'ArrowDownIcon' }, + { key: 'PlusIcon', Icon: PlusIcon, replaces: 'PlusIcon' }, + { + key: 'MinusIcon', + Icon: MinusIcon, + replaces: 'MinusIcon', + changes: 'The number field only' + }, + { key: 'EllipsisIcon', Icon: EllipsisIcon, replaces: 'DotsHorizontalIcon' }, + { + key: 'ExpandIcon', + Icon: ExpandIcon, + replaces: 'SizeIcon', + changes: 'The chat panel. The appearance changes' + }, + { + key: 'ShrinkIcon', + Icon: ShrinkIcon, + replaces: 'MinusIcon', + changes: + 'The chat panel. The appearance changes. Expand and Shrink are a matched pair' + }, + { + key: 'SquareIcon', + Icon: SquareIcon, + replaces: 'StopIcon', + changes: 'The radix icon is solid' + }, + { + key: 'CalendarDaysIcon', + Icon: CalendarDaysIcon, + replaces: 'CalendarIcon', + changes: 'The Figma file gives calendar-days, not calendar' + }, + { key: 'FileTextIcon', Icon: FileTextIcon, replaces: 'FileTextIcon' }, + { key: 'TableIcon', Icon: TableIcon, replaces: 'TableIcon' }, + { + key: 'SlidersHorizontalIcon', + Icon: SlidersHorizontalIcon, + replaces: 'MixerHorizontalIcon' + }, + { + key: 'ArrowUpNarrowWideIcon', + Icon: ArrowUpNarrowWideIcon, + replaces: 'TextAlignTopIcon', + changes: 'The appearance changes' + }, + { + key: 'ArrowDownWideNarrowIcon', + Icon: ArrowDownWideNarrowIcon, + replaces: 'TextAlignBottomIcon', + changes: 'The appearance changes' + }, + { key: 'SunIcon', Icon: SunIcon, replaces: 'SunIcon' }, + { key: 'MoonIcon', Icon: MoonIcon, replaces: 'MoonIcon' }, + { + key: 'ListFilterIcon', + Icon: ListFilterIcon, + replaces: 'in-house FilterIcon', + changes: + 'No radix equivalent, so the left cell shows the lucide icon too. The shape is almost the same as the in-house SVG' + }, + { + key: 'CoPilotIcon', + Icon: CoPilotIcon, + replaces: 'in-house co-pilot.svg', + changes: 'The default stays the in-house SVG, so both cells are identical' + } +]; + +const panel = { + padding: 'var(--rs-space-5)', + borderRadius: 'var(--rs-space-3)', + border: '1px solid var(--rs-color-border-base-primary)', + backgroundColor: 'var(--rs-color-background-base-primary)' +}; + +const cell = { + padding: 'var(--rs-space-3) var(--rs-space-4)', + borderBottom: '1px solid var(--rs-color-border-base-secondary)', + textAlign: 'left' as const, + verticalAlign: 'middle' as const +}; + +/** Side by side: the same children under radix, then under the lucide default. */ +function AB({ + children, + before = radixIcons +}: { + children: React.ReactNode; + before?: IconOverrides; +}) { + return ( + + + {children} + + + {children} + + + ); +} + +export default function IconsExamplePage() { + return ( + + + + Icon registry β€” A/B review + + + The left column of every pair restores the old radix appearance + through <Theme icons={radixIcons}>. The + right column is the lucide default that Apsara now ships. 29 internal + keys, 9 of which change the appearance. + + + Base props: width=16 height=16 strokeWidth=1.5. lucide + draws in a 24-unit viewBox, so the rendered stroke is{' '} + strokeWidth Γ— 16 / 24 = 1px β€” the stroke weight the Figma + library uses at 16px. + + + + + + The internal 29 + + + + + + + + + + + + + {ROWS.map(({ key, Icon, replaces, changes, before }) => ( + + + + + + + + ))} + +
+ + Key + + + + Before (radix) + + + + After (lucide) + + + + Replaces + + + + Note + +
+ + {key} + + + + + + + + + + {replaces} + + + + {changes ?? ''} + +
+
+ + + + Stroke weight + + + 1.5 matches the Figma library, which draws a 1px stroke in a 16px + frame. The neighbours are here to confirm that reading on screen. + + + {[1, 1.25, 1.5, 1.75, 2].map(weight => ( + + + + + + + + + + + {weight} + {weight === 1.5 ? ' (design)' : ''} + + + ))} + + + + + + The components that show these icons + + + Each pair is the same component: radix on the left, lucide on the + right. + + + + + + + + + + + + + + + + + + + + + + + + { + // The review page keeps the callout mounted. + }} + > + A callout + + + + + + + + + Is it accessible? + Yes. + + + + + + + + + Home + + + + + Shoes + + + + + + + + + }> + Actions + + + Assign member… + + Export + + CSV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +function Labelled({ + label, + children +}: { + label: string; + children: React.ReactNode; +}) { + return ( + + + {label} + + {children} + + ); +} diff --git a/apps/www/src/app/examples/table/page.tsx b/apps/www/src/app/examples/table/page.tsx index 67b2737f1..5934cbf25 100644 --- a/apps/www/src/app/examples/table/page.tsx +++ b/apps/www/src/app/examples/table/page.tsx @@ -157,13 +157,13 @@ const Page = () => { trigger={({ appliedFilters }) => appliedFilters.size > 0 ? ( - + ) : ( + + + +

+ {copied + ? `Copied ${copied}` + : query + ? `${matches.length} of ${ICONS.length} icons` + : `${ICONS.length} icons`} +

+ + {matches.length === 0 ? ( +

No icon matches β€œ{query}”.

+ ) : ( +
+ {matches.map(({ name, Icon }) => ( + copy(name)} + data-copied={copied === name || undefined} + aria-label={`Copy ${name}`} + > + + + ))} +
+ )} + + + {({ payload }) => ( + + {copied && copied === payload ? 'Copied' : payload} + + )} + + + ); +} 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({ 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({ )} 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' ? (