feat(ui): SidebarPane component
* implement Divider component using Kobalte's Separator * refine read only state of form components to match the Sidebar Pane design * introduce a SidebarPane component with sections that can toggle between editing and view states.
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
div.divider {
|
hr {
|
||||||
@apply bg-inv-2;
|
@apply border-none outline-none bg-inv-2;
|
||||||
|
|
||||||
&.inverted {
|
&.inverted {
|
||||||
@apply bg-def-3;
|
@apply bg-def-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&[data-orientation="horizontal"] {
|
||||||
@apply w-full h-px;
|
@apply w-full h-px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.vertical {
|
&[data-orientation="vertical"] {
|
||||||
@apply h-full w-px;
|
@apply h-full w-px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import "./Divider.css";
|
import "./Divider.css";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { Separator, SeparatorRootProps } from "@kobalte/core/separator";
|
||||||
|
|
||||||
export interface DividerProps {
|
export interface DividerProps extends Pick<SeparatorRootProps, "orientation"> {
|
||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
orientation?: "horizontal" | "vertical";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Divider = (props: DividerProps) => {
|
export const Divider = (props: DividerProps) => {
|
||||||
const inverted = props.inverted || false;
|
const inverted = props.inverted || false;
|
||||||
const orientation = () => props.orientation || "horizontal";
|
|
||||||
|
|
||||||
return <div class={cx("divider", orientation(), { inverted: inverted })} />;
|
return (
|
||||||
|
<Separator
|
||||||
|
class={cx({ inverted: inverted })}
|
||||||
|
orientation={props.orientation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ div.form-field {
|
|||||||
&[data-invalid] {
|
&[data-invalid] {
|
||||||
@apply border-semantic-error-4;
|
@apply border-semantic-error-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply cursor-default bg-inherit border-none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,10 @@ div.form-field {
|
|||||||
&[data-disabled] {
|
&[data-disabled] {
|
||||||
@apply bg-def-4 border-none;
|
@apply bg-def-4 border-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply bg-inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,15 @@ export const Disabled: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReadOnly: Story = {
|
export const ReadOnlyUnchecked: Story = {
|
||||||
|
args: {
|
||||||
|
...Tooltip.args,
|
||||||
|
readOnly: true,
|
||||||
|
defaultChecked: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadOnlyChecked: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
|||||||
@@ -11,37 +11,64 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|||||||
import "./Checkbox.css";
|
import "./Checkbox.css";
|
||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { Orienter } from "./Orienter";
|
import { Orienter } from "./Orienter";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
export type CheckboxProps = FieldProps &
|
export type CheckboxProps = FieldProps &
|
||||||
KCheckboxRootProps & {
|
KCheckboxRootProps & {
|
||||||
input?: PolymorphicProps<"input", KCheckboxInputProps<"input">>;
|
input?: PolymorphicProps<"input", KCheckboxInputProps<"input">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Checkbox = (props: CheckboxProps) => (
|
export const Checkbox = (props: CheckboxProps) => {
|
||||||
<KCheckbox
|
const alignment = () =>
|
||||||
class={cx("form-field", "checkbox", props.size, props.orientation, {
|
(props.orientation || "vertical") == "vertical" ? "start" : "center";
|
||||||
inverted: props.inverted,
|
|
||||||
ghost: props.ghost,
|
const iconChecked = (
|
||||||
})}
|
<Icon
|
||||||
{...props}
|
icon="Checkmark"
|
||||||
>
|
inverted={props.inverted}
|
||||||
<Orienter orientation={props.orientation} align={"start"}>
|
color="secondary"
|
||||||
<Label
|
size="100%"
|
||||||
labelComponent={KCheckbox.Label}
|
/>
|
||||||
descriptionComponent={KCheckbox.Description}
|
);
|
||||||
{...props}
|
|
||||||
/>
|
const iconUnchecked = (
|
||||||
<KCheckbox.Input {...props.input} />
|
<Icon
|
||||||
<KCheckbox.Control class="checkbox-control">
|
icon="Close"
|
||||||
<KCheckbox.Indicator>
|
inverted={props.inverted}
|
||||||
<Icon
|
color="secondary"
|
||||||
icon="Checkmark"
|
size="100%"
|
||||||
inverted={props.inverted}
|
/>
|
||||||
color="secondary"
|
);
|
||||||
size="100%"
|
|
||||||
/>
|
return (
|
||||||
</KCheckbox.Indicator>
|
<KCheckbox
|
||||||
</KCheckbox.Control>
|
class={cx("form-field", "checkbox", props.size, props.orientation, {
|
||||||
</Orienter>
|
inverted: props.inverted,
|
||||||
</KCheckbox>
|
ghost: props.ghost,
|
||||||
);
|
})}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Orienter orientation={props.orientation} align={alignment()}>
|
||||||
|
<Label
|
||||||
|
labelComponent={KCheckbox.Label}
|
||||||
|
descriptionComponent={KCheckbox.Description}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<KCheckbox.Input {...props.input} />
|
||||||
|
<KCheckbox.Control class="checkbox-control">
|
||||||
|
{props.readOnly && (
|
||||||
|
<Show
|
||||||
|
when={props.checked || props.defaultChecked}
|
||||||
|
fallback={iconUnchecked}
|
||||||
|
>
|
||||||
|
{iconChecked}
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
{!props.readOnly && (
|
||||||
|
<KCheckbox.Indicator>{iconChecked}</KCheckbox.Indicator>
|
||||||
|
)}
|
||||||
|
</KCheckbox.Control>
|
||||||
|
</Orienter>
|
||||||
|
</KCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
div.form-field.combobox {
|
div.form-field.combobox {
|
||||||
div.control {
|
div.control {
|
||||||
@apply flex flex-col w-full gap-2;
|
@apply flex flex-col size-full gap-2;
|
||||||
|
|
||||||
div.selected-options {
|
div.selected-options {
|
||||||
@apply flex flex-wrap gap-1 w-full min-h-5;
|
@apply flex flex-wrap gap-1 size-full min-h-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.input-container {
|
div.input-container {
|
||||||
@@ -44,7 +44,8 @@ div.form-field.combobox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-readonly] {
|
&[data-readonly] {
|
||||||
@apply outline-def-2 cursor-not-allowed;
|
@apply outline-none border-none bg-inherit;
|
||||||
|
@apply p-0 resize-none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +77,10 @@ div.form-field.combobox {
|
|||||||
& > input {
|
& > input {
|
||||||
@apply px-1.5 py-1;
|
@apply px-1.5 py-1;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > button.trigger {
|
& > button.trigger {
|
||||||
@@ -111,6 +116,10 @@ div.form-field.combobox {
|
|||||||
&[data-invalid] {
|
&[data-invalid] {
|
||||||
@apply outline-semantic-error-4;
|
@apply outline-semantic-error-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply outline-none border-none bg-inherit cursor-auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export const ReadOnly: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
defaultValue: "foo",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,18 @@ export const DefaultItemControl = <Option,>(
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="input-container">
|
{!(props.readOnly && props.multiple) && (
|
||||||
<KCombobox.Input />
|
<div class="input-container">
|
||||||
<KCombobox.Trigger class="trigger">
|
<KCombobox.Input />
|
||||||
<KCombobox.Icon class="icon">
|
{!props.readOnly && (
|
||||||
<Icon icon="Expand" inverted={props.inverted} size="100%" />
|
<KCombobox.Trigger class="trigger">
|
||||||
</KCombobox.Icon>
|
<KCombobox.Icon class="icon">
|
||||||
</KCombobox.Trigger>
|
<Icon icon="Expand" inverted={props.inverted} size="100%" />
|
||||||
</div>
|
</KCombobox.Icon>
|
||||||
|
</KCombobox.Trigger>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,7 +105,13 @@ export const Combobox = <Option, OptGroup = never>(
|
|||||||
const itemControl = () => props.itemControl || DefaultItemControl;
|
const itemControl = () => props.itemControl || DefaultItemControl;
|
||||||
const itemComponent = () => props.itemComponent || DefaultItemComponent;
|
const itemComponent = () => props.itemComponent || DefaultItemComponent;
|
||||||
|
|
||||||
const align = () => (props.orientation === "horizontal" ? "start" : "center");
|
const align = () => {
|
||||||
|
if (props.readOnly) {
|
||||||
|
return "center";
|
||||||
|
} else {
|
||||||
|
return props.orientation === "horizontal" ? "start" : "center";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KCombobox
|
<KCombobox
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ div.form-label {
|
|||||||
@apply flex items-center gap-1;
|
@apply flex items-center gap-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > label[data-required] {
|
& > label[data-required] & not(label[data-readonly]) {
|
||||||
span.typography::after {
|
span.typography::after {
|
||||||
@apply fg-def-4 ml-1;
|
@apply fg-def-4 ml-1;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface LabelProps {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
validationState?: "valid" | "invalid";
|
validationState?: "valid" | "invalid";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export const Label = (props: LabelProps) => {
|
|||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size={props.size || "default"}
|
size={props.size || "default"}
|
||||||
color={props.validationState == "invalid" ? "error" : "primary"}
|
color={props.validationState == "invalid" ? "error" : "primary"}
|
||||||
weight="bold"
|
weight={props.readOnly ? "normal" : "bold"}
|
||||||
inverted={props.inverted}
|
inverted={props.inverted}
|
||||||
>
|
>
|
||||||
{props.label}
|
{props.label}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ div.orienter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
@apply flex-row gap-2 justify-between;
|
@apply flex-row justify-start;
|
||||||
|
|
||||||
& > div.form-label {
|
& > div.form-label {
|
||||||
@apply w-1/2 shrink;
|
@apply w-1/2 shrink;
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ div.form-field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-readonly] {
|
&[data-readonly] {
|
||||||
@apply outline-def-2 cursor-not-allowed;
|
@apply outline-none border-none bg-inherit p-0 cursor-auto resize-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.textarea textarea {
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply overflow-y-hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +78,10 @@ div.form-field {
|
|||||||
&.textarea textarea {
|
&.textarea textarea {
|
||||||
@apply px-1.5 py-1;
|
@apply px-1.5 py-1;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.text div.input-container {
|
&.text div.input-container {
|
||||||
@@ -114,6 +124,11 @@ div.form-field {
|
|||||||
&[data-invalid] {
|
&[data-invalid] {
|
||||||
@apply outline-semantic-error-4;
|
@apply outline-semantic-error-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-readonly] {
|
||||||
|
@apply outline-def-2 cursor-auto;
|
||||||
|
@apply outline-none border-none bg-inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const TextInput = (props: TextInputProps) => (
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
{props.icon && (
|
{props.icon && !props.readOnly && (
|
||||||
<Icon
|
<Icon
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
inverted={props.inverted}
|
inverted={props.inverted}
|
||||||
@@ -42,7 +42,7 @@ export const TextInput = (props: TextInputProps) => (
|
|||||||
)}
|
)}
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
{...props.input}
|
{...props.input}
|
||||||
classList={{ "has-icon": props.icon }}
|
classList={{ "has-icon": props.icon && !props.readOnly }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Orienter>
|
</Orienter>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ div.sidebar-header {
|
|||||||
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
theme(colors.bg.inv.3) 0%,
|
theme(colors.bg.inv.2) 0%,
|
||||||
theme(colors.bg.inv.4) 0%
|
theme(colors.bg.inv.3) 0%
|
||||||
);
|
);
|
||||||
|
|
||||||
& > .dropdown-trigger {
|
& > .dropdown-trigger {
|
||||||
|
|||||||
33
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.css
Normal file
33
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
div.sidebar-pane {
|
||||||
|
@apply h-full w-auto max-w-60 border-none;
|
||||||
|
|
||||||
|
& > div.header {
|
||||||
|
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];
|
||||||
|
@apply border-t-[1px] border-t-bg-inv-3
|
||||||
|
border-r-[1px] border-r-bg-inv-3
|
||||||
|
border-b-2 border-b-bg-inv-4
|
||||||
|
border-l-[1px] border-l-bg-inv-3;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
theme(colors.bg.inv.3) 0%,
|
||||||
|
theme(colors.bg.inv.4) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.body {
|
||||||
|
@apply flex flex-col gap-4 px-2 pt-4 pb-3 w-full h-full;
|
||||||
|
@apply backdrop-blur-md;
|
||||||
|
@apply rounded-b-[0.5rem]
|
||||||
|
border-r-[1px] border-r-bg-inv-3
|
||||||
|
border-b-2 border-b-bg-inv-4
|
||||||
|
border-l-[1px] border-l-bg-inv-3;
|
||||||
|
|
||||||
|
background:
|
||||||
|
linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
theme(colors.bg.inv.2) 0%,
|
||||||
|
theme(colors.bg.inv.3) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||||
|
import {
|
||||||
|
SidebarPane,
|
||||||
|
SidebarPaneProps,
|
||||||
|
} from "@/src/components/v2/Sidebar/SidebarPane";
|
||||||
|
import { SidebarSection } from "./SidebarSection";
|
||||||
|
import { Divider } from "@/src/components/v2/Divider/Divider";
|
||||||
|
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 { Combobox } from "../Form/Combobox";
|
||||||
|
|
||||||
|
const meta: Meta<SidebarPaneProps> = {
|
||||||
|
title: "Components/Sidebar/Pane",
|
||||||
|
component: SidebarPane,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<SidebarPaneProps>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Neptune",
|
||||||
|
onClose: () => {
|
||||||
|
console.log("closing");
|
||||||
|
},
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<SidebarSection
|
||||||
|
title="General"
|
||||||
|
onSave={async () => {
|
||||||
|
console.log("saving general");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(editing) => (
|
||||||
|
<form class="flex flex-col gap-3">
|
||||||
|
<TextInput
|
||||||
|
label="First Name"
|
||||||
|
size="s"
|
||||||
|
inverted={true}
|
||||||
|
required={true}
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={{ value: "Ron" }}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<TextInput
|
||||||
|
label="Last Name"
|
||||||
|
size="s"
|
||||||
|
inverted={true}
|
||||||
|
required={true}
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={{ value: "Burgundy" }}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<TextArea
|
||||||
|
label="Bio"
|
||||||
|
size="s"
|
||||||
|
inverted={true}
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
input={{
|
||||||
|
value:
|
||||||
|
"It's actually an optical illusion, it's the pattern on the pants.",
|
||||||
|
rows: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Checkbox
|
||||||
|
size="s"
|
||||||
|
label="Share Profile"
|
||||||
|
required={true}
|
||||||
|
inverted={true}
|
||||||
|
readOnly={!editing}
|
||||||
|
checked={true}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</SidebarSection>
|
||||||
|
<SidebarSection
|
||||||
|
title="Tags"
|
||||||
|
onSave={async () => {
|
||||||
|
console.log("saving general");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(editing) => (
|
||||||
|
<form class="flex flex-col gap-3">
|
||||||
|
<Combobox
|
||||||
|
size="s"
|
||||||
|
inverted={true}
|
||||||
|
required={true}
|
||||||
|
readOnly={!editing}
|
||||||
|
orientation="horizontal"
|
||||||
|
multiple={true}
|
||||||
|
options={["All", "Home Server", "Backup", "Random"]}
|
||||||
|
defaultValue={["All", "Home Server", "Backup", "Random"]}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</SidebarSection>
|
||||||
|
<SidebarSection
|
||||||
|
title="Advanced Settings"
|
||||||
|
onSave={async () => {
|
||||||
|
console.log("saving general");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(editing) => <></>}
|
||||||
|
</SidebarSection>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
27
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.tsx
Normal file
27
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { JSX } from "solid-js";
|
||||||
|
import "./SidebarPane.css";
|
||||||
|
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||||
|
import Icon from "../Icon/Icon";
|
||||||
|
import { Button as KButton } from "@kobalte/core/button";
|
||||||
|
|
||||||
|
export interface SidebarPaneProps {
|
||||||
|
title: string;
|
||||||
|
onClose: () => void;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarPane = (props: SidebarPaneProps) => {
|
||||||
|
return (
|
||||||
|
<div class="sidebar-pane">
|
||||||
|
<div class="header">
|
||||||
|
<Typography hierarchy="body" size="s" weight="bold" inverted={true}>
|
||||||
|
{props.title}
|
||||||
|
</Typography>
|
||||||
|
<KButton onClick={props.onClose}>
|
||||||
|
<Icon icon="Close" color="primary" size="0.75rem" inverted={true} />
|
||||||
|
</KButton>
|
||||||
|
</div>
|
||||||
|
<div class="body">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
div.sidebar-section {
|
||||||
|
@apply flex flex-col gap-2 w-full h-full;
|
||||||
|
|
||||||
|
& > div.header {
|
||||||
|
@apply flex items-center justify-between px-1.5;
|
||||||
|
|
||||||
|
& > div.controls {
|
||||||
|
@apply flex justify-end gap-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.content {
|
||||||
|
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { createSignal, JSX } from "solid-js";
|
||||||
|
import "./SidebarSection.css";
|
||||||
|
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||||
|
import { Button as KButton } from "@kobalte/core/button";
|
||||||
|
import Icon from "../Icon/Icon";
|
||||||
|
|
||||||
|
export interface SidebarSectionProps {
|
||||||
|
title: string;
|
||||||
|
onSave: () => Promise<void>;
|
||||||
|
children: (editing: boolean) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarSection = (props: SidebarSectionProps) => {
|
||||||
|
const [editing, setEditing] = createSignal(false);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
// todo how do we surface errors?
|
||||||
|
await props.onSave();
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="header">
|
||||||
|
<Typography
|
||||||
|
hierarchy="label"
|
||||||
|
size="xs"
|
||||||
|
family="mono"
|
||||||
|
weight="light"
|
||||||
|
transform="uppercase"
|
||||||
|
color="tertiary"
|
||||||
|
inverted={true}
|
||||||
|
>
|
||||||
|
{props.title}
|
||||||
|
</Typography>
|
||||||
|
<div class="controls">
|
||||||
|
{editing() && (
|
||||||
|
<KButton>
|
||||||
|
<Icon
|
||||||
|
icon="Checkmark"
|
||||||
|
color="tertiary"
|
||||||
|
size="0.75rem"
|
||||||
|
inverted={true}
|
||||||
|
onClick={save}
|
||||||
|
/>
|
||||||
|
</KButton>
|
||||||
|
)}
|
||||||
|
<KButton onClick={() => setEditing(!editing())}>
|
||||||
|
<Icon
|
||||||
|
icon={editing() ? "Close" : "Edit"}
|
||||||
|
color="tertiary"
|
||||||
|
size="0.75rem"
|
||||||
|
inverted={true}
|
||||||
|
/>
|
||||||
|
</KButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">{props.children(editing())}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
/* Body */
|
/* Body */
|
||||||
.typography {
|
.typography {
|
||||||
|
&.weight-light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
&.weight-normal {
|
&.weight-normal {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Color, fgClass } from "@/src/components/v2/colors";
|
|||||||
|
|
||||||
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
||||||
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
|
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
|
||||||
export type Weight = "normal" | "medium" | "bold";
|
export type Weight = "normal" | "medium" | "bold" | "light";
|
||||||
export type Family = "regular" | "condensed" | "mono";
|
export type Family = "regular" | "condensed" | "mono";
|
||||||
export type Transform = "uppercase" | "lowercase" | "capitalize";
|
export type Transform = "uppercase" | "lowercase" | "capitalize";
|
||||||
|
|
||||||
@@ -80,9 +80,10 @@ const defaultFamilyMap: Record<Hierarchy, Family> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const weightMap: Record<Weight, string> = {
|
const weightMap: Record<Weight, string> = {
|
||||||
normal: cx("weight-normal"),
|
normal: "weight-normal",
|
||||||
medium: cx("weight-medium"),
|
medium: "weight-medium",
|
||||||
bold: cx("weight-bold"),
|
bold: "weight-bold",
|
||||||
|
light: "weight-light",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface _TypographyProps<H extends Hierarchy> {
|
interface _TypographyProps<H extends Hierarchy> {
|
||||||
|
|||||||
Reference in New Issue
Block a user