Files
supabase/packages/icons/scripts/validateIcons.mjs
Danny White 73c9dbfa52 fix(studio): standardise custom icon weight (#48478)
## What kind of change does this PR introduce?

Bug fix and internal tooling update. Resolves FE-3472.

## What is the current behavior?

Custom Studio icons use inconsistent source stroke widths, and some
child-level styling prevents component props from overriding them. Mixed
custom and Lucide icon sets can therefore appear uneven.

## What is the new behavior?

Custom stroke icons use a root-level `stroke-width="1.5"`; fill-only
logos use `stroke="none"`. The build validates that contract and
regenerated components preserve existing exports and props.

Studio applies the same `1.5` weight across Reports categories and uses
one shared destination icon mapping in the replication selector,
destination rows and diagram.

| Before | After |
| --- | --- |
| <img width="418" height="516" alt="56398"
src="https://github.com/user-attachments/assets/6afa7042-e6be-40e7-9911-af2f61238c9d"
/> | <img width="390" height="550" alt="CleanShot 2026-07-30 at 17 12
37@2x"
src="https://github.com/user-attachments/assets/870f49cf-c8fa-40db-8be8-2eb5f264ff4a"
/> |
| <img width="510" height="734" alt="CleanShot 2026-07-30 at 17 19
28@2x"
src="https://github.com/user-attachments/assets/a5b2c088-dcd2-4907-976b-5820794d06e3"
/> | <img width="554" height="742" alt="CleanShot 2026-07-30 at 17 16
06@2x"
src="https://github.com/user-attachments/assets/ed3a77c4-5d94-4ca7-b9e4-1403b725a981"
/> |


## Testing

At 100% zoom, compare custom and Lucide icon weight in:

- Reports: **Add your first chart** and **Add block**
- Database > Replication: the destination selector, destination rows and
replication diagram
- Command menu (`⌘K`): **Search Database Tables**, **Search RLS
Policies**, **Search Edge Functions** and **Search Storage**
- Authentication > Users: right-click a user row and compare the
context-menu icons
- Database > Schema Visualizer: open a table node overflow menu
- A paused project: **Export your data > Download backups**

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added consistent destination icons across replication panels, rows,
and diagrams.
  * Updated instance health and metric icons for clearer identification.
* Standardized icon stroke weight and reduced default icon stroke
thickness.

* **Documentation**
* Clarified custom icon requirements, default properties, and validation
guidance.

* **Bug Fixes**
* Improved consistency of icon rendering across replication destinations
and reports.

* **Tests**
* Added coverage for icon SVG validation and replication destination
icon rendering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 17:48:50 +10:00

97 lines
3.0 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const CANONICAL_STROKE_WIDTH = '1.5'
const CHILD_STROKE_ATTRIBUTES = ['stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin']
const readAttributes = (element) =>
Object.fromEntries(
[...element.matchAll(/([\w:-]+)\s*=\s*["']([^"']*)["']/g)].map((match) => [match[1], match[2]])
)
export const validateIconSource = (source, filename = 'icon.svg') => {
const errors = []
const rootMatch = source.match(/<svg\b[^>]*>/s)
if (!rootMatch) return [`${filename}: missing root <svg> element`]
const rootAttributes = readAttributes(rootMatch[0])
const childElements = source
.slice((rootMatch.index ?? 0) + rootMatch[0].length)
.match(/<(?!\/)[a-z][^>]*>/gi)
const childAttributeViolations = []
let hasChildInlineStyle = false
if ('style' in rootAttributes) {
errors.push(`${filename}: inline style attributes are not allowed on the root <svg>`)
}
for (const element of childElements ?? []) {
const attributes = readAttributes(element)
if ('style' in attributes) hasChildInlineStyle = true
for (const attribute of CHILD_STROKE_ATTRIBUTES) {
if (attribute in attributes) childAttributeViolations.push(attribute)
}
}
if (hasChildInlineStyle) {
errors.push(`${filename}: inline style attributes are not allowed on child elements`)
}
if (childAttributeViolations.length > 0) {
errors.push(
`${filename}: move shared ${[...new Set(childAttributeViolations)].join(
', '
)} attributes to the root <svg>`
)
}
if (rootAttributes.stroke === 'none') {
if ('stroke-width' in rootAttributes) {
errors.push(`${filename}: fill-only icons must not declare stroke-width`)
}
return errors
}
if (rootAttributes.fill !== 'none') {
errors.push(`${filename}: stroke icons must use fill="none" on the root <svg>`)
}
if (rootAttributes.stroke !== 'currentColor') {
errors.push(
`${filename}: stroke icons must use stroke="currentColor"; use stroke="none" for fill-only icons`
)
}
if (rootAttributes['stroke-width'] !== CANONICAL_STROKE_WIDTH) {
errors.push(
`${filename}: stroke icons must use stroke-width="${CANONICAL_STROKE_WIDTH}" on the root <svg>`
)
}
return errors
}
export const validateIconDirectory = (directory) => {
const errors = fs
.readdirSync(directory)
.filter((filename) => filename.endsWith('.svg'))
.sort()
.flatMap((filename) =>
validateIconSource(fs.readFileSync(path.join(directory, filename), 'utf8'), filename)
)
if (errors.length > 0) {
throw new Error(
`Icon source validation failed:\n${errors.map((error) => `- ${error}`).join('\n')}`
)
}
}
const isExecutedDirectly =
process.argv[1] !== undefined && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
if (isExecutedDirectly) {
validateIconDirectory(path.resolve(process.cwd(), 'src/raw-icons'))
console.log('Validated icon sources.')
}