feat: add new components and improve styling utilities

- Add chip, mark, and iconButton recipes with semantic color support
- Introduce skeleton pattern and shimmer gradient for loading states
- Enhance button and details recipes with refined transition properties
- Update border radius tokens to include 'full' and negative spacing values
- Refactor details recipe export and semantic color key handling
- Bump package version to 0.0.6
This commit is contained in:
2025-09-24 15:20:56 -06:00
parent 2f2b01fcde
commit d21d55f481
12 changed files with 554 additions and 69 deletions

View File

@@ -7,7 +7,7 @@
"README.md"
],
"main": "dist/index.js",
"version": "0.0.5",
"version": "0.0.6",
"scripts": {
"prepare": "ts-patch install -s && bun .husky/install.ts",
"prepublishOnly": "bun run build",

View File

@@ -2,10 +2,16 @@ import { definePreset } from '@pandacss/dev'
import generateColors from '@/colors/generateColors.js'
import generateNeutralColor from '@/colors/generateNeutralColor.js'
import buttonRecipe from '@/recipes/button.js'
import { detailsRecipe } from '@/recipes/details.js'
import detailsRecipe from '@/recipes/details.js'
import inputRecipe from '@/recipes/input.js'
import { type BrandColor, type Color, type ColorVariation, color, type NeutralColor } from '@/types.js'
import generateSemanticColors from './colors/generateSemanticColors.js'
import pulseKeyframes from './keyframes/pulse.js'
import shimmerKeyframes from './keyframes/shimmer.js'
import skeletonPattern from './patterns/skeleton.js'
import chipRecipe from './recipes/chip.js'
import iconButtonRecipe from './recipes/iconButton.js'
import markRecipe from './recipes/mark.js'
export type ThemeConfig = {
neutral?: NeutralColor
@@ -26,13 +32,20 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => {
const colors = generateColors(config?.includeColors)
const neutral = generateNeutralColor(mergedConfig.neutral, mergedConfig.colorVariation)
const semanticColors = generateSemanticColors(mergedConfig.semanticColors, mergedConfig.colorVariation)
const semanticColorKeysArray = Object.keys(semanticColors)
return definePreset({
name: 'srjuggernaut-panda-preset',
patterns: {
skeleton: skeletonPattern
},
theme: {
recipes: {
button: buttonRecipe({ semanticColors: Object.keys(semanticColors) }),
details: detailsRecipe({ semanticColors: Object.keys(semanticColors) }),
input: inputRecipe
button: buttonRecipe({ semanticColorNames: semanticColorKeysArray }),
chip: chipRecipe({ semanticColorNames: semanticColorKeysArray }),
details: detailsRecipe({ semanticColorNames: semanticColorKeysArray }),
mark: markRecipe({ semanticColorNames: semanticColorKeysArray }),
input: inputRecipe({ semanticColorNames: semanticColorKeysArray }),
iconButton: iconButtonRecipe({ semanticColorNames: semanticColorKeysArray })
},
tokens: {
animations: {},
@@ -102,15 +115,16 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => {
black: { value: '900' }
},
gradients: {
skeleton: {
shimmer: {
value: {
type: 'linear',
placement: 'to right',
stops: [
'color-mix(in srgb, {colors.neutral.12} 0%, transparent 100%) 0%',
'color-mix(in srgb, {colors.neutral.12} 10%, transparent 100%) 20%',
'color-mix(in srgb, {colors.neutral.12} 30%, transparent 100%) 30%',
'color-mix(in srgb, {colors.neutral.12} 0%, transparent 100%) 40%'
'color-mix(in srgb, {colors.neutral.12} 0%, transparent) 0%',
'color-mix(in srgb, {colors.neutral.12} 0%, transparent) 15%',
'color-mix(in srgb, {colors.neutral.12} 10%, transparent) 20%',
'color-mix(in srgb, {colors.neutral.12} 20%, transparent) 25%',
'color-mix(in srgb, {colors.neutral.12} 0%, transparent) 30%'
]
}
}
@@ -131,7 +145,8 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => {
md: { value: '0.375rem' },
lg: { value: '0.5rem' },
xl: { value: '0.75rem' },
xxl: { value: '1rem' }
xxl: { value: '1rem' },
full: { value: '9999px' }
},
shadows: {},
opacity: {},
@@ -144,19 +159,21 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => {
lg: { value: '1.5rem' },
xl: { value: '2rem' },
xxl: { value: '3rem' },
xxxl: { value: '4rem' }
xxxl: { value: '4rem' },
'-xs': { value: '-0.25rem' },
'-sm': { value: '-0.5rem' },
'-md': { value: '-1rem' },
'-lg': { value: '-1.5rem' },
'-xl': { value: '-2rem' },
'-xxl': { value: '-3rem' },
'-xxxl': { value: '-4rem' }
},
zIndex: {}
},
animationStyles: {},
keyframes: {
shimmerRight: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(150%)' }
},
shimmerLeft: {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(-150%)' }
}
...shimmerKeyframes,
...pulseKeyframes
},
semanticTokens: {
colors: {
@@ -169,6 +186,10 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => {
}
export default srJuggernautPandaPreset
export { buttonVariants } from '@/recipes/button.js'
export { chipVariants } from '@/recipes/chip.js'
export { iconButtonShapes, iconButtonVariants } from '@/recipes/iconButton.js'
export { markVariants } from '@/recipes/mark.js'
export { BrandColor, ColorVariation, NeutralColor } from '@/types.js'

11
src/keyframes/pulse.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineKeyframes } from '@pandacss/dev'
const pulseKeyframes = defineKeyframes({
pulse: {
'0%': { opacity: '0.8' },
'50%': { opacity: '0.2' },
'100%': { opacity: '0.8' }
}
})
export default pulseKeyframes

10
src/keyframes/shimmer.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineKeyframes } from '@pandacss/dev'
const shimmerKeyframes = defineKeyframes({
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(150%)' }
}
})
export default shimmerKeyframes

43
src/patterns/skeleton.ts Normal file
View File

@@ -0,0 +1,43 @@
import { definePattern } from '@pandacss/dev'
import type { SystemStyleObject } from '@pandacss/types'
const skeletonPattern = definePattern({
properties: {
variant: { type: 'enum', value: ['shimmerLeft', 'shimmerRight', 'pulse'] },
duration: { type: 'number' }
} as const,
defaultValues: {
variant: 'pulse',
duration: 2
},
transform(props) {
const { variant, ...allOther } = props as SystemStyleObject & {
variant: 'shimmerLeft' | 'shimmerRight' | 'pulse'
duration: number
}
return {
...allOther,
position: 'relative',
overflow: 'hidden',
_before: {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
background: variant === 'pulse' ? '{colors.neutral.3}' : undefined,
backgroundImage: variant === 'shimmerRight' || variant === 'shimmerLeft' ? '{gradients.shimmer}' : undefined,
animationDirection: variant === 'shimmerLeft' ? 'reverse' : 'normal',
animationName: variant === 'pulse' ? 'pulse' : 'shimmer',
animationDuration: `${props.duration}s`,
animationIterationCount: 'infinite',
animationTimingFunction: 'easeInOut'
}
} as SystemStyleObject
}
})
export default skeletonPattern

View File

@@ -3,11 +3,11 @@ import { defineRecipe } from '@pandacss/dev'
export const buttonVariants = ['solid', 'outline', 'ghost'] as const
interface ButtonRecipeArg {
semanticColors: string[]
semanticColorNames: string[]
}
const buttonRecipe = ({ semanticColors }: ButtonRecipeArg) => {
const colorVariants = ['neutral', ...semanticColors]
const buttonRecipe = ({ semanticColorNames }: ButtonRecipeArg) => {
const colorVariants = ['neutral', ...semanticColorNames]
return defineRecipe({
className: 'button',
@@ -15,13 +15,15 @@ const buttonRecipe = ({ semanticColors }: ButtonRecipeArg) => {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5em',
gap: 'xs',
width: 'fit-content',
borderRadius: '4px',
paddingBlock: '0.25em',
paddingInline: '0.5em',
borderRadius: 'sm',
paddingBlock: 'xs',
paddingInline: 'sm',
cursor: 'pointer',
transition: 'all 200ms ease-in-out'
transition: 'all',
transitionDuration: 'normal',
transitionTimingFunction: 'easeOut'
},
variants: {
color: {
@@ -29,13 +31,19 @@ const buttonRecipe = ({ semanticColors }: ButtonRecipeArg) => {
},
size: {
small: {
fontSize: '0.875em'
paddingBlock: 'calc({spacing.xs} * 0.5)',
paddingInline: 'calc({spacing.xs} * 1)',
fontSize: 'sm'
},
medium: {
fontSize: '1em'
paddingBlock: 'calc({spacing.xs} * 0.75)',
paddingInline: 'calc({spacing.xs} * 1.25)',
fontSize: 'base'
},
large: {
fontSize: '1.125em'
paddingBlock: 'calc({spacing.xs} * 1)',
paddingInline: 'calc({spacing.xs} * 1.5)',
fontSize: 'lg'
}
},
variant: {

97
src/recipes/chip.ts Normal file
View File

@@ -0,0 +1,97 @@
import { defineRecipe } from '@pandacss/dev'
export const chipVariants = ['solid', 'outline', 'ghost'] as const
interface ChipRecipeArg {
semanticColorNames: string[]
}
const chipRecipe = ({ semanticColorNames }: ChipRecipeArg) => {
const colors = ['neutral', ...semanticColorNames]
return defineRecipe({
className: 'chip',
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 'xs',
lineHeight: '1',
borderRadius: 'sm',
fontWeight: 'semibold',
lineHeightStep: 'none',
userSelect: 'none'
},
variants: {
color: {
...Object.fromEntries(colors.map((color) => [color, {}]))
},
variant: {
...Object.fromEntries(chipVariants.map((variant) => [variant, {}]))
},
size: {
small: {
paddingBlock: 'calc({spacing.sm} * 0.5)',
paddingInline: 'calc({spacing.sm} * 0.75)',
fontSize: 'xs'
},
medium: {
paddingBlock: 'calc({spacing.sm} * 0.75)',
paddingInline: 'calc({spacing.sm})',
fontSize: 'sm'
},
large: {
paddingBlock: 'calc({spacing.sm})',
paddingInline: 'calc({spacing.sm} * 1.25)',
fontSize: 'base'
}
}
},
compoundVariants: colors.flatMap((color) => {
return chipVariants.map((variant) => {
switch (variant) {
case 'solid':
return {
color,
variant,
css: {
backgroundColor: `{colors.${color}.9}`,
color: `{colors.${color}.foreground}`
}
}
case 'outline':
return {
color,
variant,
css: {
border: '1px solid',
borderColor: `{colors.${color}.6}`,
color: `{colors.${color}.9}`
}
}
case 'ghost':
return {
color,
variant,
css: {
backgroundColor: `{colors.${color}.2}`,
color: `{colors.${color}.9}`
}
}
default:
return {
color,
variant,
css: {}
}
}
})
}),
defaultVariants: {
color: 'neutral',
size: 'medium',
variant: 'solid'
}
})
}
export default chipRecipe

View File

@@ -1,11 +1,11 @@
import { defineSlotRecipe } from '@pandacss/dev'
export interface DetailsRecipeArg {
semanticColors: string[]
semanticColorNames: string[]
}
export const detailsRecipe = ({ semanticColors }: DetailsRecipeArg) => {
const colorVariants = ['neutral', ...semanticColors]
const detailsRecipe = ({ semanticColorNames }: DetailsRecipeArg) => {
const colorVariants = ['neutral', ...semanticColorNames]
const recipe = defineSlotRecipe({
className: 'details',
slots: ['summary', 'details'],
@@ -13,16 +13,20 @@ export const detailsRecipe = ({ semanticColors }: DetailsRecipeArg) => {
details: {
display: 'block',
overflow: 'hidden',
borderRadius: '0.25em',
transition: 'all 300ms ease-in-out'
borderRadius: 'sm',
transition: 'all',
transitionTimingFunction: 'easeOut',
transitionDuration: 'normal'
},
summary: {
display: 'block',
overflow: 'hidden',
borderRadius: '0.25em',
padding: '0.25em',
borderRadius: 'sm',
padding: 'xs',
cursor: 'pointer',
transition: 'all 300ms ease-in-out'
transition: 'all',
transitionTimingFunction: 'easeOut',
transitionDuration: 'normal'
}
},
variants: {
@@ -57,3 +61,5 @@ export const detailsRecipe = ({ semanticColors }: DetailsRecipeArg) => {
return recipe
}
export default detailsRecipe

176
src/recipes/iconButton.ts Normal file
View File

@@ -0,0 +1,176 @@
import { defineRecipe, type SystemStyleObject } from '@pandacss/dev'
export const iconButtonVariants = ['solid', 'outline', 'ghost'] as const
export const iconButtonShapes = ['circle', 'square'] as const
interface IconButtonRecipeArg {
semanticColorNames: string[]
}
const iconButtonRecipe = ({ semanticColorNames }: IconButtonRecipeArg) => {
const colors = ['neutral', ...semanticColorNames]
return defineRecipe({
className: 'iconButton',
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 'calc(max-content)',
height: 'auto',
aspectRatio: '1/1',
cursor: 'pointer',
transitionProperty: 'color, background-color, border-color',
transitionDuration: 'normal',
transitionTimingFunction: 'easeOut',
_disabled: {
cursor: 'not-allowed'
}
},
variants: {
shape: {
...Object.fromEntries(
iconButtonShapes.map((shape) => {
switch (shape) {
case 'circle':
return [
shape,
{
borderRadius: 'full'
}
]
case 'square':
return [
shape,
{
borderRadius: 'none'
}
]
default:
return [shape, {}]
}
})
)
},
color: {
...Object.fromEntries(colors.map((color) => [color, {}]))
},
variant: {
...Object.fromEntries(iconButtonVariants.map((variant) => [variant, {}]))
},
size: {
small: {
padding: 'calc({spacing.xs} * 0.75) '
},
medium: {
padding: 'xs'
},
large: {
padding: 'calc({spacing.xs} * 1.25)'
}
}
},
compoundVariants: colors.flatMap((color) =>
iconButtonVariants.map((variant) => {
switch (variant) {
case 'solid':
return {
color,
variant,
css: {
border: 'none',
backgroundColor: `${color}.9`,
color: `${color}.foreground`,
_hover: {
backgroundColor: `${color}.10`
},
_active: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 80%, {colors.${color}.1})`
},
_disabled: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 40%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.foreground} 40%, transparent)`,
cursor: 'not-allowed',
pointerEvents: 'none',
_hover: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 40%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.foreground} 40%, transparent)`
}
}
} as SystemStyleObject
}
case 'outline':
return {
color,
variant,
css: {
border: '1px solid',
borderColor: `${color}.9`,
backgroundColor: 'transparent',
color: `${color}.9`,
_hover: {
backgroundColor: `${color}.10`,
color: `${color}.foreground`
},
_active: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 80%, {colors.${color}.1})`,
color: `${color}.foreground`
},
_disabled: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 10%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.foreground} 40%, transparent)`,
borderColor: `color-mix(in srgb, {colors.${color}.9} 10%, transparent)`,
cursor: 'not-allowed',
pointerEvents: 'none',
_hover: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 10%, transparent)`,
color: `${color}.4/50`
}
}
}
}
case 'ghost':
return {
color,
variant,
css: {
border: 'none',
backgroundColor: 'transparent',
color: `${color}.9`,
_hover: {
backgroundColor: `${color}.10`,
color: `${color}.foreground`
},
_active: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 80%, {colors.${color}.1})`,
color: `${color}.foreground`
},
_disabled: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 10%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.foreground} 40%, transparent)`,
cursor: 'not-allowed',
pointerEvents: 'none',
_hover: {
backgroundColor: `color-mix(in srgb, {colors.${color}.9} 10%, transparent)`
}
}
}
}
default:
return {
color,
variant,
css: {}
}
}
})
),
defaultVariants: {
shape: 'circle',
color: 'neutral',
variant: 'solid',
size: 'medium'
}
})
}
export default iconButtonRecipe

View File

@@ -1,36 +1,69 @@
import { defineRecipe } from '@pandacss/dev'
export const inputRecipe = defineRecipe({
className: 'input',
base: {
display: 'inline-flex',
borderRadius: '4px',
cursor: 'pointer',
paddingBlock: '0.25em',
paddingInline: '0.5em',
border: '1px solid',
borderColor: 'neutral.7',
backgroundColor: 'neutral.3',
color: 'neutral.12',
outline: 'none',
transition: 'all 200ms ease-in-out',
_hover: {
backgroundColor: 'neutral.4'
},
_disabled: {
backgroundColor: 'neutral.3/50',
color: 'neutral.11/50',
borderColor: 'neutral.6/50',
cursor: 'not-allowed',
_hover: {
backgroundColor: 'neutral.6/50',
color: 'neutral.11/50'
export interface InputRecipeArg {
semanticColorNames: string[]
}
export const inputRecipe = ({ semanticColorNames }: InputRecipeArg) => {
const colorVariants = ['neutral', ...semanticColorNames]
return defineRecipe({
className: 'input',
base: {
display: 'inline-flex',
borderRadius: 'sm',
paddingBlock: 'xs',
paddingInline: 'sm',
border: '1px solid',
outline: 'none',
transitionProperty: 'color, background-color, border-color',
transitionDuration: 'normal',
transitionTimingFunction: 'easeOut',
cursor: 'text',
_disabled: {
cursor: 'not-allowed'
}
},
_active: {
backgroundColor: 'neutral.8'
variants: {
color: {
...Object.fromEntries(
colorVariants.map((color) => [
color,
{
borderColor: `${color}.6`,
backgroundColor: `${color}.2`,
color: `${color}.12`,
_hover: {
backgroundColor: `${color}.3`
},
_focus: {
backgroundColor: `${color}.3`,
borderColor: `${color}.7`
},
_disabled: {
backgroundColor: `color-mix(in srgb, {colors.${color}.2} 30%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.11} 30%, transparent)`,
borderColor: `color-mix(in srgb, {colors.${color}.6} 30%, transparent)`,
_hover: {
backgroundColor: `color-mix(in srgb, {colors.${color}.2} 30%, transparent)`,
borderColor: `color-mix(in srgb, {colors.${color}.6} 30%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.11} 30%, transparent)`
},
_focus: {
backgroundColor: `color-mix(in srgb, {colors.${color}.2} 30%, transparent)`,
borderColor: `color-mix(in srgb, {colors.${color}.6} 30%, transparent)`,
color: `color-mix(in srgb, {colors.${color}.11} 30%, transparent)`
}
}
}
])
)
}
},
defaultVariants: {
color: 'neutral'
}
}
})
})
}
export default inputRecipe

80
src/recipes/mark.ts Normal file
View File

@@ -0,0 +1,80 @@
import { defineRecipe, type SystemStyleObject } from '@pandacss/dev'
const markVariants = ['highlight', 'bold', 'underline'] as const
interface MarkRecipeArg {
semanticColorNames: string[]
}
const markRecipe = ({ semanticColorNames }: MarkRecipeArg) => {
const colors = ['neutral', ...semanticColorNames] as const
return defineRecipe({
className: 'mark',
base: {
backgroundColor: 'transparent',
display: 'inline-block',
alignItems: 'center',
justifyContent: 'center',
color: 'inherit',
gap: 'xs'
},
variants: {
color: {
...Object.fromEntries(colors.map((color) => [color, {}]))
},
variant: {
...Object.fromEntries(markVariants.map((variant) => [variant, {}]))
}
},
compoundVariants: markVariants.flatMap((variant) =>
colors.map((color) => {
switch (variant) {
case 'highlight':
return {
color,
variant,
css: {
margin: '0 -0.4em',
padding: '0.1em 0.4em 0.1em 0.4em',
borderRadius: '0.8em 0.3em',
boxDecorationBreak: 'clone',
WebkitBoxDecorationBreak: 'clone',
backgroundClip: 'padding-box',
fontWeight: 'semibold',
backgroundImage: `linear-gradient(to right, color-mix(in srgb, {colors.${color}.9} 10%,transparent) 0%, color-mix(in srgb, {colors.${color}.9} 73%, transparent) 12%, color-mix(in srgb, {colors.${color}.9} 25%, transparent) 100%)`,
color: `{colors.${color}.foreground}`
} as SystemStyleObject
}
case 'bold':
return {
color,
variant,
css: {
fontWeight: 'bold',
color: `{colors.${color}.9}`
} as SystemStyleObject
}
case 'underline':
return {
color,
variant,
css: {
textDecorationLine: 'underline',
textDecorationThickness: '2px',
textDecorationColor: `{colors.${color}.9}`
} as SystemStyleObject
}
default:
return { color, variant, css: {} }
}
})
),
defaultVariants: {
color: 'neutral',
variant: 'highlight'
}
})
}
export default markRecipe
export { markVariants }

View File

@@ -3,7 +3,7 @@
"compilerOptions": {
// Environment setup & latest features
"lib": ["esnext"],
"target": "ES6",
"target": "es2024",
"module": "NodeNext",
"moduleDetection": "auto",
"allowJs": false,