diff --git a/packages/kit/package.json b/packages/kit/package.json index 063c8e2..891cdfd 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -55,39 +55,39 @@ }, "devDependencies": { "@codeui/vanilla-extract": "workspace:*", - "@testing-library/jest-dom": "^5.16.5", - "jest": "^28.1.3", - "jest-environment-jsdom": "^28.1.3", - "rollup": "^3.20.2", + "@testing-library/jest-dom": "^5.17.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rollup": "^4.15.0", "rollup-preset-solid": "^2.0.1", - "solid-js": "^1.7.3", - "solid-testing-library": "^0.3.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", - "vite-tsconfig-paths": "^4.0.7" + "solid-js": "^1.8.16", + "solid-testing-library": "^0.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.4.5", + "vite-tsconfig-paths": "^4.3.2" }, "peerDependencies": { "solid-js": "^1.7.0" }, "packageManager": "pnpm@7.5.0", "dependencies": { - "@kobalte/core": "^0.11.0", + "@kobalte/core": "^0.12.6", "@kobalte/utils": "^0.9.0", - "@kobalte/vanilla-extract": "^0.4.0", - "@maskito/core": "^1.9.0", - "@maskito/kit": "^1.9.0", + "@kobalte/vanilla-extract": "^0.5.0", + "@maskito/core": "^2.3.0", + "@maskito/kit": "^2.3.0", "@motionone/solid": "^10.16.0", "@radix-ui/colors": "^0.1.8", - "@solid-primitives/pagination": "^0.2.5", - "@solid-primitives/scheduled": "^1.4.1", - "@tanstack/solid-virtual": "^3.0.0-beta.6", - "@tanstack/virtual-core": "^3.0.0-alpha.1", - "@vanilla-extract/css": "^1.11.0", - "@vanilla-extract/dynamic": "^2.0.3", - "@vanilla-extract/recipes": "^0.4.0", - "@vanilla-extract/vite-plugin": "^3.8.0", - "motion": "^10.15.5", - "polished": "^4.2.2", - "statebuilder": "^0.3.1" + "@solid-primitives/pagination": "^0.3.0", + "@solid-primitives/scheduled": "^1.4.3", + "@tanstack/solid-virtual": "^3.4.0", + "@tanstack/virtual-core": "^3.4.0", + "@vanilla-extract/css": "^1.14.2", + "@vanilla-extract/dynamic": "^2.1.0", + "@vanilla-extract/recipes": "^0.5.2", + "@vanilla-extract/vite-plugin": "^4.0.7", + "motion": "^10.17.0", + "polished": "^4.3.1", + "statebuilder": "^0.6.0" } } diff --git a/packages/kit/src/components/Combobox/Combobox.css.ts b/packages/kit/src/components/Combobox/Combobox.css.ts new file mode 100644 index 0000000..e755aee --- /dev/null +++ b/packages/kit/src/components/Combobox/Combobox.css.ts @@ -0,0 +1,211 @@ +import { createTheme, keyframes, style } from "@vanilla-extract/css"; +import { themeTokens } from "../../foundation/themes.css"; +import { componentStateStyles } from "@kobalte/vanilla-extract"; +import { tokens } from "../../foundation/contract.css"; +import { baseFieldTheme, baseFieldVars } from "../Field/Field.css"; +import { responsiveStyle } from "../../foundation/responsive"; +import { themeVars } from "../../foundation"; + +export const [selectTheme, selectThemeVars] = createTheme({ + contentBackground: tokens.dropdownBackground, + contentRadius: themeTokens.radii.lg, + contentBoxShadow: tokens.dropdownBoxShadow, + contentPadding: themeTokens.spacing["2"], + contentBorderColor: tokens.dropdownBorder, + contentMaxHeight: "400px", + contentMaxHeightXs: "270px", + separator: tokens.dropdownBorder, + itemMinHeight: "2.60rem", + itemTextColor: tokens.dropdownItemTextColor, + itemHoverBackground: tokens.dropdownItemHoverBackground, + itemHoverTextColor: tokens.dropdownItemHoverTextColor, + itemDisabledOpacity: ".4", + indicatorSize: "20px", +}); + +const contentShow = keyframes({ + from: { + opacity: 0, + transform: "translateY(-10px)", + }, + to: { + opacity: 1, + transform: "translateY(0px)", + }, +}); + +const contentHide = keyframes({ + from: { + opacity: 1, + transform: "translateY(0px)", + }, + to: { + opacity: 0, + transform: "translateY(-10px)", + }, +}); + +// TODO: common popover/dropdown style +export const content = style([ + selectTheme, + { + boxShadow: selectThemeVars.contentBoxShadow, + backgroundColor: selectThemeVars.contentBackground, + borderRadius: selectThemeVars.contentRadius, + padding: selectThemeVars.contentPadding, + overflow: "auto", + zIndex: themeTokens.zIndex["50"], + listStyleType: "none", + display: "flex", + flexDirection: "column", + rowGap: themeTokens.spacing["1"], + outline: "none", + maxHeight: selectThemeVars.contentMaxHeight, + animation: `${contentHide} 250ms ease-in-out`, + border: `1px solid ${selectThemeVars.contentBorderColor}`, + }, + responsiveStyle({ + xs: { + vars: { + [selectThemeVars.contentMaxHeight]: selectThemeVars.contentMaxHeightXs, + }, + }, + sm: { + vars: { + [selectThemeVars.contentMaxHeight]: "400px", + }, + }, + }), + componentStateStyles({ + expanded: { + animation: `${contentShow} 250ms ease-in-out`, + }, + }), +]); + +export const input = style([ + { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }, +]); + +export const item = style([ + { + textAlign: "left", + justifyContent: "space-between", + border: 0, + padding: `${themeTokens.spacing["2"]} ${themeTokens.spacing["3"]}`, + borderRadius: themeTokens.radii.sm, + background: "transparent", + color: selectThemeVars.itemTextColor, + userSelect: "none", + display: "flex", + alignItems: "center", + outline: "none", + fontWeight: themeTokens.fontWeight.normal, + transition: "opacity .2s, background-color .2s, transform .2s", + gap: themeTokens.spacing["2"], + margin: `${themeTokens.spacing["1"]} 0`, + minHeight: selectThemeVars.itemMinHeight, + selectors: { + "&:first-child,&:last-child": { + margin: 0, + }, + }, + }, + { + ":disabled": { + opacity: selectThemeVars.itemDisabledOpacity, + }, + ":focus": { + boxShadow: "none", + outline: "none", + backgroundColor: selectThemeVars.itemHoverBackground, + color: selectThemeVars.itemHoverTextColor, + }, + ":focus-visible": { + backgroundColor: selectThemeVars.itemHoverBackground, + color: selectThemeVars.itemHoverTextColor, + }, + }, + componentStateStyles({ + highlighted: { + boxShadow: "none", + outline: "none", + backgroundColor: selectThemeVars.itemHoverBackground, + color: selectThemeVars.itemHoverTextColor, + }, + selected: { + not: { + paddingRight: `calc(${themeTokens.spacing["3"]} + ${selectThemeVars.indicatorSize} + ${themeTokens.spacing["2"]})`, + }, + }, + disabled: { + opacity: selectThemeVars.itemDisabledOpacity, + not: { + ":hover": {}, + }, + }, + }), +]); + +export const field = style([ + baseFieldTheme, + { + display: "flex", + flexDirection: "column", + gap: themeTokens.spacing["3"], + flex: 1, + height: "100%", + }, +]); + +// TODO: Unify with Select? +export const comboboxField = style([ + { + display: "inline-flex", + alignItems: "center", + justifyContent: "space-between", + paddingRight: themeTokens.spacing["3"], + paddingLeft: themeTokens.spacing["3"], + paddingTop: 0, + paddingBottom: 0, + outline: "none", + width: "100%", + fontSize: baseFieldVars.fontSize, + }, +]); + +export const comboboxInput = style({ + color: themeVars.foreground, + appearance: "none", + background: "transparent", + outline: "none", + border: 0, + display: "inline-flex", + minWidth: 0, + width: "100%", +}); + +export const comboboxTrigger = style({ + border: 0, + background: "transparent", + outline: "none", + padding: 0, + color: themeVars.foreground, +}); + +export const itemIndicator = style({ + marginLeft: "auto", + height: selectThemeVars.indicatorSize, + width: selectThemeVars.indicatorSize, + flexShrink: 0, +}); + +export const comboboxInputWorkaround = style({ + width: 0, + height: 0, + opacity: 0, +}); diff --git a/packages/kit/src/components/Combobox/Combobox.tsx b/packages/kit/src/components/Combobox/Combobox.tsx new file mode 100644 index 0000000..8891a3a --- /dev/null +++ b/packages/kit/src/components/Combobox/Combobox.tsx @@ -0,0 +1,186 @@ +import { Combobox as KCombobox, createControllableBooleanSignal } from "@kobalte/core"; +import { Accessor, JSX, JSXElement, Show, createSignal, splitProps } from "solid-js"; +import { CheckIcon } from "../../icons/CheckIcon"; +import { SelectorIcon } from "../../icons/SelectorIcon"; +import { mergeClasses } from "../../utils/css"; +import { highlight } from "../../utils/highlight/highlight"; +import { + FieldWithErrorMessageSupport, + createFieldErrorMessageProps, +} from "../Field/FieldError/createFieldErrorMessageProps"; +import { createFieldLabelProps } from "../Field/FieldLabel/createFieldLabelProps"; +import { createFieldMessageProps } from "../Field/FieldMessage/createFieldMessageProps"; +import { BaseFieldProps, createBaseFieldProps } from "../Field/createBaseFieldProps"; +import * as styles from "./Combobox.css"; + +void highlight; + +export type ComboboxProps = KCombobox.ComboboxRootProps< + Option, + OptGroup +> & + BaseFieldProps & { + "aria-label": string; + placeholder?: string; + description?: string; + label?: JSX.Element; + } & FieldWithErrorMessageSupport & { + itemLabel?: (item: Option) => JSXElement; + valueComponent?: (state: Accessor