diff --git a/.changeset/many-schools-reflect.md b/.changeset/many-schools-reflect.md new file mode 100644 index 00000000..475004db --- /dev/null +++ b/.changeset/many-schools-reflect.md @@ -0,0 +1,20 @@ +--- +"frog": minor +--- + +**Breaking change** Frog UI `icon` property requires an icon map imported from the `'frog/ui/icons'` entrypoint. This also makes it easier for you to supply your own custom icons. + +```diff ++ import { lucide } from 'frog/ui/icons' + +export const system = createSystem({ +- icons: 'lucide', ++ icons: lucide, +}) +``` + +In addition, the following separate entrypoints were added for resource constrained environments. + +- `frog/ui/icons/heroicons` +- `frog/ui/icons/lucide` +- `frog/ui/icons/radix-icons` diff --git a/.gitignore b/.gitignore index fa4ae553..aa96bb19 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ create-frog/templates node_modules site/dist src/ui/.frog -src/ui/icons.ts +src/ui/icons/** +!src/ui/icons/package.json tsconfig.*.tsbuildinfo tsconfig.tsbuildinfo diff --git a/.scripts/gen-icons.ts b/.scripts/gen-icons.ts index 7c8975b0..3d9bbbef 100644 --- a/.scripts/gen-icons.ts +++ b/.scripts/gen-icons.ts @@ -1,7 +1,8 @@ import path from 'node:path' import { IconifyJSONIconsData } from '@iconify/types' -import { getIconData, iconToHTML, iconToSVG } from '@iconify/utils' +import { camelize, getIconData, iconToHTML, iconToSVG } from '@iconify/utils' import { glob } from 'fast-glob' +import { ensureDir } from 'fs-extra' console.log('Copying icons to package.') @@ -16,31 +17,54 @@ const collections = (await glob('**/@iconify/json/json/*.json')) }) .filter((collection) => collectionSet.has(collection.name)) -const iconMap: Record> = {} -for (const collection of collectionSet) { - iconMap[collection] = {} -} - +const iconCollectionExports: string[] = [] let count = 0 for (const collection of collections) { const file = Bun.file(collection.path) const json = (await file.json()) as IconifyJSONIconsData + const iconMap: Record = {} for (const key of Object.keys(json.icons)) { const item = getIconData(json, key) if (!item) throw new TypeError(`Invalid icon: ${key}`) const svg = iconToSVG(item) const text = iconToHTML(svg.body, svg.attributes) - iconMap[collection.name][key] = encodeURIComponent(text) + iconMap[key] = encodeURIComponent(text) } + const iconsContent = `export const ${camelize( + collection.name, + )} = ${JSON.stringify(iconMap, null, 2)}` + const iconsDist = path.resolve( + import.meta.dirname, + `../src/ui/icons/${collection.name}`, + ) + await ensureDir(iconsDist) + await Bun.write(`${iconsDist}/index.ts`, `${iconsContent}\n`) + + const proxyPackageContent = JSON.stringify( + { + type: 'module', + types: `../../_lib/ui/icons/${collection.name}/index.d.ts`, + module: `../../_lib/ui/icons/${collection.name}/index.js`, + }, + null, + 2, + ) + await Bun.write(`${iconsDist}/package.json`, proxyPackageContent) + + iconCollectionExports.push( + `export { ${camelize(collection.name)} } from './${ + collection.name + }/index.js'`, + ) + console.log(collection.name) count += 1 } -const iconsExport = `export const icons = ${JSON.stringify(iconMap, null, 2)}` -const dist = path.resolve(import.meta.dirname, '../src/ui') -await Bun.write(`${dist}/icons.ts`, `${iconsExport}\n`) +const dist = path.resolve(import.meta.dirname, '../src/ui/icons') +await Bun.write(`${dist}/index.ts`, `${iconCollectionExports.join('\n')}\n`) console.log( `Done. Copied ${count} ${count === 1 ? 'collection' : 'collections'}.`, diff --git a/playground/src/ui-system.tsx b/playground/src/ui-system.tsx index 2786dbe6..c6b75aa5 100644 --- a/playground/src/ui-system.tsx +++ b/playground/src/ui-system.tsx @@ -1,5 +1,6 @@ import { Button, Frog } from 'frog' import { serveStatic } from 'frog/serve-static' +import { heroicons, lucide, radixIcons } from 'frog/ui/icons' import { Box, @@ -186,19 +187,19 @@ export const app = new Frog({ diff --git a/site/pages/ui/Icon.mdx b/site/pages/ui/Icon.mdx index ddb8a78a..be4fbc69 100644 --- a/site/pages/ui/Icon.mdx +++ b/site/pages/ui/Icon.mdx @@ -116,12 +116,12 @@ function Example() { ### `collection` -Icon collection to use for resolving icons. Defaults to `'lucide'`. +Icon collection to use for resolving icons. Defaults to `lucide` imported from `'frog/ui/icons'`. The following collections are available: -- [Heroicons](https://heroicons.com) -- [Lucide](https://lucide.dev) -- [Radix Icons](https://www.radix-ui.com/icons) +- [Heroicons - `frog/ui/icons/heroicons`](https://heroicons.com) +- [Lucide - `frog/ui/icons/lucide`](https://lucide.dev) +- [Radix Icons - `frog/ui/icons/radix-icons`](https://www.radix-ui.com/icons) Collection is mapped to the [`icons` property](/ui/ui-system#icons) on the UI System Variables. @@ -129,6 +129,7 @@ Collection is mapped to the [`icons` property](/ui/ui-system#icons) on the UI Sy ```tsx twoslash [Code] /** @jsxImportSource frog/jsx */ import { createSystem } from 'frog/ui' +import { heroicons } from 'frog/ui/icons' // [!code focus] const { Icon } = createSystem() @@ -137,7 +138,7 @@ function Example() { // ---cut--- diff --git a/site/pages/ui/createSystem.mdx b/site/pages/ui/createSystem.mdx index b992f874..46bd3085 100644 --- a/site/pages/ui/createSystem.mdx +++ b/site/pages/ui/createSystem.mdx @@ -159,8 +159,8 @@ export const system = createSystem({ ### icons -- **Type:** `'heroicons' | 'lucide' | 'radix-icons'` -- **Default:** `'lucide'` +- **Type:** `Record` +- **Default:** lucide Icon collection to use for resolving icons. The following collections are available: @@ -170,9 +170,10 @@ Icon collection to use for resolving icons. The following collections are availa ```tsx twoslash import { createSystem } from 'frog/ui' +import { lucide } from 'frog/ui/icons' // [!code focus] // ---cut--- export const system = createSystem({ - icons: 'lucide', // [!code focus] + icons: lucide, // [!code focus] }) ``` diff --git a/site/pages/ui/ui-system.mdx b/site/pages/ui/ui-system.mdx index a0dd0f5f..6a7eb7f5 100644 --- a/site/pages/ui/ui-system.mdx +++ b/site/pages/ui/ui-system.mdx @@ -270,9 +270,10 @@ The `icons` variable is used to set the icon collection for the [`` compon /** @jsxImportSource frog/jsx */ // ---cut--- import { createSystem } from 'frog/ui' +import { lucide } from 'frog/ui/icons' const { Icon } = createSystem({ - icons: 'lucide', + icons: lucide, }) function Example() { diff --git a/src/package.json b/src/package.json index 4f22dde1..9bed1033 100644 --- a/src/package.json +++ b/src/package.json @@ -68,6 +68,22 @@ "types": "./_lib/ui/index.d.ts", "default": "./_lib/ui/index.js" }, + "./ui/icons": { + "types": "./_lib/ui/icons/index.d.ts", + "default": "./_lib/ui/icons/index.js" + }, + "./ui/icons/heroicons": { + "types": "./_lib/ui/icons/heroicons/index.d.ts", + "default": "./_lib/ui/icons/heroicons/index.js" + }, + "./ui/icons/lucide": { + "types": "./_lib/ui/icons/lucide/index.d.ts", + "default": "./_lib/ui/icons/lucide/index.js" + }, + "./ui/icons/radix-icons": { + "types": "./_lib/ui/icons/radix-icons/index.d.ts", + "default": "./_lib/ui/icons/radix-icons/index.js" + }, "./vercel": { "types": "./_lib/vercel/index.d.ts", "default": "./_lib/vercel/index.js" @@ -105,10 +121,7 @@ "license": "MIT", "homepage": "https://frog.fm", "repository": "wevm/frog", - "authors": [ - "awkweb.eth", - "jxom.eth" - ], + "authors": ["awkweb.eth", "jxom.eth"], "funding": [ { "type": "github", diff --git a/src/ui/Icon.test-d.tsx b/src/ui/Icon.test-d.tsx new file mode 100644 index 00000000..45487c81 --- /dev/null +++ b/src/ui/Icon.test-d.tsx @@ -0,0 +1,31 @@ +import { expectTypeOf, test } from 'vitest' + +import { createSystem } from './createSystem.js' +import { heroicons, lucide } from './icons/index.js' + +test('defaults', () => { + const { Icon } = createSystem() + type IconProps = Parameters[0] + expectTypeOf().toEqualTypeOf< + Record | undefined + >() + expectTypeOf().toEqualTypeOf() +}) + +test('custom system collection', () => { + const { Icon } = createSystem({ icons: heroicons }) + type IconProps = Parameters[0] + expectTypeOf().toEqualTypeOf< + Record | undefined + >() + expectTypeOf().toEqualTypeOf() +}) + +test('custom system collection', () => { + const { Icon, vars } = createSystem() + type IconProps = Parameters>[0] + expectTypeOf().toEqualTypeOf< + typeof heroicons | Record | undefined + >() + expectTypeOf().toEqualTypeOf() +}) diff --git a/src/ui/Icon.tsx b/src/ui/Icon.tsx index 5cc2adc8..499caadf 100644 --- a/src/ui/Icon.tsx +++ b/src/ui/Icon.tsx @@ -1,10 +1,10 @@ import { Box, type BoxProps, resolveColorToken } from './Box.js' -import { icons } from './icons.js' +import { lucide } from './icons/lucide/index.js' import { type DefaultVars, type Vars, defaultVars } from './vars.js' export type IconProps< vars extends Vars = DefaultVars, - collection extends Vars['icons'] = DefaultVars['icons'], + collection extends Vars['icons'] = vars['icons'], > = { __context?: { vars?: Vars | undefined } | undefined /** @@ -22,13 +22,13 @@ export type IconProps< /** * Icon collection to use for resolving icons. * - * @default 'lucide' + * @default lucide (from 'frog/ui/icons') */ collection?: collection | Vars['icons'] | undefined /** Icon name in the current icon collection. */ - name: keyof (typeof icons)[collection extends keyof typeof icons - ? collection - : never] + name: Record | undefined extends collection + ? keyof vars['icons'] + : keyof collection /** Sets the size of the icon. */ size?: BoxProps['width'] } @@ -39,14 +39,13 @@ export function Icon< >(props: IconProps) { const { __context, - collection = __context?.vars?.icons ?? 'lucide', + collection = __context?.vars?.icons ?? lucide, mode = 'auto', name, size = '24', } = props - const iconMap = icons[collection] - let text: string = iconMap[name as keyof typeof iconMap] + let text: string = collection[name as keyof typeof collection] if (!text) throw new TypeError(`Invalid set: ${collection}`) const resolvedMode = (() => { diff --git a/src/ui/createSystem.test-d.ts b/src/ui/createSystem.test-d.ts new file mode 100644 index 00000000..4b3913b2 --- /dev/null +++ b/src/ui/createSystem.test-d.ts @@ -0,0 +1,14 @@ +import { expectTypeOf, test } from 'vitest' + +import { createSystem } from './createSystem.js' +import { heroicons, lucide } from './icons/index.js' + +test('defaults', () => { + const { vars } = createSystem() + expectTypeOf(vars.icons).toEqualTypeOf() +}) + +test('custom', () => { + const { vars } = createSystem({ icons: heroicons }) + expectTypeOf(vars.icons).toEqualTypeOf() +}) diff --git a/src/ui/createSystem.tsx b/src/ui/createSystem.tsx index 9cc086a2..5d89956a 100644 --- a/src/ui/createSystem.tsx +++ b/src/ui/createSystem.tsx @@ -1,4 +1,4 @@ -import type { Assign } from '../types/utils.js' +import type { Assign, Pretty } from '../types/utils.js' import { Box } from './Box.js' import { Column, Columns } from './Columns.js' import { Divider } from './Divider.js' @@ -32,15 +32,22 @@ import { type DefaultVars, type Vars, defaultVars } from './vars.js' * }) * ``` */ -export function createSystem( +export function createSystem( vars?: vars | undefined, ) { + type Icons = unknown extends vars['icons'] + ? DefaultVars['icons'] + : vars['icons'] + type MergedVars = Pretty< + Omit, 'icons'> & { + icons: Icons + } + > + const mergedVars = { ...defaultVars, ...vars, - } - - type MergedVars = Assign + } as MergedVars function createComponent< const component extends (...args: any[]) => JSX.Element, @@ -149,7 +156,7 @@ export function createSystem( */ Icon: < vars extends MergedVars, - collection extends Vars['icons'] = DefaultVars['icons'], + collection extends Vars['icons'] = MergedVars['icons'], >( props: IconProps, ) => , diff --git a/src/ui/icons/package.json b/src/ui/icons/package.json new file mode 100644 index 00000000..5d503cd4 --- /dev/null +++ b/src/ui/icons/package.json @@ -0,0 +1,5 @@ +{ + "type": "module", + "types": "../../_lib/ui/icons/index.d.ts", + "module": "../../_lib/ui/icons/index.js" +} diff --git a/src/ui/vars.ts b/src/ui/vars.ts index 6d3087d4..bf8cff06 100644 --- a/src/ui/vars.ts +++ b/src/ui/vars.ts @@ -1,5 +1,5 @@ import type { Font } from '../types/frame.js' -import type { icons } from './icons.js' +import { lucide } from './icons/lucide/index.js' export type Vars = { colors?: Record | undefined @@ -15,7 +15,7 @@ export type Vars = { width: number } | undefined - icons?: keyof typeof icons | undefined + icons?: Record | undefined units?: Record | undefined } @@ -282,7 +282,7 @@ export const defaultVars = { height: 630, width: 1200, }, - icons: 'lucide', + icons: lucide, units, } as const satisfies Vars export type DefaultVars = typeof defaultVars