فهرست منبع

feat(component-library): add first round of form components

Connor Prussin 1 سال پیش
والد
کامیت
2ac2b4f1e3

+ 4 - 4
packages/component-library/.storybook/preview.tsx

@@ -38,10 +38,10 @@ export const decorators: Decorator[] = [
   withRootClasses("font-sans antialiased", sans.variable),
   withThemeByClassName({
     themes: {
-      white: "light bg-white",
-      light: "light bg-beige-50",
-      dark: "dark bg-steel-800",
-      darker: "dark bg-steel-900",
+      white: "light bg-white text-steel-900",
+      light: "light bg-beige-100 text-steel-900",
+      dark: "dark bg-steel-800 text-steel-50",
+      darker: "dark bg-steel-900 text-steel-50",
     },
     defaultTheme: "light",
   }),

+ 37 - 0
packages/component-library/src/Checkbox/index.stories.tsx

@@ -0,0 +1,37 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Checkbox as CheckboxComponent } from "./index.js";
+
+const meta = {
+  component: CheckboxComponent,
+  argTypes: {
+    children: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    isDisabled: {
+      control: "boolean",
+      table: {
+        category: "State",
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <div className="max-w-sm">
+        <Story />
+      </div>
+    ),
+  ],
+} satisfies Meta<typeof CheckboxComponent>;
+export default meta;
+
+export const Checkbox = {
+  args: {
+    children:
+      "By clicking here you agree that this is a checkbox and it's super duper checkboxy",
+    isDisabled: false,
+  },
+} satisfies StoryObj<typeof CheckboxComponent>;

+ 42 - 0
packages/component-library/src/Checkbox/index.tsx

@@ -0,0 +1,42 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import { Checkbox as BaseCheckbox } from "react-aria-components";
+
+export const Checkbox = ({
+  children,
+  className,
+  ...props
+}: ComponentProps<typeof BaseCheckbox>) => (
+  <BaseCheckbox
+    className={clsx(
+      "group/checkbox inline-flex cursor-pointer flex-row gap-2 py-1 text-sm data-[disabled]:cursor-not-allowed",
+      className,
+    )}
+    {...props}
+  >
+    {(args) => (
+      <>
+        <div className="relative top-[0.0625rem] mx-1 size-4 flex-none">
+          <div className="size-full rounded border border-stone-300 bg-white outline-4 outline-violet-500/40 transition duration-100 group-data-[hovered]/checkbox:border-2 group-data-[disabled]/checkbox:border-none group-data-[hovered]/checkbox:border-stone-400 group-data-[pressed]/checkbox:border-stone-500 group-data-[disabled]/checkbox:bg-stone-200 group-data-[focus-visible]/checkbox:outline dark:border-steel-700 dark:bg-steel-800 dark:group-data-[hovered]/checkbox:border-steel-600 dark:group-data-[pressed]/checkbox:border-steel-500 dark:group-data-[disabled]/checkbox:bg-steel-600" />
+          <div className="absolute inset-0 grid place-content-center rounded bg-violet-500 stroke-white p-1 opacity-0 transition duration-100 group-data-[disabled]/checkbox:bg-transparent group-data-[disabled]/checkbox:stroke-stone-400 group-data-[selected]/checkbox:opacity-100 dark:bg-violet-600 dark:stroke-steel-950 dark:group-data-[disabled]/checkbox:stroke-steel-400">
+            <svg
+              className="w-full"
+              viewBox="0 0 8 6"
+              fill="none"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <path
+                d="M1 3L2.76471 5L7 1"
+                strokeWidth="2"
+                strokeLinecap="round"
+                strokeLinejoin="round"
+              />
+            </svg>
+          </div>
+          <div className="pointer-events-none absolute -inset-1.5 -z-10 rounded-full bg-black/20 opacity-0 transition duration-100 group-data-[focus-visible]/checkbox:opacity-0 group-data-[hovered]/checkbox:opacity-50 group-data-[pressed]/checkbox:opacity-100 dark:bg-white/20" />
+        </div>
+        {typeof children === "function" ? children(args) : children}
+      </>
+    )}
+  </BaseCheckbox>
+);

+ 66 - 0
packages/component-library/src/CheckboxGroup/index.stories.tsx

@@ -0,0 +1,66 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import {
+  ORIENTATIONS,
+  CheckboxGroup as CheckboxGroupComponent,
+} from "./index.js";
+import { Checkbox } from "../Checkbox/index.js";
+
+const meta = {
+  component: CheckboxGroupComponent,
+  argTypes: {
+    label: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    description: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    isDisabled: {
+      control: "boolean",
+      table: {
+        category: "State",
+      },
+    },
+    orientation: {
+      control: "inline-radio",
+      options: ORIENTATIONS,
+      table: {
+        category: "Layout",
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <div className="max-w-sm">
+        <Story />
+      </div>
+    ),
+  ],
+  render: (args) => (
+    <CheckboxGroupComponent {...args}>
+      <Checkbox value="one">
+        {
+          "By clicking here you agree that this is a checkbox and it's super duper checkboxy"
+        }
+      </Checkbox>
+      <Checkbox value="two">Second</Checkbox>
+      <Checkbox value="three">Third</Checkbox>
+    </CheckboxGroupComponent>
+  ),
+} satisfies Meta<typeof CheckboxGroupComponent>;
+export default meta;
+
+export const CheckboxGroup = {
+  args: {
+    label: "This is a checkbox group!",
+    description: "",
+    isDisabled: false,
+    orientation: "vertical",
+  },
+} satisfies StoryObj<typeof CheckboxGroupComponent>;

+ 44 - 0
packages/component-library/src/CheckboxGroup/index.tsx

@@ -0,0 +1,44 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import {
+  CheckboxGroup as BaseCheckboxGroup,
+  Label,
+  Text,
+} from "react-aria-components";
+
+export const ORIENTATIONS = ["vertical", "horizontal"] as const;
+
+type CheckboxGroupProps = ComponentProps<typeof BaseCheckboxGroup> & {
+  label: ComponentProps<typeof Label>["children"];
+  description?: ComponentProps<typeof Text>["children"] | undefined;
+  orientation?: (typeof ORIENTATIONS)[number] | undefined;
+};
+
+export const CheckboxGroup = ({
+  children,
+  className,
+  label,
+  description,
+  orientation = "vertical",
+  ...props
+}: CheckboxGroupProps) => (
+  <BaseCheckboxGroup
+    data-orientation={orientation}
+    className={clsx("group/checkbox-group", className)}
+    {...props}
+  >
+    {(args) => (
+      <>
+        <Label className="mb-1 text-sm font-medium">{label}</Label>
+        <div className="flex group-data-[orientation=horizontal]/checkbox-group:flex-row group-data-[orientation=vertical]/checkbox-group:flex-col group-data-[orientation=horizontal]/checkbox-group:gap-6">
+          {typeof children === "function" ? children(args) : children}
+        </div>
+        {description && description !== "" && (
+          <Text slot="description" className="text-xs font-light">
+            {description}
+          </Text>
+        )}
+      </>
+    )}
+  </BaseCheckboxGroup>
+);

+ 43 - 0
packages/component-library/src/Link/index.stories.tsx

@@ -0,0 +1,43 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Link as LinkComponent } from "./index.js";
+
+const meta = {
+  component: LinkComponent,
+  argTypes: {
+    children: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    href: {
+      control: "text",
+      table: {
+        category: "Link",
+      },
+    },
+    target: {
+      control: "text",
+      table: {
+        category: "Link",
+      },
+    },
+    isDisabled: {
+      control: "boolean",
+      table: {
+        category: "State",
+      },
+    },
+  },
+} satisfies Meta<typeof LinkComponent>;
+export default meta;
+
+export const Link = {
+  args: {
+    children: "Link",
+    href: "https://www.pyth.network",
+    target: "_blank",
+    isDisabled: false,
+  },
+} satisfies StoryObj<typeof LinkComponent>;

+ 16 - 0
packages/component-library/src/Link/index.tsx

@@ -0,0 +1,16 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import { Link as BaseLink } from "react-aria-components";
+
+export const Link = ({
+  className,
+  ...props
+}: ComponentProps<typeof BaseLink>) => (
+  <BaseLink
+    className={clsx(
+      "underline outline-0 outline-offset-4 outline-inherit data-[disabled]:cursor-not-allowed data-[disabled]:text-stone-400 data-[disabled]:no-underline data-[focus-visible]:outline-2 hover:no-underline dark:data-[disabled]:text-steel-400",
+      className,
+    )}
+    {...props}
+  />
+);

+ 38 - 0
packages/component-library/src/Radio/index.stories.tsx

@@ -0,0 +1,38 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { RadioGroup } from "react-aria-components";
+
+import { Radio as RadioComponent } from "./index.js";
+
+const meta = {
+  component: RadioComponent,
+  argTypes: {
+    children: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    isDisabled: {
+      control: "boolean",
+      table: {
+        category: "State",
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <RadioGroup className="max-w-sm">
+        <Story />
+      </RadioGroup>
+    ),
+  ],
+} satisfies Meta<typeof RadioComponent>;
+export default meta;
+
+export const Radio = {
+  args: {
+    children:
+      "This is a radio button, check out how radioish it is and how it handles multiline labels",
+    isDisabled: false,
+  },
+} satisfies StoryObj<typeof RadioComponent>;

+ 28 - 0
packages/component-library/src/Radio/index.tsx

@@ -0,0 +1,28 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import { Radio as BaseRadio } from "react-aria-components";
+
+export const Radio = ({
+  children,
+  className,
+  ...props
+}: ComponentProps<typeof BaseRadio>) => (
+  <BaseRadio
+    className={clsx(
+      "group/radio inline-flex cursor-pointer flex-row gap-2 py-1 text-sm data-[disabled]:cursor-not-allowed data-[selected]:cursor-default",
+      className,
+    )}
+    {...props}
+  >
+    {(args) => (
+      <>
+        <div className="relative top-[0.0625rem] mx-1 size-4 flex-none">
+          <div className="size-full rounded-full border border-stone-300 bg-white outline-4 outline-violet-500/40 transition duration-100 group-data-[hovered]/radio:border-2 group-data-[disabled]/radio:border-none group-data-[hovered]/radio:border-stone-400 group-data-[pressed]/radio:border-stone-500 group-data-[disabled]/radio:bg-stone-200 group-data-[focus-visible]/radio:outline dark:border-steel-700 dark:bg-steel-800 dark:group-data-[hovered]/radio:border-steel-600 dark:group-data-[pressed]/radio:border-steel-500 dark:group-data-[disabled]/radio:bg-steel-600" />
+          <div className="absolute inset-0 rounded-full border-[0.3rem] border-violet-500 bg-white opacity-0 transition duration-100 group-data-[disabled]/radio:border-transparent group-data-[disabled]/radio:bg-stone-400 group-data-[selected]/radio:opacity-100 dark:border-violet-600 dark:bg-steel-950 dark:group-data-[disabled]/radio:bg-steel-400" />
+          <div className="pointer-events-none absolute -inset-1.5 -z-10 rounded-full bg-black/20 opacity-0 transition duration-100 group-data-[focus-visible]/radio:opacity-0 group-data-[hovered]/radio:opacity-50 group-data-[pressed]/radio:opacity-100 group-data-[selected]/radio:opacity-0 dark:bg-white/20" />
+        </div>
+        {typeof children === "function" ? children(args) : children}
+      </>
+    )}
+  </BaseRadio>
+);

+ 62 - 0
packages/component-library/src/RadioGroup/index.stories.tsx

@@ -0,0 +1,62 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { RadioGroup as RadioGroupComponent } from "./index.js";
+import { Radio } from "../Radio/index.js";
+
+const meta = {
+  component: RadioGroupComponent,
+  argTypes: {
+    label: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    description: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    isDisabled: {
+      control: "boolean",
+      table: {
+        category: "State",
+      },
+    },
+    orientation: {
+      control: "inline-radio",
+      options: ["vertical", "horizontal"],
+      table: {
+        category: "Layout",
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <div className="max-w-sm">
+        <Story />
+      </div>
+    ),
+  ],
+  render: (args) => (
+    <RadioGroupComponent {...args}>
+      <Radio value="one">
+        This is a radio button, check out how radioish it is and how it handles
+        multiline labels
+      </Radio>
+      <Radio value="two">Second</Radio>
+      <Radio value="three">Third</Radio>
+    </RadioGroupComponent>
+  ),
+} satisfies Meta<typeof RadioGroupComponent>;
+export default meta;
+
+export const RadioGroup = {
+  args: {
+    label: "This is a radio group!",
+    description: "",
+    isDisabled: false,
+    orientation: "vertical",
+  },
+} satisfies StoryObj<typeof RadioGroupComponent>;

+ 36 - 0
packages/component-library/src/RadioGroup/index.tsx

@@ -0,0 +1,36 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import {
+  RadioGroup as BaseRadioGroup,
+  Label,
+  Text,
+} from "react-aria-components";
+
+type CheckboxGroupProps = ComponentProps<typeof BaseRadioGroup> & {
+  label: ComponentProps<typeof Label>["children"];
+  description?: ComponentProps<typeof Text>["children"] | undefined;
+};
+
+export const RadioGroup = ({
+  children,
+  className,
+  label,
+  description,
+  ...props
+}: CheckboxGroupProps) => (
+  <BaseRadioGroup className={clsx("group/radio-group", className)} {...props}>
+    {(args) => (
+      <>
+        <Label className="mb-1 text-sm font-medium">{label}</Label>
+        <div className="flex group-data-[orientation=horizontal]/radio-group:flex-row group-data-[orientation=vertical]/radio-group:flex-col group-data-[orientation=horizontal]/radio-group:gap-6">
+          {typeof children === "function" ? children(args) : children}
+        </div>
+        {description && description !== "" && (
+          <Text slot="description" className="text-xs font-light">
+            {description}
+          </Text>
+        )}
+      </>
+    )}
+  </BaseRadioGroup>
+);