feat(ui): flatten the Field pattern and introduce Orienter component
This commit is contained in:
22
pkgs/clan-app/ui/package-lock.json
generated
22
pkgs/clan-app/ui/package-lock.json
generated
@@ -43,9 +43,11 @@
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"extend": "^3.0.2",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
@@ -4529,6 +4531,13 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5684,6 +5693,19 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-to-jsx": {
|
||||
"version": "7.7.10",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.10.tgz",
|
||||
"integrity": "sha512-au62yyLyJukhC2P1TYi3uBi/RScGYai69uT72D8a048QH8rRj+yhND3C21GdZHE+6emtsf6Yqemcf//K+EIWDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -43,9 +43,11 @@
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"extend": "^3.0.2",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
|
||||
@@ -144,7 +144,7 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<ButtonProps>;
|
||||
|
||||
const timeout = process.env.NODE_ENV === "test" ? 100 : 2000;
|
||||
const timeout = process.env.NODE_ENV === "test" ? 500 : 2000;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
@@ -158,12 +158,6 @@ export const Primary: Story = {
|
||||
}
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
test: {
|
||||
// increase test timeout to allow for the loading action
|
||||
mockTimers: true,
|
||||
},
|
||||
},
|
||||
|
||||
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
|
||||
const buttons = await canvas.findAllByRole("button");
|
||||
@@ -195,7 +189,8 @@ export const Primary: Story = {
|
||||
await userEvent.click(button);
|
||||
|
||||
// check the button has changed
|
||||
await waitFor(async () => {
|
||||
await waitFor(
|
||||
async () => {
|
||||
// the action handler should have been called
|
||||
await expect(args.onAction).toHaveBeenCalled();
|
||||
// the button should have a loading class
|
||||
@@ -204,7 +199,9 @@ export const Primary: Story = {
|
||||
await expect(loader.clientWidth).toBeGreaterThan(0);
|
||||
// the pointer should have changed to wait
|
||||
await expect(getCursorStyle(button)).toEqual("wait");
|
||||
});
|
||||
},
|
||||
{ timeout: timeout + 500 },
|
||||
);
|
||||
|
||||
// wait for the action handler to finish
|
||||
await waitFor(
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import "./Divider.css";
|
||||
import cx from "classnames";
|
||||
import { Orientation } from "@/src/components/v2/shared";
|
||||
|
||||
export interface DividerProps {
|
||||
inverted?: boolean;
|
||||
orientation?: Orientation;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export const Divider = (props: DividerProps) => {
|
||||
const inverted = props.inverted || false;
|
||||
const orientation = props.orientation || "horizontal";
|
||||
const orientation = () => props.orientation || "horizontal";
|
||||
|
||||
return <div class={cx("divider", orientation, { inverted: inverted })} />;
|
||||
return <div class={cx("divider", orientation(), { inverted: inverted })} />;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
@import "./Field.css";
|
||||
|
||||
div.form-field {
|
||||
&.checkbox {
|
||||
@apply items-start;
|
||||
|
||||
& > div.checkbox-control {
|
||||
& div.checkbox-control {
|
||||
@apply w-5 h-5 rounded-sm bg-def-1 border border-inv-1 p-[0.0625rem];
|
||||
|
||||
&:hover {
|
||||
@@ -21,13 +19,9 @@ div.form-field {
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal.checkbox {
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
&.inverted {
|
||||
&.checkbox {
|
||||
& > div.checkbox-control {
|
||||
& div.checkbox-control {
|
||||
@apply bg-inv-1;
|
||||
|
||||
&:hover,
|
||||
@@ -43,7 +37,7 @@ div.form-field {
|
||||
}
|
||||
|
||||
&.s {
|
||||
& > div.checkbox-control {
|
||||
& div.checkbox-control {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Label } from "./Label";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import "./Checkbox.css";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
|
||||
export type CheckboxProps = FieldProps &
|
||||
KCheckboxRootProps & {
|
||||
@@ -24,6 +25,7 @@ export const Checkbox = (props: CheckboxProps) => (
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={KCheckbox.Label}
|
||||
descriptionComponent={KCheckbox.Description}
|
||||
@@ -40,5 +42,6 @@ export const Checkbox = (props: CheckboxProps) => (
|
||||
/>
|
||||
</KCheckbox.Indicator>
|
||||
</KCheckbox.Control>
|
||||
</Orienter>
|
||||
</KCheckbox>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
@import "Field.css";
|
||||
|
||||
div.form-field.combobox {
|
||||
& > div.control {
|
||||
div.control {
|
||||
@apply flex flex-col w-full gap-2;
|
||||
|
||||
& > div.selected-options {
|
||||
div.selected-options {
|
||||
@apply flex flex-wrap gap-1 w-full min-h-5;
|
||||
}
|
||||
|
||||
& > div.input-container {
|
||||
div.input-container {
|
||||
@apply relative left-0 top-0;
|
||||
@apply inline-flex justify-between w-full;
|
||||
|
||||
@@ -68,13 +66,13 @@ div.form-field.combobox {
|
||||
&.horizontal {
|
||||
@apply flex-row gap-2 justify-between;
|
||||
|
||||
& > div.control {
|
||||
div.control {
|
||||
@apply w-1/2 grow;
|
||||
}
|
||||
}
|
||||
|
||||
&.s {
|
||||
& > div.control > div.input-container {
|
||||
div.control > div.input-container {
|
||||
& > input {
|
||||
@apply px-1.5 py-1;
|
||||
font-size: 0.75rem;
|
||||
@@ -87,7 +85,7 @@ div.form-field.combobox {
|
||||
}
|
||||
|
||||
&.inverted {
|
||||
& > div.control > div.input-container {
|
||||
div.control > div.input-container {
|
||||
& > button.trigger {
|
||||
@apply bg-inv-2;
|
||||
}
|
||||
@@ -118,7 +116,7 @@ div.form-field.combobox {
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
& > div.control > div.input-container {
|
||||
div.control > div.input-container {
|
||||
& > input {
|
||||
@apply outline-none;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CollectionNode } from "@kobalte/core";
|
||||
import { Label } from "./Label";
|
||||
import cx from "classnames";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Accessor, Component, For, Show, splitProps } from "solid-js";
|
||||
import { Tag } from "@/src/components/v2/Tag/Tag";
|
||||
@@ -100,6 +101,8 @@ export const Combobox = <Option, OptGroup = never>(
|
||||
const itemControl = () => props.itemControl || DefaultItemControl;
|
||||
const itemComponent = () => props.itemComponent || DefaultItemComponent;
|
||||
|
||||
const align = () => (props.orientation === "horizontal" ? "start" : "center");
|
||||
|
||||
return (
|
||||
<KCombobox
|
||||
class={cx("form-field", "combobox", props.size, props.orientation, {
|
||||
@@ -109,6 +112,7 @@ export const Combobox = <Option, OptGroup = never>(
|
||||
{...props}
|
||||
itemComponent={itemComponent()}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={align()}>
|
||||
<Label
|
||||
labelComponent={KCombobox.Label}
|
||||
descriptionComponent={KCombobox.Description}
|
||||
@@ -132,6 +136,7 @@ export const Combobox = <Option, OptGroup = never>(
|
||||
<KCombobox.Listbox class="listbox" />
|
||||
</KCombobox.Content>
|
||||
</KCombobox.Portal>
|
||||
</Orienter>
|
||||
</KCombobox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
div.form-field {
|
||||
@apply flex flex-col gap-2 items-center w-full;
|
||||
|
||||
&.horizontal {
|
||||
@apply flex-row gap-2 justify-between;
|
||||
|
||||
& > div.form-label {
|
||||
@apply w-1/2 shrink;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Size } from "@/src/components/v2/Form/Label";
|
||||
import { Orientation } from "@/src/components/v2/shared";
|
||||
|
||||
export interface FieldProps {
|
||||
class?: string;
|
||||
label?: string;
|
||||
@@ -8,7 +5,7 @@ export interface FieldProps {
|
||||
tooltip?: string;
|
||||
ghost?: boolean;
|
||||
|
||||
size?: Size;
|
||||
orientation?: Orientation;
|
||||
size?: "default" | "s";
|
||||
orientation?: "horizontal" | "vertical";
|
||||
inverted?: boolean;
|
||||
}
|
||||
|
||||
22
pkgs/clan-app/ui/src/components/v2/Form/Orienter.css
Normal file
22
pkgs/clan-app/ui/src/components/v2/Form/Orienter.css
Normal file
@@ -0,0 +1,22 @@
|
||||
div.orienter {
|
||||
@apply flex flex-col gap-2 w-full;
|
||||
|
||||
&.vertical {
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
@apply flex-row gap-2 justify-between;
|
||||
|
||||
& > div.form-label {
|
||||
@apply w-1/2 shrink;
|
||||
}
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
&.align-start {
|
||||
@apply items-start;
|
||||
}
|
||||
}
|
||||
20
pkgs/clan-app/ui/src/components/v2/Form/Orienter.tsx
Normal file
20
pkgs/clan-app/ui/src/components/v2/Form/Orienter.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import cx from "classnames";
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
import "./Orienter.css";
|
||||
|
||||
export interface OrienterProps {
|
||||
orientation?: "vertical" | "horizontal";
|
||||
align?: "center" | "start";
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const Orienter = (props: OrienterProps) => {
|
||||
const alignment = () => `align-${props.align || "center"}`;
|
||||
|
||||
return (
|
||||
<div class={cx("orienter", alignment(), props.orientation)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
|
||||
import "./TextInput.css";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
|
||||
export type TextAreaProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
@@ -24,11 +25,13 @@ export const TextArea = (props: TextAreaProps) => (
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
{...props}
|
||||
/>
|
||||
<TextField.TextArea {...props.input} />
|
||||
</Orienter>
|
||||
</TextField>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "./Field.css";
|
||||
|
||||
div.form-field {
|
||||
&.text input,
|
||||
&.textarea textarea {
|
||||
@@ -47,10 +45,6 @@ div.form-field {
|
||||
&.textarea textarea {
|
||||
@apply w-1/2 grow;
|
||||
}
|
||||
|
||||
&.textarea:has(> textarea) {
|
||||
@apply items-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.text div.input-container {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Label } from "./Label";
|
||||
import "./TextInput.css";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
|
||||
export type TextInputProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
@@ -25,6 +26,7 @@ export const TextInput = (props: TextInputProps) => (
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
@@ -43,5 +45,6 @@ export const TextInput = (props: TextInputProps) => (
|
||||
classList={{ "has-icon": props.icon }}
|
||||
/>
|
||||
</div>
|
||||
</Orienter>
|
||||
</TextField>
|
||||
);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type Orientation = "horizontal" | "vertical";
|
||||
Reference in New Issue
Block a user