a homebrewed DnD campaign based in the Honkai: Star Rail universe
hsr honkaistarrail dnd

refactor: move ElementIcon + MechanicIcon to `@starlight/icons` (#93)

authored by samanthanguyen.me and committed by

GitHub a1ce5ec4 22a88250

+156 -145
-50
app/src/lib/hsr/elements.ts
··· 1 - import FlameIcon from '@lucide/svelte/icons/flame' 2 - import SnowflakeIcon from '@lucide/svelte/icons/snowflake' 3 - import SwordsIcon from '@lucide/svelte/icons/swords' 4 - import WindIcon from '@lucide/svelte/icons/wind' 5 - import ZapIcon from '@lucide/svelte/icons/zap' 6 - import UniverseIcon from '@starlight/icons/universe' 7 - import WhirlIcon from '@starlight/icons/whirl' 8 - import type { Element } from '@starlight/types/hsr' 9 - import type { Component } from 'svelte' 10 - 11 - export type ElementColor = 'red' | 'white' | 'yellow' | 'green' | 'cyan' | 'indigo' | 'purple' 12 - 13 - type ElementRecord = { 14 - color: ElementColor 15 - icon: Component 16 - } 17 - 18 - const elementMap: Record<Element, ElementRecord> = { 19 - physical: { 20 - color: 'white', 21 - icon: SwordsIcon, 22 - }, 23 - fire: { 24 - color: 'red', 25 - icon: FlameIcon, 26 - }, 27 - imaginary: { 28 - color: 'yellow', 29 - icon: UniverseIcon, 30 - }, 31 - wind: { 32 - color: 'green', 33 - icon: WindIcon, 34 - }, 35 - ice: { 36 - color: 'cyan', 37 - icon: SnowflakeIcon, 38 - }, 39 - quantum: { 40 - color: 'indigo', 41 - icon: WhirlIcon, 42 - }, 43 - lightning: { 44 - color: 'purple', 45 - icon: ZapIcon, 46 - }, 47 - } 48 - 49 - export const elementColor = (element: Element): ElementColor => elementMap[element].color 50 - export const elementIcon = (element: Element): Component => elementMap[element].icon
-51
app/src/lib/hsr/mechanic.ts
··· 1 - import type { Component } from 'svelte' 2 - import type { Mechanic } from '@starlight/types/hsr' 3 - 4 - // offense mechanic icons 5 - import Crosshair from '@lucide/svelte/icons/crosshair' 6 - import CircleDotDashed from '@lucide/svelte/icons/circle-dot-dashed' 7 - import Split from '@lucide/svelte/icons/split' 8 - import Bomb from '@lucide/svelte/icons/bomb' 9 - import ChevronsDown from '@lucide/svelte/icons/chevrons-down' 10 - 11 - // defense mechanic icons 12 - import Sparkles from '@lucide/svelte/icons/sparkles' 13 - import HeartPlus from '@lucide/svelte/icons/heart-plus' 14 - import ShieldHalf from '@lucide/svelte/icons/shield-half' 15 - import HandHelping from '@lucide/svelte/icons/hand-helping' 16 - 17 - type MechanicRecord = { 18 - icon: Component 19 - } 20 - 21 - const mechanicMap: Record<Mechanic, MechanicRecord> = { 22 - 'single-target': { 23 - icon: Crosshair, 24 - }, 25 - aoe: { 26 - icon: CircleDotDashed, 27 - }, 28 - bounce: { 29 - icon: Split, 30 - }, 31 - blast: { 32 - icon: Bomb, 33 - }, 34 - impair: { 35 - icon: ChevronsDown, 36 - }, 37 - support: { 38 - icon: HandHelping, 39 - }, 40 - restore: { 41 - icon: HeartPlus, 42 - }, 43 - enhance: { 44 - icon: Sparkles, 45 - }, 46 - defense: { 47 - icon: ShieldHalf, 48 - }, 49 - } 50 - 51 - export const mechanicIcon = (mechanic: Mechanic): Component => mechanicMap[mechanic].icon
+2 -1
app/src/lib/patterns/AbilityCard/AbilityCard.svelte
··· 1 1 <script lang="ts"> 2 + import ElementIcon from '@starlight/icons/element' 2 3 import type { DiceRoll, AbilityRoll, SpellComponentLetter } from '@starlight/types/dnd' 3 4 import { getSpellComponentName } from '@starlight/types/dnd' 4 5 import type { Element, Mechanic } from '@starlight/types/hsr' 5 6 import { CombatText } from '$patterns/CombatText' 6 - import { ElementIcon, ElementText } from '$patterns/Element' 7 + import { ElementText } from '$patterns/Element' 7 8 import { MechanicChip } from '$patterns/Mechanic' 8 9 import { Card } from '$ui/Card' 9 10 import { DescList, DescListItem } from '$ui/DescList'
+2 -3
app/src/lib/patterns/Element/ElementChip.svelte
··· 1 1 <script lang="ts"> 2 - import type { Element } from '@starlight/types/hsr' 3 - import { elementColor } from '$lib/hsr/elements' 2 + import { type Element, getElementColor } from '@starlight/types/hsr' 4 3 import { Chip, type ChipProps } from '$ui/Chip' 5 4 import ElementText from './ElementText.svelte' 6 5 ··· 19 18 <Chip 20 19 {...other} 21 20 withIcon 22 - color={elementColor(element)} 21 + color={getElementColor(element)} 23 22 style={style} 24 23 > 25 24 <ElementText element={element} dark={style === 'fill'} />
+1 -1
app/src/lib/patterns/Element/ElementIcon.stories.svelte packages/icons/src/lib/ElementIcon.stories.svelte
··· 1 1 <script module> 2 2 import { defineMeta } from '@storybook/addon-svelte-csf' 3 3 import { typeAs } from '@starlight/storybook-utils' 4 - import { ElementIcon } from '$patterns/Element' 4 + import ElementIcon from './ElementIcon.svelte' 5 5 6 6 const { Story } = defineMeta({ 7 7 component: ElementIcon,
+15 -15
app/src/lib/patterns/Element/ElementIcon.svelte packages/icons/src/lib/ElementIcon.svelte
··· 2 2 import type { IconProps } from '@lucide/svelte' 3 3 import type { Element } from '@starlight/types/hsr' 4 4 import { tv } from 'tailwind-variants' 5 - import { elementIcon } from '$lib/hsr/elements' 6 - import { cn } from '$lib/types' 5 + import { getElementIcon } from './ElementIcon.js' 6 + import { cn } from './cn.js' 7 7 8 - type ElementIconRootProps = IconProps 9 - type ElementIconProps = ElementIconRootProps & { 10 - element: Element, 11 - dark?: boolean, 12 - } 13 - let { 14 - element, 15 - dark = false, 16 - class: className, 17 - ...other 18 - }: ElementIconProps = $props() 19 - 20 - let Icon = $derived(elementIcon(element)) 21 8 const elementIconTv = tv({ 22 9 variants: { 23 10 element: { ··· 69 56 } 70 57 ], 71 58 }) 59 + 60 + type ElementIconRootProps = IconProps 61 + type ElementIconProps = ElementIconRootProps & { 62 + element: Element, 63 + dark?: boolean, 64 + } 65 + let { 66 + element, 67 + dark = false, 68 + class: className, 69 + ...other 70 + }: ElementIconProps = $props() 71 + let Icon = $derived(getElementIcon(element)) 72 72 </script> 73 73 74 74 <Icon
+2 -3
app/src/lib/patterns/Element/ElementText.svelte
··· 1 1 <script lang="ts"> 2 - import { getElementName, type Element } from '@starlight/types/hsr' 2 + import { getElementName, getElementColor, type Element } from '@starlight/types/hsr' 3 3 import type { SvelteHTMLElements } from 'svelte/elements' 4 - import { elementColor } from '$lib/hsr/elements' 5 4 import Text from '$ui/Text/Text.svelte' 6 5 import type { TextProps } from '$ui/Text/Text' 7 6 ··· 26 25 }) 27 26 </script> 28 27 29 - <Text {...props} color={!dark ? elementColor(element) : undefined}> 28 + <Text {...props} color={!dark ? getElementColor(element) : undefined}> 30 29 {renderedText} 31 30 </Text>
-1
app/src/lib/patterns/Element/index.ts
··· 1 1 export { default as ElementChip } from './ElementChip.svelte' 2 - export { default as ElementIcon } from './ElementIcon.svelte' 3 2 export { default as ElementText } from './ElementText.svelte'
+1 -1
app/src/lib/patterns/Mechanic/MechanicChip.svelte
··· 1 1 <script lang="ts"> 2 + import MechanicIcon from '@starlight/icons/mechanic' 2 3 import { getMechanicName, type Mechanic } from '@starlight/types/hsr' 3 4 import { Chip, type ChipProps } from '$ui/Chip' 4 - import MechanicIcon from './MechanicIcon.svelte' 5 5 6 6 type Style = 'fill' | 'outline' 7 7 type Size = 'md' | 'sm'
+1 -1
app/src/lib/patterns/Mechanic/MechanicIcon.stories.svelte packages/icons/src/lib/MechanicIcon.stories.svelte
··· 1 1 <script module> 2 2 import { typeAs } from '@starlight/storybook-utils' 3 3 import { defineMeta } from '@storybook/addon-svelte-csf' 4 - import { MechanicIcon } from '$patterns/Mechanic' 4 + import MechanicIcon from './MechanicIcon.svelte' 5 5 6 6 const { Story } = defineMeta({ 7 7 component: MechanicIcon,
+16 -6
app/src/lib/patterns/Mechanic/MechanicIcon.svelte packages/icons/src/lib/MechanicIcon.svelte
··· 2 2 import type { IconProps } from '@lucide/svelte' 3 3 import type { Mechanic } from '@starlight/types/hsr' 4 4 import { tv, type VariantProps } from 'tailwind-variants' 5 - import { mechanicIcon } from '$lib/hsr/mechanic' 5 + import { getMechanicIcon } from './MechanicIcon.js' 6 + import { cn } from './cn.js' 6 7 7 - const styles = tv({ 8 + const mechanicIconTv = tv({ 8 9 base: 'stroke-2', 9 10 variants: { 10 11 color: { ··· 18 19 }, 19 20 }) 20 21 21 - type MechanicIconVariants = VariantProps<typeof styles> 22 + type MechanicIconVariants = VariantProps<typeof mechanicIconTv> 22 23 type VariantColor = MechanicIconVariants['color'] 23 24 type VariantSize = MechanicIconVariants['size'] 24 25 ··· 29 30 color: VariantColor, 30 31 } 31 32 32 - let { mechanic, size = 'md', color }: MechanicIconProps = $props() 33 - let Icon = $derived(mechanicIcon(mechanic)) 33 + let { 34 + mechanic, 35 + size = 'md', 36 + color, 37 + class: className, 38 + ...other 39 + }: MechanicIconProps = $props() 40 + let Icon = $derived(getMechanicIcon(mechanic)) 34 41 </script> 35 42 36 - <Icon class={styles({color, size})} /> 43 + <Icon 44 + {...other} 45 + class={cn(mechanicIconTv({color, size}), className)} 46 + />
-1
app/src/lib/patterns/Mechanic/index.ts
··· 1 1 export { default as MechanicChip } from './MechanicChip.svelte' 2 - export { default as MechanicIcon } from './MechanicIcon.svelte'
+9 -1
package-lock.json
··· 4190 4190 "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", 4191 4191 "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", 4192 4192 "license": "MIT", 4193 + "peer": true, 4193 4194 "engines": { 4194 4195 "node": ">=6" 4195 4196 } ··· 7680 7681 "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", 7681 7682 "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", 7682 7683 "license": "MIT", 7684 + "peer": true, 7683 7685 "engines": { 7684 7686 "node": ">=16.x", 7685 7687 "pnpm": ">=7.x" ··· 9188 9190 "packages/icons": { 9189 9191 "name": "@starlight/icons", 9190 9192 "version": "0.0.1", 9193 + "dependencies": { 9194 + "@starlight/types": "file:../types" 9195 + }, 9191 9196 "devDependencies": { 9192 9197 "@sveltejs/adapter-auto": "^7.0.0", 9193 9198 "@sveltejs/kit": "^2.49.2", ··· 9198 9203 }, 9199 9204 "peerDependencies": { 9200 9205 "@lucide/svelte": "latest", 9201 - "svelte": "^5.0.0" 9206 + "clsx": "^2.1.1", 9207 + "svelte": "^5.0.0", 9208 + "tailwind-merge": "latest", 9209 + "tailwind-variants": "latest" 9202 9210 } 9203 9211 }, 9204 9212 "packages/storybook-utils": {
+16 -2
packages/icons/package.json
··· 18 18 "types": "./dist/index.d.ts", 19 19 "svelte": "./dist/index.js" 20 20 }, 21 + "./element": { 22 + "types": "./dist/ElementIcon.svelte.d.ts", 23 + "svelte": "./dist/ElementIcon.svelte" 24 + }, 25 + "./mechanic": { 26 + "types": "./dist/MechanicIcon.svelte.d.ts", 27 + "svelte": "./dist/MechanicIcon.svelte" 28 + }, 21 29 "./universe": { 22 30 "types": "./dist/UniverseIcon.svelte.d.ts", 23 31 "svelte": "./dist/UniverseIcon.svelte" ··· 27 35 "svelte": "./dist/WhirlIcon.svelte" 28 36 } 29 37 }, 38 + "dependencies": { 39 + "@starlight/types": "file:../types" 40 + }, 30 41 "peerDependencies": { 42 + "@lucide/svelte": "latest", 43 + "clsx": "^2.1.1", 31 44 "svelte": "^5.0.0", 32 - "@lucide/svelte": "latest" 45 + "tailwind-merge": "latest", 46 + "tailwind-variants": "latest" 33 47 }, 34 48 "devDependencies": { 35 49 "@sveltejs/adapter-auto": "^7.0.0", ··· 47 61 "fix": "oxlint --fix", 48 62 "preview": "vite preview", 49 63 "prepare": "svelte-kit sync || echo ''", 50 - "prepack": "svelte-kit sync && svelte-package && publint", 64 + "prepack": "svelte-kit sync && svelte-package", 51 65 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 52 66 "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 53 67 "storybook": "storybook dev -p 6006",
+23
packages/icons/src/lib/ElementIcon.ts
··· 1 + import FlameIcon from '@lucide/svelte/icons/flame' 2 + import SnowflakeIcon from '@lucide/svelte/icons/snowflake' 3 + import SwordsIcon from '@lucide/svelte/icons/swords' 4 + import WindIcon from '@lucide/svelte/icons/wind' 5 + import ZapIcon from '@lucide/svelte/icons/zap' 6 + import type { Element } from '@starlight/types/hsr' 7 + import type { Component } from 'svelte' 8 + import UniverseIcon from './UniverseIcon.svelte' 9 + import WhirlIcon from './WhirlIcon.svelte' 10 + 11 + const elementMap: Record<Element, Component> = { 12 + physical: SwordsIcon, 13 + fire: FlameIcon, 14 + imaginary: UniverseIcon, 15 + wind: WindIcon, 16 + ice: SnowflakeIcon, 17 + quantum: WhirlIcon, 18 + lightning: ZapIcon, 19 + } 20 + 21 + export function getElementIcon(element: Element): Component { 22 + return elementMap[element] 23 + }
+27
packages/icons/src/lib/MechanicIcon.ts
··· 1 + import BombIcon from '@lucide/svelte/icons/bomb' 2 + import ChevronsDownIcon from '@lucide/svelte/icons/chevrons-down' 3 + import CircleDotDashedIcon from '@lucide/svelte/icons/circle-dot-dashed' 4 + import CrosshairIcon from '@lucide/svelte/icons/crosshair' 5 + import HandHelpingIcon from '@lucide/svelte/icons/hand-helping' 6 + import HeartPlusIcon from '@lucide/svelte/icons/heart-plus' 7 + import ShieldHalfIcon from '@lucide/svelte/icons/shield-half' 8 + import SparklesIcon from '@lucide/svelte/icons/sparkles' 9 + import SplitIcon from '@lucide/svelte/icons/split' 10 + import type { Mechanic } from '@starlight/types/hsr' 11 + import type { Component } from 'svelte' 12 + 13 + const mechanicMap: Record<Mechanic, Component> = { 14 + 'single-target': CrosshairIcon, 15 + aoe: CircleDotDashedIcon, 16 + bounce: SplitIcon, 17 + blast: BombIcon, 18 + impair: ChevronsDownIcon, 19 + support: HandHelpingIcon, 20 + restore: HeartPlusIcon, 21 + enhance: SparklesIcon, 22 + defense: ShieldHalfIcon, 23 + } 24 + 25 + export function getMechanicIcon(mechanic: Mechanic): Component { 26 + return mechanicMap[mechanic] 27 + }
+6
packages/icons/src/lib/cn.ts
··· 1 + import { clsx, type ClassValue } from 'clsx' 2 + import { twMerge } from 'tailwind-merge' 3 + 4 + export function cn(...inputs: ClassValue[]): string { 5 + return twMerge(clsx(inputs)) 6 + }
+2
packages/icons/src/lib/index.ts
··· 1 + export { default as ElementIcon } from './ElementIcon.svelte' 2 + export { default as MechanicIcon } from './MechanicIcon.svelte' 1 3 export { default as UniverseIcon } from './UniverseIcon.svelte' 2 4 export { default as WhirlIcon } from './WhirlIcon.svelte'
+12 -7
packages/types/src/hsr/element.ts
··· 17 17 // lookup 18 18 type LookupRecordKey = { 19 19 name: string 20 + color: ElementColor 20 21 } 22 + export type ElementColor = 'red' | 'white' | 'yellow' | 'green' | 'cyan' | 'indigo' | 'purple' 21 23 const ElementLookupMap: Record<Element, LookupRecordKey> = { 22 - fire: { name: 'Fire' }, 23 - ice: { name: 'Ice' }, 24 - imaginary: { name: 'Imaginary' }, 25 - lightning: { name: 'Lightning' }, 26 - physical: { name: 'Physical' }, 27 - quantum: { name: 'Quantum' }, 28 - wind: { name: 'Wind' }, 24 + fire: { name: 'Fire', color: 'red' }, 25 + ice: { name: 'Ice', color: 'cyan' }, 26 + imaginary: { name: 'Imaginary', color: 'yellow' }, 27 + lightning: { name: 'Lightning', color: 'purple' }, 28 + physical: { name: 'Physical', color: 'white' }, 29 + quantum: { name: 'Quantum', color: 'indigo' }, 30 + wind: { name: 'Wind', color: 'green' }, 29 31 } 30 32 export function getElementName(e: Element): string { 31 33 return ElementLookupMap[e].name 32 34 } 35 + export function getElementColor(e: Element): ElementColor { 36 + return ElementLookupMap[e].color 37 + }
+21 -1
packages/types/tests/hsr/element.test.ts
··· 1 1 import { describe, expect, test } from 'vitest' 2 - import { Element, ElementSchema, getElementName } from '../../src/hsr/element' 2 + import { 3 + Element, 4 + ElementColor, 5 + ElementSchema, 6 + getElementName, 7 + getElementColor, 8 + } from '../../src/hsr/element' 3 9 4 10 describe('ElementSchema', () => { 5 11 describe('safeParse()', () => { ··· 32 38 ['wind', 'Wind'], 33 39 ] as [Element, string][])("is ok: '%s'", ([element, expectedName]) => { 34 40 expect(getElementName(element)).toEqual(expectedName) 41 + }) 42 + }) 43 + 44 + describe('getElementColor()', () => { 45 + test.for([ 46 + ['fire', 'red'], 47 + ['ice', 'cyan'], 48 + ['imaginary', 'yellow'], 49 + ['lightning', 'purple'], 50 + ['physical', 'white'], 51 + ['quantum', 'indigo'], 52 + ['wind', 'green'], 53 + ] as [Element, ElementColor][])("is ok: '%s'", ([element, expectedName]) => { 54 + expect(getElementColor(element)).toEqual(expectedName) 35 55 }) 36 56 }) 37 57 })