diff --git a/.github/workflows/code-connect.yaml b/.github/workflows/code-connect.yaml index ca88bab38..c6f9e5339 100644 --- a/.github/workflows/code-connect.yaml +++ b/.github/workflows/code-connect.yaml @@ -10,7 +10,6 @@ on: paths: - "packages/raystack/figma/**" - "packages/raystack/figma.config.json" - - "packages/raystack/scripts/generate-icons-code-connect.js" - ".github/workflows/code-connect.yaml" # Avoid overlapping publishes racing against the same Figma file. @@ -41,28 +40,17 @@ jobs: - name: Install Dependencies πŸ”§ run: pnpm install --frozen-lockfile - # Redact the Figma file keys from all subsequent step logs. The publish + # Redact the Figma file key from all subsequent step logs. The publish # command prints every mapped node URL; masking turns the file key into # *** so links read https://figma.com/file/***/?node-id=... in CI logs. - - name: Mask Figma file keys πŸ”’ + - name: Mask Figma file key πŸ”’ env: FIGMA_FILE_URL: ${{ secrets.FIGMA_FILE_URL }} - FIGMA_ICONS_FILE_URL: ${{ secrets.FIGMA_ICONS_FILE_URL }} run: | - for url in "$FIGMA_FILE_URL" "$FIGMA_ICONS_FILE_URL"; do - key=$(printf '%s' "$url" | sed -E 's#.*/(design|file)/([^/?]+).*#\2#') - if [ -n "$key" ] && [ "$key" != "$url" ]; then - echo "::add-mask::$key" - fi - done - - # Generate the per-icon batch JSON from the Figma icons file. - - name: Generate icon mappings 🎨 - working-directory: ./packages/raystack - env: - FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} - FIGMA_ICONS_FILE_URL: ${{ secrets.FIGMA_ICONS_FILE_URL }} - run: pnpm figma:generate-icons + key=$(printf '%s' "$FIGMA_FILE_URL" | sed -E 's#.*/(design|file)/([^/?]+).*#\2#') + if [ -n "$key" ] && [ "$key" != "$FIGMA_FILE_URL" ]; then + echo "::add-mask::$key" + fi # Substitute the real Figma file URL into figma.config.json. - name: Update Figma config πŸ”§ diff --git a/.gitignore b/.gitignore index 11abb209e..8e5cfef62 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,3 @@ yarn-error.log* # npm pack artifacts *.tgz - -# generated Code Connect batch file (created by figma:generate-icons at publish time) -packages/raystack/figma/icons.figma.batch.json diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 05bdef754..c59c477f1 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -191,11 +191,21 @@ pnpm lint # Clean build artifacts pnpm clean - -# Build icon components -pnpm build:icons ``` +### Icons + +The icons live in one committed file, `packages/raystack/icons/icons.tsx`: one +`createIcon` call per key, 31 of them. There is no generator and no build step β€” +to add, remove, or repoint an icon, edit that file. + +`icons/types.ts` derives `IconName` from its exports with `keyof typeof icons`, +so the union cannot drift and nothing needs to be kept in step by hand. + +`icons/__tests__/bundle.test.ts` is the test that matters here. It bundles a +three-icon fixture with the real rollup and asserts the other 28 keys and their +lucide imports are gone. That is what keeps one file from becoming one bundle. + ## Testing Apsara uses Vitest with React Testing Library for testing: diff --git a/apps/www/package.json b/apps/www/package.json index 817675695..f66b19f81 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -13,7 +13,6 @@ "format": "biome format --write ." }, "dependencies": { - "@radix-ui/react-icons": "^1.3.2", "@raystack/apsara": "workspace:*", "@types/mdast": "^4.0.4", "class-variance-authority": "^0.7.1", diff --git a/apps/www/src/components/dataview-demo.tsx b/apps/www/src/components/dataview-demo.tsx index e092eaa25..d4d603441 100644 --- a/apps/www/src/components/dataview-demo.tsx +++ b/apps/www/src/components/dataview-demo.tsx @@ -1,6 +1,5 @@ 'use client'; -import { ListBulletIcon, RowsIcon, TransformIcon } from '@radix-ui/react-icons'; import { Avatar, Badge, @@ -18,6 +17,7 @@ import { type TimelineCardContext, useDataView } from '@raystack/apsara'; +import { Frame, LayoutList, Rows3 } from 'lucide-react'; import { useMemo, useRef, useState } from 'react'; type Person = { @@ -243,8 +243,16 @@ export function DataViewSearchDemo() { export function DataViewMultiViewDemo() { const views = useMemo( () => [ - { value: 'table', label: 'Table', leadingIcon: }, - { value: 'list', label: 'List', leadingIcon: } + { + value: 'table', + label: 'Table', + leadingIcon: + }, + { + value: 'list', + label: 'List', + leadingIcon: + } ], [] ); @@ -525,8 +533,16 @@ export function DataViewLoadingDemo() { export function DataViewPerViewFieldsDemo() { const views = useMemo( () => [ - { value: 'table', label: 'Table', leadingIcon: }, - { value: 'list', label: 'List', leadingIcon: } + { + value: 'table', + label: 'Table', + leadingIcon: + }, + { + value: 'list', + label: 'List', + leadingIcon: + } ], [] ); @@ -611,7 +627,7 @@ function SelectionBar() { variant='outline' size='large' color='neutral' - leadingIcon={} + leadingIcon={} isDismissible onDismiss={() => table.resetRowSelection()} > diff --git a/apps/www/src/components/demo/demo-controls.tsx b/apps/www/src/components/demo/demo-controls.tsx index a94e194f2..9318795f2 100644 --- a/apps/www/src/components/demo/demo-controls.tsx +++ b/apps/www/src/components/demo/demo-controls.tsx @@ -1,20 +1,16 @@ -import { - InfoCircledIcon, - Pencil2Icon, - PlusIcon, - TransformIcon, - UploadIcon -} from '@radix-ui/react-icons'; import { Field, Flex, IconButton, + InfoIcon, Input, + PlusIcon, Select, Switch, Text } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; +import { Frame, Pencil, Upload } from 'lucide-react'; import { camelCaseToWords } from '@/lib/utils'; import styles from './styles.module.css'; import { @@ -32,10 +28,19 @@ type PropControlsProps = { const ICONS_MAP = { plus: { icon: , value: '' }, - transform: { icon: , value: '' }, - pencil: { icon: , value: '' }, - info: { icon: , value: '' }, - upload: { icon: , value: '' } + transform: { + icon: , + value: '' + }, + pencil: { + icon: , + value: '' + }, + info: { icon: , value: '' }, + upload: { + icon: , + value: '' + } }; export default function DemoControls({ diff --git a/apps/www/src/components/demo/demo-playground.tsx b/apps/www/src/components/demo/demo-playground.tsx index 7ef253f08..7e1231403 100644 --- a/apps/www/src/components/demo/demo-playground.tsx +++ b/apps/www/src/components/demo/demo-playground.tsx @@ -1,9 +1,8 @@ 'use client'; -import { Cross2Icon } from '@radix-ui/react-icons'; -import { Dialog, Flex, IconButton } from '@raystack/apsara'; -import { ResetIcon } from '@raystack/apsara/icons'; +import { Dialog, Flex, IconButton, XIcon } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; +import { RotateCcw } from 'lucide-react'; import { ReadonlyURLSearchParams, useRouter, @@ -114,14 +113,14 @@ export default function DemoPlayground({ onClick={resetProps} aria-label='Reset to default props' > - + setOpenPlayground(false)} aria-label='Close playground' > - + diff --git a/apps/www/src/components/demo/demo.tsx b/apps/www/src/components/demo/demo.tsx index 7b9df8814..5b5226e94 100644 --- a/apps/www/src/components/demo/demo.tsx +++ b/apps/www/src/components/demo/demo.tsx @@ -1,39 +1,36 @@ 'use client'; -import { - BorderSolidIcon, - CheckCircledIcon, - ColorWheelIcon, - Component1Icon, - FileIcon, - FontBoldIcon, - FontFamilyIcon, - FontItalicIcon, - InfoCircledIcon, - LayersIcon, - OpenInNewWindowIcon, - Pencil2Icon, - PlusIcon, - ShadowIcon, - Share2Icon, - SpaceBetweenHorizontallyIcon, - TextAlignCenterIcon, - TextAlignLeftIcon, - TextAlignRightIcon, - TransformIcon, - UnderlineIcon, - UploadIcon -} from '@radix-ui/react-icons'; import * as Apsara from '@raystack/apsara'; -import { - BellIcon, - FilterIcon, - OrganizationIcon, - ShoppingBagFilledIcon, - SidebarIcon -} from '@raystack/apsara/icons'; import dayjs from 'dayjs'; -import { Home, Info, Laugh, X } from 'lucide-react'; +import { + AlignCenter, + AlignLeft, + AlignRight, + Bell, + Bold, + Building2, + Component, + Contrast, + ExternalLink, + Frame, + Home, + Info, + Italic, + Laugh, + Layers, + LayoutList, + Minus, + Palette, + Pencil, + Rows3, + Share2, + ShoppingBag, + StretchHorizontal, + Type, + Underline, + Upload, + X +} from 'lucide-react'; import NextLink from 'next/link'; import { Suspense } from 'react'; import ChipInputDemo from '../chip-input-demo'; @@ -64,13 +61,13 @@ import { DemoProps } from './types'; export default function Demo(props: DemoProps) { const { data, + // `...Apsara` carries the 31 icons Apsara publishes, so none of those needs + // its own entry β€” and nothing below may repeat one of their keys, because a + // later key shadows the spread. A demo that needs any other glyph names a + // lucide component from the block above and sizes it at the call site, + // which is exactly what an application does. scope = { ...Apsara, - BellIcon, - FilterIcon, - ShoppingBagFilledIcon, - OrganizationIcon, - SidebarIcon, DataViewTableDemo, DataViewListDemo, DataViewMultiViewDemo, @@ -90,33 +87,34 @@ export default function Demo(props: DemoProps) { LinearMenuDemo, PopoverColorPicker, TourDemo, - Info, - X, + NextLink, + AlignCenter, + AlignLeft, + AlignRight, + Bell, + Bold, + Building2, + Component, + Contrast, + ExternalLink, + Frame, Home, + Info, + Italic, Laugh, - NextLink, - PlusIcon, - TransformIcon, - CheckCircledIcon, - Pencil2Icon, - InfoCircledIcon, - UploadIcon, - FontBoldIcon, - FontItalicIcon, - UnderlineIcon, - TextAlignLeftIcon, - TextAlignCenterIcon, - TextAlignRightIcon, - Component1Icon, - FileIcon, - FontFamilyIcon, - LayersIcon, - OpenInNewWindowIcon, - Share2Icon, - BorderSolidIcon, - ColorWheelIcon, - ShadowIcon, - SpaceBetweenHorizontallyIcon, + LayoutList, + Layers, + Minus, + Palette, + Pencil, + Rows3, + Share2, + ShoppingBag, + StretchHorizontal, + Type, + Underline, + Upload, + X, dayjs } } = props; diff --git a/apps/www/src/components/docs/footer.tsx b/apps/www/src/components/docs/footer.tsx index 8abb405a8..ff3689d83 100644 --- a/apps/www/src/components/docs/footer.tsx +++ b/apps/www/src/components/docs/footer.tsx @@ -1,6 +1,6 @@ -import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons'; import { Button, Flex, Link } from '@raystack/apsara'; import { findNeighbour } from 'fumadocs-core/page-tree'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; import { docs } from '@/lib/source'; type DocsFooterProps = { @@ -17,7 +17,7 @@ export default function DocsFooter({ url }: DocsFooterProps) { variant='outline' color='neutral' size='normal' - leadingIcon={} + leadingIcon={} > {neighbours.previous.name} @@ -31,7 +31,7 @@ export default function DocsFooter({ url }: DocsFooterProps) { variant='outline' color='neutral' size='normal' - trailingIcon={} + trailingIcon={} > {neighbours.next.name} diff --git a/apps/www/src/components/docs/navbar.tsx b/apps/www/src/components/docs/navbar.tsx index 32d1487ef..babe95fc2 100644 --- a/apps/www/src/components/docs/navbar.tsx +++ b/apps/www/src/components/docs/navbar.tsx @@ -1,12 +1,8 @@ 'use client'; -import { - ComponentBooleanIcon, - CopyIcon, - GitHubLogoIcon -} from '@radix-ui/react-icons'; -import { Breadcrumb, Button } from '@raystack/apsara'; +import { Breadcrumb, Button, CopyIcon } from '@raystack/apsara'; import { useBreadcrumb } from 'fumadocs-core/breadcrumb'; import { Root } from 'fumadocs-core/page-tree'; +import { Component, Github } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Fragment, useState } from 'react'; @@ -94,11 +90,7 @@ export default function DocsNavbar({ disabled={isLoading} onClick={() => setOpenPlayground(true)} leadingIcon={ - + } > Playground @@ -123,11 +115,7 @@ export default function DocsNavbar({ color='neutral' size='small' leadingIcon={ - + } > View source diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index 84a33fd87..902a36c47 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -1,27 +1,32 @@ 'use client'; import { ChevronDownIcon, - ColorWheelIcon, - ComponentInstanceIcon, - CornersIcon, - Cross1Icon, - DashboardIcon, - ExclamationTriangleIcon, - MagnifyingGlassIcon, - MixIcon, - ReaderIcon, - RocketIcon, - ShadowIcon, - SpaceBetweenVerticallyIcon, - TextIcon -} from '@radix-ui/react-icons'; -import { Command, EmptyState, IconButton, Spinner } from '@raystack/apsara'; + Command, + EmptyState, + IconButton, + SearchIcon, + Spinner, + WarningIcon, + XIcon +} from '@raystack/apsara'; import { flattenTree, type Item as PageItem, Root } from 'fumadocs-core/page-tree'; import { useDocsSearch } from 'fumadocs-core/search/client'; +import { + Blend, + BookOpen, + Component, + Contrast, + LayoutDashboard, + Palette, + Radius, + Rocket, + StretchVertical, + Type +} from 'lucide-react'; import { useRouter } from 'next/navigation'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { getFileFromUrl, getFolderFromUrl } from '@/lib/utils'; @@ -37,20 +42,23 @@ type SearchItems = { }; /* Map known page slugs to icons; everything else falls back to a generic one. */ -const PAGE_ICONS: Record = { - docs: ReaderIcon, - 'getting-started': RocketIcon, - styling: MixIcon, - overview: DashboardIcon, - colors: ColorWheelIcon, - typography: TextIcon, - spacing: SpaceBetweenVerticallyIcon, - radius: CornersIcon, - effects: ShadowIcon +// Section glyphs. None of these is an icon Apsara publishes β€” the set holds +// only what Apsara's own components draw β€” so they come from lucide directly +// and are sized at the call site. +const PAGE_ICONS: Record = { + docs: BookOpen, + 'getting-started': Rocket, + styling: Blend, + overview: LayoutDashboard, + colors: Palette, + typography: Type, + spacing: StretchVertical, + radius: Radius, + effects: Contrast }; -const FallbackPageIcon = ComponentInstanceIcon; +const FallbackPageIcon = Component; -const getPageIcon = (url: string): typeof MagnifyingGlassIcon => +const getPageIcon = (url: string): typeof BookOpen => PAGE_ICONS[getFileFromUrl(url)] ?? FallbackPageIcon; export default function DocsSearch({ pageTree }: { pageTree: Root }) { @@ -188,7 +196,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { } > - +
} + leadingIcon={} placeholder='Search docs' autoComplete='off' /> @@ -213,7 +221,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { onClick={() => setSearch('')} className={styles.searchClearButton} > - + )}
@@ -226,7 +234,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { variant='empty1' heading='No result found' subHeading='The keyword you’re searching for isn’t in the documentβ€”try using a different term.' - icon={} + icon={} /> )} diff --git a/apps/www/src/components/icongallery/icongallery.module.css b/apps/www/src/components/icongallery/icongallery.module.css new file mode 100644 index 000000000..37e231626 --- /dev/null +++ b/apps/www/src/components/icongallery/icongallery.module.css @@ -0,0 +1,235 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--rs-space-3); + margin: var(--rs-space-5) 0; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +/* Toolbar -------------------------------------------------------------------- */ + +/* Sticky, because the sliders are worth keeping in reach while scrolling the + grid. 50px clears the docs navbar, which is sticky at the top of the page; + its z-index is the portal one, so this passes underneath it. */ +.bar { + position: sticky; + top: 50px; + z-index: 1; + padding: var(--rs-space-5) 0 var(--rs-space-3); + background-color: var(--rs-color-background-base-primary); +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--rs-space-3); + padding: var(--rs-space-3); + background-color: var(--rs-color-background-base-secondary); + border: 1px solid var(--rs-color-border-base-primary); + border-radius: 14px; +} + +/* Every segment is one raised pill on the tray. */ +.control, +.color, +.reset { + box-sizing: border-box; + display: flex; + align-items: center; + height: 40px; + padding: 0 var(--rs-space-4); + background-color: var(--rs-color-background-base-primary); + border: 1px solid var(--rs-color-border-base-primary); + border-radius: 10px; +} + +.control { + flex: none; + gap: var(--rs-space-3); +} + +.label { + font-size: 12px; + color: var(--rs-color-foreground-base-tertiary); +} + +.value { + min-width: 32px; + font-size: 13px; + color: var(--rs-color-foreground-base-primary); + font-variant-numeric: tabular-nums; +} + +.color { + flex: none; + gap: var(--rs-space-3); + color: var(--rs-color-foreground-base-secondary); + cursor: pointer; +} + +.swatch { + flex: none; + width: 14px; + height: 14px; + border-radius: 4px; + background: currentColor; + box-shadow: inset 0 0 0 1px var(--rs-color-border-base-primary); +} + +.hex { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + color: var(--rs-color-foreground-base-primary); +} + +.pickerPopup { + width: 248px; +} + +.picker { + width: 100%; + height: 208px; +} + +.reset { + flex: none; + justify-content: center; + width: 40px; + padding: 0; + color: var(--rs-color-foreground-base-secondary); + cursor: pointer; +} + +.color:hover, +.reset:hover:not(:disabled) { + background-color: var(--rs-color-background-base-primary-hover); +} + +.reset:disabled { + color: var(--rs-color-foreground-base-tertiary); + cursor: default; +} + +.color:focus-visible, +.reset:focus-visible { + outline: 2px solid var(--rs-color-border-accent-emphasis); + outline-offset: -1px; +} + +/* Sliders. The filled portion comes from --ig-fill, set inline from the value, + because a range input cannot express it in CSS alone. */ + +.slider { + flex: none; + width: 72px; + height: 16px; + background: none; + cursor: pointer; + appearance: none; +} + +.slider::-webkit-slider-runnable-track { + height: 4px; + border-radius: 999px; + background: linear-gradient( + to right, + var(--rs-color-border-accent-emphasis) var(--ig-fill), + var(--rs-color-border-base-primary) var(--ig-fill) + ); +} + +.slider::-webkit-slider-thumb { + width: 14px; + height: 14px; + margin-top: -5px; + background-color: var(--rs-color-background-base-primary); + border: 1px solid var(--rs-color-border-accent-emphasis); + border-radius: 50%; + appearance: none; +} + +.slider::-moz-range-track { + height: 4px; + border-radius: 999px; + background-color: var(--rs-color-border-base-primary); +} + +.slider::-moz-range-progress { + height: 4px; + border-radius: 999px; + background-color: var(--rs-color-border-accent-emphasis); +} + +.slider::-moz-range-thumb { + width: 14px; + height: 14px; + background-color: var(--rs-color-background-base-primary); + border: 1px solid var(--rs-color-border-accent-emphasis); + border-radius: 50%; +} + +.slider:focus-visible { + outline: 2px solid var(--rs-color-border-accent-emphasis); + outline-offset: 4px; + border-radius: 999px; +} + +/* Grid ---------------------------------------------------------------------- */ + +.grid { + /* Tiles grow with the icon so the padding stays even at every size. */ + --ig-tile: max(56px, calc(var(--ig-size) + 34px)); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--ig-tile), 1fr)); + gap: 8px; +} + +.tile { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + padding: 0; + color: inherit; + /* neutral-3, a step darker than the toolbar tray, so a 16px glyph still + reads as sitting on a tile at the smallest size. */ + background-color: var(--rs-color-background-base-primary-hover); + border: 1px solid transparent; + border-radius: 12px; + cursor: pointer; +} + +/* data-popup-open is on the trigger whose tooltip is showing, so the tile the + name belongs to lights up whether it was reached by pointer or by keyboard. */ +.tile:hover, +.tile[data-popup-open] { + background-color: var(--rs-color-background-neutral-secondary); +} + +.tile:focus-visible { + outline: 2px solid var(--rs-color-border-accent-emphasis); + outline-offset: 2px; +} + +.tile[data-copied] { + background-color: var(--rs-color-background-neutral-secondary); + border-color: var(--rs-color-border-accent-emphasis); +} + +/* The customizer reaches the icons through these three declarations only. */ +.tile svg { + width: var(--ig-size); + height: var(--ig-size); + stroke-width: var(--ig-stroke); +} diff --git a/apps/www/src/components/icongallery/icongallery.tsx b/apps/www/src/components/icongallery/icongallery.tsx new file mode 100644 index 000000000..b392221f6 --- /dev/null +++ b/apps/www/src/components/icongallery/icongallery.tsx @@ -0,0 +1,237 @@ +'use client'; + +import * as Apsara from '@raystack/apsara'; +import { ColorPicker, Popover, Tooltip } from '@raystack/apsara'; +import { RotateCcw } from 'lucide-react'; +import { + type ComponentType, + type CSSProperties, + type SVGProps, + useCallback, + useEffect, + useRef, + useState +} from 'react'; +import styles from './icongallery.module.css'; + +// The whole set fits on one screen, so the gallery shows every icon at once and +// the size, stroke and colour controls apply to all of them. +// +// The grid itself is plain elements: the icons are what the page documents, so +// their surroundings should not compete with them. The toolbar does use Apsara β€” +// the colour popover is our ColorPicker, and the names come from our Tooltip. + +type IconEntry = { + name: string; + Icon: ComponentType>; +}; + +/** + * Every wrapper the set publishes, read off the package barrel. `createIcon` + * sets `displayName` to the key, so that test picks out the icons and leaves + * `IconButton`, `IconProvider`, and the rest alone. Reading the barrel means the + * gallery never drifts from `icons/icons.tsx`. + */ +const ICONS: IconEntry[] = Object.entries( + Apsara as unknown as Record +) + .filter( + ([name, value]) => + name.endsWith('Icon') && + typeof value === 'function' && + (value as { displayName?: string }).displayName === name + ) + .map(([name, value]) => ({ + name, + Icon: value as ComponentType> + })) + .sort((a, b) => a.name.localeCompare(b.name)); + +/** + * One tooltip for the whole grid, wired up with Base UI's detached triggers: + * each tile is a `Tooltip.Trigger` carrying its name as the payload, and the + * single `Tooltip` below renders whichever payload the active trigger sends. + * + * https://base-ui.com/react/components/tooltip#detached-triggers + */ +const nameTooltip = Tooltip.createHandle(); + +/** The values `createIcon` applies, so a reset returns the real defaults. */ +const DEFAULT_STROKE = 1.5; +const DEFAULT_SIZE = 16; + +const STROKE_MIN = 0.5; +const STROKE_MAX = 3; +const SIZE_MIN = 12; +const SIZE_MAX = 48; + +/** Where the picker starts when nothing has been picked yet. */ +const PICKER_SEED = '#000000'; + +/** Trims a trailing `.00` / `.50` so 1.5 reads as "1.5" and 2 as "2". */ +const trim = (value: number) => String(Number(value.toFixed(2))); + +const percent = (value: number, min: number, max: number) => + `${((value - min) / (max - min)) * 100}%`; + +export function IconGallery() { + const [stroke, setStroke] = useState(DEFAULT_STROKE); + const [size, setSize] = useState(DEFAULT_SIZE); + const [color, setColor] = useState(''); + const [copied, setCopied] = useState(null); + const timer = useRef(undefined); + + useEffect(() => () => window.clearTimeout(timer.current), []); + + const copy = useCallback((name: string) => { + navigator.clipboard?.writeText(name); + setCopied(name); + window.clearTimeout(timer.current); + timer.current = window.setTimeout(() => setCopied(null), 1600); + }, []); + + const isDefault = + stroke === DEFAULT_STROKE && size === DEFAULT_SIZE && color === ''; + + const reset = () => { + setStroke(DEFAULT_STROKE); + setSize(DEFAULT_SIZE); + setColor(''); + }; + + return ( +
+
+
+
+ + {size}px + setSize(Number(event.target.value))} + style={ + { + '--ig-fill': percent(size, SIZE_MIN, SIZE_MAX) + } as CSSProperties + } + /> +
+ +
+ + {trim(stroke)} + setStroke(Number(event.target.value))} + title={`${trim((stroke * size) / 24)}px on screen at ${size}px`} + style={ + { + '--ig-fill': percent(stroke, STROKE_MIN, STROKE_MAX) + } as CSSProperties + } + /> +
+ + + + {/* Unset falls back to `currentColor` in CSS, which is what the + icons themselves inherit β€” so the swatch stays honest. */} + + {color || 'Inherit'} + + + + + + + + + + + +
+
+ +

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

+ +
+ {ICONS.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 c832e0ae3..eb6487844 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) => ( - + - + @@ -85,7 +85,7 @@ export default function TourDemo() { notifications β€” each anchored and spotlighted. diff --git a/apps/www/src/content/docs/(overview)/getting-started.mdx b/apps/www/src/content/docs/(overview)/getting-started.mdx index e25e4e6a4..94fe5c2b9 100644 --- a/apps/www/src/content/docs/(overview)/getting-started.mdx +++ b/apps/www/src/content/docs/(overview)/getting-started.mdx @@ -148,9 +148,9 @@ Apsara exports a set of icons that can be imported separately: ```tsx import { Button } from "@raystack/apsara"; -import { MagnifyingGlassIcon, Cross2Icon } from "@raystack/apsara/icons"; +import { SearchIcon, XIcon } from "@raystack/apsara/icons"; - + ``` ## Importing Hooks diff --git a/apps/www/src/content/docs/(overview)/migrating-to-lucide-icons.mdx b/apps/www/src/content/docs/(overview)/migrating-to-lucide-icons.mdx new file mode 100644 index 000000000..8dd13d8cb --- /dev/null +++ b/apps/www/src/content/docs/(overview)/migrating-to-lucide-icons.mdx @@ -0,0 +1,211 @@ +--- +title: Migrating to lucide icons +description: What changes when Apsara replaces @radix-ui/react-icons with lucide, and how to keep the old appearance. +--- + +Apsara used to draw its icons with [`@radix-ui/react-icons`](https://www.radix-ui.com/icons). +It now draws them with [lucide](https://lucide.dev), behind stable keys you can +replace one at a time. + +This is a breaking release: some icons look different, `lucide-react` is a new +peer dependency, and the names `@raystack/apsara/icons` exports have changed. + +## 1. Install the peer dependency + +```bash +npm install lucide-react +``` + +The range is wide β€” `>=0.500.0 <1.0.0` β€” so your app picks the version. If a +lucide release changes a drawing you care about, replace that one icon (step 5) +rather than pinning the whole library. + + + `@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. Rename what you imported from `@raystack/apsara/icons` + +That path used to export raw in-house SVG components. It now exports the 31 +icons Apsara's components draw, as replaceable icon components. Twelve of the old +names are gone. + + + These twelve imports fail to resolve. Import the glyph from `lucide-react` + instead. + + +| Removed name | Use instead | Appearance | +| --- | --- | --- | +| `BellIcon` | lucide `Bell` | Same glyph | +| `BellSlashIcon` | lucide `BellOff` | Similar | +| `BuildingsFilledIcon` | lucide `Building2` | Solid becomes stroke | +| `CheckCircleFilledIcon` | lucide `CircleCheck` | Solid becomes stroke | +| `CoinIcon` | lucide `Coins` | Similar | +| `CoinColoredIcon` | lucide `Coins` | Loses its colour | +| `CrossCircleFilledIcon` | lucide `CircleX` | Solid becomes stroke | +| `OrganizationIcon` | lucide `Building2` | Similar | +| `ResetIcon` | lucide `RotateCcw` | Similar | +| `ShoppingBagFilledIcon` | lucide `ShoppingBag` | Solid becomes stroke | +| `SidebarIcon` | `PanelLeftIcon`, or lucide `PanelLeft` | Similar | +| `TriangleRightIcon` | `ChevronRightIcon` | Solid triangle becomes a chevron | + +Two names survive, both with a new drawing: + +- `CoPilotIcon` β€” lucide `Sparkles` in place of the in-house solid sparkle. +- `FilterIcon` β€” lucide `ListFilter` in place of the in-house solid funnel. + +A raw lucide component draws 24Γ—24 at `strokeWidth={2}`, so set +`size={16} strokeWidth={1.5}` at the call site to match the Apsara icons beside +it β€” or wrap it once with `createIcon`, which applies those for you: + +```tsx +// src/icons.ts +import { createIcon } from '@raystack/apsara/icons'; +import { Bell } from 'lucide-react'; + +export const BellIcon = createIcon('BellIcon', Bell); +``` + +See [Building your own icons](/docs/theme/icons#building-your-own-icons). + +## 3. Check the icons that changed shape + +These are inside Apsara's own components, so they change without you touching a +call site. Everything else is the same glyph in a different drawing style. + +| Where | Before (radix) | After | What changed | +| --- | --- | --- | --- | +| `Sidebar` collapse | `ViewVerticalIcon` | `PanelLeftIcon` | A different glyph | +| `Sidebar` group toggle | `TriangleDownIcon` | `ChevronDownIcon` | A solid triangle becomes a chevron | +| `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` | `StopIcon` (lucide `Square`) | Solid becomes stroke | +| `DataTable` sort ascending | `TextAlignTopIcon` | `SortAscendingIcon` | A different glyph | +| `DataTable` and `DataView` sort descending | `TextAlignBottomIcon` | `SortDescendingIcon` | A different glyph | +| `DataTable` and `DataView` display settings | `MixerHorizontalIcon` | `DisplayIcon` | Similar | +| `DataTable` and `DataView` filters | in-house `FilterIcon` | `FilterIcon` (lucide `ListFilter`) | A solid funnel becomes filter lines | +| `ChatPanel` minimized bubble | in-house `CoPilotIcon` | `CoPilotIcon` (lucide `Sparkles`) | A solid sparkle pair becomes a stroked sparkle | + +Two more are worth a look, though the glyph is nearly the same: + +- `DatePicker` and `RangePicker` draw `CalendarIcon`, which is lucide + `CalendarDays`, so the glyph has day marks inside it. +- `Search`'s clear button and `Toast`'s error status draw `CircleX` in place of + radix `CrossCircledIcon`. + +## 4. Expect a 1px size change in some places + +Every Apsara icon 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. + +To change the size or the stroke of every icon at once, use the `props` half of +``: + +```tsx + +``` + +## 5. If you want the radix appearance back + +Apsara ships no radix preset, so register the radix icons yourself at ``. +Keep `@radix-ui/react-icons` in your own dependencies and copy this map: + +```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'; + +const radixIcons: IconOverrides = { + ArrowDownIcon: ArrowDownIcon, + ArrowUpIcon: ArrowUpIcon, + CalendarIcon: CalendarIcon, + CheckIcon: CheckIcon, + ChevronDownIcon: ChevronDownIcon, + ChevronLeftIcon: ChevronLeftIcon, + ChevronRightIcon: ChevronRightIcon, + ClearIcon: CrossCircledIcon, + CopyIcon: CopyIcon, + DisplayIcon: MixerHorizontalIcon, + EllipsisIcon: DotsHorizontalIcon, + ErrorIcon: CrossCircledIcon, + ExpandIcon: SizeIcon, + FileTextIcon: FileTextIcon, + InfoIcon: InfoCircledIcon, + MinusIcon: MinusIcon, + MoonIcon: MoonIcon, + PlusIcon: PlusIcon, + SearchIcon: MagnifyingGlassIcon, + ShrinkIcon: MinusIcon, + SortAscendingIcon: TextAlignTopIcon, + SortDescendingIcon: TextAlignBottomIcon, + StopIcon: StopIcon, + SuccessIcon: CheckCircledIcon, + SunIcon: SunIcon, + TableIcon: TableIcon, + WarningIcon: ExclamationTriangleIcon, + XIcon: Cross1Icon +}; + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} +``` + +Three keys are not in the map, because radix has no equivalent: `FilterIcon`, +`PanelLeftIcon` and `CoPilotIcon`. All three were in-house SVGs before. Radix +`MagicWandIcon` is the nearest stand-in for `CoPilotIcon` if you want one. + +You do not have to take the whole map. A partial map changes only the keys it +names. + +## 6. Register from a client component + +An override 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 applies to any runtime icon override. + +## Next + +See [Icons](/docs/theme/icons) for the full set, the override API, and what each +key draws. diff --git a/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx b/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx index 7c105f172..5ccba356f 100644 --- a/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx +++ b/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx @@ -84,7 +84,7 @@ also accept a render function receiving `{ floating }` for per-state icons: ```tsx - {({ floating }) => (floating ? : )} + {({ floating }) => (floating ? : )} ``` diff --git a/apps/www/src/content/docs/ai-elements/prompt-input/demo.ts b/apps/www/src/content/docs/ai-elements/prompt-input/demo.ts index b0b86c8db..fbdbbc0d1 100644 --- a/apps/www/src/content/docs/ai-elements/prompt-input/demo.ts +++ b/apps/www/src/content/docs/ai-elements/prompt-input/demo.ts @@ -138,7 +138,7 @@ export const editorCapabilitiesDemo = { id: 'page', label: 'This page', type: 'page', - icon: , + icon: , data: { path: '/docs/ai-elements/prompt-input' } }, { @@ -146,7 +146,7 @@ export const editorCapabilitiesDemo = { label: 'Button', type: 'component', group: 'Components', - icon: , + icon: , data: { status: 'stable' } }, { @@ -154,7 +154,7 @@ export const editorCapabilitiesDemo = { label: 'DataTable', type: 'component', group: 'Components', - icon: , + icon: , data: { status: 'stable' } }, { @@ -162,7 +162,7 @@ export const editorCapabilitiesDemo = { label: 'PromptInput', type: 'component', group: 'Components', - icon: , + icon: , data: { status: 'beta' } }, { @@ -240,7 +240,7 @@ export const editorCapabilitiesDemo = { id: 'data-table', label: 'DataTable', type: 'component', - icon: , + icon: , data: { status: 'stable' } }) } @@ -427,7 +427,7 @@ export const insertMentionDemo = { id: 'prompt-input', label: 'PromptInput', type: 'component', - icon: + icon: }) } > diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index f5101ec04..ad8ce10b7 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -150,44 +150,44 @@ export const iconsDemo = { name: 'Leading Icon', code: ` - }>Home + }>Home }>Documents - }>Settings + }>Settings ` }, { name: 'Trailing Icon', code: ` - }>Home + }>Home }>Documents - }>Settings + }>Settings ` }, { name: 'Both Icons', code: ` - } trailingIcon={}>Home + } trailingIcon={}>Home - } trailingIcon={}>Documents + } trailingIcon={}>Documents - } trailingIcon={}>Settings + } trailingIcon={}>Settings ` }, { name: 'Only Icon', code: ` - }/> + }/> }/> - }/> + }/> ` } ] diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index 35a817508..eba73e64f 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -73,7 +73,7 @@ export interface BreadcrumbSeparatorProps { export interface BreadcrumbEllipsisProps { /** * Custom ellipsis element - * @defaultValue + * @defaultValue */ children?: ReactNode; /** Custom CSS class names */ diff --git a/apps/www/src/content/docs/components/callout/props.ts b/apps/www/src/content/docs/components/callout/props.ts index 72c3696cb..4a3b0ec68 100644 --- a/apps/www/src/content/docs/components/callout/props.ts +++ b/apps/www/src/content/docs/components/callout/props.ts @@ -35,7 +35,7 @@ export interface CalloutProps { /** * Custom leading icon - * @defaultValue InfoCircledIcon + * @defaultValue InfoIcon */ icon?: React.ReactNode; diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts index fc5ba8ff1..9a54b8b07 100644 --- a/apps/www/src/content/docs/components/command/demo.ts +++ b/apps/www/src/content/docs/components/command/demo.ts @@ -26,7 +26,7 @@ export const preview = { Actions } + leadingIcon={} trailingIcon={ ⌘ @@ -39,7 +39,7 @@ export const preview = { Create AOI... } + leadingIcon={} trailingIcon={ ⌘ @@ -53,8 +53,8 @@ export const preview = { Run workflow... } - trailingIcon={} + leadingIcon={} + trailingIcon={} onClick={() => setOpen(false)} > View documentation @@ -62,25 +62,25 @@ export const preview = { Foundations - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Typography - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Design Tokens - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Styles - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Colors - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Spacing - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Borders - } onClick={() => setOpen(false)}> + } onClick={() => setOpen(false)}> Shadows @@ -132,11 +132,11 @@ export const withIconsDemo = { No results found. } onClick={() => setOpen(false)}>Home - } onClick={() => setOpen(false)}>Notifications + } onClick={() => setOpen(false)}>Notifications } onClick={() => setOpen(false)}>Filters - } onClick={() => setOpen(false)}>Organization - } onClick={() => setOpen(false)}>Orders - } onClick={() => setOpen(false)}>Layout + } onClick={() => setOpen(false)}>Organization + } onClick={() => setOpen(false)}>Orders + } onClick={() => setOpen(false)}>Layout `) }; diff --git a/apps/www/src/content/docs/components/dataview/demo.ts b/apps/www/src/content/docs/components/dataview/demo.ts index fcc4666ba..b1991813e 100644 --- a/apps/www/src/content/docs/components/dataview/demo.ts +++ b/apps/www/src/content/docs/components/dataview/demo.ts @@ -51,8 +51,8 @@ export const multiViewPreview = { /* The view switcher lives inside the DisplayControls popover. Give each view an optional leadingIcon to show alongside its label. */ const views = [ - { value: "table", label: "Table", leadingIcon: }, - { value: "list", label: "List", leadingIcon: }, + { value: "table", label: "Table", leadingIcon: }, + { value: "list", label: "List", leadingIcon: }, ]; = { accessorKey: "select", @@ -386,7 +386,7 @@ function SelectionBar() { variant="outline" size="large" color="neutral" - leadingIcon={} + leadingIcon={} isDismissible onDismiss={() => table.resetRowSelection()} > diff --git a/apps/www/src/content/docs/components/empty-state/demo.ts b/apps/www/src/content/docs/components/empty-state/demo.ts index b01a1c9fa..1a556daaf 100644 --- a/apps/www/src/content/docs/components/empty-state/demo.ts +++ b/apps/www/src/content/docs/components/empty-state/demo.ts @@ -58,7 +58,7 @@ export const variantsDemo = { } + icon={} heading="No notifications yet" subHeading="When you have notifications, they will appear here" primaryAction={ @@ -70,7 +70,7 @@ export const variantsDemo = { /> } + icon={} heading="Organization" subHeading="An organization in Aurora is a shared workspace where teams manage projects, AOIs, and image orders. It streamlines collaboration, analysis, and decision-making across industries." primaryAction={ diff --git a/apps/www/src/content/docs/components/empty-state/index.mdx b/apps/www/src/content/docs/components/empty-state/index.mdx index d5db1210e..c55713002 100644 --- a/apps/www/src/content/docs/components/empty-state/index.mdx +++ b/apps/www/src/content/docs/components/empty-state/index.mdx @@ -18,9 +18,9 @@ Import and assemble the component: ```tsx import { EmptyState } from '@raystack/apsara' -import { BellIcon } from '@raystack/apsara/icons' +import { Bell } from 'lucide-react' -} heading="No notifications" /> +} heading="No notifications" /> ``` ## API Reference diff --git a/apps/www/src/content/docs/components/floating-actions/demo.ts b/apps/www/src/content/docs/components/floating-actions/demo.ts index 59f2d7683..bc5704c97 100644 --- a/apps/www/src/content/docs/components/floating-actions/demo.ts +++ b/apps/www/src/content/docs/components/floating-actions/demo.ts @@ -15,7 +15,7 @@ export const preview = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -35,7 +35,7 @@ export const inlineDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -75,7 +75,7 @@ export const variantsDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -94,7 +94,7 @@ export const variantsDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -116,7 +116,7 @@ export const bulkActionsDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 5 selected @@ -137,10 +137,10 @@ export const iconOnlyDemo = { aria-label="Row actions" style={{ gap: 'var(--rs-space-5)' }} > - - + + - + ` }; @@ -173,7 +173,7 @@ export const scrollingDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -210,7 +210,7 @@ const placementWrapper = (props: string) => variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 2 selected @@ -246,7 +246,7 @@ export const groupDemo = { variant="outline" size="large" color="accent" - leadingIcon={} + leadingIcon={} isDismissible > 3 selected diff --git a/apps/www/src/content/docs/components/navbar/demo.ts b/apps/www/src/content/docs/components/navbar/demo.ts index fc8bf6665..3c36c2958 100644 --- a/apps/www/src/content/docs/components/navbar/demo.ts +++ b/apps/www/src/content/docs/components/navbar/demo.ts @@ -17,7 +17,7 @@ export const preview = { - diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts index b74ae4b62..4a638a6b5 100644 --- a/apps/www/src/content/docs/components/sidebar/demo.ts +++ b/apps/www/src/content/docs/components/sidebar/demo.ts @@ -25,26 +25,26 @@ export const preview = { - + Apsara - }> + }> Overview - }> + }> Preview - } active> + } active> Dashboard }> Analytics - }> + }> Settings @@ -52,13 +52,13 @@ export const preview = { }> Reports - }> + }> Activities - }> + }> Help & Support @@ -76,20 +76,20 @@ export const positionDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -101,20 +101,20 @@ export const positionDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -133,15 +133,15 @@ export const variantDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -154,15 +154,15 @@ export const variantDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -175,15 +175,15 @@ export const variantDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -202,20 +202,20 @@ export const stateDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -226,20 +226,20 @@ export const stateDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -250,20 +250,20 @@ export const stateDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -274,20 +274,20 @@ export const stateDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `) @@ -305,15 +305,15 @@ export const collapsedAppearanceDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -328,15 +328,15 @@ export const collapsedAppearanceDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -355,17 +355,17 @@ export const peekOnHoverDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings `) @@ -379,17 +379,17 @@ export const peekOnHoverDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings `) @@ -407,20 +407,20 @@ export const tooltipDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics - }>Settings + }>Settings - }>Help + }>Help `), @@ -433,15 +433,15 @@ export const collapsibleDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -455,18 +455,18 @@ export const hideTooltipDemo = { - + Apsara - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Settings - }> + }> Help @@ -480,25 +480,25 @@ export const collapsibleGroupDemo = { - + Apsara - }> + }> Overview }> Reports - }> + }> Activities - }> - }> + }> + }> Settings @@ -519,13 +519,13 @@ export const controlledGroupDemo = { - + Apsara - }> + }> Overview }> Reports - }> + }> Activities @@ -550,7 +550,7 @@ export const controlledGroupDemo = { onClick={() => setResourcesOpen(open => !open)} /> } - leadingIcon={} + leadingIcon={} > {resourcesOpen ? 'Collapse Resources' : 'Expand Resources'} @@ -566,14 +566,14 @@ export const groupIconDemo = { - + Apsara - }> - } active> + }> + } active> Dashboard }> @@ -581,10 +581,10 @@ export const groupIconDemo = { }> - }> + }> Reports - }> + }> Activities @@ -600,7 +600,7 @@ export const triggerDemo = { - + Apsara @@ -608,9 +608,9 @@ export const triggerDemo = { - }>Overview + }>Overview - } active>Dashboard + } active>Dashboard }>Analytics @@ -624,27 +624,27 @@ export const moreDemo = { - + Apsara - } active> + } active> Dashboard }> Analytics - }> + }> Reports - }> + }> Activities - } disabled> + } disabled> Notifications @@ -652,7 +652,7 @@ export const moreDemo = { - }> + }> Preferences }> diff --git a/apps/www/src/content/docs/components/sidebar/props.ts b/apps/www/src/content/docs/components/sidebar/props.ts index 3ad45c7d7..d1038ac7e 100644 --- a/apps/www/src/content/docs/components/sidebar/props.ts +++ b/apps/www/src/content/docs/components/sidebar/props.ts @@ -121,7 +121,7 @@ export interface SidebarItemProps { export interface SidebarTriggerProps { /** Icon rendered inside the trigger. - * @default "" + * @default "" */ children?: ReactNode; } diff --git a/apps/www/src/content/docs/components/toast/demo.ts b/apps/www/src/content/docs/components/toast/demo.ts index 914e3cdc9..fce5f3cae 100644 --- a/apps/www/src/content/docs/components/toast/demo.ts +++ b/apps/www/src/content/docs/components/toast/demo.ts @@ -145,7 +145,7 @@ export const leadingIconDemo = { @@ -153,14 +153,14 @@ export const leadingIconDemo = { title: "Upload failed", description: "We couldn't upload your file. Please try again.", type: "error", - leadingIcon: + leadingIcon: })}> Error with icon diff --git a/apps/www/src/content/docs/components/toggle/demo.ts b/apps/www/src/content/docs/components/toggle/demo.ts index a52db8584..6812adf43 100644 --- a/apps/www/src/content/docs/components/toggle/demo.ts +++ b/apps/www/src/content/docs/components/toggle/demo.ts @@ -5,7 +5,7 @@ import { getPropsString } from '@/lib/utils'; const getCode = (props: ComponentPropsType) => { return ` - + `; }; @@ -40,13 +40,13 @@ export const groupDemo = { type: 'code', code: ` - + - + - + ` }; @@ -55,13 +55,13 @@ export const multipleDemo = { type: 'code', code: ` - + - + - + ` }; @@ -86,16 +86,16 @@ export const sizeDemo = { type: 'code', code: ` - + - + - + - + ` }; @@ -113,10 +113,10 @@ export const disabledDemo = { name: 'Group', code: ` - + - + ` } diff --git a/apps/www/src/content/docs/components/toolbar/demo.ts b/apps/www/src/content/docs/components/toolbar/demo.ts index 2c4338094..545c94478 100644 --- a/apps/www/src/content/docs/components/toolbar/demo.ts +++ b/apps/www/src/content/docs/components/toolbar/demo.ts @@ -51,8 +51,8 @@ export const compositionDemo = { type: 'code', code: ` }> - } > - }> + } > + }> diff --git a/apps/www/src/content/docs/theme/icons/demo.ts b/apps/www/src/content/docs/theme/icons/demo.ts new file mode 100644 index 000000000..54306bb7e --- /dev/null +++ b/apps/www/src/content/docs/theme/icons/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 globalPropsDemo = { + 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/theme/icons/index.mdx b/apps/www/src/content/docs/theme/icons/index.mdx new file mode 100644 index 000000000..79f8b66f5 --- /dev/null +++ b/apps/www/src/content/docs/theme/icons/index.mdx @@ -0,0 +1,323 @@ +--- +title: Icons +description: The icons Apsara draws, how to replace any of them, and how to build your own. +source: packages/raystack/icons +--- + +import { + basicDemo, + sizeDemo, + colorDemo, + overrideDemo, + globalPropsDemo, + nestedDemo, +} from "./demo.ts"; + +Apsara ships the 31 icons its own components draw. Each one is exported under a +stable key, renders at 16Γ—16, and can be replaced with a component of your own. +Every one is a [lucide](https://lucide.dev) drawing. + + + +Import an icon by name from `@raystack/apsara/icons`: + +```tsx +import { SearchIcon, ChevronDownIcon } from '@raystack/apsara/icons' + + +``` + +You pay only for the icons you import. A build that shows three icons ships +three icons, not all 31. + +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. + +## The set + + + +Need an icon that is not here? `createIcon` is public, so +[build your own](#building-your-own-icons) and it behaves like the ones above. +lucide draws over 1,700 icons and your app can reach all of them β€” Apsara +exports these 31 because they are the ones inside its components, which you +cannot reach any other way. + +## What each key draws + +Most keys name the glyph, so the name tells you the shape: `ChevronDownIcon`, +`XIcon`, `CheckIcon`, `PlusIcon`, `SearchIcon`. The rest name the job, because +the drawing behind them is a choice rather than a shape you asked for: + +| Key | Draws | +| --- | --- | +| `SortAscendingIcon` / `SortDescendingIcon` | lucide `ArrowUpNarrowWide` / `ArrowDownWideNarrow` | +| `FilterIcon` | lucide `ListFilter` | +| `DisplayIcon` | lucide `SlidersHorizontal` | +| `SuccessIcon` / `WarningIcon` / `ErrorIcon` | lucide `CircleCheck` / `TriangleAlert` / `CircleX` | +| `ClearIcon` | lucide `CircleX` | +| `StopIcon` | lucide `Square` | +| `CalendarIcon` | lucide `CalendarDays` | +| `CoPilotIcon` | lucide `Sparkles` | + +A key is yours to rely on; the drawing behind it can change in a release. So +prefer the key that says the job β€” `SortDescendingIcon` β€” over one that repeats +lucide's own vocabulary. + +Two keys can share a drawing. `ErrorIcon` and `ClearIcon` are both `CircleX`, so +they look identical until you override one. That is the point: the error status +of a `Toast` and the clear button of a `Search` field are different jobs, and you +can change one without the other. + +## 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 a 1px stroke 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 stop working as soon as an icon is overridden with a component +from somewhere else. `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 + +`` takes one object with two halves: `components` replaces a +drawing by key, and `props` applies to every icon. + + + +```tsx +import { Theme } from '@raystack/apsara' +import { X, ChevronDown } from 'lucide-react' + +const icons = { + components: { XIcon: X, ChevronDownIcon: ChevronDown } +} + + + + +``` + +Apsara then uses your component everywhere that icon appears β€” inside its own +components too. The map is partial: a key you do not name keeps its default, so +you never have to supply a complete set. + + + You may pass the object as an inline literal. Apsara compares its contents, so + a new literal on every render costs nothing. + + +### Props for every icon + +`props` applies 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 β€” `props` flows through +React and re-renders the icons, and CSS does not. + +### Nesting + +A nested `` layers on the one above it, one key at a time. So a subtree +can change one icon and keep everything else it inherited. + + + +### What an override costs + +An overridden icon ships twice: your component, and the default it never draws. +Overrides are resolved at runtime, so a bundler cannot know which defaults to +drop. + +Replacing all 31 costs about 3.6 kB gzipped of lucide that never draws. The cost +is flat β€” it does not grow as you override more. If you want the default gone +from the bundle entirely, import your own icon at the call site instead of +overriding it. + +## Building your own icons + +`createIcon` is public, so an icon Apsara does not ship behaves exactly like one +that does: + +```tsx +// src/icons.ts β€” the app's single place for icons +import { createIcon } from '@raystack/apsara/icons'; +import { Rocket, Trash2 } from 'lucide-react'; + +export const RocketIcon = createIcon('RocketIcon', Rocket); +export const TrashIcon = createIcon('TrashIcon', Trash2); +``` + +```tsx +import { RocketIcon } from '@/icons'; + +; // 16Γ—16, strokeWidth 1.5, data-icon="RocketIcon" +``` + +You get the base props, `data-icon`, and one file to edit if you ever change +icon library. + +The `props` half of `` reaches your icons too, since they read the +same context. The `components` half is typed to the keys Apsara ships β€” to +change one of your own, edit the file above. You own it, so you do not need a +provider to reach into it. + + + lucide exports several aliases per icon β€” `Search`, `SearchIcon` and + `LucideSearch` are the same drawing. Apsara's names collide with the middle + flavour, so `import { SearchIcon } from 'lucide-react'` and `import + { SearchIcon } from '@raystack/apsara/icons'` are different components with one + name. Importing both in a file is a compile error rather than a silent bug, but + it is worth knowing when you read an import list. + + +## API Reference + +### Theme props + + + +### IconOptions + + + +### IconProvider + +`` mounts this for you. Use it directly only if you want icon overrides +without a theme scope. It takes the two halves of `IconOptions` as flat props. + + + +### createIcon + +```tsx +function createIcon(name: string, Default: IconComponent): IconComponent; +``` + +Wraps a component as an Apsara icon: base props below the call site, +`data-icon=""`, and the provider `props`. `name` is any string β€” +`IconName` covers only what Apsara ships, and only those keys can be replaced +through ``. + +### Types + +`IconProps` is `Omit, 'children'>`: every SVG attribute +React accepts, minus `children`. In practice you pass `width`, `height`, +`strokeWidth`, `className`, `style` or `color` β€” see [base props](#base-props). + +`IconComponent` is `ComponentType`, so any component with those props +can stand in for an icon. `IconOverrides` is +`Partial>`, and `IconName` is the union of the 31 +keys, so a typo in an override map is a type error. + +```tsx +import { createIcon } from '@raystack/apsara'; +import type { + IconComponent, + IconName, + IconOptions, + 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 = { components: { 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 31 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. 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 every component module, while +`require('@raystack/apsara/icons')` loads the icons and nothing else. diff --git a/apps/www/src/content/docs/theme/icons/props.ts b/apps/www/src/content/docs/theme/icons/props.ts new file mode 100644 index 000000000..9fc65727e --- /dev/null +++ b/apps/www/src/content/docs/theme/icons/props.ts @@ -0,0 +1,67 @@ +// The types the Icons page renders, declared here rather than read from the +// package, as every docs page does. Two notes for whoever edits this file: +// +// - `IconOverrides` is written as an index signature so the tables print the +// name `IconOverrides`. In the package the key is `IconName`, the union of the +// keys Apsara ships; a `Record` here prints as its resolved +// shape, which reads as though any name works. +// - `ThemeIconProps` has no counterpart in the package. It exists so the tables +// can show the one icon prop of `` beside the rest; the name is never +// rendered. + +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` β€” some + * libraries declare `children?: undefined` to forbid children, and function + * props are contravariant, so a type that permits `children` would reject them. + */ +export type IconProps = Omit, 'children'>; + +/** Any component that can stand in for an Apsara icon. */ +export type IconComponent = ComponentType; + +/** + * A partial map of icon key to replacement component: + * `Partial>`. Every key is optional, so a map + * that names one icon changes that icon only, and a key Apsara does not ship is + * a type error. + */ +export type IconOverrides = { [icon: string]: IconComponent | undefined }; + +/** What `` takes: the drawings and the props, in one object. */ +export interface IconOptions { + /** + * Icon components that replace the Apsara defaults, keyed by icon key β€” for + * example `{ ErrorIcon: MyError }`. A partial map changes only the keys it + * names, and a nested `` layers on the one above it, per key. + */ + components?: IconOverrides; + + /** + * Props applied to every icon built by `createIcon`, your own included β€” for + * example `{ strokeWidth: 2 }`. These beat the Apsara base values and lose to + * the props at the call site. + */ + props?: IconProps; +} + +/** `IconProvider` takes the two halves of `IconOptions` as flat props. */ +export interface IconProviderProps extends IconOptions { + children: ReactNode; +} + +/** The icon half of ``'s props. */ +export interface ThemeIconProps { + /** + * The icons inside Apsara's components, and the props applied to every icon. + * Apsara mounts the icon provider only when one of the two halves is set. + * + * The map holds functions, so a React Server Component cannot pass it β€” set it + * from a client component. + */ + icons?: IconOptions; +} diff --git a/apps/www/src/content/docs/theme/meta.json b/apps/www/src/content/docs/theme/meta.json index 0b2d1fb8b..47be1c910 100644 --- a/apps/www/src/content/docs/theme/meta.json +++ b/apps/www/src/content/docs/theme/meta.json @@ -1,4 +1,12 @@ { "title": "Theme", - "pages": ["overview", "colors", "typography", "spacing", "radius", "effects"] + "pages": [ + "overview", + "colors", + "typography", + "spacing", + "radius", + "effects", + "icons" + ] } diff --git a/docs/V1-migration.md b/docs/V1-migration.md index 952a6d918..0b443e140 100644 --- a/docs/V1-migration.md +++ b/docs/V1-migration.md @@ -78,7 +78,7 @@ This guide covers all breaking changes when upgrading from the last stable Radix - **React 19 required.** The peer dependency changed from `^18 || ^19` to `^19` only. React 18 is no longer supported. - The library no longer uses `radix-ui`, `@ariakit/react`, or `sonner`. It now uses `@base-ui/react` and `@base-ui/utils` internally. These are installed automatically as dependencies of the package. -- `@radix-ui/react-icons` is still used. +- Icons are drawn with `lucide-react`, which is a peer dependency β€” install it. `@radix-ui/react-icons` is no longer used. See [Migrating to lucide icons](https://apsara.raystack.io/docs/migrating-to-lucide-icons). --- ## Cross-Cutting Changes diff --git a/package.json b/package.json index a9d204358..6fcd4b344 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,6 @@ "devDependencies": { "@biomejs/biome": "2.3.8", "semver": "^7.6.0", - "@parcel/packager-ts": "2.9.2", - "@parcel/transformer-typescript-types": "2.9.2", "@raystack/tools-config": "workspace:*", "@turbo/gen": "^1.9.7", "concurrently": "^9.1.2", diff --git a/packages/raystack/CHANGELOG.md b/packages/raystack/CHANGELOG.md index 238c21663..78d5caeb2 100644 --- a/packages/raystack/CHANGELOG.md +++ b/packages/raystack/CHANGELOG.md @@ -1,5 +1,75 @@ # @raystack/apsara +## Unreleased + +### Icons β€” lucide replaces @radix-ui/react-icons (BREAKING) + +Apsara names every icon with a stable key that does not name a library β€” +`SearchIcon`, `SortAscendingIcon`, `ClearIcon` β€” and draws it with lucide. +The package exports the 31 icons its own components use, and `createIcon` +is public, so an app builds any other icon the same way. See the +[migration guide](https://apsara.raystack.io/docs/migrating-to-lucide-icons) +and [Icons](https://apsara.raystack.io/docs/theme/icons). + +#### Breaking changes + +- **`lucide-react` is a new peer dependency**, range `>=0.500.0 <1.0.0`. + Install it. +- **`@radix-ui/react-icons` is no longer a dependency.** If your own code + imports from it, keep it in your own `dependencies`. +- **`@raystack/apsara/icons` exports icon components, not raw SVG + assets.** Twelve names are gone: `BellIcon`, `BellSlashIcon`, + `BuildingsFilledIcon`, `CheckCircleFilledIcon`, `CoinIcon`, + `CoinColoredIcon`, `CrossCircleFilledIcon`, `OrganizationIcon`, + `ResetIcon`, `ShoppingBagFilledIcon`, `SidebarIcon` and + `TriangleRightIcon`. Import the glyph from `lucide-react` instead, and + wrap it with `createIcon` if you want it replaceable. `CoPilotIcon` and + `FilterIcon` keep their names but draw lucide `Sparkles` and + `ListFilter` rather than the in-house solid SVGs. +- **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`. +- **Some icons inside Apsara's components draw a different shape.** The + `Sidebar` collapse control and the submenu marker become chevrons + (lucide has no solid caret); `ChatPanel` expand and minimize become the + matched `ExpandIcon` / `ShrinkIcon` pair; the `PromptInput` stop button + goes from solid to stroke; the `DataTable` and `DataView` sort controls + change glyph; the `ChatPanel` bubble goes from a solid sparkle pair to a + stroked `Sparkles`; and `DatePicker` gains day marks inside its calendar + glyph. +- **Icon replacements must be registered from a client component.** An + override map is an object of functions, and a function cannot cross the + boundary from a React Server Component, so a `` that sits + directly in a server layout must move into a `providers.tsx` marked + `'use client'`. + +#### New features + +- **31 icons**, from `@raystack/apsara` and from + `@raystack/apsara/icons` β€” all drawn by lucide, from 30 drawings + (`ClearIcon` and `ErrorIcon` share one). These are the icons Apsara's + own components draw: the keys you cannot reach from your own call + sites. Prefer the subpath + when you want icons without the component library β€” it reaches no + component module, so one icon costs one icon even in CJS, where the + root barrel eagerly requires every component. +- **``** replaces the icons inside + Apsara's components. `components` swaps a drawing by key, `props` + applies to every icon built with `createIcon`. Both maps are partial, + the props at the call site still win, and a nested `` layers on + the one above it key by key. +- **`createIcon(key, Default)` is public.** Wrap any component with it + and it gets the same base props, the same `data-icon`, and the same + replaceability as Apsara's own icons. +- **`data-icon=""`** on every icon, so CSS can style one icon + without a re-render and a test can select it. +- **New public types**: `IconName` (the union of the 31 keys, so a typo + in an override map is a type error), `IconOptions`, `IconOverrides`, + `IconComponent`, `IconProps`, `IconProviderProps`, and `IconProvider` + itself. + ## 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 1221cd189..64cb41ef3 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'; import styles from './accordion.module.css'; export const AccordionTrigger = ({ diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index a016c01e3..3e5b8d083 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -487,7 +487,7 @@ describe('Breadcrumb', () => { const ellipsis = screen.getByTestId('breadcrumb-ellipsis'); expect(ellipsis).toBeInTheDocument(); - // Default icon is DotsHorizontalIcon + // Default icon is EllipsisIcon expect(ellipsis?.querySelector('svg')).toBeInTheDocument(); }); diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 5783e2844..7a841967e 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'; 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 982db8f09..a39e47471 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'; 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 c260e1645..0af8c9eac 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'; 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 519689f9c..aae740c0b 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, useRef, useState } from 'react'; import { PropsBase } from 'react-day-picker'; +import { CalendarIcon } from '~/icons'; import { Input } from '../input'; import { InputProps } from '../input/input'; import { Popover } from '../popover'; diff --git a/packages/raystack/components/calendar/range-picker.tsx b/packages/raystack/components/calendar/range-picker.tsx index 6385311ad..88ddc21fc 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 { CalendarIcon } from '~/icons'; import { Flex } from '../flex'; import { Input } from '../input'; import { InputProps } from '../input/input'; diff --git a/packages/raystack/components/callout/callout.tsx b/packages/raystack/components/callout/callout.tsx index e0e2f7c8c..eef8dba8c 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'; 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 @@ -139,7 +139,7 @@ export function Callout({ aria-label='Dismiss message' data-slot='callout-dismiss' > - + )} diff --git a/packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx b/packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx index f22b0b90d..dd6b0084d 100644 --- a/packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx +++ b/packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx @@ -803,10 +803,7 @@ describe('ChatPanel', () => { const icon = screen.getByTestId('bubble').querySelector('svg'); expect(icon).not.toBeNull(); expect(icon).toHaveAttribute('aria-hidden', 'true'); - expect(icon?.querySelector('path')).toHaveAttribute( - 'fill', - 'currentColor' - ); + expect(icon).toHaveAttribute('data-icon', 'CoPilotIcon'); }); }); diff --git a/packages/raystack/components/chat-panel/chat-panel-parts.tsx b/packages/raystack/components/chat-panel/chat-panel-parts.tsx index 6a3b9c3d0..3562d0e70 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'; import { IconButton, type IconButtonProps } from '../icon-button/icon-button'; import styles from './chat-panel.module.css'; import { useChatPanelContext } from './chat-panel-context'; @@ -110,7 +110,7 @@ export function ChatPanelMinimizeTrigger({ data-slot='chat-panel-minimize-trigger' {...props} > - {children ?? } + {children ?? } ); } @@ -159,7 +159,7 @@ export function ChatPanelExpandTrigger({ data-slot='chat-panel-expand-trigger' {...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 0d4d6d487..98edeb070 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'; 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 f717fc0aa..5ead2ccc0 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'; import { IconButton } from '../icon-button'; import { Spinner } from '../spinner'; import styles from './chat.module.css'; @@ -96,7 +96,7 @@ export function ChatAttachment({ onClick={onRemove} data-slot='chat-attachment-remove' > - + )} diff --git a/packages/raystack/components/chat/chat-messages.tsx b/packages/raystack/components/chat/chat-messages.tsx index 73f330828..354c4d88d 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'; 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/code-block/__tests__/data-slots.test.tsx b/packages/raystack/components/code-block/__tests__/data-slots.test.tsx index 62e0f02ae..312fd2678 100644 --- a/packages/raystack/components/code-block/__tests__/data-slots.test.tsx +++ b/packages/raystack/components/code-block/__tests__/data-slots.test.tsx @@ -8,14 +8,6 @@ vi.mock('~/hooks/useCopyToClipboard', () => ({ useCopyToClipboard: () => ({ copy: vi.fn() }) })); -// SVG icons are inlined via @svgr/rollup at build time; stub them for jsdom. -vi.mock('@radix-ui/react-icons', () => ({ - CopyIcon: () => null, - CheckIcon: () => null, - ChevronDownIcon: () => null, - CaretSortIcon: () => null -})); - const CODE = `function hello() { console.log('hi'); }`; diff --git a/packages/raystack/components/combobox/combobox-input.tsx b/packages/raystack/components/combobox/combobox-input.tsx index 874e0cfab..4bd7913c9 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'; 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 1fb8b9019..9822a36f5 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'; 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 40bd36dfc..b7387d8ae 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'; 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..f049f719e 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'; 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..eb71d3a15 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 { DisplayIcon } from '~/icons'; 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/ordering.tsx b/packages/raystack/components/data-table/components/ordering.tsx index ea28a3135..58bcf8250 100644 --- a/packages/raystack/components/data-table/components/ordering.tsx +++ b/packages/raystack/components/data-table/components/ordering.tsx @@ -1,7 +1,6 @@ 'use client'; -import { TextAlignBottomIcon, TextAlignTopIcon } from '@radix-ui/react-icons'; - +import { SortAscendingIcon, SortDescendingIcon } from '~/icons'; import { Flex } from '../../flex'; import { IconButton } from '../../icon-button'; import { Select } from '../../select'; @@ -74,9 +73,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..5238fcc13 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'; 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-slots.test.tsx b/packages/raystack/components/data-view/__tests__/data-slots.test.tsx index 8bba2cb37..8fd48283d 100644 --- a/packages/raystack/components/data-view/__tests__/data-slots.test.tsx +++ b/packages/raystack/components/data-view/__tests__/data-slots.test.tsx @@ -1,14 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { 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 -})); - import { expectSlots, getAllSlots, getSlot } from '~/test-utils/data-slots'; // biome-ignore lint/suspicious/noShadowRestrictedNames: legitimate export name import { DataView } from '../data-view'; 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 bc3770db9..d768c7a1e 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 15c341aab..3d1479d34 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'; import { Button } from '../../button'; import { Flex } from '../../flex'; import styles from '../data-view.module.css'; @@ -96,7 +96,7 @@ export function FilterSummary({ className }: DataViewClearFiltersProps) { variant='text' color='neutral' size='small' - trailingIcon={} + trailingIcon={} onClick={handleClearFilters} data-slot='data-view-filter-summary-clear' > diff --git a/packages/raystack/components/data-view/components/display-controls.tsx b/packages/raystack/components/data-view/components/display-controls.tsx index 35761a294..0d94c4dc3 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 { DisplayIcon } from '~/icons'; import { Button } from '../../button'; import { Flex } from '../../flex'; @@ -35,7 +35,7 @@ export function DisplayControls({ variant='outline' color='neutral' size='small' - leadingIcon={} + leadingIcon={} data-slot='data-view-display-trigger' > Display diff --git a/packages/raystack/components/data-view/components/ordering.tsx b/packages/raystack/components/data-view/components/ordering.tsx index e63dd16ce..791e00a96 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 { SortDescendingIcon } from '~/icons'; import { Flex } from '../../flex'; import { IconButton } from '../../icon-button'; import { Select } from '../../select'; @@ -72,7 +71,7 @@ export function Ordering({ columnList, onChange, value }: OrderingProps) { disabled={columnList.length === 0} data-slot='data-view-ordering-direction' > - diff --git a/packages/raystack/components/dialog/dialog-misc.tsx b/packages/raystack/components/dialog/dialog-misc.tsx index 2265b1a5d..e46154c1e 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'; import { Flex } from '../flex'; import { IconButton } from '../icon-button'; import styles from './dialog.module.css'; @@ -67,7 +67,7 @@ export function CloseButton(props: DialogPrimitive.Close.Props) { data-slot='dialog-close' {...props} > -