axel/custom components (#98)

* button

* dropdown
This commit is contained in:
Axel Adlercreutz
2025-02-19 10:28:28 +02:00
committed by GitHub
parent ccb75bb6b5
commit 55a6070f50
9 changed files with 310 additions and 35 deletions
+10 -1
View File
@@ -16,7 +16,8 @@
"pg": "^8.13.1",
"qs": "^6.14.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -9034,6 +9035,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+2 -1
View File
@@ -19,7 +19,8 @@
"pg": "^8.13.1",
"qs": "^6.14.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+33 -33
View File
@@ -10,12 +10,12 @@
/* foreground colors (text, icons, etc.) */
--fg1: hsl(240, 40%, 5%);
--fg2: hsl(240, 40%, 9%);
--fg3: hsl(240, 40%, 13%);
--fg4: hsl(240, 40%, 17%);
--fg5: hsl(240, 40%, 21%);
--fg6: hsl(240, 40%, 25%);
--fg7: hsl(240, 40%, 29%);
--fg2: hsl(240, 40%, 11%);
--fg3: hsl(240, 40%, 17%);
--fg4: hsl(240, 40%, 23%);
--fg5: hsl(240, 40%, 29%);
--fg6: hsl(240, 40%, 35%);
--fg7: hsl(240, 40%, 41%);
--tertiary1: hsl(351, 90%, 90%);
--tertiary2: hsl(351, 90%, 80%);
@@ -68,9 +68,9 @@
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-2xl: 32px;
--radius-lg: 10px;
--radius-xl: 16px;
--radius-2xl: 24px;
}
@media (prefers-color-scheme: dark) {
@@ -85,32 +85,32 @@
--bg7: hsl(240, 40%, 29%);
--fg1: hsl(240, 40%, 98%);
--fg2: hsl(240, 40%, 95%);
--fg3: hsl(240, 40%, 92%);
--fg4: hsl(240, 40%, 89%);
--fg5: hsl(240, 40%, 86%);
--fg6: hsl(240, 40%, 83%);
--fg7: hsl(240, 40%, 80%);
--fg2: hsl(240, 40%, 92%);
--fg3: hsl(240, 40%, 86%);
--fg4: hsl(240, 40%, 80%);
--fg5: hsl(240, 40%, 74%);
--fg6: hsl(240, 40%, 68%);
--fg7: hsl(240, 40%, 62%);
--tertiary1: hsl(351, 90%, 30%);
--tertiary2: hsl(351, 90%, 40%);
--tertiary3: hsl(351, 90%, 50%);
--tertiary4: hsl(351, 90%, 60%);
--tertiary5: hsl(351, 90%, 70%);
--tertiary6: hsl(351, 90%, 80%);
--tertiary7: hsl(351, 90%, 85%);
--tertiary8: hsl(351, 90%, 90%);
--tertiary9: hsl(351, 90%, 95%);
--tertiary1: hsl(351, 40%, 30%);
--tertiary2: hsl(351, 40%, 40%);
--tertiary3: hsl(351, 40%, 50%);
--tertiary4: hsl(351, 40%, 60%);
--tertiary5: hsl(351, 40%, 70%);
--tertiary6: hsl(351, 40%, 80%);
--tertiary7: hsl(351, 40%, 85%);
--tertiary8: hsl(351, 40%, 90%);
--tertiary9: hsl(351, 40%, 95%);
--secondary1: hsl(249, 90%, 30%);
--secondary2: hsl(249, 90%, 40%);
--secondary3: hsl(249, 90%, 50%);
--secondary4: hsl(249, 90%, 60%);
--secondary5: hsl(249, 90%, 70%);
--secondary6: hsl(249, 90%, 80%);
--secondary7: hsl(249, 90%, 85%);
--secondary8: hsl(249, 90%, 90%);
--secondary9: hsl(249, 90%, 95%);
--secondary1: hsl(249, 60%, 30%);
--secondary2: hsl(249, 60%, 40%);
--secondary3: hsl(249, 60%, 50%);
--secondary4: hsl(249, 60%, 60%);
--secondary5: hsl(249, 60%, 70%);
--secondary6: hsl(249, 60%, 80%);
--secondary7: hsl(249, 60%, 85%);
--secondary8: hsl(249, 60%, 90%);
--secondary9: hsl(249, 60%, 95%);
--primary1: hsl(161, 35%, 30%);
--primary2: hsl(161, 35%, 40%);
+37
View File
@@ -3,6 +3,8 @@ import Image from "next/image";
import styles from "./page.module.css";
import useQuery from "@/shared/hooks/useQuery";
import { useState } from "react";
import { Button } from "@/components/ui/button/button";
import { Dropdown } from "@/components/ui/dropdown/dropdown";
export default function Home() {
const [mode, setMode] = useState("normal");
@@ -11,6 +13,41 @@ export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<Button>Default</Button>
<Button variant="primary" size="sm">
Primary
</Button>
<Button variant="secondary" size="md" width="fit">
Secondary
</Button>
<Button variant="tertiary" size="lg" width="full">
Tertiary
</Button>
<Button variant="none" size="xl">
None
</Button>
<Button variant="outline" size="sm" width="fit">
Outline
</Button>
<Dropdown
options={[
{
label: "Option 1",
value: "option1",
},
{
label: "Option 2",
value: "option2",
},
{
label: "Option 3",
value: "option3",
disabled: true,
},
]}
>
<Button>Dropdown</Button>
</Dropdown>
<Image
className={styles.logo}
src="/next.svg"
@@ -0,0 +1,26 @@
"use client";
import styles from "./button.module.css";
export const Button = ({
children,
variant = "primary",
size = "md",
width,
className,
...props
}) => {
return (
<button
{...props}
className={[
styles.baseButton,
styles[variant],
styles[size],
styles[width],
className,
].join(" ")}
>
{children}
</button>
);
};
@@ -0,0 +1,111 @@
.baseButton {
background-color: var(--bg1);
color: var(--fg1);
border: 1px solid var(--fg1);
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
transition:
background-color 0.1s ease-in-out,
border-color 0.1s ease-in-out,
transform 0.1s ease-in-out,
opacity 0.1s ease-in-out;
&:focus {
outline: none;
}
&:active {
transform: scale(0.99);
opacity: 0.9;
}
}
.baseButton.sm {
padding: 0.25rem 0.5rem;
width: 8rem;
border-radius: var(--radius-sm);
font-size: var(--font-sm);
}
.baseButton.md {
padding: 0.5rem 1rem;
width: 12rem;
font-size: var(--font-md);
border-radius: var(--radius-md);
}
.baseButton.lg {
padding: 0.75rem 1.5rem;
width: 16rem;
font-size: var(--font-lg);
border-radius: var(--radius-md);
}
.baseButton.xl {
padding: 1rem 2rem;
width: 20rem;
font-size: var(--font-xl);
border-radius: var(--radius-lg);
}
.baseButton.full {
width: 100%;
}
.baseButton.fit {
width: fit-content;
}
.baseButton.primary {
background-color: var(--primary1);
color: var(--fg1);
border: 1px solid var(--primary5);
&:hover {
background-color: var(--primary2);
border: 1px solid var(--primary6);
}
}
.baseButton.secondary {
background-color: var(--secondary1);
color: var(--fg1);
border: 1px solid var(--secondary5);
&:hover {
background-color: var(--secondary2);
border: 1px solid var(--secondary6);
}
}
.baseButton.tertiary {
background-color: var(--tertiary1);
color: var(--fg1);
border: 1px solid var(--tertiary5);
&:hover {
background-color: var(--tertiary2);
border: 1px solid var(--tertiary6);
}
}
.baseButton.none {
background-color: transparent;
color: var(--fg1);
border: none;
&:hover {
text-decoration: underline;
}
}
.baseButton.outline {
background-color: transparent;
color: var(--fg1);
border: 1px solid var(--fg7);
&:hover {
background-color: var(--bg2);
border: 1px solid var(--fg2);
}
}
@@ -0,0 +1,54 @@
"use client";
import styles from "./dropdown.module.css";
import { DropdownMenu as _DropdownMenu } from "radix-ui";
import { z } from "zod";
import { useSchema } from "@/shared/hooks/useSchema";
const optionSchema = z.array(
z.object({
label: z.string(),
value: z.string(),
disabled: z.boolean().optional(),
})
);
export const Dropdown = ({
children,
options,
onSelect,
contentProps,
...props
}) => {
const validatedOptions = useSchema(options, optionSchema);
const handleSelect = (value) => {
onSelect?.(value);
};
const _contentProps = {
sideOffset: 8,
...contentProps,
};
const renderOptions = () => {
return validatedOptions.map((option) => (
<_DropdownMenu.Item
key={option.value}
className={styles.item}
onClick={() => handleSelect(option.value)}
>
{option.label}
</_DropdownMenu.Item>
));
};
return (
<_DropdownMenu.Root {...props}>
<_DropdownMenu.Trigger asChild>{children}</_DropdownMenu.Trigger>
<_DropdownMenu.Portal className={styles.portal}>
<_DropdownMenu.Content className={styles.content} {..._contentProps}>
{renderOptions()}
</_DropdownMenu.Content>
</_DropdownMenu.Portal>
</_DropdownMenu.Root>
);
};
@@ -0,0 +1,32 @@
.content {
background-color: var(--bg1);
border-radius: var(--radius-md);
padding: var(--u-xs);
display: flex;
flex-direction: column;
min-width: 12rem;
border: 1px solid var(--fg5);
box-shadow: 0 0 4 4px #000000dd;
&:focus-within {
outline: none;
}
&:focus {
outline: none;
}
}
.item {
cursor: pointer;
padding: 4px 6px;
border-radius: var(--radius-sm);
&:hover {
background-color: var(--bg3);
}
&:focus {
outline: none;
}
}
.portal {
z-index: 1000;
}
+5
View File
@@ -0,0 +1,5 @@
import { z } from "zod";
import { useMemo } from "react";
export const useSchema = (value, schema) => {
return useMemo(() => schema.parse(value), [value, schema]);
};