Skip to content

Commit

Permalink
feat: add Cascader shared components
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonn committed Dec 22, 2023
1 parent ebd0e18 commit aa6ff49
Show file tree
Hide file tree
Showing 16 changed files with 5,700 additions and 227 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"root": true,
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"next/core-web-vitals",
Expand Down
19 changes: 19 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { StorybookConfig } from '@storybook/nextjs';

const config: StorybookConfig = {
stories: ['../src/components/shared/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
16 changes: 16 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Preview } from '@storybook/react';
import '../src/styles/tailwind.css';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;
49 changes: 31 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
"name": "flycat",
"version": "0.2.7",
"private": true,
"scripts": {
"wasm:profile": "cd wasm && rustc --crate-type cdylib --target wasm32-unknown-unknown -C lto -C opt-level=z -o profile.wasm examples/profile.rs",
"dev": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next dev",
"build:wasm": "cd wasm && wasm-pack build --target web --release && cd ..",
"build": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next build",
"start": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next start",
"lint": "yarn run lint:js && yarn run lint:css && next lint",
"lint:js": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix",
"lint:css": "stylelint ./**/*.{scss,css} --fix",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start:prod": "yarn run build && serve -s build",
"test:generators": "ts-node ./internals/testing/generators/test-generators.ts",
"generate": "plop --plopfile internals/generators/plopfile.ts",
"extract-messages": "i18next-scanner --config=internals/extractMessages/i18next-scanner.config.js",
"prepare": "husky install",
"preinstall": "./scripts/install.sh",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ant-design/cssinjs": "1.9.1",
"@emoji-mart/react": "1.1.1",
Expand All @@ -11,6 +31,7 @@
"@next/bundle-analyzer": "14.0.3",
"@noble/hashes": "1.1.5",
"@noble/secp256k1": "1.7.0",
"@radix-ui/react-hover-card": "1.0.7",
"@radix-ui/react-popover": "1.0.7",
"@reduxjs/toolkit": "1.7.1",
"@testing-library/jest-dom": "5.16.1",
Expand Down Expand Up @@ -84,6 +105,7 @@
"react-github-contribution-calendar": "2.2.0",
"react-helmet-async": "1.3.0",
"react-i18next": "12.2.0",
"react-icons": "4.12.0",
"react-is": "18.1.0",
"react-lazyload": "3.2.0",
"react-markdown": "8.0.4",
Expand All @@ -109,24 +131,6 @@
"wasm": "file:./wasm/pkg",
"web-vitals": "2.1.2"
},
"scripts": {
"wasm:profile": "cd wasm && rustc --crate-type cdylib --target wasm32-unknown-unknown -C lto -C opt-level=z -o profile.wasm examples/profile.rs",
"dev": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next dev",
"build:wasm": "cd wasm && wasm-pack build --target web --release && cd ..",
"build": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next build",
"start": "REACT_APP_COMMIT_HASH=$(git rev-parse --short HEAD) next start",
"lint": "yarn run lint:js && yarn run lint:css && next lint",
"lint:js": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix",
"lint:css": "stylelint ./**/*.{scss,css} --fix",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start:prod": "yarn run build && serve -s build",
"test:generators": "ts-node ./internals/testing/generators/test-generators.ts",
"generate": "plop --plopfile internals/generators/plopfile.ts",
"extract-messages": "i18next-scanner --config=internals/extractMessages/i18next-scanner.config.js",
"prepare": "husky install",
"preinstall": "./scripts/install.sh"
},
"browserslist": {
"production": [
"chrome >= 67",
Expand Down Expand Up @@ -175,6 +179,14 @@
}
},
"devDependencies": {
"@storybook/addon-essentials": "^7.6.6",
"@storybook/addon-interactions": "^7.6.6",
"@storybook/addon-links": "^7.6.6",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/blocks": "^7.6.6",
"@storybook/nextjs": "^7.6.6",
"@storybook/react": "^7.6.6",
"@storybook/test": "^7.6.6",
"@stylelint/postcss-css-in-js": "0.38.0",
"@types/browser-image-compression": "1.0.9",
"@types/lodash-es": "4.17.12",
Expand All @@ -190,6 +202,7 @@
"eslint-plugin-unused-imports": "2.0.0",
"postcss": "8.4.32",
"postcss-scss": "4.0.9",
"storybook": "^7.6.6",
"stylelint": "16.0.2",
"stylelint-config-standard": "35.0.0",
"stylelint-config-standard-scss": "12.0.0",
Expand Down
4 changes: 1 addition & 3 deletions postcss.config.mjs → postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const config = {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

export default config;
52 changes: 52 additions & 0 deletions src/components/shared/Cascader/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Cascader } from '.';

const meta: Meta<typeof Cascader> = {
component: Cascader,
};

export default meta;
type Story = StoryObj<typeof Cascader>;

const options = [
{
value: 'Relay Group',
children: [
{
value: 'NIP Relay List',
},
{
value: 'Default Group',
},
],
group: 'Global',
},
{
value: 'Single Relay',
children: [
{
value: 'relay.nostr.band',
},
{
value: 'relay.snort.social',
},
],
group: 'Global',
},
{
value: 'Script',
disabled: true,
group: 'Rules',
},
];

export const Primary: Story = {
render: () => (
<div className="max-w-screen-sm">
<Cascader
options={options}
defaultValue={['Relay Group', 'Default Group']}
/>
</div>
),
};
53 changes: 42 additions & 11 deletions src/components/shared/Cascader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as Popover from '@radix-ui/react-popover';
import { groupBy } from 'lodash-es';
import { PropsWithChildren, useMemo, useState } from 'react';

interface Option {
value: string | number;
label?: React.ReactNode;
disabled?: boolean;
children?: Option[];
}
import { Option } from './types';
import { CascaderOption } from './option';

export type CascaderProps = PropsWithChildren<{
defaultValue?: string[];
Expand All @@ -21,6 +17,7 @@ const defaultDisplayRender = (label: string[]) => label.join(' / ');

export function Cascader(props: CascaderProps) {
const { defaultValue, options } = props;
const [opened, setOpened] = useState(false);
const [activeValue, setActiveValue] = useState<string[]>(defaultValue ?? []);
const displayRender = useMemo(
() => props.displayRender ?? defaultDisplayRender,
Expand All @@ -41,12 +38,46 @@ export function Cascader(props: CascaderProps) {
return selectedOptions;
}, [activeValue, options]);

const optionsGroup = useMemo(() => groupBy(options, 'group'), [options]);

const handleClick = (value: string[]) => {
setActiveValue(value);
setOpened(false);
};

return (
<Popover.Root>
<Popover.Trigger>
<div>{displayRender(activeValue, selectedOptions)}</div>
<Popover.Root open={opened} onOpenChange={setOpened}>
<Popover.Trigger asChild>
<div className="h-10 w-full border border-gray-200 hover:border-brand rounded-lg cursor-pointer select-none transition-colors">
<div className="h-full w-full py-2 px-3 flex items-center">
{displayRender(activeValue, selectedOptions)}
</div>
</div>
</Popover.Trigger>
<Popover.Content>Content</Popover.Content>
<Popover.Content
className="w-[var(--radix-popover-trigger-width)] rounded-lg overflow-hidden bg-surface-02 outline-none shadow"
sideOffset={5}
>
{Object.entries(optionsGroup).map(([group, options]) => (
<div key={group}>
<div className="py-2 px-3">
<span className="text-text-secondary font-noto font-medium text-xs leading-4">
{group}
</span>
</div>
<div className="flex flex-col">
{options.map(option => (
<CascaderOption
key={option.value}
option={option}
value={activeValue}
onClick={handleClick}
/>
))}
</div>
</div>
))}
</Popover.Content>
</Popover.Root>
);
}
81 changes: 81 additions & 0 deletions src/components/shared/Cascader/option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Option } from './types';
import { FiCheck } from 'react-icons/fi';
import { FiChevronRight } from 'react-icons/fi';
import * as HoverCard from '@radix-ui/react-hover-card';
import classnames from 'classnames';

export type CascaderOptionProps = {
option: Option;
value: string[];
path?: string[];
onClick(value: string[]): void;
};

export function CascaderOption(props: CascaderOptionProps) {
const { option, value, path = [], onClick } = props;
const isLeaf = !option.children || option.children.length === 0;
const isActive = value[path.length] === option.value;

const handleClick = () => {
if (!isLeaf || option.disabled) {
return;
}
onClick([...path, option.value]);
};

return (
<HoverCard.Root openDelay={200} closeDelay={200}>
<HoverCard.Trigger asChild>
<div
className={classnames(
'min-w-[320px] py-2 px-1 flex justify-between cursor-pointer select-none',
{
'bg-conditional-selected01': isActive,
'cursor-not-allowed': option.disabled,
},
'hover:bg-conditional-hover01',
)}
onClick={handleClick}
>
<div className="flex items-center gap-1">
<FiCheck
className={classnames('text-text-primary w-4 h-4 opacity-0', {
'opacity-100': isActive,
})}
/>
<span
className={classnames('font-noto text-text-primary text-sm', {
'text-text-secondary': option.disabled,
})}
>
{option.label ?? option.value}
</span>
</div>
{!isLeaf && (
<FiChevronRight className="text-text-secondary w-4 h-4" />
)}
</div>
</HoverCard.Trigger>
{!isLeaf && (
<HoverCard.Portal>
<HoverCard.Content
side="right"
align="start"
sideOffset={5}
className="rounded-lg bg-surface-02 overflow-hidden outline-none shadow"
>
{option.children?.map(child => (
<CascaderOption
key={child.value}
option={child}
value={value}
path={[...path, option.value]}
onClick={onClick}
/>
))}
</HoverCard.Content>
</HoverCard.Portal>
)}
</HoverCard.Root>
);
}
7 changes: 7 additions & 0 deletions src/components/shared/Cascader/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Option {
value: string;
label?: React.ReactNode;
disabled?: boolean;
children?: Option[];
group?: string;
}
1 change: 1 addition & 0 deletions src/pages/_app.page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../window';
import 'styles/tailwind.css';
import 'styles/global.scss';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
Expand Down
3 changes: 0 additions & 3 deletions src/styles/global.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './reset';
@import './react-tags-input';
@import './mui';
Expand Down
3 changes: 3 additions & 0 deletions src/styles/tailwind.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
Loading

0 comments on commit aa6ff49

Please sign in to comment.