diff --git a/src/index.ts b/src/index.ts index a9bfb2c..bc4caa8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import alertRecipe from './recipes/alert.js' import chipRecipe from './recipes/chip.js' import iconButtonRecipe from './recipes/iconButton.js' import markRecipe from './recipes/mark.js' +import selectRecipe from './recipes/select.js' export type ThemeConfig = { neutral?: NeutralColor @@ -48,7 +49,8 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => { details: detailsRecipe({ semanticColorNames: semanticColorKeysArray }), mark: markRecipe({ semanticColorNames: semanticColorKeysArray }), input: inputRecipe({ semanticColorNames: semanticColorKeysArray }), - iconButton: iconButtonRecipe({ semanticColorNames: semanticColorKeysArray }) + iconButton: iconButtonRecipe({ semanticColorNames: semanticColorKeysArray }), + select: selectRecipe({ semanticColorNames: semanticColorKeysArray }) }, tokens: { animations: {}, @@ -186,6 +188,11 @@ const srJuggernautPandaPreset = (config?: ThemeConfig) => { } } }, + conditions: { + extend: { + active: '&[data-state="open"]' + } + }, globalCss: { '*, *:before, *:after': { boxSizing: 'border-box', diff --git a/src/recipes/select.ts b/src/recipes/select.ts new file mode 100644 index 0000000..a7d63fc --- /dev/null +++ b/src/recipes/select.ts @@ -0,0 +1,234 @@ +import { defineSlotRecipe, type SystemStyleObject } from '@pandacss/dev' + +const selectSlots = [ + 'trigger', + 'content', + 'viewport', + 'scrollButton', + 'item', + 'itemIndicator', + 'group', + 'groupLabel', + 'separator' +] as const + +export interface SelectRecipeArg { + semanticColorNames: string[] +} + +const selectRecipe = ({ semanticColorNames }: SelectRecipeArg) => { + const colorVariants = ['neutral', ...semanticColorNames] + return defineSlotRecipe({ + className: 'select', + slots: selectSlots, + base: { + trigger: { + all: 'unset', + boxSizing: 'border-box', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 'xs', + minWidth: 'fit-content', + borderRadius: 'sm', + border: '1px solid', + cursor: 'pointer', + transitionProperty: 'color, background-color, border-color', + transitionDuration: 'normal', + transitionTimingFunction: 'easeOut' + }, + content: { + overflow: 'hidden', + maxHeight: 'var(--radix-select-content-available-height, 90dvh)', + width: 'var(--radix-select-trigger-width, min(max-content, 90dvw))', + backgroundColor: 'var(--select-background-color)', + color: 'var(--select-color)', + borderRadius: 'sm', + _active: { + animationName: 'fade', + animationDuration: 'normal' + }, + '&[data-state=closed]': { + animationName: 'fade', + animationDuration: 'normal', + animationDirection: 'reverse' + } + }, + viewport: { + padding: 'xs' + }, + scrollButton: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: 'var(--select-color-subtle)', + cursor: 'pointer', + transitionProperty: 'color, background-color', + transitionDuration: 'normal', + transitionTimingFunction: 'easeOut', + '&:hover': { + color: 'var(--select-color)' + } + }, + item: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + gap: 'xs', + padding: 'xs', + borderRadius: 'sm', + cursor: 'pointer', + transitionProperty: 'color, background-color', + transitionDuration: 'normal', + transitionTimingFunction: 'easeOut', + _highlighted: { + backgroundColor: 'var(--select-background-color-hover)' + }, + '&[data-state=checked]': { + backgroundColor: 'var(--select-background-color-active)' + }, + _disabled: { + color: 'var(--select-color-disabled)', + backgroundColor: 'var(--select-background-color-disabled)', + cursor: 'not-allowed', + _highlighted: { + backgroundColor: 'var(--select-background-color-disabled)', + color: 'var(--select-color-disabled)' + } + } + }, + itemIndicator: { + color: 'var(--select-color-subtle)' + }, + group: { + display: 'flex', + flexDirection: 'column', + gap: 'xs', + marginInline: '-xs', + borderBlock: '1px solid var(--select-border-color)', + paddingInline: 'xs' + }, + groupLabel: { + color: 'var(--select-color-subtle)', + fontSize: 'sm', + fontWeight: 'semibold', + lineHeight: 'tight', + userSelect: 'none' + }, + separator: { + height: '1px', + backgroundColor: 'var(--select-border-color)', + marginBlock: 'xs', + marginInline: '-xs' + } + }, + variants: { + size: { + small: { + trigger: { + paddingBlock: 'calc({spacing.xs} * 0.5)', + paddingInline: 'calc({spacing.xs} * 1)', + fontSize: 'sm' + } + }, + medium: { + trigger: { + paddingBlock: 'calc({spacing.xs} * 0.75)', + paddingInline: 'calc({spacing.xs} * 1.25)', + fontSize: 'base' + } + }, + large: { + trigger: { + paddingBlock: 'calc({spacing.xs} * 1)', + paddingInline: 'calc({spacing.xs} * 1.5)', + fontSize: 'lg' + } + } + }, + color: { + ...Object.fromEntries( + colorVariants.map((color) => [ + color, + Object.fromEntries( + selectSlots.map((slot) => { + switch (slot) { + case 'trigger': + // Trigger is outside the content, so it needs to be set independently + return [ + slot, + { + backgroundColor: `${color}.2`, + color: `${color}.12`, + borderColor: `${color}.6`, + _placeholder: { + color: `${color}.11` + }, + _hover: { + backgroundColor: `${color}.3` + }, + _active: { + backgroundColor: `${color}.4` + }, + + _focus: { + backgroundColor: `${color}.3`, + borderColor: `${color}.7` + }, + _disabled: { + cursor: 'not-allowed', + backgroundColor: `color-mix(in srgb, {colors.${color}.2} 40%, transparent)`, + color: `color-mix(in srgb, {colors.${color}.11} 40%, transparent)`, + _placeholder: { + color: `color-mix(in srgb, {colors.${color}.11} 40%, transparent)` + }, + _hover: { + backgroundColor: `color-mix(in srgb, {colors.${color}.2} 40%, transparent)`, + color: `color-mix(in srgb, {colors.${color}.11} 40%, transparent)` + }, + _active: { + backgroundColor: `color-mix(in srgb, {colors.${color}.2} 40%, transparent)`, + color: `color-mix(in srgb, {colors.${color}.11} 40%, transparent)` + } + } + } as SystemStyleObject + ] + case 'content': + // Items, Group, ScrollButtons are inside the content, so we can set them together + return [ + slot, + { + '--select-background-color': `{colors.${color}.2}`, + '--select-background-color-hover': `{colors.${color}.4}`, + '--select-background-color-active': `{colors.${color}.5}`, + '--select-background-color-disabled': `color-mix(in srgb, {colors.${color}.2} 40%, transparent)`, + '--select-border-color': `{colors.${color}.6}`, + '--select-color-subtle': `{colors.${color}.11}`, + '--select-color': `{colors.${color}.12}`, + '--select-color-disabled': `color-mix(in srgb, {colors.${color}.11} 40%, transparent)` + } as SystemStyleObject + ] + default: + return [slot, {}] + } + }) + ) + ]) + ) + }, + fullWidth: { + true: { + trigger: { + width: '100%' + } + } + } + }, + defaultVariants: { + color: 'neutral', + size: 'medium' + } + }) +} + +export default selectRecipe