diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..e420363b Binary files /dev/null and b/.DS_Store differ diff --git a/.changeset/config.json b/.changeset/config.json index 4a9378af..784a1913 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["crossed-docs", "crossed-expo", "@crossed/demo"] + "ignore": ["crossed-docs", "crossed-expo", "@crossed/demo", "server-component"] } diff --git a/.changeset/weak-squids-stare.md b/.changeset/weak-squids-stare.md new file mode 100644 index 00000000..810d8620 --- /dev/null +++ b/.changeset/weak-squids-stare.md @@ -0,0 +1,7 @@ +--- +'@crossed/primitive': minor +'@crossed/styled': minor +'@crossed/ui': minor +--- + +add theme dark light diff --git a/.github/workflows/merge-request.yml b/.github/workflows/merge-request.yml index c3dcc0d6..40ab12ba 100644 --- a/.github/workflows/merge-request.yml +++ b/.github/workflows/merge-request.yml @@ -14,12 +14,28 @@ jobs: - name: Checkout uses: actions/checkout@v3 # 👇 Build steps - - name: Set up Node.js - uses: pnpm/action-setup@v2 + - name: Install Node.js + uses: actions/setup-node@v3 with: - version: 7 + node-version: 18 + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lock - name: Linter run: pnpm lint - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..428fee60 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install Dependencies + run: pnpm install --frozen-lock + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + # This expects you to have a script called release which does a build for your packages and calls changeset publish + publish: pnpm build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/apps/docs/components/CodeDemo.tsx b/apps/docs/components/CodeDemo.tsx index e16fa619..acb8a9e8 100644 --- a/apps/docs/components/CodeDemo.tsx +++ b/apps/docs/components/CodeDemo.tsx @@ -1,4 +1,4 @@ -import { UilCheck } from '@iconscout/react-native-unicons'; +import { UilCheck, UilEye, UilEyeSlash } from '@iconscout/react-native-unicons'; import { Props } from '@crossed/demo/lib/typescript/props'; import { Box, @@ -33,14 +33,18 @@ export const CodeDemo = ({ }) => { const code = (useData() || { [name]: '' })[name] || ''; const [show, setShow] = useState(false); - const [color, setColor] = useState('zinc'); + const [color, setColor] = useState('neutral'); const [space, setSpace] = useState('md'); const [size, setSize] = useState('md'); const [variant, setVariant] = useState(actions.variant?.[0] || ''); const hasActions = Object.keys(actions || {}).length > 0; return ( - + {hasActions && ( <> {actions.variant && ( { return ( )} diff --git a/apps/docs/package.json b/apps/docs/package.json index 2b324a2a..551390bb 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -31,9 +31,11 @@ "nextra": "latest", "nextra-theme-docs": "latest", "postcss": "^8.4.27", + "raf": "^3.4.1", "react": "^18.2.0", "react-code-blocks": "^0.1.3", "react-dom": "^18.2.0", + "react-native-reanimated-swc-plugin": "^0.3.0", "react-native-svg": "^13.10.0", "react-native-web": "^0.19.7", "tailwindcss": "^3.3.3" diff --git a/apps/docs/pages/_app.mdx b/apps/docs/pages/_app.mdx index d85ebe48..cd560646 100644 --- a/apps/docs/pages/_app.mdx +++ b/apps/docs/pages/_app.mdx @@ -1,10 +1,14 @@ +import 'raf/polyfill'; import '../styles/global.css'; -import { CrossedProvider } from '@crossed/ui'; +import { CrossedTheme } from '@crossed/ui'; +import { PortalProvider } from '@gorhom/portal'; export default function App({ Component, pageProps }) { return ( - - - + + + + + ); } diff --git a/apps/docs/pages/components/display/menu.mdx b/apps/docs/pages/components/display/list.mdx similarity index 62% rename from apps/docs/pages/components/display/menu.mdx rename to apps/docs/pages/components/display/list.mdx index fd73c62a..502a9924 100644 --- a/apps/docs/pages/components/display/menu.mdx +++ b/apps/docs/pages/components/display/list.mdx @@ -1,29 +1,29 @@ -import { MenuDemo } from '@crossed/demo'; +import { ListDemo } from '@crossed/demo'; import { CodeDemo } from 'components/CodeDemo'; import { getStaticDemo } from 'components/getStaticDemo'; import { Tabs } from 'components/Tabs'; import { Table, Tr, Td, Th } from 'nextra/components'; -export const getStaticProps = getStaticDemo('ui/display/Menu'); +export const getStaticProps = getStaticDemo('ui/display/List'); -# Menu +# List - + ```tsx - - - - - - + + + + + + ``` - ## Menu + ## List diff --git a/apps/docs/pages/components/forms/button.mdx b/apps/docs/pages/components/forms/button.mdx index a6e23b17..a41158d7 100644 --- a/apps/docs/pages/components/forms/button.mdx +++ b/apps/docs/pages/components/forms/button.mdx @@ -36,7 +36,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Button'); ["loading", "boolean", "false", "Loading state button"], ["variant", "outline | filled", "outline", "Variant of button"], ["size", "xs | sm | md | lg | xl", "md", "Size of button"], - ["color", "type Colors", "zinc", "Color of button"], + ["color", "type Colors", "neutral", "Color of button"], ["text", "string", "-", "Text of button in simple usage"], ["icon", "ComponentType<{ size: number; color: string; }>", "-", "Icon on left in simple usage"], ["iconAfter", "ComponentType<{ size: number; color: string; }>", "-", "Icon on right in simple usage"], @@ -47,7 +47,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Button');
Props
## Button.Icon diff --git a/apps/docs/pages/components/forms/checkbox.mdx b/apps/docs/pages/components/forms/checkbox.mdx index ab9f88ae..887e3290 100644 --- a/apps/docs/pages/components/forms/checkbox.mdx +++ b/apps/docs/pages/components/forms/checkbox.mdx @@ -34,7 +34,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Checkbox'); ["onChangeValue", "(value: boolean) => void", "undefined", "Fire when user toggle checkbox"], ["variant", "outline | filled", "outline", "Variant of button"], ["size", "xs | sm | md | lg | xl", "md", "Size of button"], - ["color", "type Colors", "zinc", "Color of button"], + ["color", "type Colors", "neutral", "Color of button"], ]} /> @@ -46,7 +46,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Checkbox'); ["onChangeValue", "(value: boolean) => void", "undefined", "Fire when user toggle checkbox"], ["variant", "outline | filled", "outline", "Variant of button"], ["size", "xs | sm | md | lg | xl", "md", "Size of button"], - ["color", "type Colors", "zinc", "Color of button"], + ["color", "type Colors", "neutral", "Color of button"], ]} /> diff --git a/apps/docs/pages/components/forms/input.mdx b/apps/docs/pages/components/forms/input.mdx index c522ec91..42d23117 100644 --- a/apps/docs/pages/components/forms/input.mdx +++ b/apps/docs/pages/components/forms/input.mdx @@ -38,7 +38,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Input'); ["onChangeValue", "(params: string) => void", "undefined", "Event when on change value of input"], ["variant", "outline | filled", "outline", "Variant of button"], ["size", "xs | sm | md | lg | xl", "md", "Size of button"], - ["color", "type Colors", "zinc", "Color of button"], + ["color", "type Colors", "neutral", "Color of button"], ["className", "string", "undefined", "Classname pass to children"], ]} /> diff --git a/apps/docs/pages/components/forms/select.mdx b/apps/docs/pages/components/forms/select.mdx index 484583ee..f33f2316 100644 --- a/apps/docs/pages/components/forms/select.mdx +++ b/apps/docs/pages/components/forms/select.mdx @@ -38,7 +38,7 @@ export const getStaticProps = getStaticDemo('ui/forms/Select');
+ - + ); } ``` diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index 7be22850..4bb7a8ef 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -21,8 +21,8 @@ export default function LandingPage() { -

crossed

-

+

crossed

+

Universal Headless Components built with tailwindcss for{" "} react-native + web

@@ -30,7 +30,7 @@ export default function LandingPage() {
+ + diff --git a/apps/docs/pages/primitive/createSelect.mdx b/apps/docs/pages/primitive/createSelect.mdx new file mode 100644 index 00000000..57055345 --- /dev/null +++ b/apps/docs/pages/primitive/createSelect.mdx @@ -0,0 +1,25 @@ +import { CreateSelectSimpleDemo } from '@crossed/demo'; +import { CodeDemo } from 'components/CodeDemo'; +import { getStaticDemo } from 'components/getStaticDemo'; +import { Tabs } from 'components/Tabs'; + +export const getStaticProps = async (e) => { + return { + props: { + ssg: { + simple: (await getStaticDemo('primitive/createSelect/simple')(e)).props.ssg.code, + } + } + } +}; + +# createSelect + + + + ## simple +
+ + +
+
diff --git a/apps/docs/pages/styled/_meta.json b/apps/docs/pages/styled/_meta.json index e98462df..fea4d8bf 100644 --- a/apps/docs/pages/styled/_meta.json +++ b/apps/docs/pages/styled/_meta.json @@ -10,5 +10,6 @@ "defaultVariants": "Default variants", "compoundVariants": "Compound variants", "extends": "Extends", + "colorMode": "Color mode (theme)", "alias": "Alias" } diff --git a/apps/docs/pages/styled/base.mdx b/apps/docs/pages/styled/base.mdx index f8cda6d6..f6615530 100644 --- a/apps/docs/pages/styled/base.mdx +++ b/apps/docs/pages/styled/base.mdx @@ -3,6 +3,7 @@ import { ButtonOnlyBaseNativeDemo, ButtonStateDemo, ButtonStateNativeDemo, + ColorModeDemo, } from '@crossed/demo'; import { CodeDemo, Code } from 'components/CodeDemo'; import { getStaticDemo } from 'components/getStaticDemo'; @@ -19,6 +20,7 @@ export const getStaticProps = async (e) => { ).props.ssg.code, state: (await getStaticDemo('styled/button/State')(e)).props.ssg.code, stateNative: (await getStaticDemo('styled/button/State.native')(e)).props.ssg.code, + colorMode: (await getStaticDemo('styled/colorMode')(e)).props.ssg.code, }, }, }; @@ -89,3 +91,9 @@ You can manage state from parent with `states` props added to component styled + +## Color mode + +When you create styled component, you can specify `:dark` or `:light` keys + + \ No newline at end of file diff --git a/apps/docs/pages/styled/colorMode.mdx b/apps/docs/pages/styled/colorMode.mdx new file mode 100644 index 00000000..ffc909a1 --- /dev/null +++ b/apps/docs/pages/styled/colorMode.mdx @@ -0,0 +1,82 @@ +import { ColorModeDemo } from '@crossed/demo'; +import { CodeDemo } from 'components/CodeDemo'; +import { getStaticDemo } from 'components/getStaticDemo'; + +export const getStaticProps = async (e) => { + return { + props: { + ssg: { + onlyBase: (await getStaticDemo('styled/button/OnlyBase')(e)).props.ssg + .code, + onlyBaseNative: ( + await getStaticDemo('styled/button/OnlyBase.native')(e) + ).props.ssg.code, + state: (await getStaticDemo('styled/button/State')(e)).props.ssg.code, + stateNative: (await getStaticDemo('styled/button/State.native')(e)) + .props.ssg.code, + colorMode: (await getStaticDemo('styled/colorMode')(e)).props.ssg.code, + }, + }, + }; +}; + +# Color mode + +You can specify color mode for each component styled + +```typescript +import { styled } from '@crossed/styled'; + +export const Button = styled('button', { + 'className': ['px-2 py-1'], + ':dark': { + className: ['bg-neutral-800 text-white'], + }, + ':light': { + className: ['bg-neutral-300 text-black'], + }, + 'variants': { + color: { + red: { + ':dark': { + className: ['bg-red-800 text-white'], + }, + ':light': { + className: ['bg-red-300 text-black'], + }, + }, + }, + } +}); +``` + +## Configuration + +First create component with dark and light config + +Second you should add `CrossedTheme` + +```typescript +import { CrossedTheme } from '@crossed/styled'; + +export const App = () => { + return {/* ... */}; +}; +``` + +Then you can change theme with `useCrossedTheme` + +```typescript +import { useCrossedTheme } from '@crossed/styled'; + +export const ButtonTheme = () => { + const { theme, setTheme } = useCrossedTheme(); + return ( + diff --git a/packages/demo/src/primitive/index.ts b/packages/demo/src/primitive/index.ts index b331b460..1f72ed43 100644 --- a/packages/demo/src/primitive/index.ts +++ b/packages/demo/src/primitive/index.ts @@ -2,5 +2,7 @@ export * from './createButton'; export * from './createDropdown'; export * from './createLabel'; export * from './createList'; +export * from './createInput'; export * from './createModal'; +export * from './createSelect'; export * from './createSheet'; diff --git a/packages/demo/src/styled/button/State.tsx b/packages/demo/src/styled/button/State.tsx index 726a2b16..e11ff11e 100644 --- a/packages/demo/src/styled/button/State.tsx +++ b/packages/demo/src/styled/button/State.tsx @@ -1,4 +1,5 @@ import { type GetProps, styled } from '@crossed/styled'; +import { memo, type PropsWithChildren } from 'react'; const Button = styled('button', { 'className': ['px-3 py-2', 'border border-neutral-700', 'bg-neutral-800'], @@ -23,6 +24,12 @@ const Button = styled('button', { // eslint-disable-next-line @typescript-eslint/no-unused-vars type ButtonProps = GetProps; +const Other = memo((props: PropsWithChildren) => { + return + ); +}; + +export const ColorModeDemo = () => { + return ( + +
+
+ Button without variant + Button with variant +
+ +
+
+ ); +}; diff --git a/packages/demo/src/styled/index.ts b/packages/demo/src/styled/index.ts index eaf5eea7..d1e9e730 100644 --- a/packages/demo/src/styled/index.ts +++ b/packages/demo/src/styled/index.ts @@ -1 +1,2 @@ export * from './button'; +export * from './colorMode'; diff --git a/packages/demo/src/ui/display/List.tsx b/packages/demo/src/ui/display/List.tsx new file mode 100644 index 00000000..cf6df6bd --- /dev/null +++ b/packages/demo/src/ui/display/List.tsx @@ -0,0 +1,34 @@ +import { UilAngleRight, UilSetting } from '@iconscout/react-native-unicons'; +import { List } from '@crossed/ui'; +import type { Props } from '../../props'; + +const ListDemo = ({ color, size }: Props) => { + return ( + + } iconAfter={} pressable> + Setting + Setting of your account + + } pressable> + Profile + Manage social information + + } pressable> + Security + Modify security account + + } + pressable + /> + } pressable /> + + ); +}; + +ListDemo.displayName = 'ListDemo'; + +export { ListDemo }; diff --git a/packages/demo/src/ui/display/Menu.tsx b/packages/demo/src/ui/display/Menu.tsx deleted file mode 100644 index b4556e78..00000000 --- a/packages/demo/src/ui/display/Menu.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { UilAngleRight, UilSetting } from '@iconscout/react-native-unicons'; -import { Menu } from '@crossed/ui'; -import type { Props } from '../../props'; - -export const MenuDemo = ({ color }: Props) => { - return ( - - } iconAfter={}> - Setting - Setting of your account - - }> - Profile - Manage social information - - }> - Security - Modify security account - - } - /> - } /> - - ); -}; diff --git a/packages/demo/src/ui/display/index.ts b/packages/demo/src/ui/display/index.ts index 629d3d0a..4994c181 100644 --- a/packages/demo/src/ui/display/index.ts +++ b/packages/demo/src/ui/display/index.ts @@ -1 +1 @@ -export * from './Menu'; +export * from './List'; diff --git a/packages/demo/src/ui/forms/Label.tsx b/packages/demo/src/ui/forms/Label.tsx index 07aa8bab..15723789 100644 --- a/packages/demo/src/ui/forms/Label.tsx +++ b/packages/demo/src/ui/forms/Label.tsx @@ -1,12 +1,14 @@ +import { tw } from '@crossed/styled'; import { Box, Label } from '@crossed/ui'; +import { TextInput } from 'react-native'; export const LabelDemo = () => { return ( - diff --git a/packages/demo/src/ui/forms/Select.tsx b/packages/demo/src/ui/forms/Select.tsx index 62abeb08..2612c44a 100644 --- a/packages/demo/src/ui/forms/Select.tsx +++ b/packages/demo/src/ui/forms/Select.tsx @@ -1,22 +1,17 @@ -import { Select, UilAngleDown } from '@crossed/ui'; +import { Select } from '@crossed/ui'; import type { Props } from '../../props'; export const SelectDemo = ({ size, variant, color }: Props) => { return ( ); diff --git a/packages/demo/src/unicons.tsx b/packages/demo/src/unicons.tsx index ea428770..75303309 100644 --- a/packages/demo/src/unicons.tsx +++ b/packages/demo/src/unicons.tsx @@ -33,7 +33,7 @@ export const UniconsDemo = () => { className="basis-2/12 items-center rounded-md py-5" > - {name} + {name} ); } diff --git a/packages/primitive/src/Input/InputGroup.tsx b/packages/primitive/src/Input/InputGroup.tsx new file mode 100644 index 00000000..d88833ea --- /dev/null +++ b/packages/primitive/src/Input/InputGroup.tsx @@ -0,0 +1,14 @@ +import { useRef, type PropsWithChildren } from 'react'; +import { InputProvider, StyleRef } from './context'; + +export const InputGroup = ({ children }: PropsWithChildren) => { + const styleRef = useRef({}); + const setStyleRef = (style: StyleRef) => { + styleRef.current = style; + }; + return ( + + {children} + + ); +}; diff --git a/packages/primitive/src/Input/InputIcon.tsx b/packages/primitive/src/Input/InputIcon.tsx new file mode 100644 index 00000000..45a0b5f0 --- /dev/null +++ b/packages/primitive/src/Input/InputIcon.tsx @@ -0,0 +1,6 @@ +import { type ComponentType, forwardRef } from 'react'; + +export const createIcon = (Styled: ComponentType

) => + forwardRef((props, ref) => { + return ; + }); diff --git a/packages/primitive/src/Input/InputInput.tsx b/packages/primitive/src/Input/InputInput.tsx new file mode 100644 index 00000000..397e5bcf --- /dev/null +++ b/packages/primitive/src/Input/InputInput.tsx @@ -0,0 +1,11 @@ +import { type ComponentType, forwardRef } from 'react'; +import { Slot } from '../utils/Slot'; + +export const createInput = (Styled: ComponentType

) => + forwardRef((props, ref) => { + return ( + + + + ); + }); diff --git a/packages/primitive/src/Input/context.ts b/packages/primitive/src/Input/context.ts new file mode 100644 index 00000000..76569572 --- /dev/null +++ b/packages/primitive/src/Input/context.ts @@ -0,0 +1,16 @@ +import { createScope } from '@crossed/core'; +import type { MutableRefObject } from 'react'; + +export type StyleRef = { + style?: any; + className?: any; +}; + +export type ContextInput = { + style: MutableRefObject; + setStyle: (style: StyleRef) => void; +}; + +export const [InputProvider, useInputContext] = createScope( + {} as ContextInput +); diff --git a/packages/primitive/src/Input/index.ts b/packages/primitive/src/Input/index.ts new file mode 100644 index 00000000..2cebcf9a --- /dev/null +++ b/packages/primitive/src/Input/index.ts @@ -0,0 +1,29 @@ +import { withStaticProperties } from '@crossed/core'; +import type { ComponentType } from 'react'; +import { createIcon } from './InputIcon'; +import { createInput as createInputInput } from './InputInput'; + +export const createInput = < + GroupProps extends Record, + IconProps extends Record, + InputProps extends Record +>(components: { + Group: ComponentType; + Icon: ComponentType; + Input: ComponentType; +}) => { + const { Group, Icon, Input } = components; + + const InputGroup = Group; + const InputInput = createInputInput(Input); + const InputIcon = createIcon(Icon); + + InputInput.displayName = 'Input'; + InputGroup.displayName = 'Input.Group'; + InputIcon.displayName = 'Input.Icon'; + + return withStaticProperties(InputInput, { + Group: InputGroup, + Icon: InputIcon, + }); +}; diff --git a/packages/primitive/src/Select/Select.tsx b/packages/primitive/src/Select/Select.tsx new file mode 100644 index 00000000..a8308dd6 --- /dev/null +++ b/packages/primitive/src/Select/Select.tsx @@ -0,0 +1,6 @@ +import { forwardRef, type ComponentType } from 'react'; + +export const createSelectMain = (StyledRoot: ComponentType

) => + forwardRef((props, ref) => { + return ; + }); diff --git a/packages/primitive/src/Select/SelectContent.tsx b/packages/primitive/src/Select/SelectContent.tsx new file mode 100644 index 00000000..e0afec91 --- /dev/null +++ b/packages/primitive/src/Select/SelectContent.tsx @@ -0,0 +1,23 @@ +import { forwardRef, type ComponentType } from 'react'; +import { useContext } from './context'; +import { RovingFocus } from '../utils/RovingFocus'; +import { VisibilityHidden } from '../utils/VisibilityHidden'; + +export const createSelectContent = (StyledRoot: ComponentType

) => + forwardRef((props, ref) => { + const { id, open } = useContext(); + return ( + + + + ); + }); diff --git a/packages/primitive/src/Select/SelectDivider.tsx b/packages/primitive/src/Select/SelectDivider.tsx new file mode 100644 index 00000000..2561c419 --- /dev/null +++ b/packages/primitive/src/Select/SelectDivider.tsx @@ -0,0 +1,6 @@ +import { forwardRef, type ComponentType } from 'react'; + +export const createSelectDivider = (StyledRoot: ComponentType

) => + forwardRef((props, ref) => { + return ; + }); diff --git a/packages/primitive/src/Select/SelectItem.tsx b/packages/primitive/src/Select/SelectItem.tsx new file mode 100644 index 00000000..7f56e309 --- /dev/null +++ b/packages/primitive/src/Select/SelectItem.tsx @@ -0,0 +1,32 @@ +import { ComponentType, forwardRef } from 'react'; +import { useContext } from './context'; +import { composeEventHandlers } from '@crossed/core'; +import { RovingFocus } from '../utils/RovingFocus'; +import type { RequiredAccessibilityProps } from 'src/types'; + +export type SelectItemProps = { + disabled?: boolean; +}; +export const createSelectItem =

>( + Styled: ComponentType

+) => + //@ts-ignore + forwardRef< + any, + SelectItemProps & RequiredAccessibilityProps + >((props, ref) => { + const { setOpen } = useContext(); + return ( + + { + setOpen(false); + })} + /> + + ); + }); diff --git a/packages/primitive/src/Select/SelectLabel.tsx b/packages/primitive/src/Select/SelectLabel.tsx new file mode 100644 index 00000000..526ac2f7 --- /dev/null +++ b/packages/primitive/src/Select/SelectLabel.tsx @@ -0,0 +1,6 @@ +import { forwardRef, type ComponentType } from 'react'; + +export const createSelectLabel = (StyledRoot: ComponentType

) => + forwardRef((props, ref) => { + return ; + }); diff --git a/packages/primitive/src/Select/SelectPortal.tsx b/packages/primitive/src/Select/SelectPortal.tsx new file mode 100644 index 00000000..c32d861f --- /dev/null +++ b/packages/primitive/src/Select/SelectPortal.tsx @@ -0,0 +1,28 @@ +import { ComponentType, forwardRef, Fragment } from 'react'; +import { Provider, useContext } from './context'; +import { RemoveScroll as RS } from '../utils'; + +export type SelectPortalProps = { + /** + * To false, not remove scroll parent + * @default true + */ + removeParentScroll?: boolean; +}; + +export const createSelectPortal = (Styled: ComponentType

) => + forwardRef( + ({ removeParentScroll = true, ...props }, ref) => { + const { children, ...otherProps } = props as any; + const context = useContext(); + + const RemoveScroll = removeParentScroll ? RS : Fragment; + return ( + + + {children} + + + ); + } + ); diff --git a/packages/primitive/src/Select/SelectTrigger.tsx b/packages/primitive/src/Select/SelectTrigger.tsx new file mode 100644 index 00000000..52a0daec --- /dev/null +++ b/packages/primitive/src/Select/SelectTrigger.tsx @@ -0,0 +1,47 @@ +import { forwardRef, type ComponentType, useEffect, useRef } from 'react'; +import type { RequiredAccessibilityProps } from 'src/types'; +import { useContext } from './context'; +import { composeEventHandlers, composeRefs } from '@crossed/core'; + +export const createSelectTrigger = (StyledRoot: ComponentType

) => + // @ts-ignore + forwardRef>((props, ref) => { + const { id, open, setOpen } = useContext(); + const refInter = useRef(null); + const openRef = useRef(open); + + useEffect(() => { + if (!open && openRef.current) { + refInter.current?.focus?.(); + } + }, [open]); + useEffect(() => { + openRef.current = open; + }, [open]); + + return ( + { + setOpen(!open); + })} + onKeyDown={composeEventHandlers((props as any).onKeyDown, (e: any) => { + e.preventDefault(); + switch (e.code) { + case 'ArrowDown': + case 'ArrowUp': + case 'Space': + case 'Enter': + setOpen(true); + break; + } + })} + ref={composeRefs(ref, refInter)} + /> + ); + }); diff --git a/packages/primitive/src/Select/context.ts b/packages/primitive/src/Select/context.ts new file mode 100644 index 00000000..f2d557f2 --- /dev/null +++ b/packages/primitive/src/Select/context.ts @@ -0,0 +1,3 @@ +import { createScope } from '@crossed/core'; + +export const [Provider, useContext] = createScope({}); diff --git a/packages/primitive/src/Select/index.tsx b/packages/primitive/src/Select/index.tsx new file mode 100644 index 00000000..9a18f34f --- /dev/null +++ b/packages/primitive/src/Select/index.tsx @@ -0,0 +1,103 @@ +import { GetProps, useUncontrolled, withStaticProperties } from '@crossed/core'; +import { useMemo, type ComponentType, useId } from 'react'; +import { createSelectMain } from './Select'; +import { createSelectTrigger } from './SelectTrigger'; +import { createSelectContent } from './SelectContent'; +import { createSelectPortal } from './SelectPortal'; +import { Provider } from './context'; +import { createSelectItem } from './SelectItem'; +import { createSelectDivider } from './SelectDivider'; +import { createSelectLabel } from './SelectLabel'; +export { + Provider as ProviderSelect, + useContext as useSelectContext, +} from './context'; + +type Arg> = { + context?: Context; +}; + +export const createSelect = < + SelectProps extends Record, + TriggerProps extends Record, + ContentProps extends Record, + PortalProps extends Record, + ItemProps extends Record, + DividerProps extends Record, + LabelProps extends Record, + C extends Record +>( + components: { + Root: ComponentType; + Trigger: ComponentType; + Content: ComponentType; + Portal: ComponentType; + Item: ComponentType; + Divider: ComponentType; + Label: ComponentType; + }, + { context }: Arg = {} +) => { + const { Root, Trigger, Content, Portal, Item, Divider, Label } = components; + const Select = createSelectMain(Root); + const SelectTrigger = createSelectTrigger(Trigger); + const SelectContent = createSelectContent(Content); + const SelectPortal = createSelectPortal(Portal); + const SelectItem = createSelectItem(Item); + const SelectDivider = createSelectDivider(Divider); + const SelectLabel = createSelectLabel(Label); + + Select.displayName = 'Select'; + SelectTrigger.displayName = 'Select.Trigger'; + SelectContent.displayName = 'Select.Content'; + SelectPortal.displayName = 'Select.Portal'; + SelectItem.displayName = 'Select.Item'; + SelectDivider.displayName = 'Select.Divider'; + SelectLabel.displayName = 'Select.Label'; + + return withStaticProperties( + ( + props: GetProps & { + value?: boolean; + defaultValue?: boolean; + onChangeOpen?: (p: boolean) => void; + } + ) => { + const id = useId(); + const { + value, + defaultValue = false, + onChangeOpen, + ...otherProps + } = props; + const [open, setOpen] = useUncontrolled({ + value, + defaultValue, + onChange: onChangeOpen, + }); + + const contextProps = useMemo(() => { + return Object.entries(context || {}).reduce((acc, [key]) => { + if ((props as any)[key]) { + (acc as any)[key] = (props as any)[key]; + } + return acc; + }, context || ({} as C)); + }, [props]); + + return ( + + - - {label} - - { - setOpen(false); - setValue(label); - }} - > - - - - ); - } -); - -type SelectProps = { - size?: InputProps['size']; - variant?: InputProps['variant']; - color?: InputProps['color']; - value?: V; - defaultValue?: V; - onChangeValue?: (value: V) => void; - open?: boolean; - - placeholder?: string; - items?: { value: string; label: string }[]; - label?: string; -}; - -function SelectRoot( - params: Omit, 'children'> -): ReactNode; -function SelectRoot( - params: Omit, 'items'> -): ReactNode; -function SelectRoot({ - size = 'md', - variant = 'outlined', - color = 'zinc', - children, - open: openProps = false, - items, - label, - value: valueProps, - defaultValue, - onChangeValue, - placeholder, -}: PropsWithChildren) { - const [open, setOpen] = useState(openProps); - - const [value, setValue] = useUncontrolled({ - value: valueProps, - defaultValue, - finalValue: '', - onChange: onChangeValue, - }); - - const floating = useFloating({ - open: open, - onOpenChange: setOpen, - middleware: [offset(10), flip(), shift()], - whileElementsMounted: autoUpdate, - }); - - const { context } = floating; - - const click = useClick(context); - const dismiss = useDismiss(context); - const role = useRole(context); - - const interactions = useInteractions([click, dismiss, role]); - - return ( - - - - {children ?? - [ - label && {label}, - - - - - - , - - {items?.map(({ value: v, label: l }) => ( - - ))} - , - ].filter(Boolean)} - - - - ); -} - -export const Select = withStaticProperties(SelectRoot, { - Label: SelectLabel, - Trigger: SelectTrigger, - Input: SelectInput, - Icon: SelectIcon, - Content: SelectContent, - Option: SelectOption, -}); diff --git a/packages/ui/src/forms/Select/index.tsx b/packages/ui/src/forms/Select/index.tsx new file mode 100644 index 00000000..4f1dd757 --- /dev/null +++ b/packages/ui/src/forms/Select/index.tsx @@ -0,0 +1,445 @@ +'use client'; +import { GetProps, composeRefs, createScope } from '@crossed/core'; +import { merge, styled } from '@crossed/styled'; +import { HtmlHTMLAttributes, PropsWithChildren, forwardRef } from 'react'; +import { TextInput } from 'react-native'; +import { Portal, PortalProvider } from '@gorhom/portal'; +import { + autoUpdate, + flip, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from '@floating-ui/react'; +import { ButtonFrame } from '../Button'; +import { createSelect, useSelectContext } from '@crossed/primitive'; +import { YBox, YBoxProps } from '../../layout/YBox'; + +// type InputProps = GetProps; + +// type ProviderContext = { +// setOpen: Dispatch>; +// open: boolean; +// setValue: (prama: any) => void; +// size: InputProps['size']; +// variant: InputProps['variant']; +// color: InputProps['color']; +// value: string; +// placeholder?: string; +// }; + +// const [Provider, useContext] = createScope( +// {} as ProviderContext +// ); + +// type FloatingProvider = ReturnType & +// ReturnType; + +// const [FloatingProvider, useFloatingProvider] = createScope( +// {} as FloatingProvider +// ); + +// const SelectTrigger = memo( +// forwardRef((props: GetProps, ref) => { +// const { setOpen, open } = useContext(); +// const { getReferenceProps, refs } = useFloatingProvider(); +// const { keyboardProps } = useKeyboard({ +// onKeyDown: (e) => { +// e.preventDefault(); +// (e.code === 'ArrowUp' || e.code === 'ArrowDown') && setOpen(true); +// }, +// }); + +// return ( +// { +// setOpen((e) => !e); +// }, props?.onPress)} +// /> +// ); +// }) +// ); + +// const SelectLabel = Input.Label; +// const SelectIcon = Input.Icon; + +// const SelectInput = memo((props: GetProps) => { +// const { placeholder } = useContext(); +// return ( +// +// ); +// }); + +// const SelectContent = memo(({ children, ...props }: GetProps) => { +// const context = useContext(); +// const { setOpen, open, variant, size, color } = context; +// const { focusWithinProps } = useFocusWithin({ +// onFocusWithinChange: setOpen, +// }); +// const { +// context: contextFloating, +// refs, +// floatingStyles, +// getFloatingProps, +// } = useFloatingProvider(); + +// return !open ? null : ( +// +// +// +// +// +// +// {children} +// +// +// +// +// +// +// ); +// }); + +// const SelectOption = memo( +// ({ value: valueProps, label }: { value: string; label: string }) => { +// const focusManager = useFocusManager(); +// const { setOpen, setValue, size, variant, color, value } = useContext(); + +// const { keyboardProps } = useKeyboard({ +// onKeyDown: (e) => { +// e.preventDefault(); +// switch (e.key) { +// case 'Escape': +// setOpen(false); +// break; +// case 'ArrowDown': +// focusManager.focusNext({ wrap: true }); +// break; +// case 'ArrowUp': +// focusManager.focusPrevious({ wrap: true }); +// break; +// case 'Enter': +// setOpen(false); +// setValue(label); +// break; +// } +// }, +// }); + +// return ( +// +// +// {label} +// +// { +// setOpen(false); +// setValue(label); +// }} +// > +// +// +// +// ); +// } +// ); + +// type SelectProps = { +// size?: InputProps['size']; +// variant?: InputProps['variant']; +// color?: InputProps['color']; +// value?: V; +// defaultValue?: V; +// onChangeValue?: (value: V) => void; +// open?: boolean; + +// placeholder?: string; +// items?: { value: string; label: string }[]; +// label?: string; +// }; + +// function SelectRoot( +// params: Omit, 'children'> +// ): ReactNode; +// function SelectRoot( +// params: Omit, 'items'> +// ): ReactNode; +// function SelectRoot({ +// size = 'md', +// variant = 'outlined', +// color = 'neutral', +// children, +// open: openProps = false, +// items, +// label, +// value: valueProps, +// defaultValue, +// onChangeValue, +// placeholder, +// }: PropsWithChildren) { +// const [open, setOpen] = useState(openProps); + +// const [value, setValue] = useUncontrolled({ +// value: valueProps, +// defaultValue, +// finalValue: '', +// onChange: onChangeValue, +// }); + +// const floating = useFloating({ +// open: open, +// onOpenChange: setOpen, +// middleware: [offset(10), flip(), shift()], +// whileElementsMounted: autoUpdate, +// }); + +// const { context } = floating; + +// const click = useClick(context); +// const dismiss = useDismiss(context); +// const role = useRole(context); + +// const interactions = useInteractions([click, dismiss, role]); + +// return ( +// +// +// +// {children ?? +// [ +// label && {label}, +// +// +// +// +// +// , +// +// {items?.map(({ value: v, label: l }) => ( +// +// ))} +// , +// ].filter(Boolean)} +// +// +// +// ); +// } + +const RootFrame = styled(TextInput, { + extends: [ButtonFrame.styles], + props: { editable: false }, + className: ['cursor-pointer'], +}); + +type FloatingProvider = ReturnType & + ReturnType; + +const [FloatingProvider, useFloatingProvider] = createScope( + {} as FloatingProvider +); + +const SelectLabel = forwardRef( + (props: HtmlHTMLAttributes, ref: any) => { + return ( +

+ ); + } +); + +const SelectContent = (props: HtmlHTMLAttributes) => { + const { refs, floatingStyles, getFloatingProps } = useFloatingProvider(); + + return ( +
+ ); +}; + +const SelectItem = forwardRef( + ( + props: HtmlHTMLAttributes & { + value: string; + label: string; + }, + ref: any + ) => { + // const { value } = useSelectContext(); + return ( +