Merge pull request 'ui: Modal component' (#4241) from feat/modal into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4241
This commit is contained in:
16
pkgs/clan-app/ui/package-lock.json
generated
16
pkgs/clan-app/ui/package-lock.json
generated
@@ -48,7 +48,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"playwright": "~1.53.2",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -6189,13 +6189,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"version": "1.53.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
|
||||
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
"playwright-core": "1.53.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -6208,9 +6208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"version": "1.53.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
|
||||
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"playwright": "~1.53.2",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
@@ -40,7 +40,7 @@ export type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
legend: "Signup",
|
||||
fields: (props: FieldProps) => (
|
||||
children: (props: FieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ export const Error: Story = {
|
||||
args: {
|
||||
legend: "Signup",
|
||||
error: "You must enter a First Name",
|
||||
fields: (props: FieldProps) => (
|
||||
children: (props: FieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
import "./Fieldset.css";
|
||||
import { JSX } from "solid-js";
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { FieldProps } from "./Field";
|
||||
|
||||
export interface FieldsetProps extends FieldProps {
|
||||
legend: string;
|
||||
disabled: boolean;
|
||||
export type FieldsetFieldProps = Pick<
|
||||
FieldProps,
|
||||
"orientation" | "inverted"
|
||||
> & {
|
||||
error?: string;
|
||||
fields: (props: FieldProps) => JSX.Element;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export interface FieldsetProps
|
||||
extends Pick<FieldProps, "orientation" | "inverted"> {
|
||||
legend?: string;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
children: (props: FieldsetFieldProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export const Fieldset = (props: FieldsetProps) => {
|
||||
const orientation = () => props.orientation || "vertical";
|
||||
|
||||
const [fieldProps] = splitProps(props, [
|
||||
"orientation",
|
||||
"inverted",
|
||||
"disabled",
|
||||
"error",
|
||||
]);
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
role="group"
|
||||
class={cx(orientation(), { inverted: props.inverted })}
|
||||
disabled={props.disabled}
|
||||
class={cx({ inverted: props.inverted })}
|
||||
disabled={props.disabled || false}
|
||||
>
|
||||
<legend>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="default"
|
||||
weight="normal"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.legend}
|
||||
</Typography>
|
||||
</legend>
|
||||
<div class="fields">
|
||||
{props.fields({ ...props, orientation: orientation() })}
|
||||
</div>
|
||||
{props.legend && (
|
||||
<legend>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="default"
|
||||
weight="normal"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.legend}
|
||||
</Typography>
|
||||
</legend>
|
||||
)}
|
||||
<div class="fields">{props.children(fieldProps)}</div>
|
||||
{props.error && (
|
||||
<div class="error" role="alert">
|
||||
<Typography
|
||||
|
||||
24
pkgs/clan-app/ui/src/components/v2/Modal/Modal.css
Normal file
24
pkgs/clan-app/ui/src/components/v2/Modal/Modal.css
Normal file
@@ -0,0 +1,24 @@
|
||||
div.modal-content {
|
||||
@apply max-w-[512px];
|
||||
@apply rounded-md;
|
||||
|
||||
/* todo replace with a theme() color */
|
||||
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-center;
|
||||
@apply w-full px-2 py-1.5;
|
||||
@apply bg-def-3;
|
||||
@apply border border-def-2 rounded-tl-md rounded-tr-md;
|
||||
@apply border-b-def-3;
|
||||
|
||||
& > .title {
|
||||
@apply mx-auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.body {
|
||||
@apply p-6 bg-def-1;
|
||||
@apply border border-def-2 rounded-bl-md rounded-br-md;
|
||||
}
|
||||
}
|
||||
74
pkgs/clan-app/ui/src/components/v2/Modal/Modal.stories.tsx
Normal file
74
pkgs/clan-app/ui/src/components/v2/Modal/Modal.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TagProps } from "@/src/components/v2/Tag/Tag";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
Modal,
|
||||
ModalContext,
|
||||
ModalProps,
|
||||
} from "@/src/components/v2/Modal/Modal";
|
||||
import { Fieldset } from "@/src/components/v2/Form/Fieldset";
|
||||
import { TextInput } from "@/src/components/v2/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/v2/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
const meta: Meta<ModalProps> = {
|
||||
title: "Components/Modal",
|
||||
component: Modal,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TagProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Example Modal",
|
||||
onClose: fn(),
|
||||
children: ({ close }: ModalContext) => (
|
||||
<form class="flex flex-col gap-5">
|
||||
<Fieldset legend="General">
|
||||
{(props) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
label="First Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ placeholder: "Ron" }}
|
||||
/>
|
||||
<TextInput
|
||||
{...props}
|
||||
label="Last Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ placeholder: "Burgundy" }}
|
||||
/>
|
||||
<TextArea
|
||||
{...props}
|
||||
label="Bio"
|
||||
size="s"
|
||||
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
||||
/>
|
||||
<Checkbox
|
||||
{...props}
|
||||
size="s"
|
||||
label="Accept Terms"
|
||||
required={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<div class="flex w-full items-center justify-end gap-4">
|
||||
<Button size="s" hierarchy="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
};
|
||||
40
pkgs/clan-app/ui/src/components/v2/Modal/Modal.tsx
Normal file
40
pkgs/clan-app/ui/src/components/v2/Modal/Modal.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createSignal, JSX } from "solid-js";
|
||||
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
||||
import "./Modal.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
|
||||
export interface ModalContext {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: (ctx: ModalContext) => JSX.Element;
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const [open, setOpen] = createSignal(true);
|
||||
|
||||
return (
|
||||
<KDialog id={props.id} open={open()} modal={true}>
|
||||
<KDialog.Portal>
|
||||
<KDialog.Content class="modal-content">
|
||||
<div class="header">
|
||||
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
</div>
|
||||
<div class="body">
|
||||
{props.children({ close: () => setOpen(false) })}
|
||||
</div>
|
||||
</KDialog.Content>
|
||||
</KDialog.Portal>
|
||||
</KDialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user