diff --git a/docs/contributor-docs/adding-icons.md b/docs/contributor-docs/adding-icons.md index a0ec2a0791..7c5c85b4d6 100644 --- a/docs/contributor-docs/adding-icons.md +++ b/docs/contributor-docs/adding-icons.md @@ -4,92 +4,52 @@ category: Contributor Guides order: 9 --- -## Adding and Modifying Icons +## Lucide Icons -- Use dashes in the name of the .svg files (e.g `calendar-month`). -- Use the same name for the "Line" and "Solid" variants, and save them in the respective folder, e.g. `instructure-ui/packages/ui-icons/svg/Line/calendar-month.svg` and `instructure-ui/packages/ui-icons/svg/Solid/calendar-month.svg`. -- Double-check that the SVG size is 1920x1920. +The bulk of the icon set comes from [Lucide](https://lucide.dev). These are not manually maintained +— they are automatically picked up from the `lucide-react` npm package at build time. -```js ---- -type: code ---- - - {...} - -``` - -- The files cannot contain [clipping paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath)! Sadly, when the Designers export icons from Figma, most of the time they have a clipping path around the whole canvas. When an SVG includes clipping paths, the `Icon Font` variant may not render correctly. Specifically, the use of `` and `` elements can cause rendering issues. If the source code has them, manually refactor the code, e.g: - -```js ---- -type: code ---- +**To get a new or updated Lucide icon**, bump the `lucide-react` version in +`packages/ui-icons/package.json`. The index is regenerated automatically as part of +`pnpm run bootstrap` (via `build-icons`), so no manual step is needed. + +Every icon exported by `lucide-react` becomes `InstUIIcon` in `@instructure/ui-icons`, +**except** for icons that are shadowed by a custom icon of the same name (see below). + +If a Lucide icon is missing or looks wrong, check whether it exists in the installed version of +`lucide-react` first — if not, the only path is to add it as a custom icon. -// Before: - - - - - - - - - - +## Adding Custom Icons -// After: - - - -``` +Custom icons live in `packages/ui-icons/svg/Custom/` and are consumed directly by the build script (`ui-scripts/lib/generate-custom-index.ts`). -- If the icon has to be bidirectional (being mirrored in RTL mode, typically arrow icons), add the icon name to the bidirectional list in `packages/ui-icons/icons.config.js`. Deprecated icons are handled here as well. +- Use kebab-case filenames ending in `.svg`. The filename becomes the React export name: `ai-info.svg` → `AiInfoInstUIIcon`, `canvas-logo.svg` → `CanvasLogoInstUIIcon`. -- Run `pnpm run bootstrap`. +- For solid/filled icons, the filename must end in `-solid.svg` (e.g. `bell-solid.svg`). -- Finally, run `pnpm run dev` and verify that the icons are displayed correctly under [Icons](/#icons). Check all 3 versions (React, SVG and icon font). +- If a custom icon has the same name as a Lucide icon (e.g. `message-square-check.svg`), the custom version takes precedence and the Lucide one is hidden from the package. -(Note: The fonts are sometimes not rendered correctly, but we decided not to fix them, because they are not really used anywhere, and we might stop supporting icon fonts in the future in general.) +- After dropping the file into `svg/Custom/`, the index is regenerated automatically as part of + `pnpm run bootstrap` (via `build-icons`). -### Guidelines for Drawing Icons +- Run `pnpm run dev` and verify the icon looks correct in the Icons gallery. -- Draw your icons on the 1920 x 1920 art-boards. +### Drawing Guidelines -- Before you flatten shapes or vectorize strokes as described below, make a hidden copy of the original paths off - to the side so that you can more easily come back and make changes later. +- Uncheck "Clip content" on the frame before exporting. Otherwise Figma wraps every layer in `` and adds a `` block, which can cause rendering issues -- Flatten your shapes. +- Use `currentColor` for all path fills and strokes. The build script reads the SVG as-is — no color replacement happens. If you exported with a hardcoded hex value, replace it manually before regenerating the index -- Export strokes to vector. +- Stroke icons: set `fill="none"` on every path, not just on the root ``. Select all shape layers and set Fill to None in the Design panel -- Don’t use borders on vectors, especially not inside/outside borders which aren’t supported in SVG. Do not use clipping paths. +- Remove `width` and `height` from the `` root — keep only `viewBox` and `xmlns`. Export at 1× in Figma -- Make sure none of the paths go outside of the art-board. If so, the glyph in the icon font will be misaligned. - Draw inside the lines. +- Flatten all transforms before exporting (Object → Flatten Selection) -- Fill the space edge-to-edge as much as possible. The build process will add margins as needed. +- Standard icons use `viewBox="0 0 24 24"`. Brand/logo icons can use any square viewBox (e.g. `0 0 1920 1920`) -- Don't use inline styles +- Mixed stroke + fill icons are supported. Paths with `fill="currentColor"` render filled; paths with `fill="none"` and a `stroke` render as outlines -- Don't use `class` or `for` attributes +- Do not use per-element `stroke-width`. The wrapper applies a uniform stroke width derived from the icon size -- Always have `` as the root tag +- Do not use `` or `` elements. These are not supported — flatten or redesign the layer diff --git a/docs/guides/upgrade-guide.md b/docs/guides/upgrade-guide.md index bcc7bb595b..396c1bcebf 100644 --- a/docs/guides/upgrade-guide.md +++ b/docs/guides/upgrade-guide.md @@ -12,25 +12,24 @@ TODO add details ## New icons -InstUI has switched to a new icon set, [Lucide](https://lucide.dev/icons/). We are still keeping some Instructure-specific icons, like product logos. We have a codemod that will help you migrate your code to the new icon set (see below). +InstUI has switched to a new icon set based on [Lucide](https://lucide.dev/icons/). We are still keeping some Instructure-specific icons, like product logos, also new custom icons are added. We have a codemod that will help you migrate your code to the new icon set. ### Lucide Icons Package -InstUI v12 introduces a new icon package **`@instructure/ui-icons-lucide`** based on the [Lucide](https://lucide.dev/icons/) icon library, providing 1,700+ icons with improved theming and RTL support. The new Lucide icons are wrapped with `wrapLucideIcon` to integrate with InstUI's theming system while maintaining access to all native Lucide props. +InstUI introduces new icons based on the [Lucide](https://lucide.dev/icons/) icon library, providing 1,900+ stroke-based icons with improved theming and RTL support. The icons are wrapped with `wrapLucideIcon` to integrate with InstUI's theming system while maintaining access to all native icon props. **Key differences from `SVGIcon`/`InlineSVG`:** -| Property | Old API (SVGIcon) | New API (Lucide) | +| Property | Old API (SVGIcon) | New API | | :-------------- | :---------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | **size** | `'x-small'` \| `'small'` \| `'medium'` \| `'large'` \| `'x-large'` | `'xs'` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` \| `'2xl'` \| `number` (pixels) | | **color** | Limited tokens: `'primary'` \| `'secondary'` \| `'success'` \| `'error'` \| `'warning'` \| etc. | 60+ theme tokens (`'baseColor'`, `'successColor'`, `'actionPrimaryBaseColor'`, etc.) or any CSS color | -| **strokeWidth** | ❌ Not available | `'xs'` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` \| `'2xl'` \| `number` \| `string` | | **children** | `React.ReactNode` | ❌ Removed | | **focusable** | `boolean` | ❌ Removed | | **description** | `string` (combined with title) | ❌ Removed (use `title` only) | | **src** | `string` | ❌ Removed | -The new icons automatically sync with theme changes, support all InstUI color tokens, and provide better TypeScript integration. All standard HTML and SVG attributes can be passed directly to Lucide icons and will be spread onto the nested SVG element. Existing `@instructure/ui-icons` package remains available for legacy Instructure-specific icons. +The new icons automatically sync with theme changes, support all InstUI color tokens, and provide better TypeScript integration. All standard HTML and SVG attributes can be passed directly to Lucide icons and will be spread onto the nested SVG element. ## Focus rings diff --git a/docs/patterns/UsingIcons.md b/docs/patterns/UsingIcons.md index 9ba9e57cc8..9be68308e1 100644 --- a/docs/patterns/UsingIcons.md +++ b/docs/patterns/UsingIcons.md @@ -7,6 +7,116 @@ relevantForAI: true ## Using Icons +Icons from `@instructure/ui-icons` are available as `InstUIIcon` components — browse them +in the [Icons gallery](#icons). Legacy class-component icons (`IconHeartLine`, `IconSearchLine`, +etc.) are still available for backwards compatibility but are deprecated — see +[Legacy Icons](#legacy-icons) below. Components with the new theming system only accept new icons. + +### Accessibility + +Without a `title` prop the icon is decorative: `aria-hidden="true"` and `role="presentation"` are +set automatically. When a `title` is provided, the icon becomes meaningful: `aria-label` is set to +the title value and `role="img"` is added. + +```js +--- +type: example +--- + + I New York + +``` + +### Size + +Use the `size` prop with a semantic size token. Stroke width scales automatically with the size. + +```js +--- +type: example +--- + + + xs + sm + md + lg + xl + 2xl + + +``` + +Illustration sizes (`illu-sm`, `illu-md`, `illu-lg`) are intended for larger decorative contexts. + +### Color + +Use the `color` prop with a semantic color token — a theme-aware named value (e.g. `errorColor`, +`successColor`) that automatically adapts to the active theme. `inherit` passes `currentColor` through from the parent element. +`ai` renders the icon with an AI gradient. + +```js +--- +type: example +--- + + + + + + + + + + + +``` + +### Rotate + +```js +--- +type: example +--- + + + + + + + + +``` + +### Stroke vs Filled + +Lucide icons are stroke-based. Several custom icons come in a filled variant, identified by the +`Solid` suffix in their export name (e.g. `BellInstUIIcon` vs `BellSolidInstUIIcon`). + +```js +--- +type: example +--- + + + Stroke + Filled + + +``` + +--- + +## Legacy Icons + +> **Deprecated.** The legacy icon set (`IconHeartLine`, `IconSearchLine`, etc.) is kept for +> backwards compatibility. Use the `InstUIIcon` components above for new code. +> To migrate existing usage run: +> +> ``` +> npx @instructure/ui-codemods migrateToNewIcons +> ``` + ### Accessibility By default, the icon's `role` is set to `presentation`. However, when the `title` prop is set, the role attribute is set to `img`. Include the `description` prop to further describe the icon. diff --git a/packages/__docs__/buildScripts/DataTypes.mts b/packages/__docs__/buildScripts/DataTypes.mts index b135f3ec84..c0cedeac3d 100644 --- a/packages/__docs__/buildScripts/DataTypes.mts +++ b/packages/__docs__/buildScripts/DataTypes.mts @@ -137,9 +137,7 @@ type Glyph = { glyphName: string } -type MainIconsData = { - glyphs: Glyph[] -} +type LegacyIconsData = Glyph[] type MainDocsData = { themes: Record @@ -170,7 +168,7 @@ export type { LibraryOptions, Glyph, MainDocsData, - MainIconsData, + LegacyIconsData, JsDocResult, MinorVersionData, Section, diff --git a/packages/__docs__/buildScripts/build-docs.mts b/packages/__docs__/buildScripts/build-docs.mts index 7730edea50..b56085b5cb 100644 --- a/packages/__docs__/buildScripts/build-docs.mts +++ b/packages/__docs__/buildScripts/build-docs.mts @@ -272,10 +272,10 @@ async function buildDocs() { ) // eslint-disable-next-line no-console - console.log('Copying icons data...') + console.log('Copying legacy icons data...') fs.copyFileSync( - projectRoot + '/packages/ui-icons/src/__build__/icons-data.json', - buildDir + 'icons-data.json' + projectRoot + '/packages/ui-icons/src/generated/legacy/legacy-icons-data.json', + buildDir + 'legacy-icons-data.json' ) // eslint-disable-next-line no-console diff --git a/packages/__docs__/globals.ts b/packages/__docs__/globals.ts index 1f6184377a..5b2914209d 100644 --- a/packages/__docs__/globals.ts +++ b/packages/__docs__/globals.ts @@ -42,8 +42,10 @@ import { getComponentsForVersion } from './versioned-components' import { rebrandDark, rebrandLight } from '@instructure/ui-themes' import { debounce } from '@instructure/debounce' -import '@instructure/ui-icons/src/__build__/icon-font/Solid/InstructureIcons-Solid.css' -import '@instructure/ui-icons/src/__build__/icon-font/Line/InstructureIcons-Line.css' +// eslint-disable-next-line @instructure/no-relative-imports +import '../ui-icons/src/generated/icon-font/Solid/InstructureIcons-Solid.css' +// eslint-disable-next-line @instructure/no-relative-imports +import '../ui-icons/src/generated/icon-font/Line/InstructureIcons-Line.css' import { DateTime } from '@instructure/ui-i18n' // @ts-ignore webpack import diff --git a/packages/__docs__/package.json b/packages/__docs__/package.json index 4abd2bcb5e..dc26426034 100644 --- a/packages/__docs__/package.json +++ b/packages/__docs__/package.json @@ -122,7 +122,6 @@ "moment": "^2.30.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-window": "^2.2.3", "semver": "^7.7.2", "uuid": "^11.1.0", "webpack-merge": "^6.0.1" diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index a39e59e328..10fa3be4a9 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -60,6 +60,7 @@ import { Theme } from '../Theme' import { Select } from '../Select' import { Section } from '../Section' import IconsPage from '../Icons' +import LegacyIconsPage from '../LegacyIcons' import { compileMarkdown } from '../compileMarkdown' import { @@ -144,7 +145,7 @@ class App extends Component { layout: 'large', docsData: null, versionsData: undefined, - iconsData: null + legacyIconsData: null } this._heroRef = createRef() @@ -234,11 +235,11 @@ class App extends Component { ).catch(errorHandler) // Icons are not version-specific; only re-fetch if not already loaded - if (!this.state.iconsData) { - fetch(`${getAssetBasePath()}/icons-data.json`, { signal }) + if (!this.state.legacyIconsData) { + fetch(`${getAssetBasePath()}/legacy-icons-data.json`, { signal }) .then((response) => response.json()) - .then((iconsData) => { - this.setState({ iconsData }) + .then((legacyIconsData) => { + this.setState({ legacyIconsData }) }) .catch(errorHandler) } @@ -299,11 +300,9 @@ class App extends Component { this.fetchVersionData(signal).catch(errorHandler) document.addEventListener('keydown', this.handleTabKey) - fetch(`${getAssetBasePath()}/icons-data.json`, { signal }) + fetch(`${getAssetBasePath()}/legacy-icons-data.json`, { signal }) .then((response) => response.json()) - .then((iconsData) => { - this.setState({ iconsData: iconsData }) - }) + .then((iconsData) => this.setState({ legacyIconsData: iconsData })) .catch(errorHandler) // Detect minor version from URL (e.g. /v11_7/Menu) @@ -312,10 +311,7 @@ class App extends Component { // Always fetch minor version data to enable the version selector fetchMinorVersionData(signal) .then((minorVersionsData) => { - if ( - minorVersionsData && - minorVersionsData.libraryVersions.length > 0 - ) { + if (minorVersionsData && minorVersionsData.libraryVersions.length > 0) { // If URL has a version, use it; otherwise use default const selectedMinorVersion = urlMinorVersion ?? minorVersionsData.defaultVersion @@ -626,7 +622,6 @@ class App extends Component { } renderIcons(key: string) { - const { iconsData } = this.state const { layout } = this.state const smallerScreens = layout === 'small' || layout === 'medium' @@ -640,7 +635,25 @@ class App extends Component { Icons - + + + ) + + return
{this.renderWrappedContent(iconContent)}
+ } + + renderLegacyIcons(key: string) { + const { layout } = this.state + const smallerScreens = layout === 'small' || layout === 'medium' + + const iconContent = ( + + ) @@ -673,7 +686,10 @@ class App extends Component { }) .catch((error: Error) => { if (error.name !== 'AbortError') { - logError(false, `Failed to fetch document ${docId}: ${error.message}`) + logError( + false, + `Failed to fetch document ${docId}: ${error.message}` + ) } }) return ( @@ -847,6 +863,17 @@ class App extends Component { {this.renderIcons(key)} ) + } else if (key === 'legacy-icons') { + return ( + + {this.renderLegacyIcons(key)} + + ) } else if (theme) { return ( { } renderLegacyDocWarning() { - const { versionsData, iconsData } = this.state + const { versionsData } = this.state - if (!versionsData || !iconsData) { + if (!versionsData) { return null } @@ -1025,9 +1052,9 @@ class App extends Component { render() { const key = this.state.key - const { showMenu, layout, docsData, iconsData } = this.state + const { showMenu, layout, docsData } = this.state - if (!docsData || !iconsData) { + if (!docsData) { return } return ( diff --git a/packages/__docs__/src/App/props.ts b/packages/__docs__/src/App/props.ts index a750e52a4a..40fef44ad4 100644 --- a/packages/__docs__/src/App/props.ts +++ b/packages/__docs__/src/App/props.ts @@ -24,7 +24,7 @@ import type { ComponentStyle, WithStyleProps } from '@instructure/emotion' import type { - MainIconsData, + LegacyIconsData, MainDocsData, MinorVersionData, ProcessedFile @@ -78,7 +78,7 @@ type LayoutSize = 'small' | 'medium' | 'large' | 'x-large' type AppState = { themeKey?: string docsData: MainDocsData | null - iconsData: MainIconsData | null + legacyIconsData: LegacyIconsData | null layout: LayoutSize showMenu: boolean key?: string diff --git a/packages/__docs__/src/Icons/LucideIconsGallery.tsx b/packages/__docs__/src/Icons/IconsGallery.tsx similarity index 65% rename from packages/__docs__/src/Icons/LucideIconsGallery.tsx rename to packages/__docs__/src/Icons/IconsGallery.tsx index 975ef30a3b..e39d323eed 100644 --- a/packages/__docs__/src/Icons/LucideIconsGallery.tsx +++ b/packages/__docs__/src/Icons/IconsGallery.tsx @@ -23,7 +23,7 @@ */ import { useState, memo, useCallback, useMemo, useRef } from 'react' -import { Grid } from 'react-window' +import type { ChangeEvent } from 'react' import { Heading } from '@instructure/ui-heading' import { TextInput } from '@instructure/ui-text-input' @@ -36,35 +36,48 @@ import { } from '@instructure/ui-a11y-content' import { Modal } from '@instructure/ui-modal' import { SourceCodeEditor } from '@instructure/ui-source-code-editor' -import * as LucideIcons from '@instructure/ui-icons' +import { LucideIcons, CustomIcons } from '@instructure/ui-icons' import { XInstUIIcon } from '@instructure/ui-icons' import { Flex } from '@instructure/ui-flex' -// Get all exported Lucide icons (excluding utilities and types) -// Lucide icons end with 'InstUIIcon', while generated SVG icons have patterns like 'IconAddLine' -const lucideIconNames = Object.keys(LucideIcons).filter((name) => - name.endsWith('InstUIIcon') -) - -type LucideIconTileProps = { +type IconInfo = { name: string + component: React.ComponentType + importPath: string +} + +type IconTileProps = { + icon: IconInfo rtl: boolean - onClick: (name: string) => void + onClick: (icon: IconInfo) => void } -function getUsageInfo(iconName: string) { - return `import { ${iconName} } from '@instructure/ui-icons' +const allIcons: IconInfo[] = [ + ...Object.entries(LucideIcons).map(([name, component]) => ({ + name, + component: component as React.ComponentType, + importPath: '@instructure/ui-icons' + })), + ...Object.entries(CustomIcons).map(([name, component]) => ({ + name, + component: component as React.ComponentType, + importPath: '@instructure/ui-icons' + })) +] + +function getUsageInfo(icon: IconInfo) { + return `import { ${icon.name} } from '${icon.importPath}' const MyIcon = () => { return ( - <${iconName} size={'2xl'} color='successColor'/> + <${icon.name} size="2xl" color="successColor" /> ) }` } -const LucideIconTile = memo( - ({ name, rtl, onClick }: LucideIconTileProps) => { - const IconComponent = (LucideIcons as any)[name] +const IconTile = memo( + ({ icon, rtl, onClick }: IconTileProps) => { + const IconComponent = icon.component if (!IconComponent) { return null @@ -76,10 +89,9 @@ const LucideIconTile = memo( display: 'flex', alignItems: 'center', flexDirection: 'column', - minWidth: '15em', - flexBasis: '15em', + minWidth: '14em', + flexBasis: '14em', flexGrow: 1, - margin: '0.5rem 0' }} >
- - {name} - +
+ {icon.name} +
) }, (prevProps, nextProps) => - prevProps.name === nextProps.name && prevProps.rtl === nextProps.rtl + prevProps.icon.name === nextProps.icon.name && + prevProps.rtl === nextProps.rtl ) -LucideIconTile.displayName = 'LucideIconTile' - -const TILE_WIDTH = 240 -const TILE_HEIGHT = 150 -const COLUMN_COUNT = 4 -const GRID_WIDTH = TILE_WIDTH * COLUMN_COUNT +IconTile.displayName = 'IconTile' -// Empty object constant for cellProps to maintain referential equality -// and prevent unnecessary re-renders of all cells -const EMPTY_CELL_PROPS = {} - -const LucideIconsGallery = () => { +const IconsGallery = () => { const [searchQuery, setSearchQuery] = useState('') - const [searchInput, setSearchInput] = useState('') - const [selectedIcon, setSelectedIcon] = useState(null) + const [selectedIcon, setSelectedIcon] = useState(null) const [rtl, setRtl] = useState(false) - const timeoutId = useRef(null) + const searchTimeoutId = useRef(null) - // Debounced search - only update searchQuery after 300ms of no typing - const handleSearchChange = useCallback( - (_e: React.ChangeEvent, value: string) => { - setSearchInput(value) + const handleSearchChange = (_e: ChangeEvent, value: string) => { + // Instant update when extending query (typing adds characters) + if (value.startsWith(searchQuery)) { + setSearchQuery(value) + return + } - if (timeoutId.current) { - clearTimeout(timeoutId.current) - } + // Debounce when deleting (reveals more icons = heavier re-render) + if (searchTimeoutId.current) { + clearTimeout(searchTimeoutId.current) + } - timeoutId.current = setTimeout(() => { - setSearchQuery(value) - }, 300) - }, - [] - ) + searchTimeoutId.current = setTimeout(() => { + setSearchQuery(value) + }, 500) + } - const handleBidirectionToggle = useCallback((e: React.ChangeEvent) => { + const handleBidirectionToggle = useCallback((e: ChangeEvent) => { setRtl(e.target.checked) }, []) - const handleIconClick = useCallback((name: string) => { - setSelectedIcon(name) + const handleIconClick = useCallback((icon: IconInfo) => { + setSelectedIcon(icon) }, []) const handleModalDismiss = useCallback(() => { @@ -170,16 +191,13 @@ const LucideIconsGallery = () => { }, []) const filteredIcons = useMemo(() => { - if (!searchQuery) return lucideIconNames - return lucideIconNames.filter((name) => - name.toLowerCase().includes(searchQuery.toLowerCase()) - ) + if (!searchQuery) return allIcons + const query = searchQuery.toLowerCase() + return allIcons.filter((icon) => icon.name.toLowerCase().includes(query)) }, [searchQuery]) - const rowCount = Math.ceil(filteredIcons.length / COLUMN_COUNT) - return ( -
+
{ > Icon Name} /> @@ -205,55 +222,35 @@ const LucideIconsGallery = () => {
- { - const index = rowIndex * COLUMN_COUNT + columnIndex - if (index >= filteredIcons.length) { - return
- } - - const iconName = filteredIcons[index] - return ( -
- -
- ) - }} - cellProps={EMPTY_CELL_PROPS} - columnCount={COLUMN_COUNT} - columnWidth={TILE_WIDTH} - rowCount={rowCount} - rowHeight={TILE_HEIGHT} - style={{ - height: '600px', - width: `${GRID_WIDTH}px`, - overflowX: 'hidden' - }} - /> + {filteredIcons.map((icon) => ( + + ))}
{selectedIcon && ( - {selectedIcon} + {selectedIcon.name} { Usage { ) } -export default LucideIconsGallery +export default IconsGallery diff --git a/packages/__docs__/src/Icons/index.tsx b/packages/__docs__/src/Icons/index.tsx index dcc89103fb..0f7ddc522f 100644 --- a/packages/__docs__/src/Icons/index.tsx +++ b/packages/__docs__/src/Icons/index.tsx @@ -22,64 +22,32 @@ * SOFTWARE. */ -import { useState, lazy, Suspense } from 'react' - -import { Tabs } from '@instructure/ui-tabs' +import { lazy, Suspense } from 'react' import { Spinner } from '@instructure/ui-spinner' -import { Glyph } from '../../buildScripts/DataTypes.mjs' - -// Lazy load gallery components for better initial load performance -const LucideIconsGallery = lazy(() => import('./LucideIconsGallery')) -const LegacyIconsGallery = lazy(() => import('./LegacyIconsGallery')) - -type IconsPageProps = { - glyphs: Glyph[] -} +import { View } from '@instructure/ui-view' -const IconsPage = ({ glyphs }: IconsPageProps) => { - const [selectedTabIndex, setSelectedTabIndex] = useState(0) - - const handleTabChange = (_event: any, { index }: { index: number }) => { - setSelectedTabIndex(index) - } +// Lazy load icons gallery component +const IconsGallery = lazy(() => import('./IconsGallery')) +const IconsPage = () => { return ( -
- - - {/* Keep both galleries mounted but hide inactive one with CSS for instant switching */} -
- - } - > - - -
-
- - -
- - } - > - - + + +
-
-
-
+ } + > + + + ) } diff --git a/packages/__docs__/src/Icons/LegacyIconsGallery.tsx b/packages/__docs__/src/LegacyIcons/LegacyIconsGallery.tsx similarity index 83% rename from packages/__docs__/src/Icons/LegacyIconsGallery.tsx rename to packages/__docs__/src/LegacyIcons/LegacyIconsGallery.tsx index 7d41fa901b..e60fda7417 100644 --- a/packages/__docs__/src/Icons/LegacyIconsGallery.tsx +++ b/packages/__docs__/src/LegacyIcons/LegacyIconsGallery.tsx @@ -23,7 +23,7 @@ */ import { useState, useRef, memo, useCallback, useMemo } from 'react' -import { Grid } from 'react-window' +import type { ChangeEvent, SyntheticEvent } from 'react' import { InlineSVG } from '@instructure/ui-svg-images' import { Heading } from '@instructure/ui-heading' @@ -43,11 +43,13 @@ import * as InstIcons from '@instructure/ui-icons' import { IconXSolid } from '@instructure/ui-icons' import { Link } from '@instructure/ui-link' import { Flex } from '@instructure/ui-flex' -import { Glyph } from '../../buildScripts/DataTypes.mjs' +import type { Glyph } from '../../buildScripts/DataTypes.mjs' type Format = 'react' | 'svg' | 'font' -type IconTileProps = { +type StyleType = 'line' | 'solid' + +type LegacyIconTileProps = { glyph: Glyph format: Format rtl: boolean @@ -58,8 +60,6 @@ type LegacyIconsGalleryProps = { glyphs: Glyph[] } -type StyleType = 'line' | 'solid' - function getUsageInfo( selectedGlyph: { glyph: Glyph; styleType: StyleType }, format: Format @@ -91,8 +91,8 @@ const MyIcon = () => { }` } -const IconTile = memo( - ({ format, glyph, rtl, onClick }: IconTileProps) => { +const LegacyIconTile = memo( + ({ format, glyph, rtl, onClick }: LegacyIconTileProps) => { const { name, glyphName, lineSrc, solidSrc } = glyph const getIconNode = (styleType: StyleType) => { if (format === 'react') { @@ -175,16 +175,8 @@ const IconTile = memo( prevProps.glyph.glyphName === nextProps.glyph.glyphName && prevProps.rtl === nextProps.rtl ) -IconTile.displayName = 'IconTile' - -const TILE_WIDTH = 240 -const TILE_HEIGHT = 180 -const COLUMN_COUNT = 4 -const GRID_WIDTH = TILE_WIDTH * COLUMN_COUNT +LegacyIconTile.displayName = 'LegacyIconTile' -// Empty object constant for cellProps to maintain referential equality -// and prevent unnecessary re-renders of all cells -const EMPTY_CELL_PROPS = {} const LegacyIconsGallery = ({ glyphs }: LegacyIconsGalleryProps) => { const [selectedFormat, setSelectedFormat] = useState('react') @@ -197,28 +189,25 @@ const LegacyIconsGallery = ({ glyphs }: LegacyIconsGalleryProps) => { const [rtl, setRtl] = useState(false) const timeoutId = useRef(null) - // Debounced search - only update searchQuery after 300ms of no typing - const handleSearchChange = useCallback( - (_e: React.ChangeEvent, value: string) => { - setSearchInput(value) + // Debounced search + const handleSearchChange = useCallback((_e: ChangeEvent, value: string) => { + setSearchInput(value) - if (timeoutId.current) { - clearTimeout(timeoutId.current) - } + if (timeoutId.current) { + clearTimeout(timeoutId.current) + } - timeoutId.current = setTimeout(() => { - setSearchQuery(value) - }, 300) - }, - [] - ) + timeoutId.current = setTimeout(() => { + setSearchQuery(value) + }, 300) + }, []) - const handleBidirectionToggle = useCallback((e: React.ChangeEvent) => { + const handleBidirectionToggle = useCallback((e: ChangeEvent) => { setRtl(e.target.checked) }, []) const handleFormatChange = useCallback( - (_e: React.SyntheticEvent, { value }: { value?: string | number }) => { + (_e: SyntheticEvent, { value }: { value?: string | number }) => { setSelectedFormat(value as Format) }, [] @@ -245,8 +234,6 @@ const LegacyIconsGallery = ({ glyphs }: LegacyIconsGalleryProps) => { ) }, [glyphs, searchQuery]) - const rowCount = Math.ceil(filteredGlyphs.length / COLUMN_COUNT) - return (
{
- { - const index = rowIndex * COLUMN_COUNT + columnIndex - if (index >= filteredGlyphs.length) { - return
- } - - const glyph = filteredGlyphs[index] - return ( -
- -
- ) - }} - cellProps={EMPTY_CELL_PROPS} - columnCount={COLUMN_COUNT} - columnWidth={TILE_WIDTH} - rowCount={rowCount} - rowHeight={TILE_HEIGHT} - style={{ - height: '600px', - width: `${GRID_WIDTH}px`, - overflowX: 'hidden' - }} - /> + {filteredGlyphs.map((glyph) => ( + + ))}
{selectedGlyph && ( void +} + +function getUsageInfo( + selectedGlyph: { glyph: Glyph; styleType: StyleType }, + format: Format +) { + const { + glyph: { name, lineSrc, solidSrc, glyphName }, + styleType + } = selectedGlyph + const styleTypeTitleCase = styleType === 'line' ? 'Line' : 'Solid' + if (format === 'react') { + const componentName = `${name}${styleTypeTitleCase}` + return `import { ${componentName} } from '@instructure/ui-icons' + +const MyIcon = () => { + return ( + <${componentName} /> + ) +}` + } else if (format === 'svg') { + return styleType === 'line' ? lineSrc : solidSrc + } + + return `import '@instructure/ui-icons/es/icon-font/${styleTypeTitleCase}/InstructureIcons-${styleTypeTitleCase}.css' + +const MyIcon = () => { + return ( +