Skip to content

Commit f72f6cb

Browse files
committed
chore(ui-scripts,ui-codemods): add icon test page
1 parent a678ae1 commit f72f6cb

9 files changed

Lines changed: 227 additions & 53 deletions

File tree

docs/guides/upgrade-guide.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ TODO add details
1212

1313
## New icons
1414

15-
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. We have a codemod that will help you migrate your code to the new icon set (see below).
15+
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. We have a codemod that will help you migrate your code to the new icon set.
1616

17-
### Stroke-Based Icons Package
17+
### Lucide Icons Package
1818

19-
InstUI v12 introduces a new icon package **`@instructure/ui-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.
19+
InstUI introduces a new icon package **`@instructure/ui-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.
2020

2121
**Key differences from `SVGIcon`/`InlineSVG`:**
2222

@@ -29,7 +29,7 @@ InstUI v12 introduces a new icon package **`@instructure/ui-icons`** based on th
2929
| **description** | `string` (combined with title) | ❌ Removed (use `title` only) |
3030
| **src** | `string` | ❌ Removed |
3131

32-
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 stroke-based icons and will be spread onto the nested SVG element.
32+
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.
3333

3434
## Focus rings
3535

packages/__docs__/src/Icons/IconsGallery.tsx

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,38 +56,31 @@ type IconTileProps = {
5656
onClick: (icon: IconInfo) => void
5757
}
5858

59-
// Get all stroke icons
60-
const strokeIconNames = Object.keys(LucideIcons).filter((name) =>
61-
name.endsWith('InstUIIcon')
62-
)
63-
64-
// Get all custom icons
65-
const customIconNames = Object.keys(CustomIcons).filter((name) =>
66-
name.endsWith('InstUIIcon')
67-
)
68-
69-
// Combine all icons with metadata
7059
const allIcons: IconInfo[] = [
71-
...strokeIconNames.map((name) => ({
72-
name,
73-
component: (LucideIcons as any)[name],
74-
source: 'lucide' as const,
75-
importPath: '@instructure/ui-icons'
76-
})),
77-
...customIconNames.map((name) => ({
78-
name,
79-
component: (CustomIcons as any)[name],
80-
source: 'custom' as const,
81-
importPath: '@instructure/ui-icons'
82-
}))
60+
...Object.entries(LucideIcons)
61+
.filter(([name]) => name.endsWith('InstUIIcon'))
62+
.map(([name, component]) => ({
63+
name,
64+
component: component as React.ComponentType<any>,
65+
source: 'lucide' as const,
66+
importPath: '@instructure/ui-icons'
67+
})),
68+
...Object.entries(CustomIcons)
69+
.filter(([name]) => name.endsWith('InstUIIcon'))
70+
.map(([name, component]) => ({
71+
name,
72+
component: component as React.ComponentType<any>,
73+
source: 'custom' as const,
74+
importPath: '@instructure/ui-icons'
75+
}))
8376
]
8477

8578
function getUsageInfo(icon: IconInfo) {
8679
return `import { ${icon.name} } from '${icon.importPath}'
8780
8881
const MyIcon = () => {
8982
return (
90-
<${icon.name} size={'2xl'} color='successColor' />
83+
<${icon.name} size="2xl" color="successColor" />
9184
)
9285
}`
9386
}

packages/ui-codemods/lib/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,5 @@ export {
3939
renameGetComputedStyleToGetCSSStyleDeclaration,
4040
warnTableCaptionMissing,
4141
warnCodeEditorRemoved,
42-
migrateToNewIcons,
43-
// Backwards compatibility alias (deprecated)
44-
migrateToNewIcons as migrateToLucideIcons
42+
migrateToNewIcons
4543
}

packages/ui-icons/src/custom/wrapCustomIcon.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ import generateStyle from '../styles'
3636
*
3737
* strokeWidth is always applied to the SVG root with absolute sizing (equivalent to
3838
* Lucide's absoluteStrokeWidth=true). Since SVG strokeWidth only affects elements
39-
* that have an explicit stroke attribute, fill-only paths are unaffected — no
40-
* isFilled flag is needed.
39+
* that have an explicit stroke attribute, fill-only paths are unaffected
4140
*
4241
* @param iconNode Flat array of [tagName, attributes] tuples from the build script.
4342
* @param iconName Used as displayName (e.g. 'AiInfo').

packages/ui-icons/src/lucide/wrapLucideIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import type { InstUIIconProps } from '../props'
3232
import generateStyle from '../styles'
3333

3434
/**
35-
* Wraps a stroke-based icon with InstUI theming, RTL support, and semantic sizing.
35+
* Wraps a Lucide icon with InstUI theming, RTL support, and semantic sizing.
3636
* Only accepts InstUI semantic tokens (size="lg", color="baseColor").
3737
* Stroke width is automatically derived from size for consistent visual weight.
3838
* Numeric and custom CSS values are not supported.

packages/ui-scripts/lib/icons/build-icons.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
* SOFTWARE.
2323
*/
24-
import { runCommandSync } from '@instructure/command-utils'
24+
import { runCommandSync, info } from '@instructure/command-utils'
2525
import path from 'path'
2626
import fs from 'fs'
2727
import process from 'process'
@@ -121,7 +121,19 @@ export default {
121121
prefix: 'icon-solid'
122122
})
123123

124-
// generate legacy icons data for documentation
125-
generateLegacyIconsData(process.cwd())
124+
// write legacy-icons-data.json for the docs gallery
125+
const legacyIconsData = generateLegacyIconsData()
126+
const legacyOutputDir = path.join(process.cwd(), 'lib/legacy/')
127+
fs.mkdirSync(legacyOutputDir, { recursive: true })
128+
const legacyOutputPath = path.join(
129+
legacyOutputDir,
130+
'legacy-icons-data.json'
131+
)
132+
fs.writeFileSync(
133+
legacyOutputPath,
134+
JSON.stringify(legacyIconsData, null, 2),
135+
'utf8'
136+
)
137+
info(`Generated ${legacyOutputPath} (${legacyIconsData.length} icons)`)
126138
}
127139
}

packages/ui-scripts/lib/icons/generate-legacy-icons-data.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@
2121
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
* SOFTWARE.
2323
*/
24-
import fs from 'fs'
2524
import path from 'path'
2625
import getGlyphData from './get-glyph-data.js'
2726

28-
export default function generateLegacyIconsData(packageRoot) {
29-
const svgDir = path.join(packageRoot, 'svg/')
30-
const outputDir = path.join(packageRoot, 'lib/legacy/')
27+
export default function generateLegacyIconsData() {
28+
const svgDir = path.join(process.cwd(), 'svg/')
3129
const deprecatedMap = {}
3230
const bidirectionalList = []
3331

34-
// Get glyph data from SVG files
3532
const glyphsRaw = getGlyphData(
3633
svgDir,
3734
deprecatedMap,
@@ -40,7 +37,7 @@ export default function generateLegacyIconsData(packageRoot) {
4037
)
4138

4239
// Group by glyphName to merge Line and Solid variants
43-
const mergedGlyphs = Object.values(
40+
return Object.values(
4441
glyphsRaw.reduce(
4542
(acc, { name, glyphName, variant, src, bidirectional, deprecated }) => {
4643
if (!deprecated) {
@@ -57,14 +54,4 @@ export default function generateLegacyIconsData(packageRoot) {
5754
{}
5855
)
5956
)
60-
61-
// Create output directory
62-
fs.mkdirSync(outputDir, { recursive: true })
63-
64-
// Write JSON file
65-
const outputPath = path.join(outputDir, 'legacy-icons-data.json')
66-
fs.writeFileSync(outputPath, JSON.stringify(mergedGlyphs, null, 2), 'utf8')
67-
68-
console.error(`Generated legacy icons data: ${outputPath}`)
69-
console.error(`Total icons: ${mergedGlyphs.length}`)
7057
}

regression-test/cypress/e2e/spec.cy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ describe('visual regression test', () => {
162162
cy.checkA11y('.axe-test', axeOptions, terminalLog)
163163
})
164164

165+
it('Custom and Lucide icons', () => {
166+
cy.visit('http://localhost:3000/custom-icons')
167+
cy.injectAxe()
168+
cy.checkA11y('.axe-test', axeOptions, terminalLog)
169+
})
170+
165171
it('Dateinput, DateInput2', () => {
166172
cy.visit('http://localhost:3000/dateinput')
167173
cy.wait(400)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
'use client'
25+
import React from 'react'
26+
import { CustomIcons, LucideIcons } from '@instructure/ui-icons'
27+
import type { InstUIIconProps } from '@instructure/ui-icons'
28+
29+
type IconComponent = React.ComponentType<InstUIIconProps>
30+
type IconEntry = [string, IconComponent]
31+
32+
const COLORS: InstUIIconProps['color'][] = [
33+
'ai',
34+
'baseColor',
35+
'errorColor',
36+
'successColor',
37+
'warningColor',
38+
'infoColor',
39+
'mutedColor',
40+
'accentVioletColor'
41+
]
42+
const SIZES: InstUIIconProps['size'][] = [
43+
'xs',
44+
'sm',
45+
'md',
46+
'lg',
47+
'xl',
48+
'2xl',
49+
'illu-sm',
50+
'illu-md',
51+
'illu-lg'
52+
]
53+
54+
const sample = (entries: IconEntry[], n = 5): IconEntry[] =>
55+
Array.from(
56+
{ length: n },
57+
(_, i) => entries[Math.floor((i * entries.length) / n)]
58+
)
59+
60+
const CUSTOM = sample(
61+
Object.entries(CustomIcons).filter(([name]) =>
62+
name.endsWith('InstUIIcon')
63+
) as IconEntry[]
64+
)
65+
const LUCIDE = sample(
66+
Object.entries(LucideIcons).filter(([name]) =>
67+
name.endsWith('InstUIIcon')
68+
) as IconEntry[]
69+
)
70+
71+
function IconGrid({
72+
icons,
73+
color,
74+
size = 'md'
75+
}: {
76+
icons: IconEntry[]
77+
color: InstUIIconProps['color']
78+
size?: InstUIIconProps['size']
79+
}) {
80+
return (
81+
<div className="flex flex-wrap gap-1">
82+
{icons.map(([name, Icon]) => (
83+
<div key={name} className="flex flex-col items-center gap-1 p-1">
84+
<Icon size={size} color={color} title={name} />
85+
<span
86+
style={{ fontSize: '8px', maxWidth: '76px' }}
87+
className="text-gray-500 text-center break-words leading-tight"
88+
>
89+
{name.replace('InstUIIcon', '')}
90+
</span>
91+
</div>
92+
))}
93+
</div>
94+
)
95+
}
96+
97+
function SizeRow({
98+
Icon,
99+
name,
100+
color
101+
}: {
102+
Icon: IconComponent
103+
name: string
104+
color: InstUIIconProps['color']
105+
}) {
106+
return (
107+
<div className="flex items-end flex-wrap gap-3">
108+
{SIZES.map((size) => (
109+
<div key={size} className="flex flex-col items-center gap-1 p-1">
110+
<Icon size={size} color={color} title={name} />
111+
<span style={{ fontSize: '8px' }} className="text-gray-500">
112+
{size}
113+
</span>
114+
</div>
115+
))}
116+
</div>
117+
)
118+
}
119+
120+
function SectionHeader({ children }: { children: React.ReactNode }) {
121+
return (
122+
<p className="text-xs font-bold mt-7 mb-2 pb-1 border-b border-gray-300 text-gray-700">
123+
{children}
124+
</p>
125+
)
126+
}
127+
128+
export default function CustomIconsPage() {
129+
return (
130+
<main className="axe-test p-6 font-sans">
131+
<h1 className="text-sm font-bold mb-1">
132+
Custom icons (sample of {CUSTOM.length})
133+
</h1>
134+
135+
{COLORS.map((color) => (
136+
<React.Fragment key={color}>
137+
<SectionHeader>color={color}</SectionHeader>
138+
<IconGrid icons={CUSTOM} color={color} />
139+
</React.Fragment>
140+
))}
141+
142+
{SIZES.map((size) => (
143+
<React.Fragment key={size}>
144+
<SectionHeader>size={size}, color=baseColor</SectionHeader>
145+
<IconGrid icons={CUSTOM} color="baseColor" size={size} />
146+
</React.Fragment>
147+
))}
148+
149+
<SectionHeader>
150+
Size scale — AiInfo (stroke) + BellSolid (filled) + CanvasLogo (brand)
151+
</SectionHeader>
152+
<SizeRow Icon={CustomIcons.AiInfoInstUIIcon} name="AiInfo" color="ai" />
153+
<SizeRow
154+
Icon={CustomIcons.BellSolidInstUIIcon}
155+
name="BellSolid"
156+
color="ai"
157+
/>
158+
<SizeRow
159+
Icon={CustomIcons.CanvasLogoInstUIIcon}
160+
name="CanvasLogo"
161+
color="baseColor"
162+
/>
163+
164+
<h1 className="text-sm font-bold mt-10 mb-1">
165+
Lucide icons (sample of {LUCIDE.length})
166+
</h1>
167+
168+
{COLORS.map((color) => (
169+
<React.Fragment key={color}>
170+
<SectionHeader>color={color}</SectionHeader>
171+
<IconGrid icons={LUCIDE} color={color} />
172+
</React.Fragment>
173+
))}
174+
175+
<SectionHeader>Size scale — Heart, color=ai</SectionHeader>
176+
<SizeRow Icon={LucideIcons.HeartInstUIIcon} name="Heart" color="ai" />
177+
</main>
178+
)
179+
}

0 commit comments

Comments
 (0)