diff --git a/package.json b/package.json index 3675532..6540e4d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 86e739f..d2048d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' diff --git a/src/keyframes/pulse.ts b/src/keyframes/pulse.ts new file mode 100644 index 0000000..64c326f --- /dev/null +++ b/src/keyframes/pulse.ts @@ -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 diff --git a/src/keyframes/shimmer.ts b/src/keyframes/shimmer.ts new file mode 100644 index 0000000..24d7840 --- /dev/null +++ b/src/keyframes/shimmer.ts @@ -0,0 +1,10 @@ +import { defineKeyframes } from '@pandacss/dev' + +const shimmerKeyframes = defineKeyframes({ + shimmer: { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(150%)' } + } +}) + +export default shimmerKeyframes diff --git a/src/patterns/skeleton.ts b/src/patterns/skeleton.ts new file mode 100644 index 0000000..fd1318b --- /dev/null +++ b/src/patterns/skeleton.ts @@ -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 diff --git a/src/recipes/button.ts b/src/recipes/button.ts index f58ff2d..f21847a 100644 --- a/src/recipes/button.ts +++ b/src/recipes/button.ts @@ -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: { diff --git a/src/recipes/chip.ts b/src/recipes/chip.ts new file mode 100644 index 0000000..c0186b6 --- /dev/null +++ b/src/recipes/chip.ts @@ -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 diff --git a/src/recipes/details.ts b/src/recipes/details.ts index fa64d72..bcb54e1 100644 --- a/src/recipes/details.ts +++ b/src/recipes/details.ts @@ -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 diff --git a/src/recipes/iconButton.ts b/src/recipes/iconButton.ts new file mode 100644 index 0000000..93aed13 --- /dev/null +++ b/src/recipes/iconButton.ts @@ -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 diff --git a/src/recipes/input.ts b/src/recipes/input.ts index 526b824..54624ae 100644 --- a/src/recipes/input.ts +++ b/src/recipes/input.ts @@ -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 diff --git a/src/recipes/mark.ts b/src/recipes/mark.ts new file mode 100644 index 0000000..e10eb14 --- /dev/null +++ b/src/recipes/mark.ts @@ -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 } diff --git a/tsconfig.json b/tsconfig.json index 7db39e9..813d9e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { // Environment setup & latest features "lib": ["esnext"], - "target": "ES6", + "target": "es2024", "module": "NodeNext", "moduleDetection": "auto", "allowJs": false,