feat: init
This commit is contained in:
3
packages/propel/.eslintignore
Normal file
3
packages/propel/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
29
packages/propel/.eslintrc.js
Normal file
29
packages/propel/.eslintrc.js
Normal file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js", "plugin:storybook/recommended"],
|
||||
rules: {
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "react",
|
||||
group: "external",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
5
packages/propel/.prettierignore
Normal file
5
packages/propel/.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.next
|
||||
.turbo
|
||||
out/
|
||||
dist/
|
||||
build/
|
||||
5
packages/propel/.prettierrc
Normal file
5
packages/propel/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
20
packages/propel/.storybook/main.ts
Normal file
20
packages/propel/.storybook/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
import { join, dirname } from "path";
|
||||
|
||||
/*
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string) {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||
addons: [getAbsolutePath("@storybook/addon-designs"), getAbsolutePath("@storybook/addon-docs")],
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/react-vite"),
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
14
packages/propel/.storybook/manager.ts
Normal file
14
packages/propel/.storybook/manager.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { addons } from "storybook/manager-api";
|
||||
import { create } from "storybook/theming";
|
||||
|
||||
const planeTheme = create({
|
||||
base: "dark",
|
||||
brandTitle: "Plane UI",
|
||||
brandUrl: "https://plane.so",
|
||||
brandImage: "plane-lockup-light.svg",
|
||||
brandTarget: "_self",
|
||||
});
|
||||
|
||||
addons.setConfig({
|
||||
theme: planeTheme,
|
||||
});
|
||||
15
packages/propel/.storybook/preview.ts
Normal file
15
packages/propel/.storybook/preview.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Preview } from "@storybook/react-vite";
|
||||
import "@plane/tailwind-config/global.css";
|
||||
import "../src/styles/react-day-picker.css";
|
||||
|
||||
const parameters: Preview["parameters"] = {
|
||||
controls: {
|
||||
matchers: {},
|
||||
},
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
export default preview;
|
||||
199
packages/propel/package.json
Normal file
199
packages/propel/package.json
Normal file
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tsdown --watch",
|
||||
"build": "tsdown",
|
||||
"check:lint": "eslint . --max-warnings 7",
|
||||
"check:types": "tsc --noEmit",
|
||||
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"fix:lint": "eslint . --fix",
|
||||
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"exports": {
|
||||
"./accordion": {
|
||||
"import": "./dist/accordion/index.mjs",
|
||||
"require": "./dist/accordion/index.js"
|
||||
},
|
||||
"./animated-counter": {
|
||||
"import": "./dist/animated-counter/index.mjs",
|
||||
"require": "./dist/animated-counter/index.js"
|
||||
},
|
||||
"./avatar": {
|
||||
"import": "./dist/avatar/index.mjs",
|
||||
"require": "./dist/avatar/index.js"
|
||||
},
|
||||
"./button": {
|
||||
"import": "./dist/button/index.mjs",
|
||||
"require": "./dist/button/index.js"
|
||||
},
|
||||
"./calendar": {
|
||||
"import": "./dist/calendar/index.mjs",
|
||||
"require": "./dist/calendar/index.js"
|
||||
},
|
||||
"./card": {
|
||||
"import": "./dist/card/index.mjs",
|
||||
"require": "./dist/card/index.js"
|
||||
},
|
||||
"./charts/area-chart": {
|
||||
"import": "./dist/charts/area-chart/index.mjs",
|
||||
"require": "./dist/charts/area-chart/index.js"
|
||||
},
|
||||
"./charts/bar-chart": {
|
||||
"import": "./dist/charts/bar-chart/index.mjs",
|
||||
"require": "./dist/charts/bar-chart/index.js"
|
||||
},
|
||||
"./charts/line-chart": {
|
||||
"import": "./dist/charts/line-chart/index.mjs",
|
||||
"require": "./dist/charts/line-chart/index.js"
|
||||
},
|
||||
"./charts/pie-chart": {
|
||||
"import": "./dist/charts/pie-chart/index.mjs",
|
||||
"require": "./dist/charts/pie-chart/index.js"
|
||||
},
|
||||
"./charts/radar-chart": {
|
||||
"import": "./dist/charts/radar-chart/index.mjs",
|
||||
"require": "./dist/charts/radar-chart/index.js"
|
||||
},
|
||||
"./charts/scatter-chart": {
|
||||
"import": "./dist/charts/scatter-chart/index.mjs",
|
||||
"require": "./dist/charts/scatter-chart/index.js"
|
||||
},
|
||||
"./charts/tree-map": {
|
||||
"import": "./dist/charts/tree-map/index.mjs",
|
||||
"require": "./dist/charts/tree-map/index.js"
|
||||
},
|
||||
"./collapsible": {
|
||||
"import": "./dist/collapsible/index.mjs",
|
||||
"require": "./dist/collapsible/index.js"
|
||||
},
|
||||
"./combobox": {
|
||||
"import": "./dist/combobox/index.mjs",
|
||||
"require": "./dist/combobox/index.js"
|
||||
},
|
||||
"./command": {
|
||||
"import": "./dist/command/index.mjs",
|
||||
"require": "./dist/command/index.js"
|
||||
},
|
||||
"./context-menu": {
|
||||
"import": "./dist/context-menu/index.mjs",
|
||||
"require": "./dist/context-menu/index.js"
|
||||
},
|
||||
"./dialog": {
|
||||
"import": "./dist/dialog/index.mjs",
|
||||
"require": "./dist/dialog/index.js"
|
||||
},
|
||||
"./emoji-icon-picker": {
|
||||
"import": "./dist/emoji-icon-picker/index.mjs",
|
||||
"require": "./dist/emoji-icon-picker/index.js"
|
||||
},
|
||||
"./emoji-reaction": {
|
||||
"import": "./dist/emoji-reaction/index.mjs",
|
||||
"require": "./dist/emoji-reaction/index.js"
|
||||
},
|
||||
"./empty-state": {
|
||||
"import": "./dist/empty-state/index.mjs",
|
||||
"require": "./dist/empty-state/index.js"
|
||||
},
|
||||
"./icons": {
|
||||
"import": "./dist/icons/index.mjs",
|
||||
"require": "./dist/icons/index.js"
|
||||
},
|
||||
"./input": {
|
||||
"import": "./dist/input/index.mjs",
|
||||
"require": "./dist/input/index.js"
|
||||
},
|
||||
"./menu": {
|
||||
"import": "./dist/menu/index.mjs",
|
||||
"require": "./dist/menu/index.js"
|
||||
},
|
||||
"./pill": {
|
||||
"import": "./dist/pill/index.mjs",
|
||||
"require": "./dist/pill/index.js"
|
||||
},
|
||||
"./popover": {
|
||||
"import": "./dist/popover/index.mjs",
|
||||
"require": "./dist/popover/index.js"
|
||||
},
|
||||
"./portal": {
|
||||
"import": "./dist/portal/index.mjs",
|
||||
"require": "./dist/portal/index.js"
|
||||
},
|
||||
"./scrollarea": {
|
||||
"import": "./dist/scrollarea/index.mjs",
|
||||
"require": "./dist/scrollarea/index.js"
|
||||
},
|
||||
"./skeleton": {
|
||||
"import": "./dist/skeleton/index.mjs",
|
||||
"require": "./dist/skeleton/index.js"
|
||||
},
|
||||
"./switch": {
|
||||
"import": "./dist/switch/index.mjs",
|
||||
"require": "./dist/switch/index.js"
|
||||
},
|
||||
"./table": {
|
||||
"import": "./dist/table/index.mjs",
|
||||
"require": "./dist/table/index.js"
|
||||
},
|
||||
"./tabs": {
|
||||
"import": "./dist/tabs/index.mjs",
|
||||
"require": "./dist/tabs/index.js"
|
||||
},
|
||||
"./toast": {
|
||||
"import": "./dist/toast/index.mjs",
|
||||
"require": "./dist/toast/index.js"
|
||||
},
|
||||
"./toolbar": {
|
||||
"import": "./dist/toolbar/index.mjs",
|
||||
"require": "./dist/toolbar/index.js"
|
||||
},
|
||||
"./tooltip": {
|
||||
"import": "./dist/tooltip/index.mjs",
|
||||
"require": "./dist/tooltip/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"import": "./dist/utils/index.mjs",
|
||||
"require": "./dist/utils/index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./styles/fonts": "./dist/styles/fonts/index.css",
|
||||
"./styles/react-day-picker": "./dist/styles/react-day-picker.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "1.0.0-beta.3",
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/hooks": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"frimousse": "^0.3.0",
|
||||
"lucide-react": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-day-picker": "9.5.0",
|
||||
"react-dom": "catalog:",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"use-font-face-observer": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@storybook/addon-designs": "10.0.2",
|
||||
"@storybook/addon-docs": "9.1.10",
|
||||
"@storybook/react-vite": "9.1.10",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"eslint-plugin-storybook": "9.1.10",
|
||||
"storybook": "9.1.10",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
2
packages/propel/postcss.config.js
Normal file
2
packages/propel/postcss.config.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
module.exports = require("@plane/tailwind-config/postcss.config.js");
|
||||
16
packages/propel/public/plane-lockup-light.svg
Normal file
16
packages/propel/public/plane-lockup-light.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="253" height="53" viewBox="0 0 253 53" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_76)">
|
||||
<path d="M217.077 51.4468H210.972L210.98 31.4317C210.717 27.7545 208.786 25.1545 205.03 24.6199C198.587 23.7006 194.663 27.8828 194.21 34.0258L194.218 51.4468H188.041V20.2543H192.158L194.255 26.8846H194.6C195.825 22.8701 198.684 20.4831 202.898 19.5757C210.126 18.0231 216.643 22.2724 217.079 29.8239V51.4468H217.077Z" fill="#ffffff"/>
|
||||
<path d="M163.812 19.7315C158.03 20.9566 153.67 25.2354 153.222 31.264H159.395C159.669 28.3484 161.561 25.9969 164.267 24.9553C167.418 23.7401 172.805 23.8881 175.178 26.6005C176.774 28.4253 177.305 31.5816 175.212 33.2249C172.675 35.2173 166.39 34.6117 163.214 34.7636C158.631 34.9826 154.141 36.8429 153.183 41.7727C151.1 52.5042 165.035 54.9267 171.902 49.8963C174.095 48.2885 175.672 45.8443 176.261 43.1792H176.577L178.788 51.4508H182.61V29.964C182.517 20.3214 171.462 18.108 163.81 19.7315H163.812ZM174.847 41.2637C172.484 45.649 166.958 49.0441 162.218 47.3318C158.44 45.9686 158.268 40.4095 161.973 38.9181C165.613 37.4524 173.732 38.9694 176.368 34.949C176.275 37.0855 175.906 39.2989 174.849 41.2637H174.847Z" fill="#ffffff"/>
|
||||
<path d="M113.082 35.5652V51.4475H106.611V9.00063H124.863C125.007 8.99274 125.141 9.06178 125.283 9.06967C133.732 9.53129 139.297 13.601 139.104 22.6025C138.928 30.7991 132.663 35.2199 124.934 35.5671H113.082V35.5652ZM113.082 14.4828V30.2941H124.088C129.595 29.8956 133.071 27.1022 132.491 21.265C131.967 15.9781 127.802 14.6544 123.107 14.4788C119.795 14.3546 116.402 14.5795 113.082 14.4808V14.4828Z" fill="#ffffff"/>
|
||||
<path d="M148.647 9H142.529V51.4468H148.647V9Z" fill="#ffffff"/>
|
||||
<path d="M245.872 42.418C244.911 44.4953 243.138 46.1603 240.934 46.8981C238.368 47.7562 234.479 47.5431 232.083 46.2569C228.67 44.4263 227.145 40.6485 227.137 36.8984H251.891C252.194 33.6908 251.902 30.3648 250.496 27.4333C247.083 20.3177 238.859 17.7236 231.513 19.9429C222.868 22.5568 220.165 31.4162 221.462 39.6543C222.86 48.5611 230.197 52.976 238.958 52.3585C244.884 51.9423 250.271 48.1192 251.788 42.4161H245.874L245.872 42.418ZM227.208 32.4381C227.778 26.2457 233.347 23.2018 239.163 24.4269C243.361 25.3107 245.75 28.9523 245.842 33.1068H227.145C227.145 33.1068 227.19 32.6255 227.208 32.4401V32.4381Z" fill="#ffffff"/>
|
||||
<path d="M44.3223 2.9264C44.3223 0.754665 46.6083 -0.65811 48.5508 0.313121L80.4551 16.2653C82.9294 17.5024 84.4922 20.0321 84.4922 22.7985V48.2487C84.4922 50.4204 82.2071 51.833 80.2646 50.8619L62.3281 41.8932V22.7975C62.3281 20.0311 60.7653 17.5015 58.291 16.2643L44.3223 9.27992V2.9264ZM0 2.92543C8.01645e-05 0.753753 2.28609 -0.659069 4.22852 0.312144L22.1582 9.27699V28.3766C22.1582 31.1428 23.7213 33.6716 26.1953 34.9088L40.1699 41.8952V48.2487C40.1697 50.4202 37.8847 51.832 35.9424 50.861L4.03711 34.9088C1.56305 33.6716 0 31.1428 0 28.3766V2.92543ZM22.1582 2.92543C22.1583 0.753753 24.4443 -0.659069 26.3867 0.312144L44.3223 9.27992V28.3776C44.3223 31.1439 45.8861 33.6727 48.3604 34.9098L62.3281 41.8932V48.2487C62.3279 50.4202 60.0429 51.832 58.1006 50.861L40.1699 41.8952V22.7975C40.1699 20.0311 38.6071 17.5015 36.1328 16.2643L22.1582 9.27699V2.92543Z" fill="#ffffff"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_76">
|
||||
<rect width="252" height="53" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
198
packages/propel/src/accordion/accordion.stories.tsx
Normal file
198
packages/propel/src/accordion/accordion.stories.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Accordion } from "./accordion";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Accordion",
|
||||
component: Accordion.Root,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { disable: true },
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
subcomponents: {
|
||||
Item: Accordion.Item,
|
||||
Trigger: Accordion.Trigger,
|
||||
Content: Accordion.Content,
|
||||
},
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
} satisfies Meta<typeof Accordion.Root>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>What is Plane?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
Plane is an open-source project management tool designed for developers and teams to plan, track, and manage
|
||||
their work efficiently.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>How do I get started?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
You can get started by signing up for an account, creating your first workspace, and inviting your team
|
||||
members to collaborate.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-3">
|
||||
<Accordion.Trigger>Is it free to use?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
Plane offers both free and paid plans. The free plan includes essential features for small teams, while paid
|
||||
plans unlock advanced functionality.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleOpen: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root defaultValue={["item-1"]} className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Section 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content for section 1. Only one section can be open at a time.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Section 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content for section 2.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-3">
|
||||
<Accordion.Trigger>Section 3</Accordion.Trigger>
|
||||
<Accordion.Content>Content for section 3.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllowMultiple: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root allowMultiple defaultValue={["item-1", "item-2"]} className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>First Section</Accordion.Trigger>
|
||||
<Accordion.Content>Multiple sections can be open at the same time.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Second Section</Accordion.Trigger>
|
||||
<Accordion.Content>This section is also open by default.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-3">
|
||||
<Accordion.Trigger>Third Section</Accordion.Trigger>
|
||||
<Accordion.Content>You can open this section while keeping the others open.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDisabledItem: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Enabled Section</Accordion.Trigger>
|
||||
<Accordion.Content>This section can be toggled.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2" disabled>
|
||||
<Accordion.Trigger>Disabled Section</Accordion.Trigger>
|
||||
<Accordion.Content>This content cannot be accessed.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-3">
|
||||
<Accordion.Trigger>Another Enabled Section</Accordion.Trigger>
|
||||
<Accordion.Content>This section can also be toggled.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIcon: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger
|
||||
icon={
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-transform group-data-[panel-open]:rotate-180"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
Custom Chevron Icon
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
This accordion uses a custom chevron icon instead of the default plus icon.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger
|
||||
icon={
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-transform group-data-[panel-open]:rotate-180"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
Another Section
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>All items in this accordion use the custom icon.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AsChildTrigger: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Accordion.Root className="w-96">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger asChild>
|
||||
<button className="w-full rounded-md bg-blue-500 px-4 py-2 text-left text-white hover:bg-blue-600">
|
||||
Custom Button Trigger
|
||||
</button>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
When using asChild, you can completely customize the trigger element without the default icon wrapper.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger asChild>
|
||||
<button className="w-full rounded-md bg-green-500 px-4 py-2 text-left text-white hover:bg-green-600">
|
||||
Another Custom Trigger
|
||||
</button>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>This gives you full control over the trigger styling and behavior.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
},
|
||||
};
|
||||
88
packages/propel/src/accordion/accordion.tsx
Normal file
88
packages/propel/src/accordion/accordion.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as React from "react";
|
||||
import { Accordion as BaseAccordion } from "@base-ui-components/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
export interface AccordionRootProps {
|
||||
defaultValue?: string[];
|
||||
allowMultiple?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface AccordionItemProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface AccordionTriggerProps {
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
export interface AccordionContentProps {
|
||||
className?: string;
|
||||
contentWrapperClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AccordionRoot: React.FC<AccordionRootProps> = ({
|
||||
defaultValue = [],
|
||||
allowMultiple = false,
|
||||
className = "",
|
||||
children,
|
||||
}) => (
|
||||
<BaseAccordion.Root defaultValue={defaultValue} openMultiple={allowMultiple} className={`text-base ${className}`}>
|
||||
{children}
|
||||
</BaseAccordion.Root>
|
||||
);
|
||||
|
||||
const AccordionItem: React.FC<AccordionItemProps> = ({ value, disabled, className = "", children }) => (
|
||||
<BaseAccordion.Item value={value} disabled={disabled} className={`relative ${className}`}>
|
||||
{children}
|
||||
</BaseAccordion.Item>
|
||||
);
|
||||
|
||||
const AccordionTrigger: React.FC<AccordionTriggerProps> = ({
|
||||
className = "",
|
||||
icon = <PlusIcon aria-hidden="true" className="transition-all ease-out group-data-[panel-open]:rotate-45" />,
|
||||
iconClassName = "",
|
||||
children,
|
||||
asChild = false,
|
||||
}) => (
|
||||
<BaseAccordion.Header>
|
||||
{asChild ? (
|
||||
<BaseAccordion.Trigger className={`w-full py-2 ${className}`}>{children}</BaseAccordion.Trigger>
|
||||
) : (
|
||||
<BaseAccordion.Trigger className={`flex w-full items-center justify-between gap-2 py-2 ${className}`}>
|
||||
{children}
|
||||
<span aria-hidden="true" className={`flex-shrink-0 ${iconClassName}`}>
|
||||
{icon}
|
||||
</span>
|
||||
</BaseAccordion.Trigger>
|
||||
)}
|
||||
</BaseAccordion.Header>
|
||||
);
|
||||
|
||||
const AccordionContent: React.FC<AccordionContentProps> = ({
|
||||
className = "",
|
||||
contentWrapperClassName = "",
|
||||
children,
|
||||
}) => (
|
||||
<BaseAccordion.Panel
|
||||
className={`h-[var(--accordion-panel-height)] overflow-hidden transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 ${className}`}
|
||||
>
|
||||
<div className={`py-2 ${contentWrapperClassName}`}>{children}</div>
|
||||
</BaseAccordion.Panel>
|
||||
);
|
||||
|
||||
export const Accordion = {
|
||||
Root: AccordionRoot,
|
||||
Item: AccordionItem,
|
||||
Trigger: AccordionTrigger,
|
||||
Content: AccordionContent,
|
||||
};
|
||||
1
packages/propel/src/accordion/index.ts
Normal file
1
packages/propel/src/accordion/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./accordion";
|
||||
@@ -0,0 +1,333 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AnimatedCounter } from "./animated-counter";
|
||||
|
||||
const meta = {
|
||||
title: "Components/AnimatedCounter",
|
||||
component: AnimatedCounter,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
size: "md",
|
||||
count: 0,
|
||||
},
|
||||
} satisfies Meta<typeof AnimatedCounter>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render(args) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white font-medium rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2 transition-colors shadow-md"
|
||||
onClick={() => setCount((prev) => Math.max(0, prev - 1))}
|
||||
>
|
||||
-1
|
||||
</button>
|
||||
<div className="flex items-center justify-center min-w-[60px] h-12 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<AnimatedCounter {...args} count={count} />
|
||||
</div>
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white font-medium rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors shadow-md"
|
||||
onClick={() => setCount((prev) => prev + 1)}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render() {
|
||||
const [count, setCount] = useState(42);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="px-3 py-1 bg-custom-background-80 text-sm rounded hover:bg-custom-background-90"
|
||||
onClick={() => setCount((prev) => Math.max(0, prev - 1))}
|
||||
>
|
||||
-1
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 bg-custom-background-80 text-sm rounded hover:bg-custom-background-90"
|
||||
onClick={() => setCount((prev) => prev + 1)}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-custom-text-400 w-20">Small:</span>
|
||||
<div className="flex items-center justify-center min-w-[40px] h-8 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={count} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-custom-text-400 w-20">Medium:</span>
|
||||
<div className="flex items-center justify-center min-w-[50px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={count} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-custom-text-400 w-20">Large:</span>
|
||||
<div className="flex items-center justify-center min-w-[60px] h-12 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={count} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeNumbers: Story = {
|
||||
render() {
|
||||
const [count, setCount] = useState(1234567);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600"
|
||||
onClick={() => setCount((prev) => Math.max(0, prev - 1000))}
|
||||
>
|
||||
-1000
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600"
|
||||
onClick={() => setCount((prev) => prev + 1000)}
|
||||
>
|
||||
+1000
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-center min-w-[100px] h-12 bg-custom-background-80 border border-custom-border-200 rounded-lg">
|
||||
<AnimatedCounter count={count} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Countdown: Story = {
|
||||
render() {
|
||||
const [count, setCount] = useState(10);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunning && count > 0) {
|
||||
const timer = setTimeout(() => setCount((prev) => prev - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
if (count === 0) {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [count, isRunning]);
|
||||
|
||||
const handleStart = () => {
|
||||
setCount(10);
|
||||
setIsRunning(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center justify-center min-w-[60px] h-16 bg-custom-background-80 border-2 border-custom-border-200 rounded-lg">
|
||||
<AnimatedCounter count={count} size="lg" className="text-2xl" />
|
||||
</div>
|
||||
<button
|
||||
className="px-6 py-2 bg-custom-primary-100 text-white font-medium rounded-lg hover:bg-custom-primary-200"
|
||||
onClick={handleStart}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{isRunning ? "Counting..." : "Start Countdown"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LiveCounter: Story = {
|
||||
render() {
|
||||
const [count, setCount] = useState(0);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunning) {
|
||||
const timer = setInterval(() => setCount((prev) => prev + 1), 500);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
}, [isRunning]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center justify-center min-w-[80px] h-16 bg-custom-background-80 border-2 border-custom-border-200 rounded-lg">
|
||||
<AnimatedCounter count={count} size="lg" className="text-2xl" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white font-medium rounded hover:bg-green-600"
|
||||
onClick={() => setIsRunning(true)}
|
||||
disabled={isRunning}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white font-medium rounded hover:bg-red-600"
|
||||
onClick={() => setIsRunning(false)}
|
||||
disabled={!isRunning}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-gray-500 text-white font-medium rounded hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
setIsRunning(false);
|
||||
setCount(0);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleCounters: Story = {
|
||||
render() {
|
||||
const [likes, setLikes] = useState(42);
|
||||
const [comments, setComments] = useState(15);
|
||||
const [shares, setShares] = useState(8);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="max-w-md border border-custom-border-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium">Engagement Stats</h3>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 flex flex-col items-center gap-2">
|
||||
<div className="text-custom-text-400 text-sm">Likes</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setLikes((prev) => prev + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={likes} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center gap-2">
|
||||
<div className="text-custom-text-400 text-sm">Comments</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setComments((prev) => prev + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={comments} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center gap-2">
|
||||
<div className="text-custom-text-400 text-sm">Shares</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setShares((prev) => prev + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
|
||||
<AnimatedCounter count={shares} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InBadge: Story = {
|
||||
render() {
|
||||
const [notifications, setNotifications] = useState(3);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative">
|
||||
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded-lg">
|
||||
Notifications
|
||||
</button>
|
||||
<div className="absolute -top-2 -right-2 min-w-[24px] h-6 flex items-center justify-center bg-red-500 text-white rounded-full px-1.5">
|
||||
<AnimatedCounter count={notifications} size="sm" className="text-xs font-medium" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
|
||||
onClick={() => setNotifications((prev) => prev + 1)}
|
||||
>
|
||||
Add Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FastAnimation: Story = {
|
||||
render() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const incrementFast = () => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
setTimeout(() => setCount((prev) => prev + 1), i * 50);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center justify-center min-w-[60px] h-12 bg-custom-background-80 border border-custom-border-200 rounded-lg">
|
||||
<AnimatedCounter count={count} size="lg" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
|
||||
onClick={incrementFast}
|
||||
>
|
||||
+10 Fast
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setCount(0)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
95
packages/propel/src/animated-counter/animated-counter.tsx
Normal file
95
packages/propel/src/animated-counter/animated-counter.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export interface AnimatedCounterProps {
|
||||
count: number;
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-xs h-4 w-4",
|
||||
md: "text-sm h-5 w-5",
|
||||
lg: "text-base h-6 w-6",
|
||||
};
|
||||
|
||||
export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({ count, className, size = "md" }) => {
|
||||
// states
|
||||
const [displayCount, setDisplayCount] = useState(count);
|
||||
const [prevCount, setPrevCount] = useState(count);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [direction, setDirection] = useState<"up" | "down" | null>(null);
|
||||
const [animationKey, setAnimationKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (count !== prevCount) {
|
||||
setDirection(count > prevCount ? "up" : "down");
|
||||
setIsAnimating(true);
|
||||
setAnimationKey((prev) => prev + 1);
|
||||
|
||||
// Update the display count immediately, animation will show the transition
|
||||
setDisplayCount(count);
|
||||
|
||||
// End animation after CSS transition
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
setDirection(null);
|
||||
setPrevCount(count);
|
||||
}, 250);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [count, prevCount]);
|
||||
|
||||
const sizeClass = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div className={cn("relative inline-flex items-center justify-center overflow-hidden", sizeClass)}>
|
||||
{/* Previous number sliding out */}
|
||||
{isAnimating && (
|
||||
<span
|
||||
key={`prev-${animationKey}`}
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center font-medium",
|
||||
"animate-[slideOut_0.25s_ease-out_forwards]",
|
||||
direction === "up" && "[--slide-out-dir:-100%]",
|
||||
direction === "down" && "[--slide-out-dir:100%]",
|
||||
sizeClass
|
||||
)}
|
||||
style={{
|
||||
animation:
|
||||
direction === "up"
|
||||
? "slideOut 0.25s ease-out forwards, fadeOut 0.25s ease-out forwards"
|
||||
: "slideOutDown 0.25s ease-out forwards, fadeOut 0.25s ease-out forwards",
|
||||
}}
|
||||
>
|
||||
{prevCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* New number sliding in */}
|
||||
<span
|
||||
key={`current-${animationKey}`}
|
||||
className={cn(
|
||||
"flex items-center justify-center font-medium",
|
||||
isAnimating && "animate-[slideIn_0.25s_ease-out_forwards]",
|
||||
!isAnimating && "opacity-100",
|
||||
sizeClass,
|
||||
className
|
||||
)}
|
||||
style={
|
||||
isAnimating
|
||||
? {
|
||||
animation:
|
||||
direction === "up"
|
||||
? "slideInFromBottom 0.25s ease-out forwards"
|
||||
: "slideInFromTop 0.25s ease-out forwards",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{displayCount}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
packages/propel/src/animated-counter/index.ts
Normal file
2
packages/propel/src/animated-counter/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AnimatedCounter } from "./animated-counter";
|
||||
export type { AnimatedCounterProps } from "./animated-counter";
|
||||
171
packages/propel/src/avatar/avatar.stories.tsx
Normal file
171
packages/propel/src/avatar/avatar.stories.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Avatar } from "./avatar";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Avatar",
|
||||
component: Avatar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
name: "John Doe",
|
||||
src: "https://i.pravatar.cc/150?img=1",
|
||||
},
|
||||
} satisfies Meta<typeof Avatar>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithName: Story = {
|
||||
args: {
|
||||
name: "Jane Smith",
|
||||
src: "https://i.pravatar.cc/150?img=5",
|
||||
},
|
||||
};
|
||||
|
||||
export const Fallback: Story = {
|
||||
args: {
|
||||
name: "Alice Johnson",
|
||||
src: "invalid-url",
|
||||
},
|
||||
};
|
||||
|
||||
export const FallbackWithCustomColor: Story = {
|
||||
args: {
|
||||
name: "Bob Wilson",
|
||||
src: "invalid-url",
|
||||
fallbackBackgroundColor: "#3b82f6",
|
||||
fallbackTextColor: "#ffffff",
|
||||
},
|
||||
};
|
||||
|
||||
export const FallbackWithCustomText: Story = {
|
||||
args: {
|
||||
fallbackText: "AB",
|
||||
src: "invalid-url",
|
||||
fallbackBackgroundColor: "#10b981",
|
||||
fallbackTextColor: "#ffffff",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
name: "Small Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=2",
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
name: "Medium Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=3",
|
||||
size: "md",
|
||||
},
|
||||
};
|
||||
|
||||
export const Base: Story = {
|
||||
args: {
|
||||
name: "Base Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=4",
|
||||
size: "base",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
name: "Large Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=6",
|
||||
size: "lg",
|
||||
},
|
||||
};
|
||||
|
||||
export const CircleShape: Story = {
|
||||
args: {
|
||||
name: "Circle Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=7",
|
||||
shape: "circle",
|
||||
},
|
||||
};
|
||||
|
||||
export const SquareShape: Story = {
|
||||
args: {
|
||||
name: "Square Avatar",
|
||||
src: "https://i.pravatar.cc/150?img=8",
|
||||
shape: "square",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllSizes: Story = {
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar name="Small" src="https://i.pravatar.cc/150?img=10" size="sm" />
|
||||
<Avatar name="Medium" src="https://i.pravatar.cc/150?img=11" size="md" />
|
||||
<Avatar name="Base" src="https://i.pravatar.cc/150?img=12" size="base" />
|
||||
<Avatar name="Large" src="https://i.pravatar.cc/150?img=13" size="lg" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllShapes: Story = {
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar name="Circle" src="https://i.pravatar.cc/150?img=14" shape="circle" />
|
||||
<Avatar name="Square" src="https://i.pravatar.cc/150?img=15" shape="square" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FallbackVariations: Story = {
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar name="Alice" src="invalid-url" fallbackBackgroundColor="#ef4444" fallbackTextColor="#ffffff" />
|
||||
<Avatar name="Bob" src="invalid-url" fallbackBackgroundColor="#f59e0b" fallbackTextColor="#ffffff" />
|
||||
<Avatar name="Charlie" src="invalid-url" fallbackBackgroundColor="#10b981" fallbackTextColor="#ffffff" />
|
||||
<Avatar name="David" src="invalid-url" fallbackBackgroundColor="#3b82f6" fallbackTextColor="#ffffff" />
|
||||
<Avatar name="Eve" src="invalid-url" fallbackBackgroundColor="#8b5cf6" fallbackTextColor="#ffffff" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AvatarGroup: Story = {
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className="flex -space-x-2">
|
||||
<Avatar name="User 1" src="https://i.pravatar.cc/150?img=20" size="md" className="ring-2 ring-white" />
|
||||
<Avatar name="User 2" src="https://i.pravatar.cc/150?img=21" size="md" className="ring-2 ring-white" />
|
||||
<Avatar name="User 3" src="https://i.pravatar.cc/150?img=22" size="md" className="ring-2 ring-white" />
|
||||
<Avatar name="User 4" src="https://i.pravatar.cc/150?img=23" size="md" className="ring-2 ring-white" />
|
||||
<Avatar
|
||||
fallbackText="+5"
|
||||
src="invalid-url"
|
||||
size="md"
|
||||
fallbackBackgroundColor="#6b7280"
|
||||
fallbackTextColor="#ffffff"
|
||||
className="ring-2 ring-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
119
packages/propel/src/avatar/avatar.tsx
Normal file
119
packages/propel/src/avatar/avatar.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
|
||||
|
||||
type Props = {
|
||||
name?: string; //The name of the avatar which will be displayed on the tooltip
|
||||
fallbackBackgroundColor?: string; //The background color if the avatar image fails to load
|
||||
fallbackText?: string;
|
||||
fallbackTextColor?: string; //The text color if the avatar image fails to load
|
||||
showTooltip?: boolean;
|
||||
size?: TAvatarSize; //The size of the avatars
|
||||
shape?: "circle" | "square";
|
||||
src?: string; //The source of the avatar image
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the size details based on the size prop
|
||||
* @param size The size of the avatar
|
||||
* @returns The size details
|
||||
*/
|
||||
export const getSizeInfo = (size: TAvatarSize) => {
|
||||
switch (size) {
|
||||
case "sm":
|
||||
return {
|
||||
avatarSize: "h-4 w-4",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
case "md":
|
||||
return {
|
||||
avatarSize: "h-5 w-5",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
case "base":
|
||||
return {
|
||||
avatarSize: "h-6 w-6",
|
||||
fontSize: "text-sm",
|
||||
spacing: "-space-x-1.5",
|
||||
};
|
||||
case "lg":
|
||||
return {
|
||||
avatarSize: "h-7 w-7",
|
||||
fontSize: "text-sm",
|
||||
spacing: "-space-x-1.5",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
avatarSize: "h-5 w-5",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the border radius based on the shape prop
|
||||
* @param shape The shape of the avatar
|
||||
* @returns The border radius
|
||||
*/
|
||||
export const getBorderRadius = (shape: "circle" | "square") => {
|
||||
switch (shape) {
|
||||
case "circle":
|
||||
return "rounded-full";
|
||||
case "square":
|
||||
return "rounded";
|
||||
default:
|
||||
return "rounded-full";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the value is a valid number
|
||||
* @param value The value to check
|
||||
* @returns Whether the value is a valid number or not
|
||||
*/
|
||||
export const isAValidNumber = (value: unknown): value is number => typeof value === "number" && !Number.isNaN(value);
|
||||
|
||||
export const Avatar: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
fallbackBackgroundColor,
|
||||
fallbackText,
|
||||
fallbackTextColor,
|
||||
size = "md",
|
||||
shape = "circle",
|
||||
src,
|
||||
className = "",
|
||||
} = props;
|
||||
|
||||
// get size details based on the size prop
|
||||
const sizeInfo = getSizeInfo(size);
|
||||
|
||||
const fallbackLetter = name?.[0]?.toUpperCase() ?? fallbackText ?? "?";
|
||||
return (
|
||||
<div
|
||||
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
|
||||
[sizeInfo.avatarSize]: !isAValidNumber(size),
|
||||
})}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<AvatarPrimitive.Root className={cn("h-full w-full", getBorderRadius(shape), className)}>
|
||||
<AvatarPrimitive.Image src={src} width="48" height="48" />
|
||||
<AvatarPrimitive.Fallback
|
||||
className={cn(sizeInfo.fontSize, "grid h-full w-full place-items-center", getBorderRadius(shape), className)}
|
||||
style={{
|
||||
backgroundColor: fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
|
||||
color: fallbackTextColor ?? "#ffffff",
|
||||
}}
|
||||
>
|
||||
{fallbackLetter}
|
||||
</AvatarPrimitive.Fallback>
|
||||
</AvatarPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
packages/propel/src/avatar/index.ts
Normal file
1
packages/propel/src/avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./avatar";
|
||||
245
packages/propel/src/button/button.stories.tsx
Normal file
245
packages/propel/src/button/button.stories.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Button } from "./button";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: "Button",
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
children: "Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const AccentPrimary: Story = {
|
||||
args: {
|
||||
variant: "accent-primary",
|
||||
children: "Accent Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlinePrimary: Story = {
|
||||
args: {
|
||||
variant: "outline-primary",
|
||||
children: "Outline Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const NeutralPrimary: Story = {
|
||||
args: {
|
||||
variant: "neutral-primary",
|
||||
children: "Neutral Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinkPrimary: Story = {
|
||||
args: {
|
||||
variant: "link-primary",
|
||||
children: "Link Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
variant: "danger",
|
||||
children: "Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const AccentDanger: Story = {
|
||||
args: {
|
||||
variant: "accent-danger",
|
||||
children: "Accent Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineDanger: Story = {
|
||||
args: {
|
||||
variant: "outline-danger",
|
||||
children: "Outline Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinkDanger: Story = {
|
||||
args: {
|
||||
variant: "link-danger",
|
||||
children: "Link Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const TertiaryDanger: Story = {
|
||||
args: {
|
||||
variant: "tertiary-danger",
|
||||
children: "Tertiary Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinkNeutral: Story = {
|
||||
args: {
|
||||
variant: "link-neutral",
|
||||
children: "Link Neutral Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
children: "Small Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
children: "Medium Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: "lg",
|
||||
children: "Large Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
size: "xl",
|
||||
children: "Extra Large Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
loading: true,
|
||||
children: "Loading Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: "Disabled Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPrependIcon: Story = {
|
||||
args: {
|
||||
prependIcon: (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
),
|
||||
children: "With Prepend Icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAppendIcon: Story = {
|
||||
args: {
|
||||
appendIcon: (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
),
|
||||
children: "With Append Icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Primary Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="accent-primary">Accent Primary</Button>
|
||||
<Button variant="outline-primary">Outline Primary</Button>
|
||||
<Button variant="neutral-primary">Neutral Primary</Button>
|
||||
<Button variant="link-primary">Link Primary</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Danger Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="danger">Danger</Button>
|
||||
<Button variant="accent-danger">Accent Danger</Button>
|
||||
<Button variant="outline-danger">Outline Danger</Button>
|
||||
<Button variant="link-danger">Link Danger</Button>
|
||||
<Button variant="tertiary-danger">Tertiary Danger</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Other Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="link-neutral">Link Neutral</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="md">Medium</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="xl">Extra Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllStates: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Button States</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>Default</Button>
|
||||
<Button loading>Loading</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
44
packages/propel/src/button/button.tsx
Normal file
44
packages/propel/src/button/button.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils";
|
||||
import { getIconStyling, getButtonStyling, TButtonVariant, TButtonSizes } from "./helper";
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: TButtonVariant;
|
||||
size?: TButtonSizes;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
appendIcon?: any;
|
||||
prependIcon?: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||
const {
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
type = "button",
|
||||
loading = false,
|
||||
disabled = false,
|
||||
prependIcon = null,
|
||||
appendIcon = null,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const buttonStyle = getButtonStyling(variant, size, disabled || loading);
|
||||
const buttonIconStyle = getIconStyling(size);
|
||||
|
||||
return (
|
||||
<button ref={ref} type={type} className={cn(buttonStyle, className)} disabled={disabled || loading} {...rest}>
|
||||
{prependIcon && <div className={buttonIconStyle}>{React.cloneElement(prependIcon, { strokeWidth: 2 })}</div>}
|
||||
{children}
|
||||
{appendIcon && <div className={buttonIconStyle}>{React.cloneElement(appendIcon, { strokeWidth: 2 })}</div>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
Button.displayName = "plane-ui-button";
|
||||
|
||||
export { Button };
|
||||
126
packages/propel/src/button/helper.tsx
Normal file
126
packages/propel/src/button/helper.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
export type TButtonVariant =
|
||||
| "primary"
|
||||
| "accent-primary"
|
||||
| "outline-primary"
|
||||
| "neutral-primary"
|
||||
| "link-primary"
|
||||
| "danger"
|
||||
| "accent-danger"
|
||||
| "outline-danger"
|
||||
| "link-danger"
|
||||
| "tertiary-danger"
|
||||
| "link-neutral";
|
||||
|
||||
export type TButtonSizes = "sm" | "md" | "lg" | "xl";
|
||||
|
||||
export interface IButtonStyling {
|
||||
[key: string]: {
|
||||
default: string;
|
||||
hover: string;
|
||||
pressed: string;
|
||||
disabled: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum buttonSizeStyling {
|
||||
sm = `px-3 py-1.5 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
lg = `px-5 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
xl = `px-5 py-3.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
}
|
||||
|
||||
enum buttonIconStyling {
|
||||
sm = "h-3 w-3 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
md = "h-3.5 w-3.5 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
lg = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
xl = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0 ",
|
||||
}
|
||||
|
||||
export const buttonStyling: IButtonStyling = {
|
||||
primary: {
|
||||
default: `text-white bg-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-200`,
|
||||
pressed: `focus:text-custom-brand-40 focus:bg-custom-primary-200`,
|
||||
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
|
||||
},
|
||||
"accent-primary": {
|
||||
default: `bg-custom-primary-100/20 text-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-100/10 hover:text-custom-primary-200`,
|
||||
pressed: `focus:bg-custom-primary-100/10`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||
},
|
||||
"outline-primary": {
|
||||
default: `text-custom-primary-100 bg-transparent border border-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-100/20`,
|
||||
pressed: `focus:text-custom-primary-100 focus:bg-custom-primary-100/30`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60 !border-custom-primary-60 `,
|
||||
},
|
||||
"neutral-primary": {
|
||||
default: `text-custom-text-200 bg-custom-background-100 border border-custom-border-200`,
|
||||
hover: `hover:bg-custom-background-90`,
|
||||
pressed: `focus:text-custom-text-300 focus:bg-custom-background-90`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
"link-primary": {
|
||||
default: `text-custom-primary-100 bg-custom-background-100`,
|
||||
hover: `hover:text-custom-primary-200`,
|
||||
pressed: `focus:text-custom-primary-80 `,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||
},
|
||||
|
||||
danger: {
|
||||
default: `text-white bg-red-500`,
|
||||
hover: ` hover:bg-red-600`,
|
||||
pressed: `focus:text-red-200 focus:bg-red-600`,
|
||||
disabled: `cursor-not-allowed !bg-red-300`,
|
||||
},
|
||||
"accent-danger": {
|
||||
default: `text-red-500 bg-red-50`,
|
||||
hover: `hover:text-red-600 hover:bg-red-100`,
|
||||
pressed: `focus:text-red-500 focus:bg-red-100`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"outline-danger": {
|
||||
default: `text-red-500 bg-transparent border border-red-500`,
|
||||
hover: `hover:text-red-400 hover:border-red-400`,
|
||||
pressed: `focus:text-red-400 focus:border-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300 !border-red-300`,
|
||||
},
|
||||
"link-danger": {
|
||||
default: `text-red-500 bg-custom-background-100`,
|
||||
hover: `hover:text-red-400`,
|
||||
pressed: `focus:text-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"tertiary-danger": {
|
||||
default: `text-red-500 bg-custom-background-100 border border-red-200`,
|
||||
hover: `hover:bg-red-50 hover:border-red-300`,
|
||||
pressed: `focus:text-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"link-neutral": {
|
||||
default: `text-custom-text-300`,
|
||||
hover: `hover:text-custom-text-200`,
|
||||
pressed: `focus:text-custom-text-100`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
};
|
||||
|
||||
export const getButtonStyling = (variant: TButtonVariant, size: TButtonSizes, disabled: boolean = false): string => {
|
||||
let tempVariant: string = ``;
|
||||
const currentVariant = buttonStyling[variant];
|
||||
|
||||
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
|
||||
currentVariant.pressed
|
||||
}`;
|
||||
|
||||
let tempSize: string = ``;
|
||||
if (size) tempSize = buttonSizeStyling[size];
|
||||
return `${tempVariant} ${tempSize}`;
|
||||
};
|
||||
|
||||
export const getIconStyling = (size: TButtonSizes): string => {
|
||||
let icon: string = ``;
|
||||
if (size) icon = buttonIconStyling[size];
|
||||
return icon;
|
||||
};
|
||||
3
packages/propel/src/button/index.ts
Normal file
3
packages/propel/src/button/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Button } from "./button";
|
||||
export * from "./helper";
|
||||
export type { ButtonProps } from "./button";
|
||||
194
packages/propel/src/calendar/calendar.stories.tsx
Normal file
194
packages/propel/src/calendar/calendar.stories.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
import { Calendar } from "./root";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Calendar",
|
||||
component: Calendar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
args: {
|
||||
showOutsideDays: true,
|
||||
},
|
||||
} satisfies Meta<typeof Calendar>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const SingleDate: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar {...args} mode="single" selected={date} onSelect={setDate} className="rounded-md border" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleDates: Story = {
|
||||
render(args) {
|
||||
const [dates, setDates] = useState<Date[] | undefined>([
|
||||
new Date(2024, 0, 15),
|
||||
new Date(2024, 0, 20),
|
||||
new Date(2024, 0, 25),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar {...args} mode="multiple" selected={dates} onSelect={setDates} className="rounded-md border" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const RangeSelection: Story = {
|
||||
render(args) {
|
||||
const [range, setRange] = useState<DateRange | undefined>({
|
||||
from: new Date(2024, 0, 10),
|
||||
to: new Date(2024, 0, 20),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar {...args} mode="range" selected={range} onSelect={setRange} className="rounded-md border" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledDates: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>();
|
||||
const disabledDays = [new Date(2024, 0, 5), new Date(2024, 0, 12), new Date(2024, 0, 19), new Date(2024, 0, 26)];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
disabled={disabledDays}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledWeekends: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>();
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
disabled={(date) => date.getDay() === 0 || date.getDay() === 6}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MinMaxDates: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>();
|
||||
const today = new Date();
|
||||
const tenDaysAgo = new Date(today);
|
||||
tenDaysAgo.setDate(today.getDate() - 10);
|
||||
const tenDaysFromNow = new Date(today);
|
||||
tenDaysFromNow.setDate(today.getDate() + 10);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
disabled={(date) => date < tenDaysAgo || date > tenDaysFromNow}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WeekStartsOnMonday: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
weekStartsOn={1}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutOutsideDays: Story = {
|
||||
render(args) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
showOutsideDays={false}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TwoMonths: Story = {
|
||||
render(args) {
|
||||
const [range, setRange] = useState<DateRange | undefined>({
|
||||
from: new Date(2024, 0, 10),
|
||||
to: new Date(2024, 1, 15),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar
|
||||
{...args}
|
||||
mode="range"
|
||||
selected={range}
|
||||
onSelect={setRange}
|
||||
numberOfMonths={2}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Uncontrolled: Story = {
|
||||
render(args) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Calendar {...args} mode="single" defaultMonth={new Date(2024, 0)} className="rounded-md border" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
2
packages/propel/src/calendar/index.ts
Normal file
2
packages/propel/src/calendar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export type { Matcher, DateRange } from "react-day-picker";
|
||||
38
packages/propel/src/calendar/root.tsx
Normal file
38
packages/propel/src/calendar/root.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "../utils";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
export const Calendar = ({ className, showOutsideDays = true, ...props }: CalendarProps) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
|
||||
const thirtyYearsFromNowFirstDay = new Date(currentYear + 30, 11, 31);
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
weekStartsOn={props.weekStartsOn}
|
||||
components={{
|
||||
Chevron: ({ className, ...props }) => (
|
||||
<ChevronLeft
|
||||
className={cn(
|
||||
"size-4",
|
||||
{ "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
startMonth={thirtyYearsAgoFirstDay}
|
||||
endMonth={thirtyYearsFromNowFirstDay}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
211
packages/propel/src/card/card.stories.tsx
Normal file
211
packages/propel/src/card/card.stories.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Card, ECardVariant, ECardSpacing, ECardDirection } from "./card";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Card",
|
||||
component: Card,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Card Title</h3>
|
||||
<p className="text-sm text-gray-600">This is a default card with shadow and large spacing.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
} satisfies Meta<typeof Card>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithShadow: Story = {
|
||||
args: {
|
||||
variant: ECardVariant.WITH_SHADOW,
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Card with Shadow</h3>
|
||||
<p className="text-sm text-gray-600">Hover over this card to see the shadow effect.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutShadow: Story = {
|
||||
args: {
|
||||
variant: ECardVariant.WITHOUT_SHADOW,
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Card without Shadow</h3>
|
||||
<p className="text-sm text-gray-600">This card has no shadow effect on hover.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallSpacing: Story = {
|
||||
args: {
|
||||
spacing: ECardSpacing.SM,
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Small Spacing</h3>
|
||||
<p className="text-sm text-gray-600">This card uses small spacing (p-4).</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeSpacing: Story = {
|
||||
args: {
|
||||
spacing: ECardSpacing.LG,
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Large Spacing</h3>
|
||||
<p className="text-sm text-gray-600">This card uses large spacing (p-6).</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const ColumnDirection: Story = {
|
||||
args: {
|
||||
direction: ECardDirection.COLUMN,
|
||||
children: (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">Column Direction</h3>
|
||||
<p className="text-sm text-gray-600">Content is arranged vertically.</p>
|
||||
<button className="rounded bg-blue-500 px-4 py-2 text-white">Action</button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const RowDirection: Story = {
|
||||
args: {
|
||||
direction: ECardDirection.ROW,
|
||||
children: (
|
||||
<>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-12 w-12 rounded bg-blue-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Row Direction</h3>
|
||||
<p className="text-sm text-gray-600">Content is arranged horizontally.</p>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const ProductCard: Story = {
|
||||
args: {
|
||||
variant: ECardVariant.WITH_SHADOW,
|
||||
spacing: ECardSpacing.LG,
|
||||
direction: ECardDirection.COLUMN,
|
||||
children: (
|
||||
<>
|
||||
<div className="h-48 w-full rounded bg-gray-200" />
|
||||
<h3 className="text-xl font-bold">Product Name</h3>
|
||||
<p className="text-sm text-gray-600">A brief description of the product goes here.</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-semibold">$99.99</span>
|
||||
<button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">Add to Cart</button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const UserCard: Story = {
|
||||
args: {
|
||||
variant: ECardVariant.WITH_SHADOW,
|
||||
spacing: ECardSpacing.LG,
|
||||
direction: ECardDirection.ROW,
|
||||
children: (
|
||||
<>
|
||||
<div className="h-16 w-16 flex-shrink-0 rounded-full bg-blue-500" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">John Doe</h3>
|
||||
<p className="text-sm text-gray-600">Software Engineer</p>
|
||||
<p className="text-xs text-gray-500">john.doe@example.com</p>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const NotificationCard: Story = {
|
||||
args: {
|
||||
variant: ECardVariant.WITHOUT_SHADOW,
|
||||
spacing: ECardSpacing.SM,
|
||||
direction: ECardDirection.COLUMN,
|
||||
children: (
|
||||
<>
|
||||
<div className="flex items-start justify-between">
|
||||
<h4 className="font-semibold">New Message</h4>
|
||||
<span className="text-xs text-gray-500">2m ago</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">You have received a new message from Alice.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card variant={ECardVariant.WITH_SHADOW}>
|
||||
<h3 className="font-semibold">With Shadow</h3>
|
||||
<p className="text-sm text-gray-600">Hover to see the shadow effect</p>
|
||||
</Card>
|
||||
<Card variant={ECardVariant.WITHOUT_SHADOW}>
|
||||
<h3 className="font-semibold">Without Shadow</h3>
|
||||
<p className="text-sm text-gray-600">No shadow on hover</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllSpacings: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card spacing={ECardSpacing.SM}>
|
||||
<h3 className="font-semibold">Small Spacing (p-4)</h3>
|
||||
<p className="text-sm text-gray-600">Compact padding</p>
|
||||
</Card>
|
||||
<Card spacing={ECardSpacing.LG}>
|
||||
<h3 className="font-semibold">Large Spacing (p-6)</h3>
|
||||
<p className="text-sm text-gray-600">More generous padding</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllDirections: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card direction={ECardDirection.COLUMN}>
|
||||
<h3 className="font-semibold">Column Direction</h3>
|
||||
<p className="text-sm text-gray-600">Vertical layout</p>
|
||||
<button className="w-fit rounded bg-blue-500 px-4 py-2 text-white">Button</button>
|
||||
</Card>
|
||||
<Card direction={ECardDirection.ROW}>
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded bg-blue-500" />
|
||||
<div>
|
||||
<h3 className="font-semibold">Row Direction</h3>
|
||||
<p className="text-sm text-gray-600">Horizontal layout</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
41
packages/propel/src/card/card.tsx
Normal file
41
packages/propel/src/card/card.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils/classname";
|
||||
import {
|
||||
ECardDirection,
|
||||
ECardSpacing,
|
||||
ECardVariant,
|
||||
getCardStyle,
|
||||
TCardDirection,
|
||||
TCardSpacing,
|
||||
TCardVariant,
|
||||
} from "./helper";
|
||||
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: TCardVariant;
|
||||
spacing?: TCardSpacing;
|
||||
direction?: TCardDirection;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
const {
|
||||
variant = ECardVariant.WITH_SHADOW,
|
||||
direction = ECardDirection.COLUMN,
|
||||
className = "",
|
||||
spacing = ECardSpacing.LG,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const style = getCardStyle(variant, spacing, direction);
|
||||
return (
|
||||
<div ref={ref} className={cn(style, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Card.displayName = "plane-ui-card";
|
||||
|
||||
export { Card, ECardVariant, ECardSpacing, ECardDirection };
|
||||
36
packages/propel/src/card/helper.tsx
Normal file
36
packages/propel/src/card/helper.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
export enum ECardVariant {
|
||||
WITHOUT_SHADOW = "without-shadow",
|
||||
WITH_SHADOW = "with-shadow",
|
||||
}
|
||||
export enum ECardDirection {
|
||||
ROW = "row",
|
||||
COLUMN = "column",
|
||||
}
|
||||
export enum ECardSpacing {
|
||||
SM = "sm",
|
||||
LG = "lg",
|
||||
}
|
||||
export type TCardVariant = ECardVariant.WITHOUT_SHADOW | ECardVariant.WITH_SHADOW;
|
||||
export type TCardDirection = ECardDirection.ROW | ECardDirection.COLUMN;
|
||||
export type TCardSpacing = ECardSpacing.SM | ECardSpacing.LG;
|
||||
|
||||
export interface ICardProperties {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const DEFAULT_STYLE =
|
||||
"bg-custom-background-100 rounded-lg border-[0.5px] border-custom-border-200 w-full flex flex-col";
|
||||
export const containerStyle: ICardProperties = {
|
||||
[ECardVariant.WITHOUT_SHADOW]: "",
|
||||
[ECardVariant.WITH_SHADOW]: "hover:shadow-custom-shadow-4xl duration-300",
|
||||
};
|
||||
export const spacings = {
|
||||
[ECardSpacing.SM]: "p-4",
|
||||
[ECardSpacing.LG]: "p-6",
|
||||
};
|
||||
export const directions = {
|
||||
[ECardDirection.ROW]: "flex-row space-x-3",
|
||||
[ECardDirection.COLUMN]: "flex-col space-y-3",
|
||||
};
|
||||
export const getCardStyle = (variant: TCardVariant, spacing: TCardSpacing, direction: TCardDirection) =>
|
||||
DEFAULT_STYLE + " " + directions[direction] + " " + containerStyle[variant] + " " + spacings[spacing];
|
||||
1
packages/propel/src/card/index.ts
Normal file
1
packages/propel/src/card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./card";
|
||||
1
packages/propel/src/charts/area-chart/index.ts
Normal file
1
packages/propel/src/charts/area-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
205
packages/propel/src/charts/area-chart/root.tsx
Normal file
205
packages/propel/src/charts/area-chart/root.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Area, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis, Line, ComposedChart, CartesianGrid } from "recharts";
|
||||
// plane imports
|
||||
import { AXIS_LABEL_CLASSNAME } from "@plane/constants";
|
||||
import { TAreaChartProps } from "@plane/types";
|
||||
// local components
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomXAxisTick, CustomYAxisTick } from "../components/tick";
|
||||
import { CustomTooltip } from "../components/tooltip";
|
||||
|
||||
export const AreaChart = React.memo(<K extends string, T extends string>(props: TAreaChartProps<K, T>) => {
|
||||
const {
|
||||
data,
|
||||
areas,
|
||||
xAxis,
|
||||
yAxis,
|
||||
className,
|
||||
legend,
|
||||
margin,
|
||||
tickCount = {
|
||||
x: undefined,
|
||||
y: 10,
|
||||
},
|
||||
customTicks,
|
||||
showTooltip = true,
|
||||
comparisonLine,
|
||||
} = props;
|
||||
// states
|
||||
const [activeArea, setActiveArea] = useState<string | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
// derived values
|
||||
const { itemKeys, itemLabels, itemDotColors } = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
const labels: Record<string, string> = {};
|
||||
const colors: Record<string, string> = {};
|
||||
|
||||
for (const area of areas) {
|
||||
keys.push(area.key);
|
||||
labels[area.key] = area.label;
|
||||
colors[area.key] = area.fill;
|
||||
}
|
||||
|
||||
return { itemKeys: keys, itemLabels: labels, itemDotColors: colors };
|
||||
}, [areas]);
|
||||
|
||||
const renderAreas = useMemo(
|
||||
() =>
|
||||
areas.map((area) => (
|
||||
<Area
|
||||
key={area.key}
|
||||
type={area.smoothCurves ? "monotone" : "linear"}
|
||||
dataKey={area.key}
|
||||
stackId={area.stackId}
|
||||
fill={area.fill}
|
||||
opacity={!!activeLegend && activeLegend !== area.key ? 0.1 : 1}
|
||||
fillOpacity={area.fillOpacity}
|
||||
strokeOpacity={area.strokeOpacity}
|
||||
stroke={area.strokeColor}
|
||||
strokeWidth={2}
|
||||
style={area.style}
|
||||
dot={
|
||||
area.showDot
|
||||
? {
|
||||
fill: area.fill,
|
||||
fillOpacity: 1,
|
||||
}
|
||||
: false
|
||||
}
|
||||
activeDot={{
|
||||
stroke: area.fill,
|
||||
}}
|
||||
onMouseEnter={() => setActiveArea(area.key)}
|
||||
onMouseLeave={() => setActiveArea(null)}
|
||||
className="[&_path]:transition-opacity [&_path]:duration-200"
|
||||
/>
|
||||
)),
|
||||
[activeLegend, areas]
|
||||
);
|
||||
|
||||
// create comparison line data for straight line from origin to last point
|
||||
const comparisonLineData = useMemo(() => {
|
||||
if (!data || data.length === 0) return [];
|
||||
// get the last data point
|
||||
const lastPoint = data[data.length - 1];
|
||||
// for the y-value in the last point, use its yAxis key value
|
||||
const lastYValue = lastPoint[yAxis.key] ?? 0;
|
||||
// create data for a straight line that has points at each x-axis position
|
||||
return data.map((item, index) => {
|
||||
// calculate the y value for this point on the straight line
|
||||
// using linear interpolation between (0,0) and (last_x, last_y)
|
||||
const ratio = index / (data.length - 1);
|
||||
const interpolatedValue = ratio * lastYValue;
|
||||
|
||||
return {
|
||||
[xAxis.key]: item[xAxis.key],
|
||||
comparisonLine: interpolatedValue,
|
||||
};
|
||||
});
|
||||
}, [data, xAxis.key, yAxis.key]);
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin?.top === undefined ? 5 : margin.top,
|
||||
right: margin?.right === undefined ? 30 : margin.right,
|
||||
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
|
||||
left: margin?.left === undefined ? 20 : margin.left,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid stroke="rgba(var(--color-border-100), 0.8)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxis.key}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.x || CustomXAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
xAxis.label && {
|
||||
value: xAxis.label,
|
||||
dy: 28,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tickCount={tickCount.x}
|
||||
/>
|
||||
<YAxis
|
||||
domain={yAxis.domain}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
yAxis.label && {
|
||||
value: yAxis.label,
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: yAxis.offset ?? -24,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.y || CustomYAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickCount={tickCount.y}
|
||||
allowDecimals={!!yAxis.allowDecimals}
|
||||
/>
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
formatter={(value) => itemLabels[value]}
|
||||
onMouseEnter={(payload) => setActiveLegend(payload.value)}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
stroke: "rgba(var(--color-text-300))",
|
||||
strokeDasharray: "4 4",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) => (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activeArea}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{renderAreas}
|
||||
{comparisonLine && (
|
||||
<Line
|
||||
data={comparisonLineData}
|
||||
type="linear"
|
||||
dataKey="comparisonLine"
|
||||
stroke={comparisonLine.strokeColor}
|
||||
fill={comparisonLine.strokeColor}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={comparisonLine.dashedLine ? "4 4" : "none"}
|
||||
activeDot={false}
|
||||
legendType="none"
|
||||
name="Comparison line"
|
||||
/>
|
||||
)}
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
AreaChart.displayName = "AreaChart";
|
||||
184
packages/propel/src/charts/bar-chart/bar.tsx
Normal file
184
packages/propel/src/charts/bar-chart/bar.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
// Constants
|
||||
const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height required to show text inside bar
|
||||
const BAR_TOP_BORDER_RADIUS = 4; // Border radius for the top of bars
|
||||
const BAR_BOTTOM_BORDER_RADIUS = 4; // Border radius for the bottom of bars
|
||||
const DEFAULT_LOLLIPOP_LINE_WIDTH = 2; // Width of lollipop stick
|
||||
const DEFAULT_LOLLIPOP_CIRCLE_RADIUS = 8; // Radius of lollipop circle
|
||||
|
||||
// Types
|
||||
interface TShapeProps {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
dataKey: string;
|
||||
payload: any;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
interface TBarProps extends TShapeProps {
|
||||
fill: string;
|
||||
stackKeys: string[];
|
||||
textClassName?: string;
|
||||
showPercentage?: boolean;
|
||||
showTopBorderRadius?: boolean;
|
||||
showBottomBorderRadius?: boolean;
|
||||
dotted?: boolean;
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
const calculatePercentage = <K extends string, T extends string>(
|
||||
data: TChartData<K, T>,
|
||||
stackKeys: T[],
|
||||
currentKey: T
|
||||
): number => {
|
||||
const total = stackKeys.reduce((sum, key) => sum + data[key], 0);
|
||||
return total === 0 ? 0 : Math.round((data[currentKey] / total) * 100);
|
||||
};
|
||||
|
||||
const getBarPath = (x: number, y: number, width: number, height: number, topRadius: number, bottomRadius: number) => `
|
||||
M${x},${y + topRadius}
|
||||
Q${x},${y} ${x + topRadius},${y}
|
||||
L${x + width - topRadius},${y}
|
||||
Q${x + width},${y} ${x + width},${y + topRadius}
|
||||
L${x + width},${y + height - bottomRadius}
|
||||
Q${x + width},${y + height} ${x + width - bottomRadius},${y + height}
|
||||
L${x + bottomRadius},${y + height}
|
||||
Q${x},${y + height} ${x},${y + height - bottomRadius}
|
||||
Z
|
||||
`;
|
||||
|
||||
const PercentageText = ({
|
||||
x,
|
||||
y,
|
||||
percentage,
|
||||
className,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
percentage: number;
|
||||
className?: string;
|
||||
}) => (
|
||||
<text x={x} y={y} textAnchor="middle" className={cn("text-xs font-medium", className)} fill="currentColor">
|
||||
{percentage}%
|
||||
</text>
|
||||
);
|
||||
|
||||
// Base Components
|
||||
const CustomBar = React.memo((props: TBarProps) => {
|
||||
const {
|
||||
opacity,
|
||||
fill,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
dataKey,
|
||||
stackKeys,
|
||||
payload,
|
||||
textClassName,
|
||||
showPercentage,
|
||||
showTopBorderRadius,
|
||||
showBottomBorderRadius,
|
||||
} = props;
|
||||
|
||||
if (!height) return null;
|
||||
|
||||
const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey);
|
||||
const TEXT_PADDING_Y = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT - height / 2));
|
||||
const textY = y + height - TEXT_PADDING_Y;
|
||||
|
||||
const showText =
|
||||
showPercentage &&
|
||||
height >= MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT &&
|
||||
currentBarPercentage !== undefined &&
|
||||
!Number.isNaN(currentBarPercentage);
|
||||
|
||||
const topBorderRadius = showTopBorderRadius ? BAR_TOP_BORDER_RADIUS : 0;
|
||||
const bottomBorderRadius = showBottomBorderRadius ? BAR_BOTTOM_BORDER_RADIUS : 0;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
d={getBarPath(x, y, width, height, topBorderRadius, bottomBorderRadius)}
|
||||
className="transition-opacity duration-200"
|
||||
fill={fill}
|
||||
opacity={opacity}
|
||||
/>
|
||||
{showText && (
|
||||
<PercentageText x={x + width / 2} y={textY} percentage={currentBarPercentage} className={textClassName} />
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
});
|
||||
|
||||
const CustomBarLollipop = React.memo((props: TBarProps) => {
|
||||
const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage, dotted } = props;
|
||||
|
||||
const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey);
|
||||
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
x1={x + width / 2}
|
||||
y1={y + height}
|
||||
x2={x + width / 2}
|
||||
y2={y}
|
||||
stroke={fill}
|
||||
strokeWidth={DEFAULT_LOLLIPOP_LINE_WIDTH}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={dotted ? "4 4" : "0"}
|
||||
/>
|
||||
<circle cx={x + width / 2} cy={y} r={DEFAULT_LOLLIPOP_CIRCLE_RADIUS} fill={fill} stroke="none" />
|
||||
{showPercentage && (
|
||||
<PercentageText x={x + width / 2} y={y} percentage={currentBarPercentage} className={textClassName} />
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
});
|
||||
|
||||
// Shape Variants
|
||||
/**
|
||||
* Factory function to create shape variants with consistent props
|
||||
* @param Component - The base component to render
|
||||
* @param factoryProps - Additional props to pass to the component
|
||||
* @returns A function that creates the shape with proper props
|
||||
*/
|
||||
const createShapeVariant =
|
||||
(Component: React.ComponentType<TBarProps>, factoryProps?: Partial<TBarProps>) =>
|
||||
(shapeProps: TShapeProps, bar: TBarItem<string>, stackKeys: string[]): React.ReactNode => {
|
||||
const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload);
|
||||
const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload);
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...shapeProps}
|
||||
fill={typeof bar.fill === "function" ? bar.fill(shapeProps.payload) : bar.fill}
|
||||
stackKeys={stackKeys}
|
||||
textClassName={bar.textClassName}
|
||||
showPercentage={bar.showPercentage}
|
||||
showTopBorderRadius={!!showTopBorderRadius}
|
||||
showBottomBorderRadius={!!showBottomBorderRadius}
|
||||
{...factoryProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const barShapeVariants: Record<
|
||||
TBarChartShapeVariant,
|
||||
(props: TShapeProps, bar: TBarItem<string>, stackKeys: string[]) => React.ReactNode
|
||||
> = {
|
||||
bar: createShapeVariant(CustomBar), // Standard bar with rounded corners
|
||||
lollipop: createShapeVariant(CustomBarLollipop), // Line with circle at top
|
||||
"lollipop-dotted": createShapeVariant(CustomBarLollipop, { dotted: true }), // Dotted line lollipop variant
|
||||
};
|
||||
|
||||
// Display names
|
||||
CustomBar.displayName = "CustomBar";
|
||||
CustomBarLollipop.displayName = "CustomBarLollipop";
|
||||
1
packages/propel/src/charts/bar-chart/index.ts
Normal file
1
packages/propel/src/charts/bar-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
209
packages/propel/src/charts/bar-chart/root.tsx
Normal file
209
packages/propel/src/charts/bar-chart/root.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
BarChart as CoreBarChart,
|
||||
Bar,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Legend,
|
||||
CartesianGrid,
|
||||
} from "recharts";
|
||||
// plane imports
|
||||
import { AXIS_LABEL_CLASSNAME } from "@plane/constants";
|
||||
import { TBarChartProps } from "@plane/types";
|
||||
// local components
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomXAxisTick, CustomYAxisTick } from "../components/tick";
|
||||
import { CustomTooltip } from "../components/tooltip";
|
||||
import { barShapeVariants } from "./bar";
|
||||
|
||||
const DEFAULT_BAR_FILL_COLOR = "#000000";
|
||||
|
||||
export const BarChart = React.memo(<K extends string, T extends string>(props: TBarChartProps<K, T>) => {
|
||||
const {
|
||||
data,
|
||||
bars,
|
||||
xAxis,
|
||||
yAxis,
|
||||
barSize = 40,
|
||||
className,
|
||||
legend,
|
||||
margin,
|
||||
tickCount = {
|
||||
x: undefined,
|
||||
y: 10,
|
||||
},
|
||||
customTicks,
|
||||
showTooltip = true,
|
||||
customTooltipContent,
|
||||
} = props;
|
||||
// states
|
||||
const [activeBar, setActiveBar] = useState<string | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
// derived values
|
||||
const { stackKeys, stackLabels } = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
const labels: Record<string, string> = {};
|
||||
|
||||
for (const bar of bars) {
|
||||
keys.push(bar.key);
|
||||
labels[bar.key] = bar.label;
|
||||
}
|
||||
|
||||
return { stackKeys: keys, stackLabels: labels };
|
||||
}, [bars]);
|
||||
|
||||
// get bar color dynamically based on payload
|
||||
const getBarColor = useCallback(
|
||||
(payload: Record<string, string>[], barKey: string) => {
|
||||
const bar = bars.find((b) => b.key === barKey);
|
||||
if (!bar) return DEFAULT_BAR_FILL_COLOR;
|
||||
|
||||
if (typeof bar.fill === "function") {
|
||||
const payloadItem = payload?.find((item) => item.dataKey === barKey);
|
||||
if (payloadItem?.payload) {
|
||||
try {
|
||||
return bar.fill(payloadItem.payload);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return DEFAULT_BAR_FILL_COLOR;
|
||||
}
|
||||
} else {
|
||||
return DEFAULT_BAR_FILL_COLOR; // fallback color when no payload data
|
||||
}
|
||||
} else {
|
||||
return bar.fill;
|
||||
}
|
||||
},
|
||||
[bars]
|
||||
);
|
||||
|
||||
// get all bar colors
|
||||
const getAllBarColors = useCallback(
|
||||
(payload: any[]) => {
|
||||
const colors: Record<string, string> = {};
|
||||
for (const bar of bars) {
|
||||
colors[bar.key] = getBarColor(payload, bar.key);
|
||||
}
|
||||
return colors;
|
||||
},
|
||||
[bars, getBarColor]
|
||||
);
|
||||
|
||||
const renderBars = useMemo(
|
||||
() =>
|
||||
bars.map((bar) => (
|
||||
<Bar
|
||||
key={bar.key}
|
||||
dataKey={bar.key}
|
||||
stackId={bar.stackId}
|
||||
opacity={!!activeLegend && activeLegend !== bar.key ? 0.1 : 1}
|
||||
shape={(shapeProps: any) => {
|
||||
const shapeVariant = barShapeVariants[bar.shapeVariant ?? "bar"];
|
||||
const node = shapeVariant(shapeProps, bar, stackKeys);
|
||||
return React.isValidElement(node) ? node : <>{node}</>;
|
||||
}}
|
||||
className="[&_path]:transition-opacity [&_path]:duration-200"
|
||||
onMouseEnter={() => setActiveBar(bar.key)}
|
||||
onMouseLeave={() => setActiveBar(null)}
|
||||
fill={getBarColor(data, bar.key)}
|
||||
/>
|
||||
)),
|
||||
[activeLegend, stackKeys, bars, getBarColor, data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<CoreBarChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin?.top === undefined ? 5 : margin.top,
|
||||
right: margin?.right === undefined ? 30 : margin.right,
|
||||
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
|
||||
left: margin?.left === undefined ? 20 : margin.left,
|
||||
}}
|
||||
barSize={barSize}
|
||||
className="recharts-wrapper"
|
||||
>
|
||||
<CartesianGrid stroke="rgba(var(--color-border-100), 0.8)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxis.key}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.x || CustomXAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: xAxis.label,
|
||||
dy: xAxis.dy ?? 28,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}}
|
||||
tickCount={tickCount.x}
|
||||
/>
|
||||
<YAxis
|
||||
domain={yAxis.domain}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: yAxis.label,
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: yAxis.offset ?? -24,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.y || CustomYAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickCount={tickCount.y}
|
||||
allowDecimals={!!yAxis.allowDecimals}
|
||||
/>
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
onMouseEnter={(payload) => setActiveLegend(payload.value)}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
formatter={(value) => stackLabels[value]}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
fill: "currentColor",
|
||||
className: "text-custom-background-90/80 cursor-pointer",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) => {
|
||||
if (customTooltipContent) return customTooltipContent({ active, label, payload });
|
||||
return (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
label={label}
|
||||
payload={payload}
|
||||
activeKey={activeBar}
|
||||
itemKeys={stackKeys}
|
||||
itemLabels={stackLabels}
|
||||
itemDotColors={getAllBarColors(payload || [])}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{renderBars}
|
||||
</CoreBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
BarChart.displayName = "BarChart";
|
||||
78
packages/propel/src/charts/components/legend.tsx
Normal file
78
packages/propel/src/charts/components/legend.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from "react";
|
||||
import { LegendProps } from "recharts";
|
||||
// plane imports
|
||||
import { TChartLegend } from "@plane/types";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
export const getLegendProps = (args: TChartLegend): LegendProps => {
|
||||
const { align, layout, verticalAlign } = args;
|
||||
return {
|
||||
layout,
|
||||
align,
|
||||
verticalAlign,
|
||||
wrapperStyle: {
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
...(layout === "vertical"
|
||||
? {
|
||||
top: 0,
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}
|
||||
: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}),
|
||||
...args.wrapperStyles,
|
||||
},
|
||||
content: <CustomLegend {...args} />,
|
||||
};
|
||||
};
|
||||
|
||||
const CustomLegend = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<LegendProps, "payload" | "formatter" | "onClick" | "onMouseEnter" | "onMouseLeave"> &
|
||||
TChartLegend
|
||||
>((props, ref) => {
|
||||
const { formatter, layout, onClick, onMouseEnter, onMouseLeave, payload } = props;
|
||||
|
||||
if (!payload?.length) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center px-4 overflow-scroll vertical-scrollbar scrollbar-sm", {
|
||||
"max-h-full flex-col items-start py-4": layout === "vertical",
|
||||
})}
|
||||
>
|
||||
{payload.map((item, index) => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn("flex items-center gap-1.5 text-custom-text-300 text-sm font-medium whitespace-nowrap", {
|
||||
"px-2": layout === "horizontal",
|
||||
"py-2": layout === "vertical",
|
||||
"pl-0 pt-0": index === 0,
|
||||
"pr-0 pb-0": index === payload.length - 1,
|
||||
"cursor-pointer": !!props.onClick,
|
||||
})}
|
||||
onClick={(e) => onClick?.(item, index, e)}
|
||||
onMouseEnter={(e) => onMouseEnter?.(item, index, e)}
|
||||
onMouseLeave={(e) => onMouseLeave?.(item, index, e)}
|
||||
>
|
||||
<div
|
||||
className="flex-shrink-0 size-2 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
{/* @ts-expect-error recharts types are not up to date */}
|
||||
{formatter?.(item.value, { value: item.value }, index) ?? item.payload?.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CustomLegend.displayName = "CustomLegend";
|
||||
45
packages/propel/src/charts/components/tick.tsx
Normal file
45
packages/propel/src/charts/components/tick.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from "react";
|
||||
|
||||
// Common classnames
|
||||
const AXIS_TICK_CLASSNAME = "fill-custom-text-300 text-sm";
|
||||
|
||||
export const CustomXAxisTick = React.memo<any>(({ x, y, payload, getLabel }: any) => (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text y={0} dy={16} textAnchor="middle" className={AXIS_TICK_CLASSNAME}>
|
||||
{getLabel ? getLabel(payload.value) : payload.value}
|
||||
</text>
|
||||
</g>
|
||||
));
|
||||
CustomXAxisTick.displayName = "CustomXAxisTick";
|
||||
|
||||
export const CustomYAxisTick = React.memo<any>(({ x, y, payload }: any) => (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text dx={-10} textAnchor="middle" className={AXIS_TICK_CLASSNAME}>
|
||||
{payload.value}
|
||||
</text>
|
||||
</g>
|
||||
));
|
||||
|
||||
CustomYAxisTick.displayName = "CustomYAxisTick";
|
||||
|
||||
export const CustomRadarAxisTick = React.memo<any>(({ x, y, payload, getLabel, cx, cy, offset = 16 }: any) => {
|
||||
// Calculate direction vector from center to tick
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
// Normalize and apply offset
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const normX = dx / length;
|
||||
const normY = dy / length;
|
||||
const labelX = x + normX * offset;
|
||||
const labelY = y + normY * offset;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${labelX},${labelY})`}>
|
||||
<text y={0} textAnchor="middle" className={AXIS_TICK_CLASSNAME}>
|
||||
{getLabel ? getLabel(payload.value) : payload.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
});
|
||||
CustomRadarAxisTick.displayName = "CustomRadarAxisTick";
|
||||
59
packages/propel/src/charts/components/tooltip.tsx
Normal file
59
packages/propel/src/charts/components/tooltip.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
import { Card, ECardSpacing } from "../../card";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
type Props = {
|
||||
active: boolean | undefined;
|
||||
activeKey?: string | null;
|
||||
label: string | undefined;
|
||||
payload: Payload<ValueType, NameType>[] | undefined;
|
||||
itemKeys: string[];
|
||||
itemLabels: Record<string, string>;
|
||||
itemDotColors: Record<string, string>;
|
||||
};
|
||||
|
||||
export const CustomTooltip = React.memo((props: Props) => {
|
||||
const { active, activeKey, label, payload, itemKeys, itemLabels, itemDotColors } = props;
|
||||
// derived values
|
||||
const filteredPayload = payload?.filter((item) => item.dataKey && itemKeys.includes(`${item.dataKey}`));
|
||||
|
||||
if (!active || !filteredPayload || !filteredPayload.length) return null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="flex flex-col max-h-[40vh] w-[12rem] overflow-y-scroll vertical-scrollbar scrollbar-sm"
|
||||
spacing={ECardSpacing.SM}
|
||||
>
|
||||
<p className="flex-shrink-0 text-xs text-custom-text-100 font-medium border-b border-custom-border-200 pb-2 truncate">
|
||||
{label}
|
||||
</p>
|
||||
{filteredPayload.map((item) => {
|
||||
if (!item.dataKey) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item?.dataKey}
|
||||
className={cn("flex items-center gap-2 text-xs transition-opacity", {
|
||||
"opacity-20": activeKey && item.dataKey !== activeKey,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{itemDotColors[item?.dataKey] && (
|
||||
<div
|
||||
className="flex-shrink-0 size-2 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: itemDotColors[item?.dataKey],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="text-custom-text-300 truncate">{itemLabels[item?.dataKey]}:</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium text-custom-text-200">{item?.value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
CustomTooltip.displayName = "CustomTooltip";
|
||||
1
packages/propel/src/charts/line-chart/index.ts
Normal file
1
packages/propel/src/charts/line-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
181
packages/propel/src/charts/line-chart/root.tsx
Normal file
181
packages/propel/src/charts/line-chart/root.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
CartesianGrid,
|
||||
LineChart as CoreLineChart,
|
||||
Legend,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
// plane imports
|
||||
import { AXIS_LABEL_CLASSNAME } from "@plane/constants";
|
||||
import { TLineChartProps } from "@plane/types";
|
||||
// local components
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomXAxisTick, CustomYAxisTick } from "../components/tick";
|
||||
import { CustomTooltip } from "../components/tooltip";
|
||||
|
||||
export const LineChart = React.memo(<K extends string, T extends string>(props: TLineChartProps<K, T>) => {
|
||||
const {
|
||||
data,
|
||||
lines,
|
||||
margin,
|
||||
xAxis,
|
||||
yAxis,
|
||||
className,
|
||||
tickCount = {
|
||||
x: undefined,
|
||||
y: 10,
|
||||
},
|
||||
customTicks,
|
||||
legend,
|
||||
showTooltip = true,
|
||||
customTooltipContent,
|
||||
} = props;
|
||||
// states
|
||||
const [activeLine, setActiveLine] = useState<string | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
// derived values
|
||||
const { itemKeys, itemLabels, itemDotColors } = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
const labels: Record<string, string> = {};
|
||||
const colors: Record<string, string> = {};
|
||||
|
||||
for (const line of lines) {
|
||||
keys.push(line.key);
|
||||
labels[line.key] = line.label;
|
||||
colors[line.key] = line.stroke;
|
||||
}
|
||||
|
||||
return { itemKeys: keys, itemLabels: labels, itemDotColors: colors };
|
||||
}, [lines]);
|
||||
|
||||
const renderLines = useMemo(
|
||||
() =>
|
||||
lines.map((line) => (
|
||||
<Line
|
||||
key={line.key}
|
||||
dataKey={line.key}
|
||||
type={line.smoothCurves ? "monotone" : "linear"}
|
||||
className="[&_path]:transition-opacity [&_path]:duration-200"
|
||||
opacity={!!activeLegend && activeLegend !== line.key ? 0.1 : 1}
|
||||
fill={line.fill}
|
||||
stroke={line.stroke}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={line.dashedLine ? "4 4" : "none"}
|
||||
dot={
|
||||
line.showDot
|
||||
? {
|
||||
fill: line.fill,
|
||||
fillOpacity: 1,
|
||||
}
|
||||
: false
|
||||
}
|
||||
activeDot={{
|
||||
stroke: line.fill,
|
||||
}}
|
||||
onMouseEnter={() => setActiveLine(line.key)}
|
||||
onMouseLeave={() => setActiveLine(null)}
|
||||
/>
|
||||
)),
|
||||
[activeLegend, lines]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<CoreLineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin?.top === undefined ? 5 : margin.top,
|
||||
right: margin?.right === undefined ? 30 : margin.right,
|
||||
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
|
||||
left: margin?.left === undefined ? 20 : margin.left,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid stroke="rgba(var(--color-border-100), 0.8)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxis.key}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.x || CustomXAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
xAxis.label && {
|
||||
value: xAxis.label,
|
||||
dy: 28,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tickCount={tickCount.x}
|
||||
/>
|
||||
<YAxis
|
||||
domain={yAxis.domain}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
yAxis.label && {
|
||||
value: yAxis.label,
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: -24,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.y || CustomYAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickCount={tickCount.y}
|
||||
allowDecimals={!!yAxis.allowDecimals}
|
||||
/>
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
onMouseEnter={(payload) => setActiveLegend(payload.value)}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
formatter={(value) => itemLabels[value]}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
stroke: "rgba(var(--color-text-300))",
|
||||
strokeDasharray: "4 4",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) => {
|
||||
if (customTooltipContent) return customTooltipContent({ active, label, payload });
|
||||
return (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activeLine}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{renderLines}
|
||||
</CoreLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
LineChart.displayName = "LineChart";
|
||||
32
packages/propel/src/charts/pie-chart/active-shape.tsx
Normal file
32
packages/propel/src/charts/pie-chart/active-shape.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Sector } from "recharts";
|
||||
import { PieSectorDataItem } from "recharts/types/polar/Pie";
|
||||
|
||||
export const CustomActiveShape = React.memo((props: PieSectorDataItem) => {
|
||||
const { cx, cy, cornerRadius, innerRadius, outerRadius, startAngle, endAngle, fill } = props;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
cornerRadius={cornerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
cornerRadius={cornerRadius}
|
||||
innerRadius={(outerRadius ?? 0) + 6}
|
||||
outerRadius={(outerRadius ?? 0) + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
});
|
||||
1
packages/propel/src/charts/pie-chart/index.ts
Normal file
1
packages/propel/src/charts/pie-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
151
packages/propel/src/charts/pie-chart/root.tsx
Normal file
151
packages/propel/src/charts/pie-chart/root.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Cell, PieChart as CorePieChart, Label, Legend, Pie, ResponsiveContainer, Tooltip } from "recharts";
|
||||
// plane imports
|
||||
import { TPieChartProps } from "@plane/types";
|
||||
// local components
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomActiveShape } from "./active-shape";
|
||||
import { CustomPieChartTooltip } from "./tooltip";
|
||||
|
||||
export const PieChart = React.memo(<K extends string, T extends string>(props: TPieChartProps<K, T>) => {
|
||||
const {
|
||||
data,
|
||||
dataKey,
|
||||
cells,
|
||||
className,
|
||||
innerRadius,
|
||||
legend,
|
||||
margin,
|
||||
outerRadius,
|
||||
showTooltip = true,
|
||||
showLabel,
|
||||
customLabel,
|
||||
centerLabel,
|
||||
cornerRadius,
|
||||
paddingAngle,
|
||||
tooltipLabel,
|
||||
} = props;
|
||||
// states
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
const renderCells = useMemo(
|
||||
() =>
|
||||
cells.map((cell, index) => (
|
||||
<Cell
|
||||
key={cell.key}
|
||||
className="transition-opacity duration-200"
|
||||
fill={cell.fill}
|
||||
opacity={!!activeLegend && activeLegend !== cell.key ? 0.1 : 1}
|
||||
style={{
|
||||
outline: "none",
|
||||
}}
|
||||
onMouseEnter={() => setActiveIndex(index)}
|
||||
onMouseLeave={() => setActiveIndex(null)}
|
||||
/>
|
||||
)),
|
||||
[activeLegend, cells]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<CorePieChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin?.top === undefined ? 5 : margin.top,
|
||||
right: margin?.right === undefined ? 30 : margin.right,
|
||||
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
|
||||
left: margin?.left === undefined ? 20 : margin.left,
|
||||
}}
|
||||
>
|
||||
<Pie
|
||||
activeIndex={activeIndex === null ? undefined : activeIndex}
|
||||
onMouseLeave={() => setActiveIndex(null)}
|
||||
data={data}
|
||||
dataKey={dataKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
blendStroke
|
||||
activeShape={<CustomActiveShape />}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
cornerRadius={cornerRadius}
|
||||
paddingAngle={paddingAngle}
|
||||
labelLine={false}
|
||||
label={
|
||||
showLabel
|
||||
? ({ payload, ...props }) => (
|
||||
<text
|
||||
className="text-sm font-medium transition-opacity duration-200"
|
||||
cx={props.cx}
|
||||
cy={props.cy}
|
||||
x={props.x}
|
||||
y={props.y}
|
||||
textAnchor={props.textAnchor}
|
||||
dominantBaseline={props.dominantBaseline}
|
||||
fill="rgba(var(--color-text-200))"
|
||||
opacity={!!activeLegend && activeLegend !== payload.key ? 0.1 : 1}
|
||||
>
|
||||
{customLabel?.(payload.count) ?? payload.count}
|
||||
</text>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{renderCells}
|
||||
{centerLabel && (
|
||||
<Label
|
||||
value={centerLabel.text}
|
||||
fill={centerLabel.fill}
|
||||
position="center"
|
||||
opacity={activeLegend ? 0.1 : 1}
|
||||
style={centerLabel.style}
|
||||
className={centerLabel.className}
|
||||
/>
|
||||
)}
|
||||
</Pie>
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
onMouseEnter={(payload) => {
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
const key: string | undefined = payload.payload?.key;
|
||||
if (!key) return;
|
||||
setActiveLegend(key);
|
||||
setActiveIndex(null);
|
||||
}}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
fill: "currentColor",
|
||||
className: "text-custom-background-90/80 cursor-pointer",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload || !payload.length) return null;
|
||||
const cellData = cells.find((c) => c.key === payload[0].payload.key);
|
||||
if (!cellData) return null;
|
||||
const label = tooltipLabel
|
||||
? typeof tooltipLabel === "function"
|
||||
? tooltipLabel(payload[0]?.payload?.payload)
|
||||
: tooltipLabel
|
||||
: dataKey;
|
||||
return <CustomPieChartTooltip dotColor={cellData.fill} label={label} payload={payload} />;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CorePieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
PieChart.displayName = "PieChart";
|
||||
40
packages/propel/src/charts/pie-chart/tooltip.tsx
Normal file
40
packages/propel/src/charts/pie-chart/tooltip.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
// plane imports
|
||||
import { Card, ECardSpacing } from "../../card";
|
||||
|
||||
type Props = {
|
||||
dotColor?: string;
|
||||
label: string;
|
||||
payload: Payload<ValueType, NameType>[];
|
||||
};
|
||||
|
||||
export const CustomPieChartTooltip = React.memo((props: Props) => {
|
||||
const { dotColor, label, payload } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="flex flex-col max-h-[40vh] w-[12rem] overflow-y-scroll vertical-scrollbar scrollbar-sm"
|
||||
spacing={ECardSpacing.SM}
|
||||
>
|
||||
<p className="flex-shrink-0 text-xs text-custom-text-100 font-medium border-b border-custom-border-200 pb-2 truncate">
|
||||
{label}
|
||||
</p>
|
||||
{payload?.map((item) => (
|
||||
<div key={item?.dataKey} className="flex items-center gap-2 text-xs capitalize">
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<div
|
||||
className="flex-shrink-0 size-2 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: dotColor,
|
||||
}}
|
||||
/>
|
||||
<span className="text-custom-text-300 truncate">{item?.name}:</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium text-custom-text-200">{item?.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
CustomPieChartTooltip.displayName = "CustomPieChartTooltip";
|
||||
1
packages/propel/src/charts/radar-chart/index.ts
Normal file
1
packages/propel/src/charts/radar-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
95
packages/propel/src/charts/radar-chart/root.tsx
Normal file
95
packages/propel/src/charts/radar-chart/root.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
PolarGrid,
|
||||
Radar,
|
||||
RadarChart as CoreRadarChart,
|
||||
ResponsiveContainer,
|
||||
PolarAngleAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from "recharts";
|
||||
import { TRadarChartProps } from "@plane/types";
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomRadarAxisTick } from "../components/tick";
|
||||
import { CustomTooltip } from "../components/tooltip";
|
||||
|
||||
const RadarChart = <T extends string, K extends string>(props: TRadarChartProps<T, K>) => {
|
||||
const { data, radars, margin, showTooltip, legend, className, angleAxis } = props;
|
||||
|
||||
// states
|
||||
const [, setActiveIndex] = useState<number | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
const { itemKeys, itemLabels, itemDotColors } = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
const labels: Record<string, string> = {};
|
||||
const colors: Record<string, string> = {};
|
||||
|
||||
for (const radar of radars) {
|
||||
keys.push(radar.key);
|
||||
labels[radar.key] = radar.name;
|
||||
colors[radar.key] = radar.stroke ?? radar.fill ?? "#000000";
|
||||
}
|
||||
return { itemKeys: keys, itemLabels: labels, itemDotColors: colors };
|
||||
}, [radars]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<CoreRadarChart cx="50%" cy="50%" outerRadius="80%" data={data} margin={margin}>
|
||||
<PolarGrid stroke="rgba(var(--color-border-100), 0.9)" />
|
||||
<PolarAngleAxis dataKey={angleAxis.key} tick={(props) => <CustomRadarAxisTick {...props} />} />
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
stroke: "rgba(var(--color-text-300))",
|
||||
strokeDasharray: "4 4",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) => (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activeLegend}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
onMouseEnter={(payload) => {
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
const key: string | undefined = payload.payload?.key;
|
||||
if (!key) return;
|
||||
setActiveLegend(key);
|
||||
setActiveIndex(null);
|
||||
}}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{radars.map((radar) => (
|
||||
<Radar
|
||||
key={radar.key}
|
||||
name={radar.name}
|
||||
dataKey={radar.key}
|
||||
stroke={radar.stroke}
|
||||
fill={radar.fill}
|
||||
fillOpacity={radar.fillOpacity}
|
||||
dot={radar.dot}
|
||||
/>
|
||||
))}
|
||||
</CoreRadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { RadarChart };
|
||||
1
packages/propel/src/charts/scatter-chart/index.ts
Normal file
1
packages/propel/src/charts/scatter-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
166
packages/propel/src/charts/scatter-chart/root.tsx
Normal file
166
packages/propel/src/charts/scatter-chart/root.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
CartesianGrid,
|
||||
ScatterChart as CoreScatterChart,
|
||||
Legend,
|
||||
Scatter,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
// plane imports
|
||||
import { AXIS_LABEL_CLASSNAME } from "@plane/constants";
|
||||
import { TScatterChartProps } from "@plane/types";
|
||||
// local components
|
||||
import { getLegendProps } from "../components/legend";
|
||||
import { CustomXAxisTick, CustomYAxisTick } from "../components/tick";
|
||||
import { CustomTooltip } from "../components/tooltip";
|
||||
|
||||
export const ScatterChart = React.memo(<K extends string, T extends string>(props: TScatterChartProps<K, T>) => {
|
||||
const {
|
||||
data,
|
||||
scatterPoints,
|
||||
margin,
|
||||
xAxis,
|
||||
yAxis,
|
||||
className,
|
||||
customTicks,
|
||||
tickCount = {
|
||||
x: undefined,
|
||||
y: 10,
|
||||
},
|
||||
legend,
|
||||
showTooltip = true,
|
||||
customTooltipContent,
|
||||
} = props;
|
||||
// states
|
||||
const [activePoint, setActivePoint] = useState<string | null>(null);
|
||||
const [activeLegend, setActiveLegend] = useState<string | null>(null);
|
||||
|
||||
//derived values
|
||||
const { itemKeys, itemLabels, itemDotColors } = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
const labels: Record<string, string> = {};
|
||||
const colors: Record<string, string> = {};
|
||||
|
||||
for (const point of scatterPoints) {
|
||||
keys.push(point.key);
|
||||
labels[point.key] = point.label;
|
||||
colors[point.key] = point.fill;
|
||||
}
|
||||
return { itemKeys: keys, itemLabels: labels, itemDotColors: colors };
|
||||
}, [scatterPoints]);
|
||||
|
||||
const renderPoints = useMemo(
|
||||
() =>
|
||||
scatterPoints.map((point) => (
|
||||
<Scatter
|
||||
key={point.key}
|
||||
dataKey={point.key}
|
||||
fill={point.fill}
|
||||
stroke={point.stroke}
|
||||
opacity={!!activeLegend && activeLegend !== point.key ? 0.1 : 1}
|
||||
onMouseEnter={() => setActivePoint(point.key)}
|
||||
onMouseLeave={() => setActivePoint(null)}
|
||||
/>
|
||||
)),
|
||||
[activeLegend, scatterPoints]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<CoreScatterChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin?.top === undefined ? 5 : margin.top,
|
||||
right: margin?.right === undefined ? 30 : margin.right,
|
||||
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
|
||||
left: margin?.left === undefined ? 20 : margin.left,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid stroke="rgba(var(--color-border-100), 0.8)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxis.key}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.x || CustomXAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
xAxis.label && {
|
||||
value: xAxis.label,
|
||||
dy: 28,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tickCount={tickCount.x}
|
||||
/>
|
||||
<YAxis
|
||||
domain={yAxis.domain}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
label={
|
||||
yAxis.label && {
|
||||
value: yAxis.label,
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: -24,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
tick={(props) => {
|
||||
const TickComponent = customTicks?.y || CustomYAxisTick;
|
||||
return <TickComponent {...props} />;
|
||||
}}
|
||||
tickCount={tickCount.y}
|
||||
allowDecimals={!!yAxis.allowDecimals}
|
||||
/>
|
||||
{legend && (
|
||||
// @ts-expect-error recharts types are not up to date
|
||||
<Legend
|
||||
onMouseEnter={(payload) => setActiveLegend(payload.value)}
|
||||
onMouseLeave={() => setActiveLegend(null)}
|
||||
formatter={(value) => itemLabels[value]}
|
||||
{...getLegendProps(legend)}
|
||||
/>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
cursor={{
|
||||
stroke: "rgba(var(--color-text-300))",
|
||||
strokeDasharray: "4 4",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) =>
|
||||
customTooltipContent ? (
|
||||
customTooltipContent({ active, label, payload })
|
||||
) : (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activePoint}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{renderPoints}
|
||||
</CoreScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ScatterChart.displayName = "ScatterChart";
|
||||
1
packages/propel/src/charts/tree-map/index.ts
Normal file
1
packages/propel/src/charts/tree-map/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
276
packages/propel/src/charts/tree-map/map-content.tsx
Normal file
276
packages/propel/src/charts/tree-map/map-content.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import React, { useMemo } from "react";
|
||||
// plane imports
|
||||
import { TBottomSectionConfig, TContentVisibility, TTopSectionConfig } from "@plane/types";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
const LAYOUT = {
|
||||
PADDING: 2,
|
||||
RADIUS: 6,
|
||||
TEXT: {
|
||||
PADDING_LEFT: 8,
|
||||
PADDING_RIGHT: 8,
|
||||
VERTICAL_OFFSET: 20,
|
||||
ELLIPSIS_OFFSET: -4,
|
||||
FONT_SIZES: {
|
||||
SM: 12.6,
|
||||
XS: 10.8,
|
||||
},
|
||||
},
|
||||
ICON: {
|
||||
SIZE: 16,
|
||||
GAP: 6,
|
||||
},
|
||||
MIN_DIMENSIONS: {
|
||||
HEIGHT_FOR_BOTH: 42,
|
||||
HEIGHT_FOR_TOP: 35,
|
||||
HEIGHT_FOR_DOTS: 20,
|
||||
WIDTH_FOR_ICON: 30,
|
||||
WIDTH_FOR_DOTS: 15,
|
||||
},
|
||||
};
|
||||
|
||||
const calculateContentWidth = (text: string | number, fontSize: number): number => String(text).length * fontSize * 0.7;
|
||||
|
||||
const calculateTopSectionConfig = (effectiveWidth: number, name: string, hasIcon: boolean): TTopSectionConfig => {
|
||||
const iconWidth = hasIcon ? LAYOUT.ICON.SIZE + LAYOUT.ICON.GAP : 0;
|
||||
const nameWidth = calculateContentWidth(name, LAYOUT.TEXT.FONT_SIZES.SM);
|
||||
const totalPadding = LAYOUT.TEXT.PADDING_LEFT + LAYOUT.TEXT.PADDING_RIGHT;
|
||||
|
||||
// First check if we can show icon
|
||||
const canShowIcon = hasIcon && effectiveWidth >= LAYOUT.MIN_DIMENSIONS.WIDTH_FOR_ICON;
|
||||
|
||||
// If we can't even show icon, check if we can show dots
|
||||
if (!canShowIcon) {
|
||||
return {
|
||||
showIcon: false,
|
||||
showName: effectiveWidth >= LAYOUT.MIN_DIMENSIONS.WIDTH_FOR_DOTS,
|
||||
nameTruncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
// We can show icon, now check if we have space for name
|
||||
const availableWidthForName = effectiveWidth - (canShowIcon ? iconWidth : 0) - totalPadding;
|
||||
const canShowFullName = availableWidthForName >= nameWidth;
|
||||
|
||||
return {
|
||||
showIcon: canShowIcon,
|
||||
showName: availableWidthForName > 0,
|
||||
nameTruncated: !canShowFullName,
|
||||
};
|
||||
};
|
||||
|
||||
const calculateBottomSectionConfig = (
|
||||
effectiveWidth: number,
|
||||
effectiveHeight: number,
|
||||
value: number | undefined,
|
||||
label: string | undefined
|
||||
): TBottomSectionConfig => {
|
||||
// If height is not enough for bottom section
|
||||
if (effectiveHeight < LAYOUT.MIN_DIMENSIONS.HEIGHT_FOR_BOTH) {
|
||||
return {
|
||||
show: false,
|
||||
showValue: false,
|
||||
showLabel: false,
|
||||
labelTruncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate widths
|
||||
const totalPadding = LAYOUT.TEXT.PADDING_LEFT + LAYOUT.TEXT.PADDING_RIGHT;
|
||||
const valueWidth = value ? calculateContentWidth(value, LAYOUT.TEXT.FONT_SIZES.XS) : 0;
|
||||
const labelWidth = label ? calculateContentWidth(label, LAYOUT.TEXT.FONT_SIZES.XS) + 4 : 0; // 4px for spacing
|
||||
const availableWidth = effectiveWidth - totalPadding;
|
||||
|
||||
// If we can't even show value
|
||||
if (availableWidth < Math.max(valueWidth, LAYOUT.MIN_DIMENSIONS.WIDTH_FOR_DOTS)) {
|
||||
return {
|
||||
show: true,
|
||||
showValue: false,
|
||||
showLabel: false,
|
||||
labelTruncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If we can show value but not full label
|
||||
const canShowFullLabel = availableWidth >= valueWidth + labelWidth;
|
||||
|
||||
return {
|
||||
show: true,
|
||||
showValue: true,
|
||||
showLabel: true,
|
||||
labelTruncated: !canShowFullLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const calculateVisibility = (
|
||||
width: number,
|
||||
height: number,
|
||||
hasIcon: boolean,
|
||||
name: string,
|
||||
value: number | undefined,
|
||||
label: string | undefined
|
||||
): TContentVisibility => {
|
||||
const effectiveWidth = width - LAYOUT.PADDING * 2;
|
||||
const effectiveHeight = height - LAYOUT.PADDING * 2;
|
||||
|
||||
// If extremely small, show only dots
|
||||
if (
|
||||
effectiveHeight < LAYOUT.MIN_DIMENSIONS.HEIGHT_FOR_DOTS ||
|
||||
effectiveWidth < LAYOUT.MIN_DIMENSIONS.WIDTH_FOR_DOTS
|
||||
) {
|
||||
return {
|
||||
top: { showIcon: false, showName: false, nameTruncated: false },
|
||||
bottom: { show: false, showValue: false, showLabel: false, labelTruncated: false },
|
||||
};
|
||||
}
|
||||
|
||||
const topSection = calculateTopSectionConfig(effectiveWidth, name, hasIcon);
|
||||
const bottomSection = calculateBottomSectionConfig(effectiveWidth, effectiveHeight, value, label);
|
||||
|
||||
return {
|
||||
top: topSection,
|
||||
bottom: bottomSection,
|
||||
};
|
||||
};
|
||||
|
||||
const truncateText = (text: string | number, maxWidth: number, fontSize: number, reservedWidth: number = 0): string => {
|
||||
const availableWidth = maxWidth - reservedWidth;
|
||||
if (availableWidth <= 0) return "";
|
||||
|
||||
const avgCharWidth = fontSize * 0.7;
|
||||
const maxChars = Math.floor(availableWidth / avgCharWidth);
|
||||
const stringText = String(text);
|
||||
|
||||
if (maxChars <= 3) return "";
|
||||
if (stringText.length <= maxChars) return stringText;
|
||||
return `${stringText.slice(0, maxChars - 3)}...`;
|
||||
};
|
||||
|
||||
export const CustomTreeMapContent: React.FC<any> = ({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
fillColor,
|
||||
fillClassName,
|
||||
textClassName,
|
||||
icon,
|
||||
}) => {
|
||||
const dimensions = useMemo(() => {
|
||||
const pX = x + LAYOUT.PADDING;
|
||||
const pY = y + LAYOUT.PADDING;
|
||||
const pWidth = Math.max(0, width - LAYOUT.PADDING * 2);
|
||||
const pHeight = Math.max(0, height - LAYOUT.PADDING * 2);
|
||||
return { pX, pY, pWidth, pHeight };
|
||||
}, [x, y, width, height]);
|
||||
|
||||
const visibility = useMemo(
|
||||
() => calculateVisibility(width, height, !!icon, name, value, label),
|
||||
[width, height, icon, name, value, label]
|
||||
);
|
||||
|
||||
if (!name || width <= 0 || height <= 0) return null;
|
||||
|
||||
const renderContent = () => {
|
||||
const { pX, pY, pWidth, pHeight } = dimensions;
|
||||
const { top, bottom } = visibility;
|
||||
|
||||
const availableTextWidth = pWidth - LAYOUT.TEXT.PADDING_LEFT - LAYOUT.TEXT.PADDING_RIGHT;
|
||||
const iconSpace = top.showIcon ? LAYOUT.ICON.SIZE + LAYOUT.ICON.GAP : 0;
|
||||
|
||||
return (
|
||||
<g>
|
||||
{/* Background shape */}
|
||||
<path
|
||||
d={`
|
||||
M${pX + LAYOUT.RADIUS},${pY}
|
||||
L${pX + pWidth - LAYOUT.RADIUS},${pY}
|
||||
Q${pX + pWidth},${pY} ${pX + pWidth},${pY + LAYOUT.RADIUS}
|
||||
L${pX + pWidth},${pY + pHeight - LAYOUT.RADIUS}
|
||||
Q${pX + pWidth},${pY + pHeight} ${pX + pWidth - LAYOUT.RADIUS},${pY + pHeight}
|
||||
L${pX + LAYOUT.RADIUS},${pY + pHeight}
|
||||
Q${pX},${pY + pHeight} ${pX},${pY + pHeight - LAYOUT.RADIUS}
|
||||
L${pX},${pY + LAYOUT.RADIUS}
|
||||
Q${pX},${pY} ${pX + LAYOUT.RADIUS},${pY}
|
||||
`}
|
||||
className={cn("transition-colors duration-200 hover:opacity-90", fillClassName)}
|
||||
fill={fillColor ?? "currentColor"}
|
||||
/>
|
||||
|
||||
{/* Top section */}
|
||||
<g>
|
||||
{top.showIcon && icon && (
|
||||
<foreignObject
|
||||
x={pX + LAYOUT.TEXT.PADDING_LEFT}
|
||||
y={pY + LAYOUT.TEXT.PADDING_LEFT}
|
||||
width={LAYOUT.ICON.SIZE}
|
||||
height={LAYOUT.ICON.SIZE}
|
||||
className={textClassName || "text-custom-text-300"}
|
||||
>
|
||||
{React.cloneElement(icon, {
|
||||
className: cn("size-4", icon?.props?.className),
|
||||
"aria-hidden": true,
|
||||
})}
|
||||
</foreignObject>
|
||||
)}
|
||||
{top.showName && (
|
||||
<text
|
||||
x={pX + LAYOUT.TEXT.PADDING_LEFT + iconSpace}
|
||||
y={pY + LAYOUT.TEXT.VERTICAL_OFFSET}
|
||||
textAnchor="start"
|
||||
className={cn(
|
||||
"text-sm font-extralight tracking-wider select-none",
|
||||
textClassName || "text-custom-text-300"
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
{top.nameTruncated ? truncateText(name, availableTextWidth, LAYOUT.TEXT.FONT_SIZES.SM, iconSpace) : name}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
|
||||
{/* Bottom section */}
|
||||
{bottom.show && (
|
||||
<g>
|
||||
{bottom.showValue && value !== undefined && (
|
||||
<text
|
||||
x={pX + LAYOUT.TEXT.PADDING_LEFT}
|
||||
y={pY + pHeight - LAYOUT.TEXT.PADDING_LEFT}
|
||||
textAnchor="start"
|
||||
className={cn(
|
||||
"text-sm font-extralight tracking-wider select-none",
|
||||
textClassName || "text-custom-text-300"
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
{value.toLocaleString()}
|
||||
{bottom.showLabel && label && (
|
||||
<tspan dx={4}>
|
||||
{bottom.labelTruncated
|
||||
? truncateText(
|
||||
label,
|
||||
availableTextWidth - calculateContentWidth(value, LAYOUT.TEXT.FONT_SIZES.SM) - 4,
|
||||
LAYOUT.TEXT.FONT_SIZES.SM
|
||||
)
|
||||
: label}
|
||||
</tspan>
|
||||
)}
|
||||
{!bottom.showLabel && label && <tspan dx={4}>...</tspan>}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<g>
|
||||
<rect x={x} y={y} width={width} height={height} fill="transparent" />
|
||||
{renderContent()}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
44
packages/propel/src/charts/tree-map/root.tsx
Normal file
44
packages/propel/src/charts/tree-map/root.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { Treemap, ResponsiveContainer, Tooltip } from "recharts";
|
||||
// plane imports
|
||||
import { TreeMapChartProps } from "@plane/types";
|
||||
// local imports
|
||||
import { cn } from "../../utils/classname";
|
||||
import { CustomTreeMapContent } from "./map-content";
|
||||
import { TreeMapTooltip } from "./tooltip";
|
||||
|
||||
export const TreeMapChart = React.memo((props: TreeMapChartProps) => {
|
||||
const { data, className = "w-full h-96", isAnimationActive = false, showTooltip = true } = props;
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<Treemap
|
||||
data={data}
|
||||
nameKey="name"
|
||||
dataKey="value"
|
||||
stroke="currentColor"
|
||||
className="text-custom-background-100 bg-custom-background-100"
|
||||
content={<CustomTreeMapContent />}
|
||||
animationEasing="ease-out"
|
||||
isUpdateAnimationActive={isAnimationActive}
|
||||
animationBegin={100}
|
||||
animationDuration={500}
|
||||
>
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload }) => <TreeMapTooltip active={active} payload={payload} />}
|
||||
cursor={{
|
||||
fill: "currentColor",
|
||||
className: "text-custom-background-90/80 cursor-pointer",
|
||||
}}
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Treemap>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TreeMapChart.displayName = "TreeMapChart";
|
||||
29
packages/propel/src/charts/tree-map/tooltip.tsx
Normal file
29
packages/propel/src/charts/tree-map/tooltip.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { Card, ECardSpacing } from "../../card";
|
||||
|
||||
interface TreeMapTooltipProps {
|
||||
active: boolean | undefined;
|
||||
payload: any[] | undefined;
|
||||
}
|
||||
|
||||
export const TreeMapTooltip = React.memo(({ active, payload }: TreeMapTooltipProps) => {
|
||||
if (!active || !payload || !payload[0]?.payload) return null;
|
||||
|
||||
const data = payload[0].payload;
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col space-y-1.5" spacing={ECardSpacing.SM}>
|
||||
<div className="flex items-center gap-2 border-b border-custom-border-200 pb-2.5">
|
||||
{data?.icon}
|
||||
<p className="text-xs text-custom-text-100 font-medium capitalize">{data?.name}</p>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-custom-text-200">
|
||||
{data?.value.toLocaleString()}
|
||||
{data.label && ` ${data.label}`}
|
||||
</span>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
TreeMapTooltip.displayName = "TreeMapTooltip";
|
||||
174
packages/propel/src/collapsible/collapsible.stories.tsx
Normal file
174
packages/propel/src/collapsible/collapsible.stories.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import { Collapsible } from "./collapsible";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Collapsible",
|
||||
component: Collapsible.CollapsibleRoot,
|
||||
subcomponents: {
|
||||
CollapsibleTrigger: Collapsible.CollapsibleTrigger,
|
||||
CollapsibleContent: Collapsible.CollapsibleContent,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: null,
|
||||
isOpen: false,
|
||||
onToggle: () => {},
|
||||
},
|
||||
render(args) {
|
||||
const [{ isOpen }, updateArgs] = useArgs();
|
||||
const toggleOpen = () => updateArgs({ isOpen: !isOpen });
|
||||
|
||||
return (
|
||||
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={toggleOpen} className="w-96">
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">Click to toggle</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-sm">This is the collapsible content that can be shown or hidden.</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
);
|
||||
},
|
||||
} satisfies Meta<typeof Collapsible.CollapsibleRoot>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const DefaultOpen: Story = {
|
||||
args: { isOpen: true },
|
||||
};
|
||||
|
||||
export const Controlled: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setIsOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-sm text-white">
|
||||
Open
|
||||
</button>
|
||||
<button onClick={() => setIsOpen(false)} className="rounded bg-gray-500 px-4 py-2 text-sm text-white">
|
||||
Close
|
||||
</button>
|
||||
<button onClick={() => setIsOpen(!isOpen)} className="rounded bg-green-500 px-4 py-2 text-sm text-white">
|
||||
Toggle
|
||||
</button>
|
||||
</div>
|
||||
<Collapsible.CollapsibleRoot isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">Controlled Collapsible</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-sm">This collapsible is controlled by external state.</p>
|
||||
<p className="mt-2 text-sm">Current state: {isOpen ? "Open" : "Closed"}</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const NestedContent: Story = {
|
||||
render(args) {
|
||||
const [isOpen, setIsOpen] = useState(args.isOpen);
|
||||
return (
|
||||
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">Collapsible with Nested Content</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="space-y-2 rounded-md border border-gray-200 p-4">
|
||||
<h4 className="font-semibold">Section 1</h4>
|
||||
<p className="text-sm">This is some content in the first section.</p>
|
||||
<h4 className="font-semibold">Section 2</h4>
|
||||
<p className="text-sm">This is some content in the second section.</p>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
render(args) {
|
||||
const [isOpen, setIsOpen] = useState(args.isOpen);
|
||||
return (
|
||||
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 px-6 py-3 text-white shadow-lg transition-all hover:shadow-xl">
|
||||
<span className="text-lg font-bold">Custom Styled Trigger</span>
|
||||
<ChevronDown className="h-5 w-5 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-4">
|
||||
<div className="rounded-lg bg-gradient-to-br from-purple-100 to-pink-100 p-6 shadow-md">
|
||||
<p className="text-purple-900">This collapsible has custom styling with gradients, shadows, and colors.</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleCollapsibles: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="w-96 space-y-2">
|
||||
<Collapsible.CollapsibleRoot>
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">First Item</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-sm">Content for the first item.</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
|
||||
<Collapsible.CollapsibleRoot>
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">Second Item</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-sm">Content for the second item.</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
|
||||
<Collapsible.CollapsibleRoot>
|
||||
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
|
||||
<span className="font-semibold">Third Item</span>
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-sm">Content for the third item.</p>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.CollapsibleRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
100
packages/propel/src/collapsible/collapsible.tsx
Normal file
100
packages/propel/src/collapsible/collapsible.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useState, useEffect, useCallback, createContext, useContext } from "react";
|
||||
import { Collapsible as BaseCollapsible } from "@base-ui-components/react/collapsible";
|
||||
import clsx from "clsx";
|
||||
|
||||
// Types
|
||||
type CollapsibleContextType = {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
};
|
||||
|
||||
type RootProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
defaultOpen?: boolean;
|
||||
};
|
||||
|
||||
type TriggerProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
buttonRef?: React.RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// Context
|
||||
const CollapsibleContext = createContext<CollapsibleContextType | undefined>(undefined);
|
||||
|
||||
// Hook
|
||||
const useCollapsible = () => {
|
||||
const context = useContext(CollapsibleContext);
|
||||
if (!context) {
|
||||
throw new Error("Collapsible compound components cannot be rendered outside the Collapsible component");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Components
|
||||
const Root: React.FC<RootProps> = ({ children, className, isOpen: controlledIsOpen, onToggle, defaultOpen }) => {
|
||||
const [localIsOpen, setLocalIsOpen] = useState<boolean>(controlledIsOpen || defaultOpen || false);
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledIsOpen !== undefined) {
|
||||
setLocalIsOpen(controlledIsOpen);
|
||||
}
|
||||
}, [controlledIsOpen]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (controlledIsOpen !== undefined) {
|
||||
onToggle?.();
|
||||
} else {
|
||||
setLocalIsOpen((prev) => !prev);
|
||||
}
|
||||
}, [controlledIsOpen, onToggle]);
|
||||
|
||||
return (
|
||||
<CollapsibleContext.Provider value={{ isOpen: localIsOpen, onToggle: handleToggle }}>
|
||||
<BaseCollapsible.Root
|
||||
className={clsx(className)}
|
||||
defaultOpen={defaultOpen}
|
||||
open={localIsOpen}
|
||||
onOpenChange={handleToggle}
|
||||
>
|
||||
{children}
|
||||
</BaseCollapsible.Root>
|
||||
</CollapsibleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Trigger: React.FC<TriggerProps> = ({ children, className, buttonRef }) => {
|
||||
const { isOpen } = useCollapsible();
|
||||
|
||||
return (
|
||||
<BaseCollapsible.Trigger data-panel-open={isOpen} ref={buttonRef} className={className}>
|
||||
{children}
|
||||
</BaseCollapsible.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
const Content: React.FC<ContentProps> = ({ children, className }) => (
|
||||
<BaseCollapsible.Panel
|
||||
className={clsx(
|
||||
"flex h-[var(--collapsible-panel-height)] flex-col overflow-hidden text-sm transition-all ease-out data-[ending-style]:h-0 data-[starting-style]:h-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</BaseCollapsible.Panel>
|
||||
);
|
||||
|
||||
// Compound Component
|
||||
export const Collapsible = {
|
||||
CollapsibleRoot: Root,
|
||||
CollapsibleTrigger: Trigger,
|
||||
CollapsibleContent: Content,
|
||||
};
|
||||
1
packages/propel/src/collapsible/index.ts
Normal file
1
packages/propel/src/collapsible/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./collapsible";
|
||||
260
packages/propel/src/combobox/combobox.stories.tsx
Normal file
260
packages/propel/src/combobox/combobox.stories.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import { Combobox } from "./combobox";
|
||||
|
||||
const frameworks = [
|
||||
{ value: "react", label: "React" },
|
||||
{ value: "vue", label: "Vue" },
|
||||
{ value: "angular", label: "Angular" },
|
||||
{ value: "svelte", label: "Svelte" },
|
||||
{ value: "solid", label: "Solid" },
|
||||
{ value: "next", label: "Next.js" },
|
||||
{ value: "nuxt", label: "Nuxt" },
|
||||
{ value: "remix", label: "Remix" },
|
||||
];
|
||||
|
||||
const meta = {
|
||||
title: "Components/Combobox",
|
||||
component: Combobox,
|
||||
subcomponents: {
|
||||
ComboboxButton: Combobox.Button,
|
||||
ComboboxOptions: Combobox.Options,
|
||||
ComboboxOption: Combobox.Option,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: null,
|
||||
value: "",
|
||||
onValueChange: () => {},
|
||||
},
|
||||
render(args) {
|
||||
const [{ value }, updateArgs] = useArgs();
|
||||
const setValue = (newValue: string | string[]) => updateArgs({ value: newValue });
|
||||
return (
|
||||
<Combobox {...args} value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
} satisfies Meta<typeof Combobox>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithoutSearch: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiSelect: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<Combobox multiSelect value={value} onValueChange={(v) => setValue(v as string[])}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span className="truncate">{value.length > 0 ? `${value.length} selected` : "Select frameworks..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value.includes(framework.value) && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiSelectWithLimit: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Combobox multiSelect maxSelections={3} value={value} onValueChange={(v) => setValue(v as string[])}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span className="truncate">
|
||||
{value.length > 0 ? `${value.length}/3 selected` : "Select up to 3 frameworks..."}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value.includes(framework.value) && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
<p className="text-xs text-gray-500">Maximum 3 selections allowed</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true },
|
||||
render() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Combobox disabled value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-gray-100 px-4 py-2 opacity-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledOptions: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
disabled={framework.value === "angular" || framework.value === "svelte"}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomMaxHeight: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options showSearch searchPlaceholder="Search framework..." maxHeight="sm" className="w-72">
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomEmptyMessage: Story = {
|
||||
render() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
|
||||
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
|
||||
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Combobox.Button>
|
||||
<Combobox.Options
|
||||
showSearch
|
||||
searchPlaceholder="Search framework..."
|
||||
emptyMessage="No frameworks found. Try a different search."
|
||||
className="w-72"
|
||||
>
|
||||
{frameworks.map((framework) => (
|
||||
<Combobox.Option
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
{value === framework.value && <Check className="h-4 w-4" />}
|
||||
<span>{framework.label}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
},
|
||||
};
|
||||
224
packages/propel/src/combobox/combobox.tsx
Normal file
224
packages/propel/src/combobox/combobox.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import * as React from "react";
|
||||
import { Combobox as BaseCombobox } from "@base-ui-components/react/combobox";
|
||||
import { Search } from "lucide-react";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
// Type definitions
|
||||
type TMaxHeight = "lg" | "md" | "rg" | "sm";
|
||||
|
||||
export interface ComboboxProps {
|
||||
value?: string | string[];
|
||||
defaultValue?: string | string[];
|
||||
onValueChange?: (value: string | string[]) => void;
|
||||
multiSelect?: boolean;
|
||||
maxSelections?: number;
|
||||
disabled?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ComboboxButtonProps {
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
ref?: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export interface ComboboxOptionsProps {
|
||||
searchPlaceholder?: string;
|
||||
emptyMessage?: string;
|
||||
showSearch?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
maxHeight?: TMaxHeight;
|
||||
inputClassName?: string;
|
||||
optionsContainerClassName?: string;
|
||||
positionerClassName?: string;
|
||||
searchQuery?: string;
|
||||
onSearchQueryChange?: (query: string) => void;
|
||||
onSearchQueryKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export interface ComboboxOptionProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const MAX_HEIGHT_CLASSES: Record<TMaxHeight, string> = {
|
||||
lg: "max-h-60",
|
||||
md: "max-h-48",
|
||||
rg: "max-h-36",
|
||||
sm: "max-h-28",
|
||||
} as const;
|
||||
|
||||
// Root component
|
||||
function ComboboxRoot({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
multiSelect = false,
|
||||
disabled = false,
|
||||
open,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: ComboboxProps) {
|
||||
const handleValueChange = React.useCallback(
|
||||
(newValue: string | string[]) => {
|
||||
onValueChange?.(newValue);
|
||||
},
|
||||
[onValueChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseCombobox.Root
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={handleValueChange}
|
||||
multiple={multiSelect}
|
||||
disabled={disabled}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
{children}
|
||||
</BaseCombobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger button component
|
||||
const ComboboxButton = React.forwardRef<HTMLButtonElement, ComboboxButtonProps>(
|
||||
({ className, children, disabled = false }, ref) => (
|
||||
<BaseCombobox.Trigger ref={ref} disabled={disabled} className={className}>
|
||||
{children}
|
||||
</BaseCombobox.Trigger>
|
||||
)
|
||||
);
|
||||
|
||||
// Options popup component
|
||||
function ComboboxOptions({
|
||||
children,
|
||||
showSearch = false,
|
||||
searchPlaceholder,
|
||||
maxHeight = "lg",
|
||||
className,
|
||||
inputClassName,
|
||||
optionsContainerClassName,
|
||||
emptyMessage = "No results found",
|
||||
positionerClassName,
|
||||
searchQuery: controlledSearchQuery,
|
||||
onSearchQueryChange,
|
||||
onSearchQueryKeyDown,
|
||||
}: ComboboxOptionsProps) {
|
||||
// const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [internalSearchQuery, setInternalSearchQuery] = React.useState("");
|
||||
|
||||
const searchQuery = controlledSearchQuery !== undefined ? controlledSearchQuery : internalSearchQuery;
|
||||
|
||||
const setSearchQuery = React.useCallback(
|
||||
(query: string) => {
|
||||
if (onSearchQueryChange) {
|
||||
onSearchQueryChange(query);
|
||||
} else {
|
||||
setInternalSearchQuery(query);
|
||||
}
|
||||
},
|
||||
[onSearchQueryChange]
|
||||
);
|
||||
|
||||
// Filter children based on search query
|
||||
const filteredChildren = React.useMemo(() => {
|
||||
if (!showSearch || !searchQuery) return children;
|
||||
|
||||
return React.Children.toArray(children).filter((child) => {
|
||||
if (!React.isValidElement(child)) return true;
|
||||
|
||||
// Only filter ComboboxOption components, leave other elements (like additional content) unfiltered
|
||||
if (child.type !== ComboboxOption) return true;
|
||||
|
||||
// Extract text content from child to search against
|
||||
const getTextContent = (node: React.ReactNode): string => {
|
||||
if (typeof node === "string") return node;
|
||||
if (typeof node === "number") return String(node);
|
||||
if (React.isValidElement(node) && node.props.children) {
|
||||
return getTextContent(node.props.children);
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(getTextContent).join(" ");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const textContent = getTextContent(child.props.children);
|
||||
const value = child.props.value || "";
|
||||
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
return textContent.toLowerCase().includes(searchLower) || String(value).toLowerCase().includes(searchLower);
|
||||
});
|
||||
}, [children, searchQuery, showSearch]);
|
||||
|
||||
return (
|
||||
<BaseCombobox.Portal>
|
||||
<BaseCombobox.Positioner sideOffset={8} className={positionerClassName}>
|
||||
<BaseCombobox.Popup
|
||||
className={cn("rounded-md border border-custom-border-200 bg-custom-background-100 p-1 shadow-lg", className)}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{showSearch && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-custom-text-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={onSearchQueryKeyDown}
|
||||
className={cn(
|
||||
"w-full rounded border border-custom-border-100 bg-custom-background-90 py-1.5 pl-8 pr-2 text-sm outline-none placeholder:text-custom-text-400",
|
||||
inputClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<BaseCombobox.List
|
||||
className={cn("overflow-auto outline-none", MAX_HEIGHT_CLASSES[maxHeight], optionsContainerClassName)}
|
||||
>
|
||||
{filteredChildren}
|
||||
{showSearch &&
|
||||
emptyMessage &&
|
||||
React.Children.count(
|
||||
React.Children.toArray(filteredChildren).filter(
|
||||
(child) => React.isValidElement(child) && child.type === ComboboxOption
|
||||
)
|
||||
) === 0 && <div className="px-2 py-1.5 text-sm text-custom-text-400">{emptyMessage}</div>}
|
||||
</BaseCombobox.List>
|
||||
</div>
|
||||
</BaseCombobox.Popup>
|
||||
</BaseCombobox.Positioner>
|
||||
</BaseCombobox.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
// Individual option component
|
||||
function ComboboxOption({ value, children, disabled, className }: ComboboxOptionProps) {
|
||||
return (
|
||||
<BaseCombobox.Item
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={cn("cursor-pointer rounded px-2 py-1.5 text-sm outline-none transition-colors", className)}
|
||||
>
|
||||
{children}
|
||||
</BaseCombobox.Item>
|
||||
);
|
||||
}
|
||||
|
||||
// Compound component export
|
||||
const Combobox = Object.assign(ComboboxRoot, {
|
||||
Button: ComboboxButton,
|
||||
Options: ComboboxOptions,
|
||||
Option: ComboboxOption,
|
||||
});
|
||||
|
||||
export { Combobox };
|
||||
1
packages/propel/src/combobox/index.ts
Normal file
1
packages/propel/src/combobox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./combobox";
|
||||
203
packages/propel/src/command/command.stories.tsx
Normal file
203
packages/propel/src/command/command.stories.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { File, Folder, Settings, User } from "lucide-react";
|
||||
import { Command } from "./command";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Command",
|
||||
component: Command,
|
||||
subcomponents: {
|
||||
CommandInput: Command.Input,
|
||||
CommandList: Command.List,
|
||||
CommandItem: Command.Item,
|
||||
CommandEmpty: Command.Empty,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Command>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 1</Command.Item>
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 2</Command.Item>
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 3</Command.Item>
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input
|
||||
placeholder="Search files and folders..."
|
||||
className="h-9 w-full bg-transparent py-3 text-sm outline-none"
|
||||
/>
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Folder className="h-4 w-4" />
|
||||
<span>Documents</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Folder className="h-4 w-4" />
|
||||
<span>Downloads</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<File className="h-4 w-4" />
|
||||
<span>README.md</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<File className="h-4 w-4" />
|
||||
<span>package.json</span>
|
||||
</Command.Item>
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">No files or folders found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCategories: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input
|
||||
placeholder="Search commands..."
|
||||
className="h-9 w-full bg-transparent py-3 text-sm outline-none"
|
||||
/>
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-gray-500">User</div>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<User className="h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</Command.Item>
|
||||
|
||||
<div className="mt-2 px-2 py-1.5 text-xs font-semibold text-gray-500">Files</div>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Folder className="h-4 w-4" />
|
||||
<span>Open Folder</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<File className="h-4 w-4" />
|
||||
<span>New File</span>
|
||||
</Command.Item>
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">No commands found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyState: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
|
||||
<Command.List className="max-h-80 overflow-auto py-2">{/* No items - will show empty state */}</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">
|
||||
<p className="font-semibold">No results found</p>
|
||||
<p className="mt-1 text-xs">Try searching for something else</p>
|
||||
</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongList: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input placeholder="Search items..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
|
||||
<Command.List className="max-h-60 overflow-auto py-2">
|
||||
{Array.from({ length: 20 }, (_, i) => (
|
||||
<Command.Item key={i} className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">
|
||||
Item {i + 1}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutSearch: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<User className="h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</Command.Item>
|
||||
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
|
||||
<Folder className="h-4 w-4" />
|
||||
<span>Files</span>
|
||||
</Command.Item>
|
||||
</Command.List>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border-2 border-blue-300 bg-blue-50 p-2 shadow-lg">
|
||||
<Command.Input
|
||||
placeholder="Search with custom styling..."
|
||||
className="h-9 w-full bg-transparent py-3 text-sm text-blue-900 outline-none placeholder:text-blue-400"
|
||||
/>
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
|
||||
Custom Item 1
|
||||
</Command.Item>
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
|
||||
Custom Item 2
|
||||
</Command.Item>
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
|
||||
Custom Item 3
|
||||
</Command.Item>
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-blue-500">No matching items found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledItems: Story = {
|
||||
render() {
|
||||
return (
|
||||
<Command className="w-96 rounded-lg border border-gray-200 p-2">
|
||||
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
|
||||
<Command.List className="max-h-80 overflow-auto py-2">
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Active Item 1</Command.Item>
|
||||
<Command.Item disabled className="cursor-not-allowed rounded px-3 py-2 opacity-50">
|
||||
Disabled Item
|
||||
</Command.Item>
|
||||
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Active Item 2</Command.Item>
|
||||
</Command.List>
|
||||
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
|
||||
</Command>
|
||||
);
|
||||
},
|
||||
};
|
||||
41
packages/propel/src/command/command.tsx
Normal file
41
packages/propel/src/command/command.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
function CommandComponent({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return <CommandPrimitive data-slot="command" className={cn("", className)} {...props} />;
|
||||
}
|
||||
|
||||
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2"
|
||||
>
|
||||
<SearchIcon className="size-3.5 flex-shrink-0 text-custom-text-400" strokeWidth={1.5} />
|
||||
<CommandPrimitive.Input data-slot="command-input" className={cn(className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({ ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return <CommandPrimitive.List data-slot="command-list" {...props} />;
|
||||
}
|
||||
|
||||
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return <CommandPrimitive.Empty data-slot="command-empty" {...props} />;
|
||||
}
|
||||
|
||||
function CommandItem({ ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return <CommandPrimitive.Item data-slot="command-item" {...props} />;
|
||||
}
|
||||
|
||||
const Command = Object.assign(CommandComponent, {
|
||||
Input: CommandInput,
|
||||
List: CommandList,
|
||||
Empty: CommandEmpty,
|
||||
Item: CommandItem,
|
||||
});
|
||||
|
||||
export { Command };
|
||||
1
packages/propel/src/command/index.ts
Normal file
1
packages/propel/src/command/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./command";
|
||||
378
packages/propel/src/context-menu/context-menu.stories.tsx
Normal file
378
packages/propel/src/context-menu/context-menu.stories.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Copy, Download, Edit, Share, Trash, ChevronRight, Star, Archive } from "lucide-react";
|
||||
import { ContextMenu } from "./context-menu";
|
||||
|
||||
// cannot use satisfies here because base-ui does not have portable types.
|
||||
const meta: Meta<typeof ContextMenu> = {
|
||||
title: "Components/ContextMenu",
|
||||
component: ContextMenu,
|
||||
subcomponents: {
|
||||
ContextMenuTrigger: ContextMenu.Trigger,
|
||||
ContextMenuPortal: ContextMenu.Portal,
|
||||
ContextMenuContent: ContextMenu.Content,
|
||||
ContextMenuItem: ContextMenu.Item,
|
||||
ContextMenuSeparator: ContextMenu.Separator,
|
||||
ContextMenuSubmenu: ContextMenu.Submenu,
|
||||
ContextMenuSubmenuTrigger: ContextMenu.SubmenuTrigger,
|
||||
},
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>Back</ContextMenu.Item>
|
||||
<ContextMenu.Item>Forward</ContextMenu.Item>
|
||||
<ContextMenu.Item>Reload</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>More Tools</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>
|
||||
<Share className="mr-2 h-4 w-4" />
|
||||
Share
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSubmenus: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Submenu>
|
||||
<ContextMenu.SubmenuTrigger>
|
||||
<Share className="mr-2 h-4 w-4" />
|
||||
Share
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenu.SubmenuTrigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>Email</ContextMenu.Item>
|
||||
<ContextMenu.Item>Message</ContextMenu.Item>
|
||||
<ContextMenu.Item>Copy Link</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Submenu>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledItems: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item disabled>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit (Disabled)
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item disabled>
|
||||
<Share className="mr-2 h-4 w-4" />
|
||||
Share (Disabled)
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OnFileCard: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="w-64 p-4 border border-custom-border-200 rounded-lg hover:bg-custom-background-80 cursor-pointer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-custom-primary-100 rounded flex items-center justify-center text-white text-lg">
|
||||
📄
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Document.pdf</div>
|
||||
<div className="text-sm text-custom-text-400">2.4 MB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Link
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Star className="mr-2 h-4 w-4" />
|
||||
Add to Favorites
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
Archive
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OnImage: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="relative w-80 h-56 bg-custom-background-80 rounded-lg overflow-hidden cursor-pointer">
|
||||
<div className="absolute inset-0 flex items-center justify-center text-custom-text-400">
|
||||
Image Placeholder
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Save Image
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Image
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Image URL
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>Open Image in New Tab</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OnText: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="w-96 p-6 border border-custom-border-200 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2">Context Menu on Text</h3>
|
||||
<p className="text-custom-text-300">
|
||||
Right click anywhere on this text area to see the context menu. This demonstrates how context menus can be
|
||||
applied to text content areas.
|
||||
</p>
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>Select All</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const NestedSubmenus: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>New File</ContextMenu.Item>
|
||||
<ContextMenu.Item>New Folder</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Submenu>
|
||||
<ContextMenu.SubmenuTrigger>
|
||||
Import
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenu.SubmenuTrigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>From File</ContextMenu.Item>
|
||||
<ContextMenu.Item>From URL</ContextMenu.Item>
|
||||
<ContextMenu.Submenu>
|
||||
<ContextMenu.SubmenuTrigger>
|
||||
From Cloud
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenu.SubmenuTrigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>Google Drive</ContextMenu.Item>
|
||||
<ContextMenu.Item>Dropbox</ContextMenu.Item>
|
||||
<ContextMenu.Item>OneDrive</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Submenu>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Submenu>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithKeyboardShortcuts: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
|
||||
Right click here
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
<span className="ml-auto text-xs text-custom-text-400">⌘C</span>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
<span className="ml-auto text-xs text-custom-text-400">⌘E</span>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
<span className="ml-auto text-xs text-custom-text-400">⌘D</span>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>
|
||||
<Trash className="mr-2 h-4 w-4 text-red-500" />
|
||||
<span className="text-red-500">Delete</span>
|
||||
<span className="ml-auto text-xs text-custom-text-400">⌘⌫</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
);
|
||||
},
|
||||
};
|
||||
128
packages/propel/src/context-menu/context-menu.tsx
Normal file
128
packages/propel/src/context-menu/context-menu.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as React from "react";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "@base-ui-components/react/context-menu";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export interface ContextMenuProps extends React.ComponentProps<typeof ContextMenuPrimitive.Root> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ContextMenuTriggerProps extends React.ComponentProps<typeof ContextMenuPrimitive.Trigger> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ContextMenuContentProps extends React.ComponentProps<typeof ContextMenuPrimitive.Positioner> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
sideOffset?: number;
|
||||
}
|
||||
|
||||
export interface ContextMenuItemProps extends React.ComponentProps<typeof ContextMenuPrimitive.Item> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ContextMenuRoot = React.forwardRef<React.ElementRef<typeof ContextMenuPrimitive.Root>, ContextMenuProps>(
|
||||
({ children, ...props }, _ref) => <ContextMenuPrimitive.Root {...props}>{children}</ContextMenuPrimitive.Root>
|
||||
);
|
||||
|
||||
const ContextMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Trigger>,
|
||||
ContextMenuTriggerProps
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Trigger ref={ref} className={cn("outline-none", className)} {...props}>
|
||||
{children}
|
||||
</ContextMenuPrimitive.Trigger>
|
||||
));
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Positioner>,
|
||||
ContextMenuContentProps
|
||||
>(({ className, children, side = "bottom", sideOffset = 4, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Positioner ref={ref} side={side} sideOffset={sideOffset} {...props}>
|
||||
<ContextMenuPrimitive.Popup
|
||||
className={cn(
|
||||
"z-50 min-w-32 overflow-hidden rounded-md border border-custom-border-200 bg-custom-background-100 p-1 shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.Popup>
|
||||
</ContextMenuPrimitive.Positioner>
|
||||
));
|
||||
|
||||
const ContextMenuItem = React.forwardRef<React.ElementRef<typeof ContextMenuPrimitive.Item>, ContextMenuItemProps>(
|
||||
({ className, disabled, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||
"focus:bg-custom-background-90 focus:text-custom-text-100",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.Item>
|
||||
)
|
||||
);
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentProps<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-custom-border-200", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
const ContextMenuSubmenu = ContextMenuPrimitive.SubmenuRoot;
|
||||
|
||||
const ContextMenuSubmenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubmenuTrigger>,
|
||||
React.ComponentProps<typeof ContextMenuPrimitive.SubmenuTrigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubmenuTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:outline-none",
|
||||
"focus:bg-custom-background-90 data-[state=open]:bg-custom-background-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.SubmenuTrigger>
|
||||
));
|
||||
|
||||
ContextMenuRoot.displayName = "ContextMenu";
|
||||
ContextMenuTrigger.displayName = "ContextMenuTrigger";
|
||||
ContextMenuContent.displayName = "ContextMenuContent";
|
||||
ContextMenuItem.displayName = "ContextMenuItem";
|
||||
ContextMenuSeparator.displayName = "ContextMenuSeparator";
|
||||
ContextMenuSubmenuTrigger.displayName = "ContextMenuSubmenuTrigger";
|
||||
|
||||
// compound components
|
||||
const ContextMenu = Object.assign(ContextMenuRoot, {
|
||||
Trigger: ContextMenuTrigger,
|
||||
Portal: ContextMenuPortal,
|
||||
Content: ContextMenuContent,
|
||||
Item: ContextMenuItem,
|
||||
Separator: ContextMenuSeparator,
|
||||
Submenu: ContextMenuSubmenu,
|
||||
SubmenuTrigger: ContextMenuSubmenuTrigger,
|
||||
});
|
||||
|
||||
export { ContextMenu };
|
||||
7
packages/propel/src/context-menu/index.ts
Normal file
7
packages/propel/src/context-menu/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ContextMenu } from "./context-menu";
|
||||
export type {
|
||||
ContextMenuProps,
|
||||
ContextMenuTriggerProps,
|
||||
ContextMenuContentProps,
|
||||
ContextMenuItemProps,
|
||||
} from "./context-menu";
|
||||
399
packages/propel/src/dialog/dialog.stories.tsx
Normal file
399
packages/propel/src/dialog/dialog.stories.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { X } from "lucide-react";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import { Dialog, EDialogWidth } from "./root";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Dialog",
|
||||
component: Dialog,
|
||||
subcomponents: {
|
||||
DialogPanel: Dialog.Panel,
|
||||
DialogTitle: Dialog.Title,
|
||||
},
|
||||
args: {
|
||||
children: null,
|
||||
open: false,
|
||||
onOpenChange: () => {},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
render(args) {
|
||||
const [{ open }, updateArgs] = useArgs();
|
||||
const setOpen = (value: boolean) => updateArgs({ open: value });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Dialog
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Dialog Title</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">This is the dialog content. You can put any content here.</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
} satisfies Meta<typeof Dialog>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const TopPosition: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Dialog (Top)
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel position="top">
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Top Positioned Dialog</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
This dialog appears at the top of the screen instead of centered.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallWidth: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Small Dialog
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel width={EDialogWidth.SM}>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Small Dialog</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">This is a small dialog.</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeWidth: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Large Dialog
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel width={EDialogWidth.XXXXL}>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Large Dialog</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
This is a large dialog with more horizontal space for content.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCloseButton: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Dialog with Close Button
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<Dialog.Title>Dialog with Close Button</Dialog.Title>
|
||||
<button onClick={() => setOpen(false)} className="rounded-full p-1 hover:bg-gray-100">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">This dialog has a close button in the header.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfirmationDialog: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
const handleConfirm = () => {
|
||||
alert("Confirmed!");
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600">
|
||||
Delete Item
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel width={EDialogWidth.SM}>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Confirm Deletion</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Are you sure you want to delete this item? This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className="rounded bg-red-500 px-4 py-2 text-sm text-white hover:bg-red-600"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FormDialog: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
alert("Form submitted!");
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Form
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel width={EDialogWidth.MD}>
|
||||
<form onSubmit={handleSubmit} className="p-6">
|
||||
<Dialog.Title>Create New Item</Dialog.Title>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
className="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
rows={3}
|
||||
className="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
placeholder="Enter description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ScrollableContent: Story = {
|
||||
render(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Open Scrollable Dialog
|
||||
</button>
|
||||
{open && (
|
||||
<Dialog {...args} open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Panel width={EDialogWidth.MD}>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>Scrollable Content</Dialog.Title>
|
||||
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||
{Array.from({ length: 20 }, (_, i) => (
|
||||
<p key={i} className="mb-2 text-sm text-gray-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllWidths: Story = {
|
||||
render() {
|
||||
const [openWidth, setOpenWidth] = useState<EDialogWidth | null>(null);
|
||||
|
||||
const widths = [
|
||||
{ width: EDialogWidth.SM, label: "Small" },
|
||||
{ width: EDialogWidth.MD, label: "Medium" },
|
||||
{ width: EDialogWidth.LG, label: "Large" },
|
||||
{ width: EDialogWidth.XL, label: "XL" },
|
||||
{ width: EDialogWidth.XXL, label: "2XL" },
|
||||
{ width: EDialogWidth.XXXL, label: "3XL" },
|
||||
{ width: EDialogWidth.XXXXL, label: "4XL" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{widths.map(({ width, label }) => (
|
||||
<button
|
||||
key={width}
|
||||
onClick={() => setOpenWidth(width)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
{widths.map(({ width, label }) => (
|
||||
<Dialog key={width} open={openWidth === width} onOpenChange={() => setOpenWidth(null)}>
|
||||
<Dialog.Panel width={width}>
|
||||
<div className="p-6">
|
||||
<Dialog.Title>{label} Dialog</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600">This dialog uses the {label} width variant.</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => setOpenWidth(null)}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
1
packages/propel/src/dialog/index.ts
Normal file
1
packages/propel/src/dialog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
115
packages/propel/src/dialog/root.tsx
Normal file
115
packages/propel/src/dialog/root.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, memo, useMemo } from "react";
|
||||
import { Dialog as BaseDialog } from "@base-ui-components/react";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
// enums
|
||||
|
||||
export enum EDialogWidth {
|
||||
SM = "sm:max-w-sm",
|
||||
MD = "sm:max-w-md",
|
||||
LG = "sm:max-w-lg",
|
||||
XL = "sm:max-w-xl",
|
||||
XXL = "sm:max-w-2xl",
|
||||
XXXL = "sm:max-w-3xl",
|
||||
XXXXL = "sm:max-w-4xl",
|
||||
VXL = "sm:max-w-5xl",
|
||||
VIXL = "sm:max-w-6xl",
|
||||
VIIXL = "sm:max-w-7xl",
|
||||
}
|
||||
|
||||
// Types
|
||||
export type DialogPosition = "center" | "top";
|
||||
|
||||
export interface DialogProps extends React.ComponentProps<typeof BaseDialog.Root> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DialogPanelProps extends React.ComponentProps<typeof BaseDialog.Popup> {
|
||||
width?: EDialogWidth;
|
||||
position?: DialogPosition;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DialogTitleProps extends React.ComponentProps<typeof BaseDialog.Title> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const OVERLAY_CLASSNAME = cn("fixed inset-0 z-backdrop bg-custom-backdrop");
|
||||
const BASE_CLASSNAME = "relative text-left bg-custom-background-100 rounded-lg shadow-md w-full z-modal";
|
||||
|
||||
// Utility functions
|
||||
const getPositionClassNames = (position: DialogPosition) =>
|
||||
cn("isolate fixed z-modal", {
|
||||
"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2": position === "center",
|
||||
"top-8 left-1/2 -translate-x-1/2": position === "top",
|
||||
});
|
||||
|
||||
const DialogPortal = memo<React.ComponentProps<typeof BaseDialog.Portal>>(({ children, ...props }) => (
|
||||
<BaseDialog.Portal data-slot="dialog-portal" {...props}>
|
||||
{children}
|
||||
</BaseDialog.Portal>
|
||||
));
|
||||
DialogPortal.displayName = "DialogPortal";
|
||||
|
||||
const DialogOverlay = memo<React.ComponentProps<typeof BaseDialog.Backdrop>>(({ className, ...props }) => (
|
||||
<BaseDialog.Backdrop data-slot="dialog-overlay" className={cn(OVERLAY_CLASSNAME, className)} {...props} />
|
||||
));
|
||||
DialogOverlay.displayName = "DialogOverlay";
|
||||
|
||||
const DialogComponent = memo<DialogProps>(({ children, ...props }) => (
|
||||
<BaseDialog.Root data-slot="dialog" {...props}>
|
||||
{children}
|
||||
</BaseDialog.Root>
|
||||
));
|
||||
DialogComponent.displayName = "Dialog";
|
||||
|
||||
const DialogTrigger = memo<React.ComponentProps<typeof BaseDialog.Trigger>>(({ children, ...props }) => (
|
||||
<BaseDialog.Trigger data-slot="dialog-trigger" {...props}>
|
||||
{children}
|
||||
</BaseDialog.Trigger>
|
||||
));
|
||||
DialogTrigger.displayName = "DialogTrigger";
|
||||
|
||||
const DialogPanel = forwardRef<React.ElementRef<typeof BaseDialog.Popup>, DialogPanelProps>(
|
||||
({ className, width = EDialogWidth.XXL, children, position = "center", ...props }, ref) => {
|
||||
const positionClassNames = useMemo(() => getPositionClassNames(position), [position]);
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<BaseDialog.Popup
|
||||
ref={ref}
|
||||
data-slot="dialog-content"
|
||||
className={cn(BASE_CLASSNAME, positionClassNames, width, className)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</BaseDialog.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
);
|
||||
DialogPanel.displayName = "DialogPanel";
|
||||
|
||||
const DialogTitle = memo<DialogTitleProps>(({ className, children, ...props }) => (
|
||||
<BaseDialog.Title data-slot="dialog-title" className={cn("text-lg leading-none font-semibold", className)} {...props}>
|
||||
{children}
|
||||
</BaseDialog.Title>
|
||||
));
|
||||
|
||||
DialogTitle.displayName = "DialogTitle";
|
||||
|
||||
// Create the compound Dialog component with proper typing
|
||||
const Dialog = Object.assign(DialogComponent, {
|
||||
Panel: DialogPanel,
|
||||
Title: DialogTitle,
|
||||
}) as typeof DialogComponent & {
|
||||
Panel: typeof DialogPanel;
|
||||
Title: typeof DialogTitle;
|
||||
};
|
||||
|
||||
export { Dialog, DialogTitle, DialogPanel };
|
||||
369
packages/propel/src/emoji-icon-picker/emoji-picker.stories.tsx
Normal file
369
packages/propel/src/emoji-icon-picker/emoji-picker.stories.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { EmojiPicker } from "./emoji-picker";
|
||||
import { EmojiIconPickerTypes, TChangeHandlerProps } from "./helper";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Emoji/EmojiPicker",
|
||||
component: EmojiPicker,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof EmojiPicker>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="😊 Pick an emoji or icon"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<div className="font-medium mb-2">Selected:</div>
|
||||
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenToEmojiTab: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="😊 Choose Emoji"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm">Selected: {selectedValue.type === "emoji" ? selectedValue.value : "Icon"}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenToIconTab: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="🎨 Choose Icon"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm">
|
||||
Selected:{" "}
|
||||
{selectedValue.type === "icon" && typeof selectedValue.value === "object"
|
||||
? selectedValue.value.name
|
||||
: "Emoji"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LucideIcons: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Lucide Icons"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
iconType="lucide"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<div className="font-medium mb-2">Selected Icon:</div>
|
||||
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MaterialIcons: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Material Icons"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
iconType="material"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<div className="font-medium mb-2">Selected Icon:</div>
|
||||
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseOnSelectDisabled: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValues, setSelectedValues] = useState<TChangeHandlerProps[]>([]);
|
||||
|
||||
const handleChange = (value: TChangeHandlerProps) => {
|
||||
setSelectedValues((prev) => [...prev, value]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleChange}
|
||||
label="Select Multiple (Stays Open)"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-1.5 text-sm bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setSelectedValues([])}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
{selectedValues.length > 0 && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<div className="font-medium mb-2">Selected ({selectedValues.length}):</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{selectedValues.map((val, idx) => (
|
||||
<span key={idx} className="text-lg">
|
||||
{val.type === "emoji" ? val.value : "🎨"}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomSearchPlaceholder: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Custom Search"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
searchPlaceholder="Type to find emojis..."
|
||||
/>
|
||||
{selectedValue && <div className="text-sm">Selected: {JSON.stringify(selectedValue)}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SearchDisabled: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="No Search"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
searchDisabled
|
||||
/>
|
||||
{selectedValue && <div className="text-sm">Selected: {JSON.stringify(selectedValue)}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIconColor: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Custom Icon Color"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
defaultIconColor="#FF5733"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DifferentPlacements: Story = {
|
||||
render() {
|
||||
const [isOpen1, setIsOpen1] = useState(false);
|
||||
const [isOpen2, setIsOpen2] = useState(false);
|
||||
const [isOpen3, setIsOpen3] = useState(false);
|
||||
const [isOpen4, setIsOpen4] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Bottom Start:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen1}
|
||||
handleToggle={setIsOpen1}
|
||||
onChange={() => {}}
|
||||
label="Bottom Start"
|
||||
placement="bottom-start"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Bottom End:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen2}
|
||||
handleToggle={setIsOpen2}
|
||||
onChange={() => {}}
|
||||
label="Bottom End"
|
||||
placement="bottom-end"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Top Start:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen3}
|
||||
handleToggle={setIsOpen3}
|
||||
onChange={() => {}}
|
||||
label="Top Start"
|
||||
placement="top-start"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Top End:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen4}
|
||||
handleToggle={setIsOpen4}
|
||||
onChange={() => {}}
|
||||
label="Top End"
|
||||
placement="top-end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InFormContext: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
emoji: null as TChangeHandlerProps | null,
|
||||
});
|
||||
|
||||
const handleEmojiChange = (value: TChangeHandlerProps) => {
|
||||
setFormData((prev) => ({ ...prev, emoji: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
alert(`Form submitted:\n${JSON.stringify(formData, null, 2)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md p-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 p-6 border border-custom-border-200 rounded-lg">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Project Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-custom-background-80 border border-custom-border-200 rounded"
|
||||
placeholder="Enter project title"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Project Icon</label>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleEmojiChange}
|
||||
label={formData.emoji && formData.emoji.type === "emoji" ? formData.emoji.value : "Click to select icon"}
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
buttonClassName="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90 w-full text-left"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
|
||||
>
|
||||
Create Project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
136
packages/propel/src/emoji-icon-picker/emoji-picker.tsx
Normal file
136
packages/propel/src/emoji-icon-picker/emoji-picker.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { Tabs } from "@base-ui-components/react";
|
||||
import { Popover } from "../popover";
|
||||
import { cn } from "../utils/classname";
|
||||
import { convertPlacementToSideAndAlign } from "../utils/placement";
|
||||
import { EmojiRoot } from "./emoji/emoji";
|
||||
import { emojiToString, TCustomEmojiPicker, EmojiIconPickerTypes } from "./helper";
|
||||
import { IconRoot } from "./icon/icon-root";
|
||||
|
||||
export const EmojiPicker: React.FC<TCustomEmojiPicker> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
handleToggle,
|
||||
buttonClassName,
|
||||
closeOnSelect = true,
|
||||
defaultIconColor = "#6d7b8a",
|
||||
defaultOpen = EmojiIconPickerTypes.EMOJI,
|
||||
disabled = false,
|
||||
dropdownClassName,
|
||||
label,
|
||||
onChange,
|
||||
placement = "bottom-start",
|
||||
searchDisabled = false,
|
||||
searchPlaceholder = "Search",
|
||||
iconType = "lucide",
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
} = props;
|
||||
|
||||
// side and align calculations
|
||||
const { finalSide, finalAlign } = useMemo(() => {
|
||||
if (placement) {
|
||||
const converted = convertPlacementToSideAndAlign(placement);
|
||||
return { finalSide: converted.side, finalAlign: converted.align };
|
||||
}
|
||||
return { finalSide: side, finalAlign: align };
|
||||
}, [placement, side, align]);
|
||||
|
||||
const handleEmojiChange = useCallback(
|
||||
(value: string) => {
|
||||
onChange({
|
||||
type: EmojiIconPickerTypes.EMOJI,
|
||||
value: emojiToString(value),
|
||||
});
|
||||
if (closeOnSelect) handleToggle(false);
|
||||
},
|
||||
[onChange, closeOnSelect, handleToggle]
|
||||
);
|
||||
|
||||
const handleIconChange = useCallback(
|
||||
(value: { name: string; color: string }) => {
|
||||
onChange({
|
||||
type: EmojiIconPickerTypes.ICON,
|
||||
value: value,
|
||||
});
|
||||
if (closeOnSelect) handleToggle(false);
|
||||
},
|
||||
[onChange, closeOnSelect, handleToggle]
|
||||
);
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
key: "emoji",
|
||||
label: "Emoji",
|
||||
content: (
|
||||
<EmojiRoot
|
||||
onChange={handleEmojiChange}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
searchDisabled={searchDisabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "icon",
|
||||
label: "Icon",
|
||||
content: (
|
||||
<IconRoot
|
||||
defaultColor={defaultIconColor}
|
||||
onChange={handleIconChange}
|
||||
searchDisabled={searchDisabled}
|
||||
iconType={iconType}
|
||||
/>
|
||||
),
|
||||
},
|
||||
].map((tab) => ({
|
||||
key: tab.key,
|
||||
label: tab.label,
|
||||
content: tab.content,
|
||||
})),
|
||||
[defaultIconColor, searchDisabled, searchPlaceholder, iconType, handleEmojiChange, handleIconChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleToggle}>
|
||||
<Popover.Button className={cn("outline-none", buttonClassName)} disabled={disabled}>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
positionerClassName="z-50"
|
||||
className={cn(
|
||||
"w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
|
||||
dropdownClassName
|
||||
)}
|
||||
side={finalSide}
|
||||
align={finalAlign}
|
||||
sideOffset={8}
|
||||
>
|
||||
<Tabs.Root defaultValue={defaultOpen}>
|
||||
<Tabs.List className="grid grid-cols-2 gap-1 px-3.5 pt-3">
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Tab
|
||||
key={tab.key}
|
||||
value={tab.key}
|
||||
className={({ selected }) =>
|
||||
cn("py-1 text-sm rounded border border-custom-border-200 bg-custom-background-80", {
|
||||
"bg-custom-background-100 text-custom-text-100": selected,
|
||||
"text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60": !selected,
|
||||
})
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Panel key={tab.key} value={tab.key} className="h-80 overflow-hidden overflow-y-auto">
|
||||
{tab.content}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
66
packages/propel/src/emoji-icon-picker/emoji/emoji.tsx
Normal file
66
packages/propel/src/emoji-icon-picker/emoji/emoji.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { EmojiPicker } from "frimousse";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
type EmojiRootProps = {
|
||||
onChange: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
searchDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const EmojiRoot = (props: EmojiRootProps) => {
|
||||
const { onChange, searchPlaceholder = "Search", searchDisabled = false } = props;
|
||||
return (
|
||||
<EmojiPicker.Root
|
||||
data-slot="emoji-picker"
|
||||
className="isolate flex flex-col rounded-md h-full w-full border-none p-2"
|
||||
onEmojiSelect={(val) => onChange(val.emoji)}
|
||||
>
|
||||
<div className="flex items-center gap-2 justify-between [&>[data-slot='emoji-picker-search-wrapper']]:flex-grow [&>[data-slot='emoji-picker-search-wrapper']]:p-0 px-1.5 py-2 sticky top-0 z-10 bg-custom-background-100">
|
||||
<div data-slot="emoji-picker-search-wrapper" className="p-2">
|
||||
<EmojiPicker.Search
|
||||
placeholder={searchPlaceholder}
|
||||
disabled={searchDisabled}
|
||||
className="block rounded-md bg-transparent placeholder-custom-text-400 focus:outline-none px-3 py-2 border-[0.5px] border-custom-border-200 text-[1rem] p-0 h-full w-full flex-grow-0 focus:border-custom-primary-100"
|
||||
/>
|
||||
</div>
|
||||
<EmojiPicker.SkinToneSelector
|
||||
data-slot="emoji-picker-skin-tone-selector"
|
||||
className="bg-custom-background-100 hover:bg-accent mx-2 mb-1.5 size-8 rounded-md text-lg flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<EmojiPicker.Viewport data-slot="emoji-picker-content" className={cn("relative flex-1 outline-none")}>
|
||||
<EmojiPicker.List
|
||||
data-slot="emoji-picker-list"
|
||||
className={cn("pb-2 select-none")}
|
||||
components={{
|
||||
CategoryHeader: ({ category, ...props }) => (
|
||||
<div
|
||||
data-slot="emoji-picker-list-category-header"
|
||||
className="bg-custom-background-100 text-custom-text-300 px-3 pb-1.5 text-xs font-medium"
|
||||
{...props}
|
||||
>
|
||||
{category.label}
|
||||
</div>
|
||||
),
|
||||
Row: ({ children, ...props }) => (
|
||||
<div data-slot="emoji-picker-list-row" className="scroll-my-1.5 px-1.5" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Emoji: ({ emoji, ...props }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={emoji?.label ?? emoji?.emoji}
|
||||
data-slot="emoji-picker-list-emoji"
|
||||
className="data-active:bg-accent flex size-8 items-center justify-center rounded-md text-lg"
|
||||
{...props}
|
||||
>
|
||||
{emoji.emoji}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EmojiPicker.Viewport>
|
||||
</EmojiPicker.Root>
|
||||
);
|
||||
};
|
||||
1
packages/propel/src/emoji-icon-picker/emoji/index.ts
Normal file
1
packages/propel/src/emoji-icon-picker/emoji/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./emoji";
|
||||
156
packages/propel/src/emoji-icon-picker/helper.tsx
Normal file
156
packages/propel/src/emoji-icon-picker/helper.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { TPlacement, TSide, TAlign } from "../utils/placement";
|
||||
|
||||
export const EmojiIconPickerTypes = {
|
||||
EMOJI: "emoji",
|
||||
ICON: "icon",
|
||||
} as const;
|
||||
|
||||
export type TChangeHandlerProps =
|
||||
| {
|
||||
type: typeof EmojiIconPickerTypes.EMOJI;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
type: typeof EmojiIconPickerTypes.ICON;
|
||||
value: {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TEmojiIconPickerTypes = typeof EmojiIconPickerTypes.EMOJI | typeof EmojiIconPickerTypes.ICON;
|
||||
|
||||
export type TCustomEmojiPicker = {
|
||||
isOpen: boolean;
|
||||
handleToggle: (value: boolean) => void;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
closeOnSelect?: boolean;
|
||||
defaultIconColor?: string;
|
||||
defaultOpen?: TEmojiIconPickerTypes;
|
||||
disabled?: boolean;
|
||||
dropdownClassName?: string;
|
||||
label: React.ReactNode;
|
||||
onChange: (value: TChangeHandlerProps) => void;
|
||||
placement?: TPlacement;
|
||||
searchDisabled?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
iconType?: "material" | "lucide";
|
||||
theme?: "light" | "dark";
|
||||
side?: TSide;
|
||||
align?: TAlign;
|
||||
};
|
||||
|
||||
export type TIconsListProps = {
|
||||
defaultColor: string;
|
||||
onChange: (val: { name: string; color: string }) => void;
|
||||
searchDisabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts the given hex color to ensure it has enough contrast.
|
||||
* @param {string} hex - The hex color code input by the user.
|
||||
* @returns {string} - The adjusted hex color code.
|
||||
*/
|
||||
export const adjustColorForContrast = (hex: string): string => {
|
||||
// Ensure hex color is valid
|
||||
if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) {
|
||||
throw new Error("Invalid hex color code");
|
||||
}
|
||||
|
||||
// Convert hex to RGB
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
if (hex.length === 4) {
|
||||
r = parseInt(hex[1] + hex[1], 16);
|
||||
g = parseInt(hex[2] + hex[2], 16);
|
||||
b = parseInt(hex[3] + hex[3], 16);
|
||||
} else if (hex.length === 7) {
|
||||
r = parseInt(hex[1] + hex[2], 16);
|
||||
g = parseInt(hex[3] + hex[4], 16);
|
||||
b = parseInt(hex[5] + hex[6], 16);
|
||||
}
|
||||
|
||||
// Calculate luminance
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
// If the color is too light, darken it
|
||||
if (luminance > 0.5) {
|
||||
r = Math.max(0, r - 50);
|
||||
g = Math.max(0, g - 50);
|
||||
b = Math.max(0, b - 50);
|
||||
}
|
||||
|
||||
// Convert RGB back to hex
|
||||
const toHex = (value: number): string => {
|
||||
const hex = value.toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLORS = ["#95999f", "#6d7b8a", "#5e6ad2", "#02b5ed", "#02b55c", "#f2be02", "#e57a00", "#f38e82"];
|
||||
|
||||
/**
|
||||
* Enhanced emoji to decimal conversion that preserves emoji sequences
|
||||
* This function handles complex emoji sequences including skin tone modifiers
|
||||
* @param emoji - The emoji string to convert
|
||||
* @returns Array of decimal Unicode code points
|
||||
*/
|
||||
export function emojiToDecimalEnhanced(emoji: string): number[] {
|
||||
const codePoints: number[] = [];
|
||||
|
||||
// Use Array.from to properly handle multi-byte Unicode characters
|
||||
const characters = Array.from(emoji);
|
||||
|
||||
for (const char of characters) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (codePoint !== undefined) {
|
||||
codePoints.push(codePoint);
|
||||
}
|
||||
}
|
||||
|
||||
return codePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced decimal to emoji conversion that handles emoji sequences
|
||||
* @param decimals - Array of decimal Unicode code points
|
||||
* @returns The reconstructed emoji string
|
||||
*/
|
||||
export function decimalToEmojiEnhanced(decimals: number[]): string {
|
||||
return decimals.map((decimal) => String.fromCodePoint(decimal)).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts emoji to a string representation for storage
|
||||
* This creates a comma-separated string of decimal values
|
||||
* @param emoji - The emoji string to convert
|
||||
* @returns String representation of decimal values
|
||||
*/
|
||||
export function emojiToString(emoji: string): string {
|
||||
const decimals = emojiToDecimalEnhanced(emoji);
|
||||
return decimals.join("-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string representation back to emoji
|
||||
* @param emojiString - Comma-separated string of decimal values
|
||||
* @returns The reconstructed emoji string
|
||||
*/
|
||||
export function stringToEmoji(emojiString: string): string {
|
||||
if (!emojiString) return "";
|
||||
const decimals = emojiString
|
||||
.split("-")
|
||||
.map((s) => Number(s.trim()))
|
||||
.filter((n) => Number.isFinite(n) && n >= 0 && n <= 0x10ffff);
|
||||
try {
|
||||
return decimalToEmojiEnhanced(decimals);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const getEmojiSize = (size: number) => size * 0.9 * 0.0625;
|
||||
128
packages/propel/src/emoji-icon-picker/icon/icon-root.tsx
Normal file
128
packages/propel/src/emoji-icon-picker/icon/icon-root.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { InfoIcon, Search } from "lucide-react";
|
||||
import { cn } from "../../utils/classname";
|
||||
import { adjustColorForContrast, DEFAULT_COLORS } from "../helper";
|
||||
import { LucideIconsList } from "./lucide-root";
|
||||
import { MaterialIconList } from "./material-root";
|
||||
|
||||
type IconRootProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
defaultColor: string;
|
||||
searchDisabled?: boolean;
|
||||
iconType: "material" | "lucide";
|
||||
};
|
||||
|
||||
export const IconRoot: React.FC<IconRootProps> = (props) => {
|
||||
const { defaultColor, onChange, searchDisabled = false, iconType } = props;
|
||||
// states
|
||||
const [activeColor, setActiveColor] = useState(defaultColor);
|
||||
const [showHexInput, setShowHexInput] = useState(false);
|
||||
const [hexValue, setHexValue] = useState("");
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase() ?? "")) setShowHexInput(false);
|
||||
else {
|
||||
setHexValue(defaultColor?.slice(1, 7) ?? "");
|
||||
setShowHexInput(true);
|
||||
}
|
||||
}, [defaultColor]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col sticky top-0 bg-custom-background-100">
|
||||
{!searchDisabled && (
|
||||
<div className="flex items-center px-2 py-[15px] w-full ">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center gap-2 bg-custom-background-90 h-10 rounded-lg w-full px-[30px] border",
|
||||
{
|
||||
"border-custom-primary-100": isInputFocused,
|
||||
"border-transparent": !isInputFocused,
|
||||
}
|
||||
)}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
>
|
||||
<Search className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
|
||||
|
||||
<input
|
||||
placeholder="Search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="block rounded-md bg-transparent placeholder-custom-text-400 focus:outline-none px-3 py-2 border-[0.5px] border-custom-border-200 text-[1rem] border-none p-0 h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-9 gap-2 items-center justify-items-center px-2.5 py-1 h-9">
|
||||
{showHexInput ? (
|
||||
<div className="col-span-8 flex items-center gap-1 justify-self-stretch ml-2">
|
||||
<span
|
||||
className="h-4 w-4 flex-shrink-0 rounded-full mr-1"
|
||||
style={{
|
||||
backgroundColor: `#${hexValue}`,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-custom-text-300 flex-shrink-0">HEX</span>
|
||||
<span className="text-xs text-custom-text-200 flex-shrink-0 -mr-1">#</span>
|
||||
<input
|
||||
type="text"
|
||||
value={hexValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setHexValue(value);
|
||||
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`));
|
||||
}}
|
||||
className="block placeholder-custom-text-400 focus:outline-none px-3 py-2 border-[0.5px] border-custom-border-200 flex-grow pl-0 text-xs text-custom-text-200 rounded border-none bg-transparent ring-0"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
DEFAULT_COLORS.map((curCol) => (
|
||||
<button
|
||||
key={curCol}
|
||||
type="button"
|
||||
className="grid place-items-center size-5"
|
||||
onClick={() => {
|
||||
setActiveColor(curCol);
|
||||
setHexValue(curCol.slice(1, 7));
|
||||
}}
|
||||
>
|
||||
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn("grid place-items-center h-4 w-4 rounded-full border border-transparent", {
|
||||
"border-custom-border-400": !showHexInput,
|
||||
})}
|
||||
onClick={() => {
|
||||
setShowHexInput((prevData) => !prevData);
|
||||
setHexValue(activeColor.slice(1, 7));
|
||||
}}
|
||||
>
|
||||
{showHexInput ? (
|
||||
<span className="conical-gradient h-4 w-4 rounded-full" />
|
||||
) : (
|
||||
<span className="text-custom-text-300 text-[0.6rem] grid place-items-center">#</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full pl-4 pr-3 py-1 h-6">
|
||||
<InfoIcon className="h-3 w-3" />
|
||||
<p className="text-xs"> Colors will be adjusted to ensure sufficient contrast.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-1 px-2.5 justify-items-center mt-2">
|
||||
{iconType === "material" ? (
|
||||
<MaterialIconList query={query} onChange={onChange} activeColor={activeColor} />
|
||||
) : (
|
||||
<LucideIconsList query={query} onChange={onChange} activeColor={activeColor} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
packages/propel/src/emoji-icon-picker/icon/index.ts
Normal file
1
packages/propel/src/emoji-icon-picker/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icon-root";
|
||||
34
packages/propel/src/emoji-icon-picker/icon/lucide-root.tsx
Normal file
34
packages/propel/src/emoji-icon-picker/icon/lucide-root.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { LUCIDE_ICONS_LIST } from "../lucide-icons";
|
||||
|
||||
type LucideIconsListProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
activeColor: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export const LucideIconsList: React.FC<LucideIconsListProps> = (props) => {
|
||||
const { query, onChange, activeColor } = props;
|
||||
|
||||
const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredArray.map((icon) => (
|
||||
<button
|
||||
key={icon.name}
|
||||
type="button"
|
||||
className="h-9 w-9 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
onChange({
|
||||
name: icon.name,
|
||||
color: activeColor,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<icon.element style={{ color: activeColor }} className="size-4" />
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
55
packages/propel/src/emoji-icon-picker/icon/material-root.tsx
Normal file
55
packages/propel/src/emoji-icon-picker/icon/material-root.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
import { MATERIAL_ICONS_LIST } from "../material-icons";
|
||||
|
||||
type MaterialIconListProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
activeColor: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export const MaterialIconList: React.FC<MaterialIconListProps> = (props) => {
|
||||
const { query, onChange, activeColor } = props;
|
||||
|
||||
const filteredArray = MATERIAL_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const isMaterialSymbolsFontLoaded = useFontFaceObserver([
|
||||
{
|
||||
family: `Material Symbols Rounded`,
|
||||
style: `normal`,
|
||||
weight: `normal`,
|
||||
stretch: `condensed`,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredArray.map((icon) => (
|
||||
<button
|
||||
key={icon.name}
|
||||
type="button"
|
||||
className="h-9 w-9 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
onChange({
|
||||
name: icon.name,
|
||||
color: activeColor,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isMaterialSymbolsFontLoaded ? (
|
||||
<span
|
||||
style={{ color: activeColor }}
|
||||
className="material-symbols-rounded !text-[1.25rem] !leading-[1.25rem]"
|
||||
>
|
||||
{icon.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="size-5 rounded animate-pulse bg-custom-background-80" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
4
packages/propel/src/emoji-icon-picker/index.ts
Normal file
4
packages/propel/src/emoji-icon-picker/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./emoji-picker";
|
||||
export * from "./helper";
|
||||
export * from "./lucide-icons";
|
||||
export * from "./material-icons";
|
||||
315
packages/propel/src/emoji-icon-picker/lucide-icons.tsx
Normal file
315
packages/propel/src/emoji-icon-picker/lucide-icons.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
Activity,
|
||||
Airplay,
|
||||
AlertCircle,
|
||||
AlertOctagon,
|
||||
AlertTriangle,
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Anchor,
|
||||
Aperture,
|
||||
Archive,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
AtSign,
|
||||
Award,
|
||||
BarChart,
|
||||
BarChart2,
|
||||
Battery,
|
||||
BatteryCharging,
|
||||
Bell,
|
||||
BellOff,
|
||||
Book,
|
||||
Bookmark,
|
||||
BookOpen,
|
||||
Box,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
Camera,
|
||||
CameraOff,
|
||||
Cast,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
Clock,
|
||||
Cloud,
|
||||
CloudDrizzle,
|
||||
CloudLightning,
|
||||
CloudOff,
|
||||
CloudRain,
|
||||
CloudSnow,
|
||||
Code,
|
||||
Codepen,
|
||||
Codesandbox,
|
||||
Coffee,
|
||||
Columns,
|
||||
Command,
|
||||
Compass,
|
||||
Copy,
|
||||
CornerDownLeft,
|
||||
CornerDownRight,
|
||||
CornerLeftDown,
|
||||
CornerLeftUp,
|
||||
CornerRightDown,
|
||||
CornerRightUp,
|
||||
CornerUpLeft,
|
||||
CornerUpRight,
|
||||
Cpu,
|
||||
CreditCard,
|
||||
Crop,
|
||||
Crosshair,
|
||||
Database,
|
||||
Delete,
|
||||
Disc,
|
||||
Divide,
|
||||
DivideCircle,
|
||||
DivideSquare,
|
||||
DollarSign,
|
||||
Download,
|
||||
DownloadCloud,
|
||||
Dribbble,
|
||||
Droplet,
|
||||
Edit,
|
||||
Edit2,
|
||||
Edit3,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Facebook,
|
||||
FastForward,
|
||||
Feather,
|
||||
Figma,
|
||||
File,
|
||||
FileMinus,
|
||||
FilePlus,
|
||||
FileText,
|
||||
Film,
|
||||
Filter,
|
||||
Flag,
|
||||
Folder,
|
||||
FolderMinus,
|
||||
FolderPlus,
|
||||
Framer,
|
||||
Frown,
|
||||
Gift,
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitMerge,
|
||||
GitPullRequest,
|
||||
Github,
|
||||
Gitlab,
|
||||
Globe,
|
||||
Grid,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Headphones,
|
||||
Heart,
|
||||
HelpCircle,
|
||||
Hexagon,
|
||||
Home,
|
||||
Image,
|
||||
Inbox,
|
||||
Info,
|
||||
Instagram,
|
||||
Italic,
|
||||
Key,
|
||||
Layers,
|
||||
Layout,
|
||||
LifeBuoy,
|
||||
Link,
|
||||
Link2,
|
||||
Linkedin,
|
||||
List,
|
||||
Loader,
|
||||
Lock,
|
||||
LogIn,
|
||||
LogOut,
|
||||
Mail,
|
||||
Map as MapIcon,
|
||||
MapPin,
|
||||
Maximize,
|
||||
Maximize2,
|
||||
Meh,
|
||||
Menu,
|
||||
MessageCircle,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
MicOff,
|
||||
Minimize,
|
||||
Minimize2,
|
||||
Minus,
|
||||
MinusCircle,
|
||||
MinusSquare,
|
||||
CircleChevronDown,
|
||||
UsersRound,
|
||||
ToggleLeft,
|
||||
Search,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
export const LUCIDE_ICONS_LIST = [
|
||||
{ name: "Activity", element: Activity },
|
||||
{ name: "Airplay", element: Airplay },
|
||||
{ name: "AlertCircle", element: AlertCircle },
|
||||
{ name: "AlertOctagon", element: AlertOctagon },
|
||||
{ name: "AlertTriangle", element: AlertTriangle },
|
||||
{ name: "AlignCenter", element: AlignCenter },
|
||||
{ name: "AlignJustify", element: AlignJustify },
|
||||
{ name: "AlignLeft", element: AlignLeft },
|
||||
{ name: "AlignRight", element: AlignRight },
|
||||
{ name: "Anchor", element: Anchor },
|
||||
{ name: "Aperture", element: Aperture },
|
||||
{ name: "Archive", element: Archive },
|
||||
{ name: "ArrowDown", element: ArrowDown },
|
||||
{ name: "ArrowLeft", element: ArrowLeft },
|
||||
{ name: "ArrowRight", element: ArrowRight },
|
||||
{ name: "ArrowUp", element: ArrowUp },
|
||||
{ name: "AtSign", element: AtSign },
|
||||
{ name: "Award", element: Award },
|
||||
{ name: "BarChart", element: BarChart },
|
||||
{ name: "BarChart2", element: BarChart2 },
|
||||
{ name: "Battery", element: Battery },
|
||||
{ name: "BatteryCharging", element: BatteryCharging },
|
||||
{ name: "Bell", element: Bell },
|
||||
{ name: "BellOff", element: BellOff },
|
||||
{ name: "Book", element: Book },
|
||||
{ name: "Bookmark", element: Bookmark },
|
||||
{ name: "BookOpen", element: BookOpen },
|
||||
{ name: "Box", element: Box },
|
||||
{ name: "Briefcase", element: Briefcase },
|
||||
{ name: "Calendar", element: Calendar },
|
||||
{ name: "Camera", element: Camera },
|
||||
{ name: "CameraOff", element: CameraOff },
|
||||
{ name: "Cast", element: Cast },
|
||||
{ name: "CircleChevronDown", element: CircleChevronDown },
|
||||
{ name: "Check", element: Check },
|
||||
{ name: "CheckCircle", element: CheckCircle },
|
||||
{ name: "CheckSquare", element: CheckSquare },
|
||||
{ name: "ChevronDown", element: ChevronDown },
|
||||
{ name: "ChevronLeft", element: ChevronLeft },
|
||||
{ name: "ChevronRight", element: ChevronRight },
|
||||
{ name: "ChevronUp", element: ChevronUp },
|
||||
{ name: "Clipboard", element: Clipboard },
|
||||
{ name: "Clock", element: Clock },
|
||||
{ name: "Cloud", element: Cloud },
|
||||
{ name: "CloudDrizzle", element: CloudDrizzle },
|
||||
{ name: "CloudLightning", element: CloudLightning },
|
||||
{ name: "CloudOff", element: CloudOff },
|
||||
{ name: "CloudRain", element: CloudRain },
|
||||
{ name: "CloudSnow", element: CloudSnow },
|
||||
{ name: "Code", element: Code },
|
||||
{ name: "Codepen", element: Codepen },
|
||||
{ name: "Codesandbox", element: Codesandbox },
|
||||
{ name: "Coffee", element: Coffee },
|
||||
{ name: "Columns", element: Columns },
|
||||
{ name: "Command", element: Command },
|
||||
{ name: "Compass", element: Compass },
|
||||
{ name: "Copy", element: Copy },
|
||||
{ name: "CornerDownLeft", element: CornerDownLeft },
|
||||
{ name: "CornerDownRight", element: CornerDownRight },
|
||||
{ name: "CornerLeftDown", element: CornerLeftDown },
|
||||
{ name: "CornerLeftUp", element: CornerLeftUp },
|
||||
{ name: "CornerRightDown", element: CornerRightDown },
|
||||
{ name: "CornerRightUp", element: CornerRightUp },
|
||||
{ name: "CornerUpLeft", element: CornerUpLeft },
|
||||
{ name: "CornerUpRight", element: CornerUpRight },
|
||||
{ name: "Cpu", element: Cpu },
|
||||
{ name: "CreditCard", element: CreditCard },
|
||||
{ name: "Crop", element: Crop },
|
||||
{ name: "Crosshair", element: Crosshair },
|
||||
{ name: "Database", element: Database },
|
||||
{ name: "Delete", element: Delete },
|
||||
{ name: "Disc", element: Disc },
|
||||
{ name: "Divide", element: Divide },
|
||||
{ name: "DivideCircle", element: DivideCircle },
|
||||
{ name: "DivideSquare", element: DivideSquare },
|
||||
{ name: "DollarSign", element: DollarSign },
|
||||
{ name: "Download", element: Download },
|
||||
{ name: "DownloadCloud", element: DownloadCloud },
|
||||
{ name: "Dribbble", element: Dribbble },
|
||||
{ name: "Droplet", element: Droplet },
|
||||
{ name: "Edit", element: Edit },
|
||||
{ name: "Edit2", element: Edit2 },
|
||||
{ name: "Edit3", element: Edit3 },
|
||||
{ name: "ExternalLink", element: ExternalLink },
|
||||
{ name: "Eye", element: Eye },
|
||||
{ name: "EyeOff", element: EyeOff },
|
||||
{ name: "Facebook", element: Facebook },
|
||||
{ name: "FastForward", element: FastForward },
|
||||
{ name: "Feather", element: Feather },
|
||||
{ name: "Figma", element: Figma },
|
||||
{ name: "File", element: File },
|
||||
{ name: "FileMinus", element: FileMinus },
|
||||
{ name: "FilePlus", element: FilePlus },
|
||||
{ name: "FileText", element: FileText },
|
||||
{ name: "Film", element: Film },
|
||||
{ name: "Filter", element: Filter },
|
||||
{ name: "Flag", element: Flag },
|
||||
{ name: "Folder", element: Folder },
|
||||
{ name: "FolderMinus", element: FolderMinus },
|
||||
{ name: "FolderPlus", element: FolderPlus },
|
||||
{ name: "Framer", element: Framer },
|
||||
{ name: "Frown", element: Frown },
|
||||
{ name: "Gift", element: Gift },
|
||||
{ name: "GitBranch", element: GitBranch },
|
||||
{ name: "GitCommit", element: GitCommit },
|
||||
{ name: "GitMerge", element: GitMerge },
|
||||
{ name: "GitPullRequest", element: GitPullRequest },
|
||||
{ name: "Github", element: Github },
|
||||
{ name: "Gitlab", element: Gitlab },
|
||||
{ name: "Globe", element: Globe },
|
||||
{ name: "Grid", element: Grid },
|
||||
{ name: "HardDrive", element: HardDrive },
|
||||
{ name: "Hash", element: Hash },
|
||||
{ name: "Headphones", element: Headphones },
|
||||
{ name: "Heart", element: Heart },
|
||||
{ name: "HelpCircle", element: HelpCircle },
|
||||
{ name: "Hexagon", element: Hexagon },
|
||||
{ name: "Home", element: Home },
|
||||
{ name: "Image", element: Image },
|
||||
{ name: "Inbox", element: Inbox },
|
||||
{ name: "Info", element: Info },
|
||||
{ name: "Instagram", element: Instagram },
|
||||
{ name: "Italic", element: Italic },
|
||||
{ name: "Key", element: Key },
|
||||
{ name: "Layers", element: Layers },
|
||||
{ name: "Layout", element: Layout },
|
||||
{ name: "LifeBuoy", element: LifeBuoy },
|
||||
{ name: "Link", element: Link },
|
||||
{ name: "Link2", element: Link2 },
|
||||
{ name: "Linkedin", element: Linkedin },
|
||||
{ name: "List", element: List },
|
||||
{ name: "Loader", element: Loader },
|
||||
{ name: "Lock", element: Lock },
|
||||
{ name: "LogIn", element: LogIn },
|
||||
{ name: "LogOut", element: LogOut },
|
||||
{ name: "Mail", element: Mail },
|
||||
{ name: "Map", element: MapIcon },
|
||||
{ name: "MapPin", element: MapPin },
|
||||
{ name: "Maximize", element: Maximize },
|
||||
{ name: "Maximize2", element: Maximize2 },
|
||||
{ name: "Meh", element: Meh },
|
||||
{ name: "Menu", element: Menu },
|
||||
{ name: "MessageCircle", element: MessageCircle },
|
||||
{ name: "MessageSquare", element: MessageSquare },
|
||||
{ name: "Mic", element: Mic },
|
||||
{ name: "MicOff", element: MicOff },
|
||||
{ name: "Minimize", element: Minimize },
|
||||
{ name: "Minimize2", element: Minimize2 },
|
||||
{ name: "Minus", element: Minus },
|
||||
{ name: "MinusCircle", element: MinusCircle },
|
||||
{ name: "MinusSquare", element: MinusSquare },
|
||||
{ name: "Search", element: Search },
|
||||
{ name: "ToggleLeft", element: ToggleLeft },
|
||||
{ name: "User", element: User },
|
||||
{ name: "UsersRound", element: UsersRound },
|
||||
];
|
||||
602
packages/propel/src/emoji-icon-picker/material-icons.tsx
Normal file
602
packages/propel/src/emoji-icon-picker/material-icons.tsx
Normal file
@@ -0,0 +1,602 @@
|
||||
export const MATERIAL_ICONS_LIST = [
|
||||
{
|
||||
name: "search",
|
||||
},
|
||||
{
|
||||
name: "home",
|
||||
},
|
||||
{
|
||||
name: "menu",
|
||||
},
|
||||
{
|
||||
name: "close",
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
},
|
||||
{
|
||||
name: "check_circle",
|
||||
},
|
||||
{
|
||||
name: "favorite",
|
||||
},
|
||||
{
|
||||
name: "add",
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
},
|
||||
{
|
||||
name: "arrow_back",
|
||||
},
|
||||
{
|
||||
name: "star",
|
||||
},
|
||||
{
|
||||
name: "logout",
|
||||
},
|
||||
{
|
||||
name: "add_circle",
|
||||
},
|
||||
{
|
||||
name: "cancel",
|
||||
},
|
||||
{
|
||||
name: "arrow_drop_down",
|
||||
},
|
||||
{
|
||||
name: "more_vert",
|
||||
},
|
||||
{
|
||||
name: "check",
|
||||
},
|
||||
{
|
||||
name: "check_box",
|
||||
},
|
||||
{
|
||||
name: "toggle_on",
|
||||
},
|
||||
{
|
||||
name: "open_in_new",
|
||||
},
|
||||
{
|
||||
name: "refresh",
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
},
|
||||
{
|
||||
name: "radio_button_unchecked",
|
||||
},
|
||||
{
|
||||
name: "more_horiz",
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
},
|
||||
{
|
||||
name: "radio_button_checked",
|
||||
},
|
||||
{
|
||||
name: "download",
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
},
|
||||
{
|
||||
name: "toggle_off",
|
||||
},
|
||||
{
|
||||
name: "bolt",
|
||||
},
|
||||
{
|
||||
name: "arrow_upward",
|
||||
},
|
||||
{
|
||||
name: "filter_list",
|
||||
},
|
||||
{
|
||||
name: "delete_forever",
|
||||
},
|
||||
{
|
||||
name: "autorenew",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
},
|
||||
{
|
||||
name: "sort",
|
||||
},
|
||||
{
|
||||
name: "sync",
|
||||
},
|
||||
{
|
||||
name: "add_box",
|
||||
},
|
||||
{
|
||||
name: "block",
|
||||
},
|
||||
{
|
||||
name: "restart_alt",
|
||||
},
|
||||
{
|
||||
name: "menu_open",
|
||||
},
|
||||
{
|
||||
name: "shopping_cart_checkout",
|
||||
},
|
||||
{
|
||||
name: "expand_circle_down",
|
||||
},
|
||||
{
|
||||
name: "backspace",
|
||||
},
|
||||
{
|
||||
name: "undo",
|
||||
},
|
||||
{
|
||||
name: "done_all",
|
||||
},
|
||||
{
|
||||
name: "do_not_disturb_on",
|
||||
},
|
||||
{
|
||||
name: "open_in_full",
|
||||
},
|
||||
{
|
||||
name: "double_arrow",
|
||||
},
|
||||
{
|
||||
name: "sync_alt",
|
||||
},
|
||||
{
|
||||
name: "zoom_in",
|
||||
},
|
||||
{
|
||||
name: "done_outline",
|
||||
},
|
||||
{
|
||||
name: "drag_indicator",
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
},
|
||||
{
|
||||
name: "star_half",
|
||||
},
|
||||
{
|
||||
name: "settings_accessibility",
|
||||
},
|
||||
{
|
||||
name: "reply",
|
||||
},
|
||||
{
|
||||
name: "exit_to_app",
|
||||
},
|
||||
{
|
||||
name: "unfold_more",
|
||||
},
|
||||
{
|
||||
name: "library_add",
|
||||
},
|
||||
{
|
||||
name: "cached",
|
||||
},
|
||||
{
|
||||
name: "select_check_box",
|
||||
},
|
||||
{
|
||||
name: "terminal",
|
||||
},
|
||||
{
|
||||
name: "change_circle",
|
||||
},
|
||||
{
|
||||
name: "disabled_by_default",
|
||||
},
|
||||
{
|
||||
name: "swap_horiz",
|
||||
},
|
||||
{
|
||||
name: "swap_vert",
|
||||
},
|
||||
{
|
||||
name: "app_registration",
|
||||
},
|
||||
{
|
||||
name: "download_for_offline",
|
||||
},
|
||||
{
|
||||
name: "close_fullscreen",
|
||||
},
|
||||
{
|
||||
name: "file_open",
|
||||
},
|
||||
{
|
||||
name: "minimize",
|
||||
},
|
||||
{
|
||||
name: "open_with",
|
||||
},
|
||||
{
|
||||
name: "dataset",
|
||||
},
|
||||
{
|
||||
name: "add_task",
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
},
|
||||
{
|
||||
name: "keyboard_voice",
|
||||
},
|
||||
{
|
||||
name: "create_new_folder",
|
||||
},
|
||||
{
|
||||
name: "forward",
|
||||
},
|
||||
{
|
||||
name: "settings_applications",
|
||||
},
|
||||
{
|
||||
name: "compare_arrows",
|
||||
},
|
||||
{
|
||||
name: "redo",
|
||||
},
|
||||
{
|
||||
name: "zoom_out",
|
||||
},
|
||||
{
|
||||
name: "publish",
|
||||
},
|
||||
{
|
||||
name: "html",
|
||||
},
|
||||
{
|
||||
name: "token",
|
||||
},
|
||||
{
|
||||
name: "switch_access_shortcut",
|
||||
},
|
||||
{
|
||||
name: "fullscreen_exit",
|
||||
},
|
||||
{
|
||||
name: "sort_by_alpha",
|
||||
},
|
||||
{
|
||||
name: "delete_sweep",
|
||||
},
|
||||
{
|
||||
name: "indeterminate_check_box",
|
||||
},
|
||||
{
|
||||
name: "view_timeline",
|
||||
},
|
||||
{
|
||||
name: "settings_backup_restore",
|
||||
},
|
||||
{
|
||||
name: "arrow_drop_down_circle",
|
||||
},
|
||||
{
|
||||
name: "assistant_navigation",
|
||||
},
|
||||
{
|
||||
name: "sync_problem",
|
||||
},
|
||||
{
|
||||
name: "clear_all",
|
||||
},
|
||||
{
|
||||
name: "density_medium",
|
||||
},
|
||||
{
|
||||
name: "heart_plus",
|
||||
},
|
||||
{
|
||||
name: "filter_alt_off",
|
||||
},
|
||||
{
|
||||
name: "expand",
|
||||
},
|
||||
{
|
||||
name: "subdirectory_arrow_right",
|
||||
},
|
||||
{
|
||||
name: "download_done",
|
||||
},
|
||||
{
|
||||
name: "arrow_outward",
|
||||
},
|
||||
{
|
||||
name: "123",
|
||||
},
|
||||
{
|
||||
name: "swipe_left",
|
||||
},
|
||||
{
|
||||
name: "auto_mode",
|
||||
},
|
||||
{
|
||||
name: "saved_search",
|
||||
},
|
||||
{
|
||||
name: "place_item",
|
||||
},
|
||||
{
|
||||
name: "system_update_alt",
|
||||
},
|
||||
{
|
||||
name: "javascript",
|
||||
},
|
||||
{
|
||||
name: "search_off",
|
||||
},
|
||||
{
|
||||
name: "output",
|
||||
},
|
||||
{
|
||||
name: "select_all",
|
||||
},
|
||||
{
|
||||
name: "fit_screen",
|
||||
},
|
||||
{
|
||||
name: "swipe_up",
|
||||
},
|
||||
{
|
||||
name: "dynamic_form",
|
||||
},
|
||||
{
|
||||
name: "hide_source",
|
||||
},
|
||||
{
|
||||
name: "swipe_right",
|
||||
},
|
||||
{
|
||||
name: "switch_access_shortcut_add",
|
||||
},
|
||||
{
|
||||
name: "browse_gallery",
|
||||
},
|
||||
{
|
||||
name: "css",
|
||||
},
|
||||
{
|
||||
name: "density_small",
|
||||
},
|
||||
{
|
||||
name: "assistant_direction",
|
||||
},
|
||||
{
|
||||
name: "check_small",
|
||||
},
|
||||
{
|
||||
name: "youtube_searched_for",
|
||||
},
|
||||
{
|
||||
name: "move_up",
|
||||
},
|
||||
{
|
||||
name: "swap_horizontal_circle",
|
||||
},
|
||||
{
|
||||
name: "data_thresholding",
|
||||
},
|
||||
{
|
||||
name: "install_mobile",
|
||||
},
|
||||
{
|
||||
name: "move_down",
|
||||
},
|
||||
{
|
||||
name: "dataset_linked",
|
||||
},
|
||||
{
|
||||
name: "keyboard_command_key",
|
||||
},
|
||||
{
|
||||
name: "view_kanban",
|
||||
},
|
||||
{
|
||||
name: "swipe_down",
|
||||
},
|
||||
{
|
||||
name: "key_off",
|
||||
},
|
||||
{
|
||||
name: "transcribe",
|
||||
},
|
||||
{
|
||||
name: "send_time_extension",
|
||||
},
|
||||
{
|
||||
name: "swipe_down_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_left_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_right_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_up_alt",
|
||||
},
|
||||
{
|
||||
name: "keyboard_option_key",
|
||||
},
|
||||
{
|
||||
name: "cycle",
|
||||
},
|
||||
{
|
||||
name: "rebase",
|
||||
},
|
||||
{
|
||||
name: "rebase_edit",
|
||||
},
|
||||
{
|
||||
name: "empty_dashboard",
|
||||
},
|
||||
{
|
||||
name: "magic_exchange",
|
||||
},
|
||||
{
|
||||
name: "acute",
|
||||
},
|
||||
{
|
||||
name: "point_scan",
|
||||
},
|
||||
{
|
||||
name: "step_into",
|
||||
},
|
||||
{
|
||||
name: "cheer",
|
||||
},
|
||||
{
|
||||
name: "emoticon",
|
||||
},
|
||||
{
|
||||
name: "explosion",
|
||||
},
|
||||
{
|
||||
name: "water_bottle",
|
||||
},
|
||||
{
|
||||
name: "weather_hail",
|
||||
},
|
||||
{
|
||||
name: "syringe",
|
||||
},
|
||||
{
|
||||
name: "pill",
|
||||
},
|
||||
{
|
||||
name: "genetics",
|
||||
},
|
||||
{
|
||||
name: "allergy",
|
||||
},
|
||||
{
|
||||
name: "medical_mask",
|
||||
},
|
||||
{
|
||||
name: "body_fat",
|
||||
},
|
||||
{
|
||||
name: "barefoot",
|
||||
},
|
||||
{
|
||||
name: "infrared",
|
||||
},
|
||||
{
|
||||
name: "wrist",
|
||||
},
|
||||
{
|
||||
name: "metabolism",
|
||||
},
|
||||
{
|
||||
name: "conditions",
|
||||
},
|
||||
{
|
||||
name: "taunt",
|
||||
},
|
||||
{
|
||||
name: "altitude",
|
||||
},
|
||||
{
|
||||
name: "tibia",
|
||||
},
|
||||
{
|
||||
name: "footprint",
|
||||
},
|
||||
{
|
||||
name: "eyeglasses",
|
||||
},
|
||||
{
|
||||
name: "man_3",
|
||||
},
|
||||
{
|
||||
name: "woman_2",
|
||||
},
|
||||
{
|
||||
name: "rheumatology",
|
||||
},
|
||||
{
|
||||
name: "tornado",
|
||||
},
|
||||
{
|
||||
name: "landslide",
|
||||
},
|
||||
{
|
||||
name: "foggy",
|
||||
},
|
||||
{
|
||||
name: "severe_cold",
|
||||
},
|
||||
{
|
||||
name: "tsunami",
|
||||
},
|
||||
{
|
||||
name: "vape_free",
|
||||
},
|
||||
{
|
||||
name: "sign_language",
|
||||
},
|
||||
{
|
||||
name: "emoji_symbols",
|
||||
},
|
||||
{
|
||||
name: "clear_night",
|
||||
},
|
||||
{
|
||||
name: "emoji_food_beverage",
|
||||
},
|
||||
{
|
||||
name: "hive",
|
||||
},
|
||||
{
|
||||
name: "thunderstorm",
|
||||
},
|
||||
{
|
||||
name: "communication",
|
||||
},
|
||||
{
|
||||
name: "rocket",
|
||||
},
|
||||
{
|
||||
name: "pets",
|
||||
},
|
||||
{
|
||||
name: "public",
|
||||
},
|
||||
{
|
||||
name: "quiz",
|
||||
},
|
||||
{
|
||||
name: "mood",
|
||||
},
|
||||
{
|
||||
name: "gavel",
|
||||
},
|
||||
{
|
||||
name: "eco",
|
||||
},
|
||||
{
|
||||
name: "diamond",
|
||||
},
|
||||
{
|
||||
name: "forest",
|
||||
},
|
||||
{
|
||||
name: "rainy",
|
||||
},
|
||||
{
|
||||
name: "skull",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,343 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { SmilePlus } from "lucide-react";
|
||||
import { stringToEmoji } from "../emoji-icon-picker";
|
||||
import { EmojiReactionGroup, EmojiReactionType } from "./emoji-reaction";
|
||||
import { EmojiReactionPicker } from "./emoji-reaction-picker";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Emoji/EmojiReactionPicker",
|
||||
component: EmojiReactionPicker,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof EmojiReactionPicker>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedEmoji}
|
||||
closeOnSelect
|
||||
label={
|
||||
<span className="flex items-center justify-center rounded-md px-2 size-8 text-xl">
|
||||
{selectedEmoji ? stringToEmoji(selectedEmoji) : <SmilePlus className="h-6 text-custom-text-100" />}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{selectedEmoji && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
Selected: {selectedEmoji}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomLabel: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedEmoji}
|
||||
closeOnSelect
|
||||
label={
|
||||
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90 flex items-center gap-2">
|
||||
{selectedEmoji ? stringToEmoji(selectedEmoji) : <SmilePlus className="h-4 w-4" />}
|
||||
<span className="text-sm">Add Reaction</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InlineReactions: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [reactions, setReactions] = useState<EmojiReactionType[]>([
|
||||
{ emoji: "👍", count: 3, reacted: false, users: ["Alice", "Bob", "Charlie"] },
|
||||
{ emoji: "❤️", count: 2, reacted: true, users: ["You", "David"] },
|
||||
]);
|
||||
|
||||
const handleReactionAdd = (emoji: string) => {
|
||||
setReactions((prev) => {
|
||||
const existing = prev.find((r) => r.emoji === emoji);
|
||||
if (existing) {
|
||||
return prev.map((r) => (r.emoji === emoji ? { ...r, count: r.count + 1, reacted: true } : r));
|
||||
}
|
||||
return [...prev, { emoji, count: 1, reacted: true, users: ["You"] }];
|
||||
});
|
||||
};
|
||||
|
||||
const handleReactionClick = (emoji: string) => {
|
||||
setReactions((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.emoji === emoji) {
|
||||
return {
|
||||
...r,
|
||||
reacted: !r.reacted,
|
||||
count: r.reacted ? r.count - 1 : r.count + 1,
|
||||
};
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<EmojiReactionGroup
|
||||
reactions={reactions}
|
||||
onReactionClick={handleReactionClick}
|
||||
onAddReaction={() => setIsOpen(true)}
|
||||
showAddButton={false}
|
||||
/>
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleReactionAdd}
|
||||
closeOnSelect
|
||||
label={
|
||||
<button className="inline-flex items-center justify-center rounded-full border border-dashed border-custom-border-300 bg-custom-background-100 text-custom-text-400 transition-all duration-200 hover:border-custom-primary-100 hover:text-custom-primary-100 hover:bg-custom-primary-100/5 h-7 w-7">
|
||||
<SmilePlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DifferentPlacements: Story = {
|
||||
render() {
|
||||
const [isOpen1, setIsOpen1] = useState(false);
|
||||
const [isOpen2, setIsOpen2] = useState(false);
|
||||
const [isOpen3, setIsOpen3] = useState(false);
|
||||
const [isOpen4, setIsOpen4] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Bottom Start:</span>
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen1}
|
||||
handleToggle={setIsOpen1}
|
||||
onChange={() => {}}
|
||||
placement="bottom-start"
|
||||
label={<SmilePlus className="h-6 w-6" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Bottom End:</span>
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen2}
|
||||
handleToggle={setIsOpen2}
|
||||
onChange={() => {}}
|
||||
placement="bottom-end"
|
||||
label={<SmilePlus className="h-6 w-6" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Top Start:</span>
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen3}
|
||||
handleToggle={setIsOpen3}
|
||||
onChange={() => {}}
|
||||
placement="top-start"
|
||||
label={<SmilePlus className="h-6 w-6" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-sm w-32">Top End:</span>
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen4}
|
||||
handleToggle={setIsOpen4}
|
||||
onChange={() => {}}
|
||||
placement="top-end"
|
||||
label={<SmilePlus className="h-6 w-6" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SearchDisabled: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedEmoji}
|
||||
closeOnSelect
|
||||
searchDisabled
|
||||
label={
|
||||
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
|
||||
No Search
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomSearchPlaceholder: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedEmoji}
|
||||
closeOnSelect
|
||||
searchPlaceholder="Find your emoji..."
|
||||
label={
|
||||
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
|
||||
Custom Search
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseOnSelectDisabled: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedEmojis, setSelectedEmojis] = useState<string[]>([]);
|
||||
|
||||
const handleChange = (emoji: string) => {
|
||||
setSelectedEmojis((prev) => [...prev, emoji]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleChange}
|
||||
closeOnSelect={false}
|
||||
label={
|
||||
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
|
||||
Select Multiple (Stays Open)
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-1.5 text-sm bg-custom-background-80 rounded hover:bg-custom-background-90"
|
||||
onClick={() => setSelectedEmojis([])}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
{selectedEmojis.length > 0 && (
|
||||
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
|
||||
<div className="font-medium mb-2">Selected ({selectedEmojis.length}):</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{selectedEmojis.map((emoji, idx) => (
|
||||
<span key={idx} className="text-xl">
|
||||
{emoji}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InMessageContext: Story = {
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [reactions, setReactions] = useState<EmojiReactionType[]>([
|
||||
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
|
||||
]);
|
||||
|
||||
const handleReactionAdd = (emoji: string) => {
|
||||
setReactions((prev) => {
|
||||
const existing = prev.find((r) => r.emoji === emoji);
|
||||
if (existing) {
|
||||
return prev.map((r) => (r.emoji === emoji ? { ...r, count: r.count + 1, reacted: true } : r));
|
||||
}
|
||||
return [...prev, { emoji, count: 1, reacted: true, users: ["You"] }];
|
||||
});
|
||||
};
|
||||
|
||||
const handleReactionClick = (emoji: string) => {
|
||||
setReactions((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.emoji === emoji) {
|
||||
return {
|
||||
...r,
|
||||
reacted: !r.reacted,
|
||||
count: r.reacted ? r.count - 1 : r.count + 1,
|
||||
};
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md border border-custom-border-200 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-custom-primary-100 flex items-center justify-center text-white text-sm">
|
||||
AB
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">Alice Brown</div>
|
||||
<div className="text-sm text-custom-text-300 mt-1">
|
||||
Just finished the design for the new dashboard! Would love to hear your thoughts.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<EmojiReactionGroup reactions={reactions} onReactionClick={handleReactionClick} showAddButton={false} />
|
||||
<EmojiReactionPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleReactionAdd}
|
||||
closeOnSelect
|
||||
label={
|
||||
<button className="inline-flex items-center justify-center rounded-full border border-dashed border-custom-border-300 bg-custom-background-100 text-custom-text-400 transition-all duration-200 hover:border-custom-primary-100 hover:text-custom-primary-100 hover:bg-custom-primary-100/5 h-7 w-7">
|
||||
<SmilePlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
84
packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx
Normal file
84
packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useMemo, useCallback } from "react";
|
||||
import { EmojiRoot } from "../emoji-icon-picker/emoji/emoji";
|
||||
import { emojiToString } from "../emoji-icon-picker/helper";
|
||||
import { Popover } from "../popover";
|
||||
import { cn } from "../utils/classname";
|
||||
import { convertPlacementToSideAndAlign, type TPlacement, type TSide, type TAlign } from "../utils/placement";
|
||||
|
||||
export interface EmojiReactionPickerProps {
|
||||
isOpen: boolean;
|
||||
handleToggle: (value: boolean) => void;
|
||||
buttonClassName?: string;
|
||||
closeOnSelect?: boolean;
|
||||
disabled?: boolean;
|
||||
dropdownClassName?: string;
|
||||
label: React.ReactNode;
|
||||
onChange: (emoji: string) => void;
|
||||
placement?: TPlacement;
|
||||
searchDisabled?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
side?: TSide;
|
||||
align?: TAlign;
|
||||
}
|
||||
|
||||
export const EmojiReactionPicker: React.FC<EmojiReactionPickerProps> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
handleToggle,
|
||||
buttonClassName,
|
||||
closeOnSelect = true,
|
||||
disabled = false,
|
||||
dropdownClassName,
|
||||
label,
|
||||
onChange,
|
||||
placement = "bottom-start",
|
||||
searchDisabled = false,
|
||||
searchPlaceholder = "Search",
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
} = props;
|
||||
|
||||
// side and align calculations
|
||||
const { finalSide, finalAlign } = useMemo(() => {
|
||||
if (placement) {
|
||||
const converted = convertPlacementToSideAndAlign(placement);
|
||||
return { finalSide: converted.side, finalAlign: converted.align };
|
||||
}
|
||||
return { finalSide: side, finalAlign: align };
|
||||
}, [placement, side, align]);
|
||||
|
||||
const handleEmojiChange = useCallback(
|
||||
(value: string) => {
|
||||
const emoji = emojiToString(value);
|
||||
onChange(emoji);
|
||||
if (closeOnSelect) handleToggle(false);
|
||||
},
|
||||
[onChange, closeOnSelect, handleToggle]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleToggle}>
|
||||
<Popover.Button className={cn("outline-none", buttonClassName)} disabled={disabled}>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
positionerClassName="z-50"
|
||||
className={cn(
|
||||
"w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
|
||||
dropdownClassName
|
||||
)}
|
||||
side={finalSide}
|
||||
align={finalAlign}
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="h-80 overflow-hidden overflow-y-auto">
|
||||
<EmojiRoot
|
||||
onChange={handleEmojiChange}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
searchDisabled={searchDisabled}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
283
packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx
Normal file
283
packages/propel/src/emoji-reaction/emoji-reaction.stories.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { EmojiReaction, EmojiReactionGroup, EmojiReactionButton, EmojiReactionType } from "./emoji-reaction";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Emoji/EmojiReaction",
|
||||
component: EmojiReaction,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof EmojiReaction>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Single: Story = {
|
||||
args: {
|
||||
emoji: "👍",
|
||||
count: 5,
|
||||
reacted: false,
|
||||
users: ["Alice", "Bob", "Charlie"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Reacted: Story = {
|
||||
args: {
|
||||
emoji: "❤️",
|
||||
count: 12,
|
||||
reacted: true,
|
||||
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
render() {
|
||||
const [reacted, setReacted] = useState(false);
|
||||
const [count, setCount] = useState(5);
|
||||
|
||||
const handleClick = () => {
|
||||
setReacted(!reacted);
|
||||
setCount(reacted ? count - 1 : count + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<EmojiReaction
|
||||
emoji="👍"
|
||||
count={count}
|
||||
reacted={reacted}
|
||||
users={["Alice", "Bob", "Charlie"]}
|
||||
onReactionClick={handleClick}
|
||||
/>
|
||||
<p className="text-sm text-custom-text-400">Click to toggle reaction</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTooltip: Story = {
|
||||
args: {
|
||||
emoji: "🎉",
|
||||
count: 8,
|
||||
reacted: false,
|
||||
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Grace", "Henry"],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutCount: Story = {
|
||||
args: {
|
||||
emoji: "🔥",
|
||||
count: 0,
|
||||
reacted: false,
|
||||
showCount: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Small</span>
|
||||
<EmojiReaction emoji="👍" count={5} size="sm" users={["Alice", "Bob"]} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Medium (default)</span>
|
||||
<EmojiReaction emoji="👍" count={5} size="md" users={["Alice", "Bob"]} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Large</span>
|
||||
<EmojiReaction emoji="👍" count={5} size="lg" users={["Alice", "Bob"]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleReactions: Story = {
|
||||
render() {
|
||||
const [reactions, setReactions] = useState<EmojiReactionType[]>([
|
||||
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
|
||||
{ emoji: "❤️", count: 12, reacted: true, users: ["David", "Emma", "Frank"] },
|
||||
{ emoji: "🎉", count: 3, reacted: false, users: ["Grace"] },
|
||||
]);
|
||||
|
||||
const handleReactionClick = (emoji: string) => {
|
||||
setReactions((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.emoji === emoji) {
|
||||
return {
|
||||
...r,
|
||||
reacted: !r.reacted,
|
||||
count: r.reacted ? r.count - 1 : r.count + 1,
|
||||
};
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{reactions.map((reaction) => (
|
||||
<EmojiReaction
|
||||
key={reaction.emoji}
|
||||
emoji={reaction.emoji}
|
||||
count={reaction.count}
|
||||
reacted={reaction.reacted}
|
||||
users={reaction.users}
|
||||
onReactionClick={handleReactionClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AddButton: Story = {
|
||||
render() {
|
||||
const handleAdd = () => {
|
||||
alert("Add reaction clicked");
|
||||
};
|
||||
|
||||
return <EmojiReactionButton onAddReaction={handleAdd} />;
|
||||
},
|
||||
};
|
||||
|
||||
export const AddButtonSizes: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<span className="text-xs text-custom-text-400">Small</span>
|
||||
<EmojiReactionButton size="sm" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<span className="text-xs text-custom-text-400">Medium</span>
|
||||
<EmojiReactionButton size="md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<span className="text-xs text-custom-text-400">Large</span>
|
||||
<EmojiReactionButton size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ReactionGroup: Story = {
|
||||
render() {
|
||||
const [reactions, setReactions] = useState<EmojiReactionType[]>([
|
||||
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
|
||||
{ emoji: "❤️", count: 12, reacted: true, users: ["David", "Emma", "Frank"] },
|
||||
{ emoji: "🎉", count: 3, reacted: false, users: ["Grace"] },
|
||||
{ emoji: "🔥", count: 8, reacted: false, users: ["Henry", "Ivy"] },
|
||||
]);
|
||||
|
||||
const handleReactionClick = (emoji: string) => {
|
||||
setReactions((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.emoji === emoji) {
|
||||
return {
|
||||
...r,
|
||||
reacted: !r.reacted,
|
||||
count: r.reacted ? r.count - 1 : r.count + 1,
|
||||
};
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddReaction = () => {
|
||||
alert("Add reaction clicked");
|
||||
};
|
||||
|
||||
return (
|
||||
<EmojiReactionGroup
|
||||
reactions={reactions}
|
||||
onReactionClick={handleReactionClick}
|
||||
onAddReaction={handleAddReaction}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ReactionGroupSizes: Story = {
|
||||
render() {
|
||||
const reactions: EmojiReactionType[] = [
|
||||
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob"] },
|
||||
{ emoji: "❤️", count: 12, reacted: true, users: ["Charlie", "David"] },
|
||||
{ emoji: "🎉", count: 3, reacted: false, users: ["Emma"] },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Small</span>
|
||||
<EmojiReactionGroup reactions={reactions} size="sm" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Medium</span>
|
||||
<EmojiReactionGroup reactions={reactions} size="md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-text-400">Large</span>
|
||||
<EmojiReactionGroup reactions={reactions} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InMessageContext: Story = {
|
||||
render() {
|
||||
const [reactions, setReactions] = useState<EmojiReactionType[]>([
|
||||
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
|
||||
{ emoji: "❤️", count: 2, reacted: true, users: ["You", "David"] },
|
||||
]);
|
||||
|
||||
const handleReactionClick = (emoji: string) => {
|
||||
setReactions((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.emoji === emoji) {
|
||||
return {
|
||||
...r,
|
||||
reacted: !r.reacted,
|
||||
count: r.reacted ? r.count - 1 : r.count + 1,
|
||||
};
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md border border-custom-border-200 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-custom-primary-100 flex items-center justify-center text-white text-sm">
|
||||
AB
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">Alice Brown</div>
|
||||
<div className="text-sm text-custom-text-300 mt-1">
|
||||
Hey everyone! Just wanted to share some exciting news about our project launch next week!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmojiReactionGroup reactions={reactions} onReactionClick={handleReactionClick} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyUsers: Story = {
|
||||
args: {
|
||||
emoji: "🎉",
|
||||
count: 47,
|
||||
reacted: true,
|
||||
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Grace", "Henry", "Ivy", "Jack", "Kate", "Liam"],
|
||||
},
|
||||
};
|
||||
182
packages/propel/src/emoji-reaction/emoji-reaction.tsx
Normal file
182
packages/propel/src/emoji-reaction/emoji-reaction.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import * as React from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { AnimatedCounter } from "../animated-counter";
|
||||
import { stringToEmoji } from "../emoji-icon-picker";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export interface EmojiReactionType {
|
||||
emoji: string;
|
||||
count: number;
|
||||
reacted?: boolean;
|
||||
users?: string[];
|
||||
}
|
||||
|
||||
export interface EmojiReactionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
emoji: string;
|
||||
count: number;
|
||||
reacted?: boolean;
|
||||
users?: string[];
|
||||
onReactionClick?: (emoji: string) => void;
|
||||
className?: string;
|
||||
showCount?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export interface EmojiReactionGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
reactions: EmojiReactionType[];
|
||||
onReactionClick?: (emoji: string) => void;
|
||||
onAddReaction?: () => void;
|
||||
className?: string;
|
||||
showAddButton?: boolean;
|
||||
maxDisplayUsers?: number;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export interface EmojiReactionButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
onAddReaction?: () => void;
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: {
|
||||
button: "px-2 py-1 text-xs gap-1",
|
||||
emoji: "text-sm",
|
||||
count: "text-xs",
|
||||
addButton: "h-6 w-6",
|
||||
addIcon: "h-3 w-3",
|
||||
},
|
||||
md: {
|
||||
button: "px-2.5 py-1.5 text-sm gap-1.5",
|
||||
emoji: "text-base",
|
||||
count: "text-sm",
|
||||
addButton: "h-7 w-7",
|
||||
addIcon: "h-3.5 w-3.5",
|
||||
},
|
||||
lg: {
|
||||
button: "px-3 py-2 text-base gap-2",
|
||||
emoji: "text-lg",
|
||||
count: "text-base",
|
||||
addButton: "h-8 w-8",
|
||||
addIcon: "h-4 w-4",
|
||||
},
|
||||
};
|
||||
|
||||
const EmojiReaction = React.forwardRef<HTMLButtonElement, EmojiReactionProps>(
|
||||
(
|
||||
{ emoji, count, reacted = false, users = [], onReactionClick, className, showCount = true, size = "md", ...props },
|
||||
ref
|
||||
) => {
|
||||
const sizeClass = sizeClasses[size];
|
||||
|
||||
const handleClick = () => {
|
||||
onReactionClick?.(emoji);
|
||||
};
|
||||
|
||||
const tooltipContent = React.useMemo(() => {
|
||||
if (!users.length) return null;
|
||||
|
||||
const displayUsers = users.slice(0, 5);
|
||||
const remainingCount = users.length - displayUsers.length;
|
||||
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<div className="font-medium mb-1">{stringToEmoji(emoji)}</div>
|
||||
<div>
|
||||
{displayUsers.join(", ")}
|
||||
{remainingCount > 0 && ` and ${remainingCount} more`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [emoji, users]);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full border transition-all duration-200 hover:scale-105",
|
||||
"focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1",
|
||||
sizeClass.button,
|
||||
reacted
|
||||
? "bg-custom-primary-100/10 border-custom-primary-100 text-custom-primary-100"
|
||||
: "bg-custom-background-100 border-custom-border-200 text-custom-text-300 hover:border-custom-border-300 hover:bg-custom-background-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className={sizeClass.emoji}>{emoji}</span>
|
||||
{showCount && count > 0 && <AnimatedCounter count={count} size={size} className={sizeClass.count} />}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (tooltipContent && users.length > 0) {
|
||||
return <Tooltip tooltipContent={tooltipContent}>{button}</Tooltip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
);
|
||||
|
||||
const EmojiReactionButton = React.forwardRef<HTMLButtonElement, EmojiReactionButtonProps>(
|
||||
({ onAddReaction, className, size = "md", ...props }, ref) => {
|
||||
const sizeClass = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={onAddReaction}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-full border border-dashed border-custom-border-300",
|
||||
"bg-custom-background-100 text-custom-text-400 transition-all duration-200",
|
||||
"hover:border-custom-primary-100 hover:text-custom-primary-100 hover:bg-custom-primary-100/5",
|
||||
"focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1",
|
||||
sizeClass.addButton,
|
||||
className
|
||||
)}
|
||||
title="Add reaction"
|
||||
{...props}
|
||||
>
|
||||
<Plus className={sizeClass.addIcon} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const EmojiReactionGroup = React.forwardRef<HTMLDivElement, EmojiReactionGroupProps>(
|
||||
(
|
||||
{
|
||||
reactions,
|
||||
onReactionClick,
|
||||
onAddReaction,
|
||||
className,
|
||||
showAddButton = true,
|
||||
maxDisplayUsers = 5,
|
||||
size = "md",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<div ref={ref} className={cn("flex flex-wrap items-center gap-2", className)} {...props}>
|
||||
{reactions.map((reaction, index) => (
|
||||
<EmojiReaction
|
||||
key={`${reaction.emoji}-${index}`}
|
||||
emoji={reaction.emoji}
|
||||
count={reaction.count}
|
||||
reacted={reaction.reacted}
|
||||
users={reaction.users?.slice(0, maxDisplayUsers)}
|
||||
onReactionClick={onReactionClick}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
{showAddButton && <EmojiReactionButton onAddReaction={onAddReaction} size={size} />}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
EmojiReaction.displayName = "EmojiReaction";
|
||||
EmojiReactionButton.displayName = "EmojiReactionButton";
|
||||
EmojiReactionGroup.displayName = "EmojiReactionGroup";
|
||||
|
||||
export { EmojiReaction, EmojiReactionButton, EmojiReactionGroup };
|
||||
10
packages/propel/src/emoji-reaction/index.ts
Normal file
10
packages/propel/src/emoji-reaction/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { EmojiReaction, EmojiReactionGroup, EmojiReactionButton } from "./emoji-reaction";
|
||||
export type {
|
||||
EmojiReactionProps,
|
||||
EmojiReactionGroupProps,
|
||||
EmojiReactionButtonProps,
|
||||
EmojiReactionType,
|
||||
} from "./emoji-reaction";
|
||||
|
||||
export { EmojiReactionPicker } from "./emoji-reaction-picker";
|
||||
export type { EmojiReactionPickerProps } from "./emoji-reaction-picker";
|
||||
15
packages/propel/src/empty-state/assets/helper.tsx
Normal file
15
packages/propel/src/empty-state/assets/helper.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export const ILLUSTRATION_COLOR_TOKEN_MAP = {
|
||||
fill: {
|
||||
primary: "rgba(var(--color-background-100))", // white or #fff,
|
||||
secondary: "rgba(var(--color-background-90))", // #F4F5F5
|
||||
tertiary: "rgba(var(--color-background-80))", // #E5E7E8
|
||||
},
|
||||
stroke: {
|
||||
primary: "rgba(var(--color-text-200))", // #1D1F20
|
||||
secondary: "rgba(var(--color-border-400))", // #7A8185 or #CFD2D3
|
||||
},
|
||||
};
|
||||
|
||||
export type TIllustrationAssetProps = {
|
||||
className?: string;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
CustomerHorizontalStackIllustration,
|
||||
EpicHorizontalStackIllustration,
|
||||
EstimateHorizontalStackIllustration,
|
||||
ExportHorizontalStackIllustration,
|
||||
IntakeHorizontalStackIllustration,
|
||||
LabelHorizontalStackIllustration,
|
||||
LinkHorizontalStackIllustration,
|
||||
MembersHorizontalStackIllustration,
|
||||
NoteHorizontalStackIllustration,
|
||||
PriorityHorizontalStackIllustration,
|
||||
SettingsHorizontalStackIllustration,
|
||||
StateHorizontalStackIllustration,
|
||||
TemplateHorizontalStackIllustration,
|
||||
TokenHorizontalStackIllustration,
|
||||
UnknownHorizontalStackIllustration,
|
||||
UpdateHorizontalStackIllustration,
|
||||
WebhookHorizontalStackIllustration,
|
||||
WorkItemHorizontalStackIllustration,
|
||||
WorklogHorizontalStackIllustration,
|
||||
} from "./";
|
||||
|
||||
export const HorizontalStackAssetsMap = [
|
||||
{
|
||||
asset: <CustomerHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Customer",
|
||||
},
|
||||
{
|
||||
asset: <EpicHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Epic",
|
||||
},
|
||||
{
|
||||
asset: <EstimateHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Estimate",
|
||||
},
|
||||
{
|
||||
asset: <ExportHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Export",
|
||||
},
|
||||
{
|
||||
asset: <IntakeHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Intake",
|
||||
},
|
||||
{
|
||||
asset: <LabelHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Label",
|
||||
},
|
||||
{
|
||||
asset: <LinkHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Link",
|
||||
},
|
||||
{
|
||||
asset: <MembersHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Members",
|
||||
},
|
||||
{
|
||||
asset: <NoteHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Note",
|
||||
},
|
||||
{
|
||||
asset: <PriorityHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Priority",
|
||||
},
|
||||
{
|
||||
asset: <SettingsHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Settings",
|
||||
},
|
||||
{
|
||||
asset: <StateHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "State",
|
||||
},
|
||||
{
|
||||
asset: <TemplateHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Template",
|
||||
},
|
||||
{
|
||||
asset: <TokenHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Token",
|
||||
},
|
||||
{
|
||||
asset: <UnknownHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Unknown",
|
||||
},
|
||||
{
|
||||
asset: <UpdateHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Update",
|
||||
},
|
||||
{
|
||||
asset: <WebhookHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Webhook",
|
||||
},
|
||||
{
|
||||
asset: <WorkItemHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "WorkItem",
|
||||
},
|
||||
{
|
||||
asset: <WorklogHorizontalStackIllustration className="w-20 h-20" />,
|
||||
title: "Worklog",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,64 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const CustomerHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="81" height="91" viewBox="0 0 81 91" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M47.8782 2.57309C46.9628 2.10751 45.787 2.14718 44.5087 2.79426L9.267 20.7542C6.37097 22.2299 4.0195 26.2861 4.0195 29.8055V72.6934C4.0195 74.6583 4.74547 76.0394 5.88968 76.6313L2.37019 74.84C1.22598 74.256 0.5 72.867 0.5 70.9021V28.0142C0.5 24.4869 2.85157 20.4386 5.7476 18.9629L40.9893 1.00296C42.2755 0.347996 43.4513 0.316215 44.3588 0.781791L47.8782 2.57309Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.878 2.57281C49.0222 3.15675 49.7482 4.54577 49.7482 6.51066V49.3986C49.7482 52.9259 47.3966 56.9742 44.5006 58.4498L9.25889 76.4102C7.97264 77.0652 6.79695 77.0966 5.88947 76.631C4.74526 76.0471 4.01929 74.658 4.01929 72.6931V29.8052C4.01929 26.2779 6.37076 22.2296 9.26679 20.754L44.5085 2.79397C45.7947 2.13901 46.9705 2.10723 47.878 2.57281Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M63.2578 9.36801C62.3425 8.90244 61.1667 8.94211 59.8883 9.58918L24.6466 27.5492C21.7506 29.0248 19.3991 33.081 19.3991 36.6004V79.4887C19.3991 81.4536 20.1251 82.8344 21.2693 83.4262L17.7498 81.6349C16.6056 81.0509 15.8796 79.6623 15.8796 77.6974V34.8091C15.8796 31.2818 18.2312 27.2335 21.1272 25.7579L56.3689 7.79788C57.6552 7.14292 58.8309 7.11114 59.7384 7.57671L63.2578 9.36801Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M63.2584 9.36578C64.4026 9.94972 65.1285 11.3387 65.1285 13.3036V56.1919C65.1285 59.7192 62.777 63.7672 59.8809 65.2428L24.6394 83.2032C23.3531 83.8581 22.1773 83.8895 21.2698 83.424C20.1256 82.84 19.3997 81.4514 19.3997 79.4865V36.5982C19.3997 33.0709 21.7511 29.0226 24.6472 27.5469L59.8888 9.58694C61.1751 8.93198 62.3509 8.9002 63.2584 9.36578Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M78.6299 16.1631C77.7145 15.6975 76.5387 15.7368 75.2604 16.3839L40.0188 34.3442C37.1228 35.8199 34.7712 39.8757 34.7712 43.3951V86.2834C34.7712 88.2483 35.4972 89.6294 36.6414 90.2213L33.122 88.43C31.9778 87.846 31.2517 86.457 31.2517 84.4921V41.6038C31.2517 38.0765 33.6033 34.0286 36.4993 32.5529L71.741 14.5926C73.0272 13.9376 74.203 13.9062 75.1105 14.3718L78.6299 16.1631Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M78.6299 16.1609C79.7741 16.7448 80.5001 18.1334 80.5001 20.0983V62.9866C80.5001 66.5139 78.1486 70.5622 75.2525 72.0379L40.0109 89.9979C38.7247 90.6528 37.5489 90.6846 36.6414 90.219C35.4972 89.6351 34.7712 88.2461 34.7712 86.2812V43.3929C34.7712 39.8656 37.1228 35.8177 40.0188 34.342L75.2604 16.3816C76.5467 15.7267 77.7225 15.6953 78.6299 16.1609Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M46.6233 45.5414C46.6233 44.9023 47.0494 44.1683 47.5702 43.9L62.7685 36.1588C63.2893 35.8905 63.7154 36.1904 63.7154 36.8296C63.7154 37.4688 63.2893 38.2027 62.7685 38.471V43.0951L70.3676 39.2205C70.8884 38.9522 71.3145 39.2521 71.3145 39.8913C71.3145 40.5305 70.8884 41.2644 70.3676 41.5327V60.0294L71.3145 59.5482C71.8353 59.2799 72.2615 59.5798 72.2615 60.219C72.2615 60.8582 71.8353 61.5917 71.3145 61.86L46.6154 74.4465C46.0946 74.7148 45.6685 74.4149 45.6685 73.7757C45.6685 73.1365 46.0946 72.4026 46.6154 72.1343L47.5623 71.653V46.22C47.0415 46.4883 46.6154 46.1883 46.6154 45.5492L46.6233 45.5414ZM49.472 45.2417V70.6747L51.3737 69.7041V65.6557C51.3737 64.0617 52.439 62.2233 53.7489 61.5525L56.5977 60.1003C57.9076 59.4296 58.9728 60.1874 58.9728 61.7814V65.8295L60.8746 64.8589V39.4258L49.4799 45.2336L49.472 45.2417ZM62.7764 45.4074V63.9041L68.4738 61V42.5033L62.7764 45.4074ZM57.0712 66.8001V62.752C57.0712 62.4364 56.8581 62.2863 56.5977 62.4126L53.7489 63.8644C53.4885 63.9985 53.2755 64.3695 53.2755 64.6851V68.7332L57.0712 66.8001ZM51.3737 48.9029C51.3737 48.2637 51.7999 47.5298 52.3207 47.2615L53.2676 46.7802C53.7884 46.5119 54.2146 46.8119 54.2146 47.451C54.2146 48.0902 53.7884 48.8242 53.2676 49.0925L52.3207 49.5737C51.7999 49.842 51.3737 49.5421 51.3737 48.9029ZM56.1242 46.4804C56.1242 45.8413 56.5503 45.1073 57.0712 44.839L58.018 44.3578C58.5389 44.0895 58.965 44.3894 58.965 45.0286C58.965 45.6678 58.5389 46.4013 58.018 46.6696L57.0712 47.1513C56.5503 47.4196 56.1242 47.1196 56.1242 46.4804ZM51.3737 53.5273C51.3737 52.8882 51.7999 52.1542 52.3207 51.8859L53.2676 51.4043C53.7884 51.136 54.2146 51.4359 54.2146 52.0751C54.2146 52.7143 53.7884 53.4482 53.2676 53.7165L52.3207 54.1978C51.7999 54.4661 51.3737 54.1665 51.3737 53.5273ZM56.1242 51.1045C56.1242 50.4653 56.5503 49.7314 57.0712 49.4631L58.018 48.9819C58.5389 48.7136 58.965 49.0135 58.965 49.6527C58.965 50.2919 58.5389 51.0258 58.018 51.2941L57.0712 51.7754C56.5503 52.0436 56.1242 51.7437 56.1242 51.1045ZM64.6703 49.0609C64.6703 48.4217 65.0964 47.6877 65.6172 47.4194H65.6251C66.1459 47.1433 66.572 47.4509 66.572 48.0822V48.098C66.572 48.7372 66.1459 49.4711 65.6251 49.7394H65.6172C65.0964 50.0156 64.6703 49.7079 64.6703 49.0767V49.0609ZM51.3737 58.1433C51.3737 57.5042 51.7999 56.7702 52.3207 56.5019L53.2676 56.0207C53.7884 55.7524 54.2146 56.0523 54.2146 56.6915C54.2146 57.3307 53.7884 58.0646 53.2676 58.3329L52.3207 58.8142C51.7999 59.0825 51.3737 58.7825 51.3737 58.1433ZM56.1242 55.7286C56.1242 55.0894 56.5503 54.3559 57.0712 54.0876L58.018 53.606C58.5389 53.3377 58.965 53.6376 58.965 54.2768C58.965 54.916 58.5389 55.6499 58.018 55.9182L57.0712 56.3994C56.5503 56.6677 56.1242 56.3678 56.1242 55.7286ZM64.6703 53.6849C64.6703 53.0458 65.0964 52.3118 65.6172 52.0435H65.6251C66.1459 51.7673 66.572 52.0754 66.572 52.7066V52.7224C66.572 53.3616 66.1459 54.0952 65.6251 54.3635H65.6172C65.0964 54.6397 64.6703 54.332 64.6703 53.7007V53.6849ZM64.6703 58.309C64.6703 57.6698 65.0964 56.9363 65.6172 56.668H65.6251C66.1459 56.3918 66.572 56.6994 66.572 57.3307V57.3465C66.572 57.9857 66.1459 58.7196 65.6251 58.9879H65.6172C65.0964 59.2641 64.6703 58.9561 64.6703 58.3248V58.309Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const EpicHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="81" height="92" viewBox="0 0 81 92" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M47.8729 3.07316C46.9576 2.60763 45.782 2.6473 44.5038 3.2943L9.26602 21.2523C6.37031 22.7277 4.01901 26.7835 4.01901 30.3025V73.1855C4.01901 75.1502 4.7449 76.5312 5.88899 77.1229L2.36998 75.3318C1.2259 74.748 0.5 73.3595 0.5 71.3948V28.5114C0.5 24.9845 2.8513 20.9366 5.74701 19.4612L40.9848 1.5032C42.2709 0.848314 43.4464 0.816538 44.3538 1.28206L47.8729 3.07316Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.8722 3.07317C49.0163 3.65704 49.7422 5.04591 49.7422 7.01057V49.894C49.7422 53.4209 47.3909 57.4684 44.4952 58.9439L9.25743 76.9022C7.97132 77.5571 6.79566 77.5885 5.88829 77.1229C4.74421 76.5391 4.01831 75.1502 4.01831 73.1855V30.3025C4.01831 26.7756 6.36962 22.7277 9.26533 21.2523L44.5031 3.29431C45.7892 2.63942 46.9648 2.60764 47.8722 3.07317Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M63.2511 9.86669C62.3358 9.40117 61.1602 9.44084 59.8819 10.0878L24.6442 28.0458C21.7485 29.5213 19.3972 33.577 19.3972 37.096V79.9795C19.3972 81.9441 20.1231 83.3247 21.2672 83.9165L17.7482 82.1254C16.6041 81.5415 15.8782 80.153 15.8782 78.1884V35.3049C15.8782 31.778 18.2295 27.7302 21.1252 26.2547L56.3629 8.29674C57.649 7.64185 58.8246 7.61046 59.732 8.07598L63.2511 9.86669Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M63.2519 9.86653C64.3959 10.4504 65.1218 11.8393 65.1218 13.8039V56.6874C65.1218 60.2143 62.7705 64.2617 59.8748 65.7372L24.6371 83.6956C23.351 84.3504 22.1753 84.3818 21.2679 83.9163C20.1238 83.3324 19.3979 81.944 19.3979 79.9793V37.0959C19.3979 33.569 21.7493 29.5211 24.645 28.0456L59.8827 10.0877C61.1688 9.43279 62.3445 9.40101 63.2519 9.86653Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M78.6293 16.668C77.714 16.2025 76.5383 16.2421 75.2601 16.8891L40.0224 34.8471C37.1267 36.3225 34.7755 40.3783 34.7755 43.8973V86.7807C34.7755 88.7454 35.5013 90.126 36.6454 90.7178L33.1263 88.9267C31.9822 88.3428 31.2563 86.9543 31.2563 84.9896V42.1062C31.2563 38.5793 33.6077 34.5314 36.5034 33.056L71.7411 15.098C73.0272 14.4431 74.2029 14.4114 75.1102 14.8769L78.6293 16.668Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M78.6299 16.668C79.774 17.2519 80.4999 18.6407 80.4999 20.6054V63.4888C80.4999 67.0157 78.1486 71.0632 75.2529 72.5387L40.0151 90.497C38.729 91.1519 37.5535 91.1833 36.6461 90.7178C35.502 90.1339 34.7761 88.7454 34.7761 86.7807V43.8973C34.7761 40.3704 37.1273 36.3225 40.023 34.8471L75.2608 16.8891C76.5469 16.2342 77.7225 16.2025 78.6299 16.668Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M73.7365 51.6298V55.4643C73.7365 55.8589 73.6103 56.2928 73.4051 56.6795C73.1842 57.0661 72.9001 57.3741 72.5924 57.524L47.4226 70.3533C46.7914 70.6768 46.2786 70.2743 46.2786 69.4537V63.3469L53.9084 46.7774C54.3345 46.0042 54.8315 45.3175 55.4391 45.0098C56.0466 44.7021 56.5359 44.8836 56.962 45.2229L63.5582 54.6044L66.4065 50.0441C66.8326 49.2708 67.4164 48.663 68.024 48.3473C68.6315 48.0317 69.2154 48.0555 69.6336 48.3947L73.7365 51.6221V51.6298Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const EstimateHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="81" height="92" viewBox="0 0 81 92" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M47.8734 3.07267C46.9581 2.60715 45.7825 2.6466 44.5042 3.2936L9.26616 21.2519C6.37042 22.7274 4.01909 26.783 4.01909 30.3021V73.1858C4.01909 75.1505 4.745 76.5313 5.88909 77.1231L2.37005 75.332C1.22595 74.7481 0.5 73.3594 0.5 71.3948V28.511C0.5 24.984 2.85133 20.9363 5.74707 19.4608L40.9851 1.5025C42.2713 0.847607 43.4469 0.816046 44.3543 1.28157L47.8734 3.07267Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.8732 3.07177C49.0173 3.65565 49.7432 5.04434 49.7432 7.00903V49.8928C49.7432 53.4197 47.3919 57.4675 44.4962 58.943L9.25809 76.9013C7.97197 77.5562 6.7963 77.5877 5.88892 77.1222C4.74482 76.5383 4.01892 75.1496 4.01892 73.1849V30.3012C4.01892 26.7742 6.37025 22.7265 9.26599 21.251L44.5041 3.29269C45.7902 2.6378 46.9658 2.60624 47.8732 3.07177Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M63.2514 9.86595C62.3361 9.40042 61.1605 9.43987 59.8822 10.0869L24.6442 28.0452C21.7484 29.5207 19.3971 33.5763 19.3971 37.0954V79.9791C19.3971 81.9438 20.123 83.3246 21.2671 83.9164L17.748 82.1253C16.604 81.5414 15.8781 80.1527 15.8781 78.188V35.3043C15.8781 31.7773 18.2293 27.7296 21.1251 26.2541L56.3632 8.29578C57.6493 7.64088 58.8249 7.60932 59.7323 8.07485L63.2514 9.86595Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M63.2514 9.86614C64.3955 10.45 65.1214 11.8387 65.1214 13.8034V56.6872C65.1214 60.2141 62.7701 64.2618 59.8743 65.7373L24.6363 83.6956C23.3501 84.3505 22.1745 84.3821 21.2671 83.9166C20.123 83.3327 19.3971 81.944 19.3971 79.9793V37.0955C19.3971 33.5686 21.7484 29.5209 24.6442 28.0454L59.8822 10.0871C61.1684 9.43217 62.344 9.40061 63.2514 9.86614Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M78.6301 16.6678C77.7148 16.2023 76.5391 16.2417 75.2609 16.8887L40.0228 34.8471C37.1271 36.3225 34.7758 40.3781 34.7758 43.8972V86.781C34.7758 88.7457 35.5017 90.1265 36.6458 90.7182L33.1267 88.9271C31.9826 88.3433 31.2567 86.9546 31.2567 84.9899V42.1061C31.2567 38.5792 33.608 34.5315 36.5037 33.056L71.7419 15.0977C73.028 14.4428 74.2036 14.4112 75.111 14.8767L78.6301 16.6678Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M78.6299 16.668C79.774 17.2519 80.4999 18.6406 80.4999 20.6053V63.489C80.4999 67.016 78.1487 71.0637 75.2529 72.5392L40.0148 90.4975C38.7287 91.1524 37.553 91.184 36.6456 90.7184C35.5015 90.1346 34.7756 88.7459 34.7756 86.7812V43.8974C34.7756 40.3705 37.127 36.3227 40.0227 34.8473L75.2608 16.8889C76.5469 16.2341 77.7226 16.2025 78.6299 16.668Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M57.2395 40.7963C57.7524 40.1808 58.3283 39.6916 58.9201 39.3918C59.5119 39.092 60.0879 38.9815 60.6008 39.0841C61.1136 39.1788 61.5397 39.4786 61.8316 39.952L70.8029 54.4938C71.0949 54.9673 71.2527 55.5906 71.2527 56.3086C71.2527 57.0266 71.0949 57.8156 70.8029 58.5889C70.511 59.3621 70.0849 60.0959 69.572 60.7193C69.0592 61.3426 68.4832 61.8239 67.8914 62.1238L49.9331 71.2686C49.3413 71.5684 48.7653 71.671 48.2524 71.5763C47.7395 71.4738 47.3135 71.1739 47.0215 70.7005C46.7296 70.2271 46.5718 69.6038 46.5718 68.8857C46.5718 68.1677 46.7296 67.3866 47.0215 66.6133L56.0007 42.9188C56.3005 42.1455 56.7187 41.4117 57.2316 40.7963H57.2395ZM58.9201 42.1218C58.7229 42.2244 58.5335 42.3822 58.3599 42.5874C58.1942 42.7925 58.0522 43.0371 57.9496 43.2896L48.9705 66.9763C48.8758 67.2367 48.8205 67.4971 48.8205 67.7338C48.8205 67.9705 48.8758 68.1835 48.9705 68.3413C49.0651 68.4991 49.2071 68.6017 49.3807 68.6332C49.5543 68.6648 49.7437 68.6332 49.941 68.5307L67.8914 59.3858C68.0886 59.2832 68.278 59.1254 68.4516 58.9203C68.6252 58.7151 68.7672 58.4706 68.8619 58.2102C68.9566 57.9498 69.0118 57.6894 69.0118 57.4527C69.0118 57.216 68.9566 57.0029 68.8619 56.8451L59.8906 42.3033C59.7959 42.1455 59.6539 42.0508 59.4803 42.0193C59.3067 41.9877 59.1174 42.0193 58.9201 42.1218Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const ExportHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="71" height="81" viewBox="0 0 71 81" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M41.9519 2.94243C41.151 2.53509 40.1223 2.5698 39.0038 3.13593L8.17037 18.8494C5.63659 20.1404 3.57921 23.6892 3.57921 26.7684V64.2916C3.57921 66.0107 4.21438 67.2191 5.21547 67.7369L2.13625 66.1696C1.13517 65.6587 0.5 64.4435 0.5 62.7244V25.2012C0.5 22.1151 2.55742 18.5732 5.0912 17.2821L35.9246 1.5687C37.05 0.995669 38.0787 0.967864 38.8726 1.3752L41.9519 2.94243Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.9519 2.94356C42.9529 3.45446 43.5881 4.66973 43.5881 6.38884V43.912C43.5881 46.9981 41.5307 50.54 38.9969 51.8311L8.16351 67.5448C7.03816 68.1179 6.00944 68.1453 5.21548 67.738C4.21439 67.2271 3.57922 66.0118 3.57922 64.2927V26.7696C3.57922 23.6835 5.6366 20.1415 8.17038 18.8505L39.0038 3.13706C40.1292 2.56403 41.1579 2.53622 41.9519 2.94356Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.4078 8.88579C54.6069 8.47845 53.5782 8.51316 52.4597 9.07929L21.6263 24.7927C19.0926 26.0838 17.0351 29.6326 17.0351 32.7118V70.2353C17.0351 71.9544 17.6703 73.1624 18.6714 73.6802L15.5922 72.113C14.5911 71.6021 13.9559 70.3872 13.9559 68.6681V31.1446C13.9559 28.0585 16.0134 24.5165 18.5471 23.2255L49.3805 7.51206C50.5059 6.93903 51.5346 6.91122 52.3286 7.31856L55.4078 8.88579Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.4079 8.88692C56.4089 9.39782 57.0441 10.6131 57.0441 12.3322V49.8557C57.0441 52.9418 54.9867 56.4834 52.4529 57.7744L21.6195 73.4882C20.4941 74.0612 19.4654 74.0887 18.6715 73.6814C17.6704 73.1704 17.0352 71.9555 17.0352 70.2364V32.7129C17.0352 29.6268 19.0926 26.0849 21.6264 24.7938L52.4598 9.08042C53.5852 8.50739 54.6139 8.47958 55.4079 8.88692Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M68.8637 14.8332C68.0629 14.4259 67.0342 14.4602 65.9157 15.0264L35.0823 30.7401C32.5485 32.0312 30.4911 35.5797 30.4911 38.6589V76.1824C30.4911 77.9015 31.1263 79.1098 32.1274 79.6276L29.0482 78.0604C28.0471 77.5495 27.4119 76.3342 27.4119 74.6151V37.0916C27.4119 34.0055 29.4693 30.4639 32.0031 29.1729L62.8365 13.4591C63.9619 12.8861 64.9906 12.8586 65.7845 13.266L68.8637 14.8332Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M68.8637 14.8304C69.8648 15.3413 70.5 16.5563 70.5 18.2754V55.7989C70.5 58.885 68.4426 62.4269 65.9088 63.7179L35.0754 79.4313C33.95 80.0044 32.9213 80.0322 32.1273 79.6248C31.1263 79.1139 30.4911 77.8987 30.4911 76.1796V38.6561C30.4911 35.57 32.5485 32.0284 35.0823 30.7373L65.9157 15.0236C67.041 14.4505 68.0698 14.4231 68.8637 14.8304Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M43.5881 41.1008C43.5881 40.4104 44.0438 39.6233 44.6099 39.3333L58.9289 32.0359C59.495 31.7459 59.9507 32.0702 59.9507 32.7607C59.9507 33.4511 59.495 34.2382 58.9289 34.5281L44.6099 41.8259C44.0438 42.1159 43.5881 41.7912 43.5881 41.1008Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M58.639 46.7626C58.2385 47.453 57.5895 47.7774 57.1891 47.5013L52.7981 44.3944V58.81C52.7981 59.5004 52.3356 60.2875 51.7764 60.5775C51.2171 60.8674 50.7546 60.5362 50.7546 59.8527V45.43L46.3636 53.0107C45.9631 53.7011 45.3142 54.0323 44.9137 53.7493C44.5202 53.4662 44.5202 52.6793 44.9137 51.9889L51.0514 41.3982C51.0514 41.3982 51.0928 41.336 51.1135 41.3014C51.1205 41.2945 51.1274 41.2878 51.1274 41.2809C51.2033 41.1704 51.2862 41.0598 51.3759 40.9701C51.4104 40.9355 51.445 40.9009 51.4795 40.8733C51.5071 40.8457 51.5416 40.8182 51.5761 40.7975C51.6107 40.7698 51.6521 40.7421 51.6866 40.7145C51.7211 40.6938 51.7487 40.68 51.7833 40.6592C51.8178 40.6385 51.8523 40.6248 51.8868 40.611C52.018 40.5558 52.1492 40.5419 52.2666 40.5558C52.2942 40.5558 52.3218 40.5628 52.3494 40.5767C52.3494 40.5767 52.4046 40.5972 52.4322 40.611C52.4737 40.6318 52.5082 40.6525 52.5427 40.6801L58.6459 44.9952C59.0394 45.2782 59.0394 46.0652 58.6459 46.7556L58.639 46.7626Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export * from "./customer";
|
||||
export * from "./epic";
|
||||
export * from "./estimate";
|
||||
export * from "./export";
|
||||
export * from "./intake";
|
||||
export * from "./label";
|
||||
export * from "./link";
|
||||
export * from "./members";
|
||||
export * from "./note";
|
||||
export * from "./priority";
|
||||
export * from "./settings";
|
||||
export * from "./state";
|
||||
export * from "./template";
|
||||
export * from "./token";
|
||||
export * from "./unknown";
|
||||
export * from "./update";
|
||||
export * from "./webhook";
|
||||
export * from "./work-item";
|
||||
export * from "./worklog";
|
||||
@@ -0,0 +1,82 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const IntakeHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="71" height="80" viewBox="0 0 71 80" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M41.9519 2.42126C41.151 2.01392 40.1223 2.04846 39.0038 2.61459L8.1704 18.3282C5.63661 19.6192 3.57922 23.1679 3.57922 26.2471V63.7706C3.57922 65.4897 4.21438 66.6979 5.21547 67.2157L2.13625 65.6485C1.13517 65.1376 0.5 63.9225 0.5 62.2034V24.6799C0.5 21.5938 2.55742 18.052 5.0912 16.761L35.9246 1.04736C37.05 0.47433 38.0787 0.446693 38.8727 0.854032L41.9519 2.42126Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.952 2.4216C42.953 2.9325 43.5882 4.1476 43.5882 5.86671V43.3902C43.5882 46.4763 41.5308 50.0181 38.997 51.3091L8.16357 67.0227C7.03821 67.5958 6.00952 67.6234 5.21555 67.216C4.21447 66.7052 3.5793 65.49 3.5793 63.7709V26.2474C3.5793 23.1613 5.6367 19.6196 8.17048 18.3285L39.0039 2.61493C40.1293 2.0419 41.158 2.01426 41.952 2.4216Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.4078 8.39993C54.607 7.99259 53.5783 8.02713 52.4598 8.59326L21.6264 24.3069C19.0926 25.5979 17.0352 29.1466 17.0352 32.2258V69.7493C17.0352 71.4684 17.6704 72.6766 18.6714 73.1944L15.5923 71.6272C14.5912 71.1163 13.956 69.9012 13.956 68.1821V30.6585C13.956 27.5724 16.0134 24.0307 18.5472 22.7396L49.3806 7.02603C50.506 6.453 51.5347 6.42536 52.3286 6.8327L55.4078 8.39993Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.4079 8.40042C56.409 8.91132 57.0442 10.1264 57.0442 11.8455V49.369C57.0442 52.4551 54.9867 55.9969 52.4529 57.2879L21.6195 73.0015C20.4942 73.5746 19.4655 73.6022 18.6715 73.1949C17.6704 72.684 17.0352 71.4689 17.0352 69.7498V32.2263C17.0352 29.1402 19.0926 25.5984 21.6264 24.3073L52.4599 8.59375C53.5852 8.02072 54.6139 7.99308 55.4079 8.40042Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M68.8638 14.3513C68.0629 13.944 67.0342 13.9785 65.9157 14.5446L35.0823 30.2582C32.5485 31.5493 30.4911 35.098 30.4911 38.1772V75.7005C30.4911 77.4196 31.1263 78.6278 32.1274 79.1456L29.0482 77.5785C28.0471 77.0676 27.4119 75.8524 27.4119 74.1333V36.6099C27.4119 33.5238 29.4693 29.9821 32.0031 28.691L62.8365 12.9774C63.9619 12.4044 64.9906 12.3767 65.7845 12.7841L68.8638 14.3513Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M68.8636 14.3517C69.8647 14.8626 70.4999 16.0777 70.4999 17.7968V55.3201C70.4999 58.4062 68.4425 61.9481 65.9087 63.2392L35.0753 78.9528C33.9499 79.5258 32.9212 79.5533 32.1273 79.1459C31.1262 78.635 30.491 77.4199 30.491 75.7008V38.1775C30.491 35.0914 32.5484 31.5496 35.0822 30.2586L65.9156 14.545C67.041 13.9719 68.0697 13.9443 68.8636 14.3517Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M57.8519 35.2914C58.1349 35.3467 58.3075 35.6296 58.3075 36.0369V46.4414C58.3075 46.8349 58.1419 47.2836 57.8726 47.6288L52.1699 54.8159C51.8937 55.168 51.5485 55.3476 51.2654 55.2924C50.9824 55.2372 50.8097 54.954 50.8097 54.5467V44.0941C50.8097 43.7005 50.9754 43.2447 51.2516 42.8995L56.9543 35.7608C57.2305 35.4156 57.5757 35.2362 57.8588 35.2914H57.8519ZM52.5979 43.8178V51.647L56.5056 46.7244V38.9297L52.5979 43.8178Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M55.3249 34.8766C55.5804 35.284 55.4354 36.0434 55.0073 36.575L49.7395 43.1615V52.979C49.7395 53.6004 49.3391 54.3115 48.842 54.5601C48.3449 54.8086 47.9445 54.5118 47.9445 53.8973V43.4445C47.9445 43.051 48.1102 42.5953 48.3863 42.2501L54.0891 35.1114C54.5171 34.5798 55.0695 34.4693 55.3249 34.8766Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M52.4253 34.1796C52.6808 34.587 52.5358 35.3464 52.1077 35.878L46.84 42.4645V52.282C46.84 52.9034 46.4395 53.6145 45.9424 53.8631C45.4454 54.1116 45.0449 53.8148 45.0449 53.2003V42.7475C45.0449 42.354 45.2106 41.8983 45.4868 41.5531L51.1895 34.4144C51.6175 33.8828 52.1699 33.7723 52.4253 34.1796Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M42.0347 52.1577C42.5318 51.9023 42.9323 52.206 42.9323 52.8205V60.8223C42.9323 61.5749 43.4293 61.9407 44.03 61.6369L59.2465 53.8837C59.8471 53.5799 60.3442 52.71 60.3442 51.9575V43.9556C60.3442 43.3343 60.7446 42.6231 61.2417 42.3746C61.7388 42.126 62.1393 42.423 62.1393 43.0375V51.0392C62.1393 53.0344 60.8413 55.3198 59.2465 56.1344L44.03 63.8877C42.4351 64.7023 41.1372 63.7357 41.1372 61.7473V53.7457C41.1372 53.1243 41.5376 52.4132 42.0347 52.1646V52.1577Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const LabelHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="81" height="92" viewBox="0 0 81 92" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M47.8737 2.47584C46.9585 2.00383 45.7828 2.04383 44.5045 2.69984L9.2662 20.9079C6.37044 22.4039 4.01909 26.5159 4.01909 30.0839V73.5641C4.01909 75.5561 4.745 76.9561 5.88911 77.5561L2.37001 75.7401C1.22591 75.1481 0.5 73.7401 0.5 71.7481V28.2679C0.5 24.6919 2.85132 20.5879 5.74708 19.0919L40.9854 0.883826C42.2716 0.219824 43.4472 0.187824 44.3546 0.659826L47.8737 2.47584Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.8737 2.47547C49.0178 3.06747 49.7437 4.47548 49.7437 6.46749V49.9476C49.7437 53.5237 47.3924 57.6277 44.4966 59.1237L9.25828 77.3318C7.97216 77.9958 6.79648 78.0278 5.88909 77.5558C4.74498 76.9638 4.01907 75.5557 4.01907 73.5637V30.0836C4.01907 26.5076 6.37042 22.4035 9.26618 20.9075L44.5045 2.69947C45.7906 2.03547 46.9663 2.00347 47.8737 2.47547Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M63.2522 9.36414C62.337 8.89214 61.1613 8.93214 59.883 9.58814L24.6447 27.7962C21.749 29.2922 19.3976 33.4042 19.3976 36.9722V80.4524C19.3976 82.4444 20.1235 83.8444 21.2676 84.4444L17.7485 82.6284C16.6044 82.0364 15.8785 80.6284 15.8785 78.6364V35.1562C15.8785 31.5802 18.2299 27.4762 21.1256 25.9802L56.364 7.77213C57.6501 7.10813 58.8257 7.07613 59.7331 7.54813L63.2522 9.36414Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M63.2521 9.36378C64.3962 9.95578 65.1221 11.3638 65.1221 13.3558V56.836C65.1221 60.412 62.7708 64.516 59.8751 66.012L24.6367 84.22C23.3505 84.8841 22.1749 84.9161 21.2675 84.4441C20.1234 83.8521 19.3975 82.4441 19.3975 80.452V36.9719C19.3975 33.3959 21.7488 29.2918 24.6446 27.7958L59.8829 9.58778C61.169 8.92377 62.3447 8.89177 63.2521 9.36378Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M78.6303 16.2602C77.715 15.7882 76.5394 15.8282 75.2612 16.4842L40.0228 34.6923C37.127 36.1883 34.7757 40.3003 34.7757 43.8683V87.3485C34.7757 89.3405 35.5016 90.7405 36.6457 91.3405L33.1266 89.5245C31.9825 88.9325 31.2566 87.5245 31.2566 85.5325V42.0523C31.2566 38.4763 33.6079 34.3723 36.5037 32.8763L71.7421 14.6682C73.0282 14.0042 74.2039 13.9722 75.1113 14.4443L78.6303 16.2602Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M78.6301 16.2599C79.7742 16.8519 80.5001 18.2599 80.5001 20.2519V63.7321C80.5001 67.3081 78.1488 71.4121 75.2531 72.9081L40.0147 91.1162C38.7286 91.7802 37.5529 91.8122 36.6455 91.3402C35.5014 90.7482 34.7755 89.3402 34.7755 87.3482V43.868C34.7755 40.292 37.1268 36.188 40.0226 34.692L75.261 16.4839C76.5471 15.8199 77.7227 15.7879 78.6301 16.2599Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M49.8068 47.9642C50.4143 46.9082 51.227 46.0682 52.087 45.6282L59.788 41.6522C60.6402 41.2122 61.4608 41.2042 62.0684 41.6362L71.4106 48.3402C72.097 48.8362 72.4758 49.7882 72.4758 50.9882C72.4758 52.1882 72.0891 53.5322 71.4106 54.7402L64.3487 67.1083C63.6622 68.3163 62.7311 69.2683 61.7606 69.7723C60.7901 70.2763 59.8591 70.2843 59.1726 69.7963L49.8304 63.0922C49.2229 62.6602 48.8836 61.8282 48.8836 60.7722V51.2762C48.8836 50.2202 49.2229 49.0362 49.8304 47.9802L49.8068 47.9642ZM52.087 48.2762C51.803 48.4202 51.5269 48.7002 51.3296 49.0522C51.1323 49.4042 51.014 49.7962 51.014 50.1482V59.6442C51.014 59.9962 51.1245 60.2762 51.3296 60.4202L60.6718 67.1163C60.9558 67.3163 61.3503 67.3163 61.7527 67.1083C62.1551 66.9003 62.5418 66.5003 62.8338 66.0043L69.8956 53.6522C70.1796 53.1482 70.3375 52.5882 70.3375 52.0922C70.3375 51.5962 70.1796 51.1962 69.8956 50.9882L60.5534 44.2922C60.3562 44.1482 60.08 44.1482 59.7959 44.2922L52.0949 48.2682L52.087 48.2762Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
d="M59.7802 51.2521C59.7802 53.4442 58.3363 55.9722 56.561 56.8842C54.7857 57.8042 53.3417 56.7722 53.3417 54.5721C53.3417 52.3721 54.7857 49.8521 56.561 48.9401C58.3363 48.0201 59.7802 49.0521 59.7802 51.2521Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const LinkHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="52" height="59" viewBox="0 0 52 59" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M30.6725 2.33465C30.0925 2.03965 29.3475 2.06465 28.5375 2.47465L6.20747 13.8546C4.37247 14.7896 2.88247 17.3596 2.88247 19.5896V46.7646C2.88247 48.0096 3.34246 48.8846 4.06746 49.2596L1.83746 48.1246C1.11246 47.7547 0.652466 46.8746 0.652466 45.6296V18.4546C0.652466 16.2196 2.14246 13.6546 3.97746 12.7196L26.3075 1.33964C27.1225 0.924644 27.8675 0.904645 28.4425 1.19964L30.6725 2.33465Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M30.6724 2.33465C31.3974 2.70465 31.8574 3.58465 31.8574 4.82965V32.0046C31.8574 34.2396 30.3674 36.8046 28.5324 37.7396L6.20245 49.1197C5.38745 49.5347 4.64244 49.5546 4.06744 49.2597C3.34244 48.8897 2.88245 48.0097 2.88245 46.7647V19.5897C2.88245 17.3547 4.37245 14.7897 6.20745 13.8547L28.5374 2.47465C29.3524 2.05965 30.0974 2.03965 30.6724 2.33465Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M40.4175 6.66967C39.8375 6.37467 39.0925 6.39967 38.2825 6.80967L15.9525 18.1897C14.1175 19.1247 12.6275 21.6947 12.6275 23.9247V51.0997C12.6275 52.3447 13.0875 53.2197 13.8125 53.5947L11.5825 52.4597C10.8575 52.0897 10.3975 51.2097 10.3975 49.9647V22.7897C10.3975 20.5547 11.8875 17.9897 13.7225 17.0547L36.0525 5.67467C36.8675 5.25967 37.6125 5.23967 38.1875 5.53467L40.4175 6.66967Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M40.4174 6.66974C41.1424 7.03974 41.6024 7.91973 41.6024 9.16473V36.3397C41.6024 38.5747 40.1124 41.1397 38.2774 42.0747L15.9474 53.4547C15.1324 53.8697 14.3874 53.8897 13.8124 53.5947C13.0874 53.2247 12.6274 52.3447 12.6274 51.0997V23.9247C12.6274 21.6897 14.1174 19.1247 15.9524 18.1897L38.2824 6.80974C39.0974 6.39474 39.8424 6.37474 40.4174 6.66974Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M50.1625 10.9747C49.5825 10.6797 48.8375 10.7047 48.0275 11.1147L25.6975 22.4947C23.8625 23.4297 22.3725 25.9997 22.3725 28.2297V55.4047C22.3725 56.6497 22.8325 57.5247 23.5575 57.8997L21.3275 56.7647C20.6025 56.3947 20.1425 55.5147 20.1425 54.2697V27.0947C20.1425 24.8597 21.6325 22.2947 23.4675 21.3597L45.7975 9.97966C46.6125 9.56466 47.3575 9.54466 47.9325 9.83966L50.1625 10.9747Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M50.1624 10.9747C50.8874 11.3447 51.3474 12.2247 51.3474 13.4697V40.6447C51.3474 42.8797 49.8574 45.4447 48.0224 46.3797L25.6924 57.7597C24.8774 58.1747 24.1324 58.1947 23.5574 57.8997C22.8324 57.5297 22.3724 56.6497 22.3724 55.4047V28.2297C22.3724 25.9947 23.8624 23.4297 25.6974 22.4947L48.0274 11.1147C48.8424 10.6997 49.5874 10.6797 50.1624 10.9747Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M33.0175 38.0797C32.4625 39.0347 32.1525 40.1097 32.1525 41.0597C32.1525 42.0097 32.4625 42.7697 33.0175 43.1597C33.5725 43.5497 34.3225 43.5497 35.1025 43.1497C35.8825 42.7497 36.6375 41.9897 37.1875 41.0347L38.2325 39.2347C38.5225 38.7347 38.9875 38.4997 39.2775 38.7047C39.5675 38.9097 39.5675 39.4797 39.2775 39.9747L38.2325 41.7747C37.4025 43.2097 36.2775 44.3497 35.1025 44.9497C33.9275 45.5497 32.8025 45.5547 31.9725 44.9647C31.1425 44.3797 30.6775 43.2447 30.6775 41.8147C30.6775 40.3847 31.1425 38.7797 31.9725 37.3447L33.0175 35.5447C33.3075 35.0447 33.7725 34.8097 34.0625 35.0147C34.3525 35.2197 34.3525 35.7847 34.0625 36.2847L33.0175 38.0847V38.0797Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M36.1474 31.3997C35.8574 31.1947 35.8574 30.6297 36.1474 30.1297L37.1924 28.3297C38.0224 26.8947 39.1474 25.7547 40.3224 25.1547C41.4974 24.5547 42.6224 24.5497 43.4524 25.1397C44.2824 25.7247 44.7474 26.8597 44.7474 28.2897C44.7474 29.7197 44.2824 31.3247 43.4524 32.7597L42.4074 34.5597C42.1174 35.0597 41.6524 35.2947 41.3624 35.0897C41.0724 34.8847 41.0724 34.3147 41.3624 33.8197L42.4074 32.0197C42.9624 31.0647 43.2724 29.9897 43.2724 29.0397C43.2724 28.0897 42.9624 27.3297 42.4074 26.9397C41.8524 26.5497 41.1024 26.5497 40.3224 26.9497C39.5374 27.3497 38.7874 28.1097 38.2374 29.0647L37.1924 30.8647C36.9024 31.3647 36.4374 31.5997 36.1474 31.3947V31.3997Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M35.1025 39.5547C34.8125 39.3497 34.8125 38.7847 35.1025 38.2847L39.2775 31.0797C39.5675 30.5797 40.0325 30.3447 40.3225 30.5497C40.6125 30.7547 40.6125 31.3197 40.3225 31.8197L36.1475 39.0247C35.8575 39.5197 35.3925 39.7597 35.1025 39.5547Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,82 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const MembersHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="71" height="80" viewBox="0 0 71 80" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M41.9518 2.84283C41.151 2.43551 40.1223 2.47003 39.0038 3.03614L8.17039 18.7493C5.6366 20.0403 3.5792 23.5889 3.5792 26.668V64.1904C3.5792 65.9094 4.21437 67.1176 5.21546 67.6354L2.13626 66.0682C1.13517 65.5573 0.5 64.3423 0.5 62.6232V25.1008C0.5 22.0148 2.5574 18.4731 5.09119 17.1821L35.9246 1.46898C37.05 0.895957 38.0787 0.868332 38.8726 1.27566L41.9518 2.84283Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.9515 2.84317C42.9526 3.35405 43.5877 4.56913 43.5877 6.28819V43.8106C43.5877 46.8966 41.5303 50.4383 38.9966 51.7293L8.16314 67.4424C7.03778 68.0154 6.00908 68.043 5.21512 67.6357C4.21403 67.1248 3.57886 65.9098 3.57886 64.1907V26.6683C3.57886 23.5823 5.63626 20.0406 8.17004 18.7496L39.0035 3.03647C40.1288 2.46346 41.1575 2.43584 41.9515 2.84317Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.4077 8.82148C54.6068 8.41415 53.5781 8.44867 52.4596 9.01478L21.6262 24.7279C19.0924 26.0189 17.035 29.5675 17.035 32.6466V70.169C17.035 71.8881 17.6702 73.0962 18.6713 73.614L15.5921 72.0468C14.591 71.536 13.9558 70.3209 13.9558 68.6018V31.0795C13.9558 27.9934 16.0132 24.4518 18.547 23.1608L49.3804 7.44761C50.5058 6.87459 51.5345 6.84697 52.3284 7.2543L55.4077 8.82148Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.4076 8.82181C56.4086 9.33269 57.0438 10.5478 57.0438 12.2668V49.7892C57.0438 52.8752 54.9864 56.4169 52.4526 57.7079L21.6192 73.421C20.4938 73.9941 19.4651 74.0217 18.6712 73.6143C17.6701 73.1035 17.0349 71.8884 17.0349 70.1693V32.647C17.0349 29.5609 19.0923 26.0193 21.6261 24.7282L52.4595 9.01511C53.5849 8.44209 54.6136 8.41448 55.4076 8.82181Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M68.8637 14.7732C68.0628 14.3659 67.0341 14.4004 65.9157 14.9666L35.0823 30.6797C32.5485 31.9707 30.4911 35.5193 30.4911 38.5984V76.1208C30.4911 77.8398 31.1262 79.048 32.1273 79.5658L29.0481 77.9986C28.047 77.4877 27.4119 76.2727 27.4119 74.5536V37.0312C27.4119 33.9452 29.4693 30.4035 32.003 29.1125L62.8365 13.3994C63.9618 12.8264 64.9905 12.7988 65.7845 13.2061L68.8637 14.7732Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M68.8638 14.7736C69.8649 15.2845 70.5001 16.4996 70.5001 18.2186V55.741C70.5001 58.827 68.4427 62.3687 65.9089 63.6597L35.0755 79.3728C33.9501 79.9458 32.9214 79.9734 32.1275 79.5661C31.1264 79.0552 30.4912 77.8402 30.4912 76.1211V38.5987C30.4912 35.5127 32.5486 31.9711 35.0824 30.68L65.9158 14.9669C67.0412 14.3939 68.0699 14.3663 68.8638 14.7736Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M47.6061 52.5984C48.8696 50.4168 50.5887 48.6771 52.3699 47.7657C54.1581 46.8544 55.8703 46.8476 57.1337 47.7381C58.3972 48.6356 59.1083 50.3547 59.1083 52.5294C59.1083 53.1784 58.6802 53.924 58.1417 54.1932C57.6032 54.4625 57.1751 54.1587 57.1751 53.5097C57.1751 51.9564 56.6711 50.7206 55.7667 50.0854C54.8623 49.4434 53.6403 49.4503 52.363 50.1062C51.0858 50.7551 49.8638 51.9978 48.9593 53.5581C48.0549 55.1183 47.5509 56.865 47.5509 58.4184C47.5509 59.0673 47.1229 59.8129 46.5913 60.0822C46.0596 60.3514 45.6316 60.0477 45.6316 59.3987C45.6316 57.224 46.3427 54.7732 47.6061 52.5915V52.5984Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M52.3766 40.7308C50.7818 41.5454 49.4907 43.7754 49.4907 45.7153C49.4907 47.6553 50.7818 48.5735 52.3766 47.7589C53.9715 46.9442 55.2625 44.7143 55.2625 42.7743C55.2625 40.8343 53.9715 39.9161 52.3766 40.7308ZM47.5576 46.7026C47.5576 43.4647 49.7117 39.7435 52.3697 38.3904C55.0278 37.0372 57.1818 38.563 57.1818 41.794C57.1818 45.025 55.0278 48.753 52.3697 50.1062C49.7117 51.4593 47.5576 49.9336 47.5576 46.7026Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M57.189 38.2798C57.189 37.6308 57.617 36.8852 58.1555 36.616C59.274 36.0498 60.3372 36.0636 61.1174 36.6367C61.8975 37.2166 62.3325 38.3074 62.3325 39.6743C62.3325 41.0413 61.8975 42.5809 61.1174 43.9478C61.0828 44.01 61.0483 44.0721 61.0138 44.1273C61.4626 44.2171 61.8837 44.3966 62.2565 44.6727C63.3405 45.4736 63.9411 46.9924 63.9411 48.891C63.9411 49.5399 63.5131 50.2856 62.9746 50.5548C62.436 50.8241 62.0149 50.5203 62.0149 49.8713C62.0149 48.5734 61.6007 47.5447 60.8757 47.0062C60.1508 46.4677 59.1773 46.4608 58.1624 46.9786C57.6308 47.2479 57.1959 46.9441 57.1959 46.2951C57.1959 45.6462 57.6239 44.9006 58.1624 44.6313C58.7493 44.3345 59.3154 43.7476 59.7435 43.002C60.1646 42.2564 60.4132 41.4003 60.4132 40.634C60.4132 39.8677 60.1715 39.267 59.7435 38.9495C59.3223 38.6319 58.7493 38.6319 58.1624 38.9357C57.6308 39.2049 57.1959 38.9011 57.1959 38.2522L57.189 38.2798Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M47.5577 43.1888C47.5577 42.5399 47.1297 42.2361 46.5981 42.5054C45.4796 43.0715 44.4164 44.1692 43.6362 45.543C42.8561 46.9169 42.4211 48.4565 42.4211 49.8165C42.4211 51.1766 42.8561 52.2812 43.6362 52.8542C43.6708 52.8818 43.7053 52.9025 43.7398 52.9302C43.291 53.4825 42.8699 54.09 42.4971 54.7459C41.4131 56.6513 40.8125 58.7846 40.8125 60.6832C40.8125 61.3321 41.2405 61.6359 41.7722 61.3667C42.3038 61.0974 42.7318 60.3518 42.7318 59.7028C42.7318 58.4049 43.1461 56.9551 43.871 55.6779C44.5959 54.4007 45.5694 53.3996 46.5843 52.8818C47.1159 52.6126 47.5439 51.867 47.5439 51.218C47.5439 50.569 47.1159 50.2653 46.5843 50.5345C45.9974 50.8314 45.4313 50.8314 45.0032 50.5207C44.5821 50.2031 44.3336 49.6025 44.3336 48.8362C44.3336 48.0699 44.5752 47.2207 45.0032 46.4751C45.4244 45.7294 45.9974 45.1495 46.5843 44.8458C47.1159 44.5765 47.5439 43.8309 47.5439 43.1819L47.5577 43.1888Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const NoteHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="52" height="59" viewBox="0 0 52 59" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M30.6721 2.43395C30.0921 2.13895 29.3471 2.16395 28.5371 2.57395L6.20711 13.9539C4.37211 14.8889 2.8821 17.4589 2.8821 19.6889V46.864C2.8821 48.109 3.34211 48.9839 4.06711 49.3589L1.8371 48.224C1.1121 47.854 0.6521 46.9739 0.6521 45.7289V18.5539C0.6521 16.3189 2.1421 13.7539 3.9771 12.8189L26.3071 1.43895C27.1221 1.02395 27.8671 1.00395 28.4421 1.29895L30.6721 2.43395Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M30.6721 2.43393C31.3971 2.80393 31.8571 3.68392 31.8571 4.92892V32.1039C31.8571 34.3389 30.3671 36.9039 28.5321 37.8389L6.20209 49.2189C5.38709 49.6339 4.64209 49.6539 4.06709 49.3589C3.34209 48.9889 2.88208 48.1089 2.88208 46.8639V19.6889C2.88208 17.4539 4.37209 14.8889 6.20709 13.9539L28.5371 2.57393C29.3521 2.15893 30.0971 2.13893 30.6721 2.43393Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M40.4171 6.76894C39.8371 6.47394 39.0921 6.49894 38.2821 6.90894L15.9521 18.2889C14.1171 19.2239 12.6271 21.7939 12.6271 24.0239V51.1989C12.6271 52.4439 13.0871 53.3189 13.8121 53.6939L11.5821 52.5589C10.8571 52.1889 10.3971 51.3089 10.3971 50.0639V22.8889C10.3971 20.6539 11.8871 18.0889 13.7221 17.1539L36.0521 5.77394C36.8671 5.35894 37.6121 5.33894 38.1871 5.63394L40.4171 6.76894Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M40.4171 6.76892C41.1421 7.13892 41.6021 8.01892 41.6021 9.26392V36.4389C41.6021 38.6739 40.1121 41.2389 38.2771 42.1739L15.9471 53.5539C15.1321 53.9689 14.3871 53.9889 13.8121 53.6939C13.0871 53.3239 12.6271 52.4439 12.6271 51.1989V24.0239C12.6271 21.7889 14.1171 19.2239 15.9521 18.2889L38.2821 6.90892C39.0971 6.49392 39.8421 6.47392 40.4171 6.76892Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M50.1621 11.0739C49.5821 10.7789 48.8371 10.8039 48.0271 11.2139L25.6971 22.5939C23.8621 23.5289 22.3721 26.0989 22.3721 28.3289V55.5039C22.3721 56.7489 22.8321 57.6239 23.5571 57.9989L21.3271 56.8639C20.6021 56.4939 20.1421 55.6139 20.1421 54.3689V27.1939C20.1421 24.9589 21.6321 22.3939 23.4671 21.4589L45.7971 10.0789C46.6121 9.66393 47.3571 9.64393 47.9321 9.93893L50.1621 11.0739Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M50.1621 11.0739C50.8871 11.4439 51.3471 12.3239 51.3471 13.5689V40.7439C51.3471 42.9789 49.8571 45.5439 48.0221 46.4789L25.6921 57.8589C24.8771 58.2739 24.1321 58.2939 23.5571 57.9989C22.8321 57.6289 22.3721 56.7489 22.3721 55.5039V28.3289C22.3721 26.0939 23.8621 23.5289 25.6971 22.5939L48.0271 11.2139C48.8421 10.7989 49.5871 10.7789 50.1621 11.0739Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M43.5321 23.1439L32.4371 28.7939C31.1271 29.4639 30.0621 31.2989 30.0621 32.8939V46.3939C30.0621 47.9889 31.1271 48.7439 32.4371 48.0789L41.1521 43.6389C41.3621 43.5289 41.5671 43.3239 41.7171 43.0689L45.6771 36.2289C45.8271 35.9739 45.9071 35.6839 45.9071 35.4239V24.8189C45.9071 23.2239 44.8371 22.4739 43.5271 23.1389L43.5321 23.1439ZM44.3221 34.3089L41.9471 35.5189C40.6371 36.1889 39.5671 38.0289 39.5671 39.6239V42.5139L32.4371 46.1489C32.0021 46.3689 31.6471 46.1239 31.6471 45.5889V32.0889C31.6471 31.5539 32.0021 30.9439 32.4371 30.7239L43.5321 25.0739C43.9721 24.8489 44.3221 25.0989 44.3221 25.6339V34.3139V34.3089Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const PriorityHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="72" height="81" viewBox="0 0 72 81" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M42.4518 2.93859C41.651 2.53126 40.6222 2.5658 39.5038 3.13193L8.67038 18.8454C6.1366 20.1365 4.0792 23.6852 4.0792 26.7644V64.2878C4.0792 66.0069 4.71437 67.2151 5.71546 67.7329L2.63626 66.1657C1.63517 65.6548 1 64.4397 1 62.7206V25.1972C1 22.1111 3.0574 18.5693 5.59118 17.2783L36.4246 1.5647C37.5499 0.991664 38.5787 0.964028 39.3726 1.37137L42.4518 2.93859Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M42.4519 2.9385C43.4529 3.4494 44.0881 4.66451 44.0881 6.38361V43.907C44.0881 46.9931 42.0307 50.5349 39.4969 51.8259L8.6635 67.5395C7.53814 68.1125 6.50945 68.1402 5.71548 67.7328C4.7144 67.2219 4.07922 66.0068 4.07922 64.2877V26.7643C4.07922 23.6782 6.13663 20.1364 8.67041 18.8453L39.5038 3.13184C40.6292 2.5588 41.6579 2.53117 42.4519 2.9385Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.9078 8.88293C55.1069 8.47559 54.0782 8.51013 52.9598 9.07626L22.1264 24.7899C19.5926 26.0809 17.5352 29.6296 17.5352 32.7088V70.2322C17.5352 71.9513 18.1704 73.1595 19.1715 73.6773L16.0923 72.11C15.0912 71.5991 14.456 70.384 14.456 68.6649V31.1415C14.456 28.0554 16.5134 24.5137 19.0472 23.2226L49.8806 7.50903C51.006 6.936 52.0347 6.90836 52.8286 7.3157L55.9078 8.88293Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.9079 8.88284C56.909 9.39374 57.5441 10.6088 57.5441 12.3279V49.8514C57.5441 52.9375 55.4867 56.4792 52.9529 57.7703L22.1195 73.4838C20.9942 74.0569 19.9655 74.0845 19.1715 73.6772C18.1704 73.1663 17.5352 71.9512 17.5352 70.2321V32.7087C17.5352 29.6226 19.5926 26.0808 22.1264 24.7898L52.9598 9.07617C54.0852 8.50314 55.1139 8.4755 55.9079 8.88284Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M69.3638 14.8343C68.5629 14.427 67.5342 14.4615 66.4158 15.0277L35.5823 30.7413C33.0486 32.0323 30.9912 35.581 30.9912 38.6602V76.1836C30.9912 77.9027 31.6263 79.1109 32.6274 79.6287L29.5482 78.0615C28.5471 77.5506 27.912 76.3355 27.912 74.6163V37.0929C27.912 34.0068 29.9694 30.4651 32.5031 29.174L63.3366 13.4605C64.4619 12.8874 65.4906 12.8598 66.2846 13.2671L69.3638 14.8343Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M69.3638 14.8343C70.3649 15.3452 71.0001 16.5603 71.0001 18.2794V55.8028C71.0001 58.8889 68.9427 62.4306 66.4089 63.7217L35.5755 79.4353C34.4501 80.0083 33.4214 80.0359 32.6275 79.6286C31.6264 79.1177 30.9912 77.9026 30.9912 76.1835V38.6601C30.9912 35.574 33.0486 32.0322 35.5824 30.7412L66.4158 15.0276C67.5412 14.4546 68.5699 14.4269 69.3638 14.8343Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.9065 64.3778C41.1954 64.7369 40.6154 64.3364 40.6154 63.4734C40.6154 62.6104 41.1816 61.6162 41.8927 61.2572H41.9065C42.6176 60.8913 43.1906 61.2987 43.1906 62.1617C43.1906 63.0247 42.6176 64.0188 41.9065 64.3778Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
d="M48.3203 61.112C47.6092 61.471 47.0362 61.0636 47.0362 60.2006V53.9456C47.0362 53.0826 47.6092 52.0883 48.3203 51.7293C49.0314 51.3703 49.6045 51.7777 49.6045 52.6407V58.8957C49.6045 59.7587 49.0314 60.7529 48.3203 61.112Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
d="M54.748 57.8396C54.0369 58.1986 53.4638 57.7913 53.4638 56.9283V44.4182C53.4638 43.5551 54.0369 42.561 54.748 42.202C55.4591 41.843 56.0321 42.2503 56.0321 43.1133V55.6234C56.0321 56.4864 55.4591 57.4806 54.748 57.8396Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
d="M61.1687 54.5603C60.4576 54.9193 59.8846 54.5119 59.8846 53.6489V34.8838C59.8846 34.0208 60.4576 33.0266 61.1687 32.6675C61.8798 32.3085 62.4529 32.7159 62.4529 33.5789V52.344C62.4529 53.207 61.8798 54.2013 61.1687 54.5603Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const SettingsHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="71" height="80" viewBox="0 0 71 80" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M41.9517 2.43803C41.1509 2.0307 40.1222 2.0654 39.0037 2.63153L8.17035 18.3449C5.63658 19.636 3.57916 23.1848 3.57916 26.264V63.787C3.57916 65.5061 4.21433 66.7145 5.21541 67.2323L2.13625 65.6651C1.13517 65.1542 0.5 63.9393 0.5 62.2202V24.6967C0.5 21.6107 2.55741 18.0687 5.09119 16.7777L35.9246 1.06431C37.0499 0.491274 38.0785 0.46347 38.8725 0.870807L41.9517 2.43803Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.9518 2.4388C42.9529 2.9497 43.5881 4.16497 43.5881 5.88407V43.4075C43.5881 46.4936 41.5306 50.0351 38.9969 51.3262L8.1635 67.0399C7.03815 67.6129 6.00943 67.6404 5.21547 67.2331C4.21439 66.7222 3.57922 65.5069 3.57922 63.7878V26.2647C3.57922 23.1786 5.63664 19.6367 8.17041 18.3457L39.0038 2.6323C40.1291 2.05927 41.1578 2.03146 41.9518 2.4388Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.4078 8.38193C54.6069 7.97459 53.5782 8.0093 52.4598 8.57543L21.6264 24.2888C19.0926 25.5799 17.0352 29.1287 17.0352 32.2079V69.7313C17.0352 71.4504 17.6705 72.6584 18.6716 73.1762L15.5923 71.609C14.5912 71.0981 13.9561 69.8831 13.9561 68.164V30.6406C13.9561 27.5545 16.0135 24.0126 18.5472 22.7216L49.3806 7.0082C50.506 6.43517 51.5347 6.4077 52.3286 6.81504L55.4078 8.38193Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.4079 8.38106C56.4089 8.89196 57.0441 10.1072 57.0441 11.8263V49.3497C57.0441 52.4358 54.9867 55.9774 52.4529 57.2685L21.6196 72.9822C20.4942 73.5552 19.4656 73.5827 18.6716 73.1753C17.6705 72.6644 17.0353 71.4495 17.0353 69.7304V32.207C17.0353 29.1209 19.0927 25.579 21.6265 24.2879L52.4598 8.57456C53.5852 8.00153 54.6139 7.97372 55.4079 8.38106Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M68.8636 14.3338C68.0627 13.9264 67.034 13.9612 65.9156 14.5273L35.0822 30.2407C32.5484 31.5317 30.4911 35.0805 30.4911 38.1597V75.6831C30.4911 77.4022 31.1263 78.6103 32.1274 79.1281L29.0481 77.5608C28.047 77.0499 27.4119 75.835 27.4119 74.1159V36.5925C27.4119 33.5064 29.4693 29.9645 32.0031 28.6734L62.8364 12.9601C63.9618 12.387 64.9905 12.3592 65.7844 12.7666L68.8636 14.3338Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M68.8636 14.3347C69.8647 14.8456 70.4998 16.0608 70.4998 17.7799V55.3033C70.4998 58.3894 68.4424 61.931 65.9086 63.2221L35.0753 78.9358C33.9499 79.5088 32.9213 79.5363 32.1273 79.129C31.1263 78.618 30.4911 77.4031 30.4911 75.684V38.1606C30.4911 35.0745 32.5484 31.5326 35.0822 30.2416L65.9156 14.5282C67.0409 13.9551 68.0696 13.9273 68.8636 14.3347Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.35"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M50.7681 61.8964C50.6645 61.9516 50.5609 62.0001 50.4574 62.0346C48.9178 62.6422 47.6889 61.7721 47.5439 59.9701C47.5163 59.5559 47.3506 59.2589 47.0813 59.1277C46.8189 58.9896 46.4876 59.0313 46.1355 59.2454C45.9836 59.3351 45.8386 59.4592 45.7005 59.6111C44.0642 61.3509 42.1449 61.4475 41.4062 59.8458C40.771 58.465 41.2129 56.2075 42.4695 54.4746C42.7594 54.0741 42.9528 53.6184 43.008 53.1696C43.0632 52.7209 42.9941 52.3413 42.7939 52.0996C42.7042 51.9961 42.6006 51.9201 42.4764 51.8718C41.7653 51.6163 41.3027 50.9397 41.1646 49.9731C41.0265 49.0066 41.2267 47.8604 41.7307 46.7558C42.6282 44.7812 44.2921 43.3866 45.6936 43.4487C46.3495 43.4832 47.1227 42.7515 47.4265 41.8056C47.4886 41.6054 47.5301 41.412 47.5439 41.2187C47.5439 41.1842 47.5439 41.1498 47.5508 41.1152C47.6475 40.052 48.0548 38.9335 48.7038 37.9531C49.3735 36.9452 50.2157 36.1997 51.0718 35.8614C52.6114 35.2538 53.8403 36.1236 53.9853 37.9255C54.0198 38.3397 54.1787 38.6368 54.4479 38.7679C54.7172 38.8991 55.0486 38.8577 55.3938 38.6506C55.5457 38.554 55.6906 38.4366 55.8287 38.2916C57.4581 36.5587 59.3843 36.4552 60.123 38.057C60.7582 39.4378 60.3095 41.6952 59.0598 43.4282C58.4661 44.2359 58.3279 45.2991 58.7353 45.8031C58.825 45.9067 58.9287 45.9825 59.0529 46.0239C59.764 46.2793 60.2265 46.9561 60.3715 47.9158C60.5096 48.8823 60.3163 50.0216 59.8124 51.1331C58.9148 53.1146 57.251 54.5091 55.8425 54.4469C55.5181 54.4262 55.1659 54.592 54.8414 54.9027C54.5169 55.2134 54.2546 55.6343 54.1096 56.09C54.0475 56.2902 54.006 56.4905 53.9922 56.6769C53.9094 57.7747 53.4952 58.9278 52.8324 59.9358C52.2456 60.8195 51.5276 61.5029 50.775 61.8826L50.7681 61.8964ZM46.2528 56.829C47.0123 56.4423 47.7372 56.3939 48.3241 56.6907C48.9868 57.029 49.3942 57.7676 49.4701 58.7825C49.5254 59.4936 50.0224 59.846 50.6369 59.6044C50.9821 59.4732 51.3204 59.1695 51.5896 58.769C51.8589 58.3617 52.0246 57.899 52.0522 57.4641C52.0868 57.0015 52.1834 56.5045 52.3422 56.0074C52.7081 54.8683 53.3571 53.8117 54.1648 53.0385C54.9795 52.2652 55.8564 51.8647 56.6365 51.9061C57.1888 51.9268 57.8585 51.3678 58.2175 50.5739C58.4177 50.1251 58.5006 49.6694 58.4454 49.2827C58.3901 48.8961 58.2037 48.6268 57.9207 48.5232C57.6238 48.4197 57.3545 48.2332 57.1405 47.964C56.1187 46.7074 56.4708 44.0701 57.9276 42.0817C58.4247 41.3982 58.6042 40.4941 58.3487 39.9418C58.0518 39.2997 57.2786 39.341 56.6296 40.0314C56.2982 40.3904 55.9323 40.6941 55.5595 40.9288C54.7034 41.4604 53.861 41.5639 53.1982 41.2326C52.5355 40.8943 52.1282 40.1555 52.0522 39.1475C51.997 38.4295 51.4999 38.0843 50.8854 38.326C50.1743 38.5952 49.5392 39.569 49.4701 40.4734C49.4701 40.501 49.4701 40.5284 49.4632 40.556C49.4218 41.0047 49.332 41.4606 49.1801 41.93C48.4276 44.2843 46.4945 46.1206 44.879 46.0239C44.3266 46.0032 43.6569 46.5627 43.2979 47.3498C43.0977 47.7985 43.0149 48.2539 43.0701 48.6406C43.1253 49.0272 43.3118 49.2965 43.5948 49.4001C43.8917 49.5036 44.161 49.6902 44.375 49.9526C44.8721 50.5601 45.0653 51.5059 44.9134 52.6174C44.7615 53.729 44.2921 54.8752 43.5879 55.8348C43.0908 56.5183 42.9113 57.4225 43.1668 57.9748C43.4636 58.6169 44.2369 58.5755 44.8859 57.8851C45.2173 57.5261 45.5831 57.2225 45.9629 56.9877C46.0595 56.9256 46.1562 56.8702 46.2528 56.8219V56.829Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
d="M50.7612 53.673C48.6279 54.7639 46.8881 53.5351 46.8881 50.9323C46.8881 48.3295 48.6279 45.3332 50.7612 44.2424C52.8945 43.1515 54.6343 44.3803 54.6343 46.9831C54.6343 49.5859 52.8945 52.5822 50.7612 53.673ZM50.7612 46.6035C49.6911 47.1489 48.8211 48.647 48.8211 49.9449C48.8211 51.2429 49.6911 51.8573 50.7612 51.3119C51.8313 50.7665 52.7012 49.2684 52.7012 47.9705C52.7012 46.6725 51.8313 46.0581 50.7612 46.6035Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const StateHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="72" height="81" viewBox="0 0 72 81" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M42.4518 2.9386C41.651 2.53126 40.6223 2.5658 39.5038 3.13193L8.6704 18.8454C6.13661 20.1365 4.07922 23.6852 4.07922 26.7644V64.2879C4.07922 66.007 4.71436 67.2152 5.71545 67.733L2.63625 66.1657C1.63517 65.6548 1 64.4397 1 62.7206V25.1972C1 22.1111 3.0574 18.5693 5.59118 17.2783L36.4246 1.5647C37.55 0.991664 38.5787 0.964027 39.3726 1.37137L42.4518 2.9386Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M42.4519 2.9385C43.453 3.4494 44.0882 4.66451 44.0882 6.38361V43.907C44.0882 46.9931 42.0307 50.5349 39.497 51.8259L8.66355 67.5395C7.5382 68.1126 6.50948 68.1402 5.71552 67.7329C4.71443 67.222 4.07928 66.0069 4.07928 64.2878V26.7643C4.07928 23.6782 6.13668 20.1364 8.67046 18.8453L39.5039 3.13184C40.6292 2.5588 41.6579 2.53117 42.4519 2.9385Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M55.9078 8.88293C55.1069 8.47559 54.0782 8.51013 52.9597 9.07626L22.1263 24.7899C19.5925 26.0809 17.5351 29.6296 17.5351 32.7088V70.2322C17.5351 71.9513 18.1703 73.1595 19.1714 73.6773L16.0922 72.1101C15.0911 71.5992 14.4559 70.3841 14.4559 68.665V31.1415C14.4559 28.0554 16.5133 24.5137 19.0471 23.2226L49.8805 7.50903C51.0059 6.936 52.0346 6.90836 52.8286 7.3157L55.9078 8.88293Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M55.9078 8.88284C56.9089 9.39374 57.5441 10.6088 57.5441 12.3279V49.8514C57.5441 52.9375 55.4867 56.4792 52.9529 57.7703L22.1194 73.4839C20.9941 74.0569 19.9654 74.0845 19.1715 73.6772C18.1704 73.1663 17.5352 71.9512 17.5352 70.2321V32.7087C17.5352 29.6226 19.5926 26.0808 22.1264 24.7898L52.9598 9.07617C54.0851 8.50314 55.1138 8.4755 55.9078 8.88284Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M69.3638 14.8343C68.5629 14.427 67.5342 14.4615 66.4158 15.0277L35.5823 30.7413C33.0485 32.0323 30.9911 35.581 30.9911 38.6602V76.1836C30.9911 77.9027 31.6263 79.1109 32.6274 79.6287L29.5482 78.0615C28.5471 77.5506 27.9119 76.3355 27.9119 74.6164V37.093C27.9119 34.0069 29.9693 30.4651 32.5031 29.174L63.3365 13.4605C64.4619 12.8874 65.4906 12.8598 66.2846 13.2671L69.3638 14.8343Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M69.3638 14.8343C70.3649 15.3452 71.0001 16.5603 71.0001 18.2794V55.8028C71.0001 58.8889 68.9427 62.4306 66.4089 63.7217L35.5755 79.4353C34.4501 80.0083 33.4214 80.036 32.6275 79.6286C31.6264 79.1177 30.9912 77.9026 30.9912 76.1835V38.6601C30.9912 35.574 33.0486 32.0322 35.5824 30.7412L66.4158 15.0276C67.5412 14.4546 68.5699 14.4269 69.3638 14.8343Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M51.9864 33.7926C45.2411 37.2308 39.78 46.6686 39.78 54.8706C39.78 63.0726 45.248 66.9389 51.9864 63.5076C58.7316 60.0694 64.1927 50.6316 64.1927 42.4296C64.1927 34.2276 58.7247 30.3613 51.9864 33.7926ZM46.9602 54.4772L44.8131 56.9764C44.1986 55.8994 43.8534 54.484 43.8534 52.7994C43.8534 51.1148 44.1986 49.3405 44.8131 47.6421L46.9602 47.9528C46.5322 49.1403 46.2905 50.3761 46.2905 51.5636C46.2905 52.7511 46.5322 53.7314 46.9602 54.4909V54.4772ZM51.9864 58.5505C50.5986 59.2547 49.2938 59.5032 48.1477 59.3444L49.3007 56.141C50.1015 56.2514 51.006 56.0858 51.9864 55.5887C52.9667 55.0916 53.8781 54.3321 54.672 53.407L55.825 55.4367C54.6858 56.7623 53.3741 57.8532 51.9864 58.5574V58.5505ZM54.672 41.1592C53.8781 41.0488 52.9667 41.2144 51.9864 41.7115C51.006 42.2086 50.1015 42.9681 49.3007 43.8932L48.1477 41.8635C49.2869 40.5379 50.5986 39.447 51.9864 38.7428C53.3741 38.0386 54.6789 37.7901 55.825 37.9488L54.672 41.1523V41.1592ZM59.1734 49.6581L57.0194 49.3543C57.4474 48.1668 57.6891 46.931 57.6891 45.7435C57.6891 44.556 57.4474 43.5688 57.0194 42.8162L59.1734 40.317C59.781 41.394 60.1331 42.8093 60.1331 44.5008C60.1331 46.1854 59.7879 47.9597 59.1734 49.6581Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper";
|
||||
|
||||
export const TemplateHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => (
|
||||
<svg width="81" height="92" viewBox="0 0 81 92" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g opacity="0.2">
|
||||
<path
|
||||
d="M47.8783 2.45609C46.9629 1.98409 45.7871 2.02411 44.5088 2.68011L9.26704 20.8881C6.37099 22.3841 4.01943 26.4961 4.01943 30.0641V73.5441C4.01943 75.5361 4.74542 76.9361 5.88964 77.5361L2.37019 75.72C1.22598 75.128 0.5 73.7201 0.5 71.7281V28.2481C0.5 24.6721 2.85155 20.5681 5.74759 19.0721L40.9893 0.864095C42.2756 0.200096 43.4514 0.168121 44.3588 0.640121L47.8783 2.45609Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.8784 2.45571C49.0226 3.04771 49.7486 4.4557 49.7486 6.4477V49.9277C49.7486 53.5037 47.397 57.6077 44.501 59.1037L9.25923 77.3117C7.97298 77.9757 6.79721 78.0077 5.88973 77.5357C4.74552 76.9437 4.01953 75.5357 4.01953 73.5437V30.0637C4.01953 26.4877 6.37109 22.3837 9.26713 20.8877L44.5089 2.67974C45.7951 2.01574 46.9709 1.98371 47.8784 2.45571Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M63.2587 9.39208C62.3433 8.92008 61.1675 8.96005 59.8891 9.61605L24.6474 27.824C21.7514 29.32 19.3998 33.432 19.3998 37V80.48C19.3998 82.472 20.1258 83.872 21.27 84.472L17.7506 82.656C16.6064 82.064 15.8804 80.656 15.8804 78.664V35.1841C15.8804 31.6081 18.2319 27.504 21.128 26.008L56.3697 7.80004C57.656 7.13604 58.8317 7.10406 59.7392 7.57606L63.2587 9.39208Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M63.2583 9.39247C64.4025 9.98447 65.1285 11.3925 65.1285 13.3845V56.8644C65.1285 60.4404 62.7769 64.5444 59.8809 66.0404L24.6391 84.2484C23.3529 84.9124 22.1771 84.9444 21.2696 84.4724C20.1254 83.8804 19.3994 82.4724 19.3994 80.4804V37.0004C19.3994 33.4244 21.751 29.3204 24.647 27.8244L59.8888 9.61644C61.175 8.95245 62.3508 8.92047 63.2583 9.39247Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M78.6297 16.2803C77.7144 15.8083 76.5386 15.8483 75.2602 16.5043L40.0185 34.7123C37.1225 36.2083 34.7709 40.3203 34.7709 43.8883V87.3683C34.7709 89.3603 35.4969 90.7603 36.6411 91.3603L33.1217 89.5443C31.9774 88.9523 31.2515 87.5443 31.2515 85.5522V42.0723C31.2515 38.4963 33.603 34.3923 36.4991 32.8963L71.7408 14.6883C73.0271 14.0243 74.2028 13.9923 75.1103 14.4643L78.6297 16.2803Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.secondary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M78.6298 16.28C79.7741 16.872 80.5 18.2799 80.5 20.2719V63.7519C80.5 67.3279 78.1485 71.4319 75.2524 72.9279L40.0107 91.1359C38.7244 91.7999 37.5487 91.8319 36.6412 91.3599C35.497 90.7679 34.771 89.3599 34.771 87.3679V43.8879C34.771 40.3119 37.1226 36.2079 40.0186 34.7119L75.2603 16.5039C76.5466 15.8399 77.7224 15.808 78.6298 16.28Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.fill.primary}
|
||||
stroke={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
strokeWidth="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M59.384 41.4397L55.6989 50.5676L63.0298 46.7837L59.384 41.4476V41.4397ZM59.313 38.2556C59.6523 38.0636 59.9838 37.9836 60.2836 38.0236C60.5835 38.0636 60.836 38.2156 61.0175 38.4636L61.0333 38.4797L65.2787 44.6877C65.4681 44.9437 65.5865 45.2956 65.6023 45.7036C65.6259 46.1276 65.547 46.6076 65.3892 47.0796C65.2235 47.5516 64.9788 48.0076 64.6711 48.3996C64.3633 48.7836 64.0161 49.0876 63.661 49.2716L55.1465 53.6716C54.7993 53.8636 54.4521 53.9436 54.1522 53.8956C53.8366 53.8476 53.5683 53.6716 53.3947 53.3756C53.2132 53.0876 53.1264 52.6956 53.1343 52.2476C53.1422 51.7996 53.2526 51.3196 53.442 50.8476L57.7111 40.2797C57.8768 39.8477 58.1057 39.4397 58.3819 39.0877C58.666 38.7197 58.9974 38.4317 59.3367 38.2397L59.313 38.2556Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M47.8789 63.1118C47.8789 61.5438 48.9048 59.7438 50.1752 59.0878L55.92 56.1198C57.1904 55.4638 58.2163 56.1998 58.2163 57.7678V64.8558C58.2163 66.4237 57.1904 68.2238 55.92 68.8798L50.1752 71.8477C48.9048 72.5037 47.8789 71.7677 47.8789 70.1997V63.1118ZM55.92 58.9518L50.1752 61.9198V69.0077L55.92 66.0398V58.9518Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M65.6887 53.9033C64.1026 54.7193 62.8163 56.9753 62.8163 58.9353C62.8163 60.8953 64.1026 61.8153 65.6887 60.9993C67.2748 60.1833 68.5611 57.9273 68.5611 55.9673C68.5611 54.0073 67.2748 53.0873 65.6887 53.9033ZM60.52 60.1193C60.52 56.5993 62.8321 52.5433 65.6887 51.0633C68.5453 49.5913 70.8574 51.2473 70.8574 54.7753C70.8574 58.2953 68.5453 62.3513 65.6887 63.8313C62.8321 65.3033 60.52 63.6473 60.52 60.1193Z"
|
||||
fill={ILLUSTRATION_COLOR_TOKEN_MAP.stroke.secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user