Merge pull request 'ui: use css modules for sidebar components' (#5217) from hgl into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5217
Reviewed-by: brianmcgee <brian@bmcgee.ie>
This commit is contained in:
brianmcgee
2025-09-22 10:07:52 +00:00
13 changed files with 282 additions and 306 deletions

View File

@@ -1,10 +1,9 @@
import "./Divider.css"; import styles from "./Divider.module.css";
import cx from "classnames"; import cx from "classnames";
import { Separator, SeparatorRootProps } from "@kobalte/core/separator"; import { Separator, SeparatorRootProps } from "@kobalte/core/separator";
export interface DividerProps extends Pick<SeparatorRootProps, "orientation"> { export interface DividerProps extends Pick<SeparatorRootProps, "orientation"> {
inverted?: boolean; inverted?: boolean;
class?: string;
} }
export const Divider = (props: DividerProps) => { export const Divider = (props: DividerProps) => {
@@ -12,7 +11,7 @@ export const Divider = (props: DividerProps) => {
return ( return (
<Separator <Separator
class={cx({ inverted: inverted }, props?.class)} class={cx({ [styles.inverted]: inverted })}
orientation={props.orientation} orientation={props.orientation}
/> />
); );

View File

@@ -1,93 +0,0 @@
div.sidebar-header {
@apply flex items-center justify-center w-full px-1 py-1;
@apply border border-inv-3 rounded-md rounded-bl-none rounded-br-none;
background: linear-gradient(
90deg,
theme(colors.bg.inv.2) 0%,
theme(colors.bg.inv.3) 0%
);
& > .dropdown-trigger {
@apply flex items-center justify-between flex-grow px-1 py-1;
@apply rounded-tl-md rounded-tr-md;
@apply border border-transparent border-b-0;
transition: all 250ms ease-in-out;
div.clan-label {
@apply flex items-center gap-2 justify-start;
& > .clan-icon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7;
}
}
.icon[data-icon-name="CaretDown"] {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
&[data-expanded] {
@apply bg-def-1 border-def-2;
.icon[data-icon-name="CaretDown"] {
transform: rotate(180deg);
}
}
}
}
.sidebar-dropdown-content {
@apply flex flex-col w-full px-2 py-1.5 z-10 gap-3;
@apply bg-def-1 rounded-bl-md rounded-br-md;
@apply border border-def-2;
animation: sidebarNavContentHide 250ms ease-in forwards;
.dropdown-item {
@apply flex items-center justify-start w-full px-1.5 py-2 gap-2 rounded;
&:hover {
@apply bg-def-acc-2 cursor-pointer;
}
}
.dropdown-group {
@apply flex flex-col gap-2;
@apply px-1;
.dropdown-group-label {
@apply flex items-baseline justify-between w-full;
}
.dropdown-group-items {
@apply rounded px-1 py-1.5 bg-def-2;
}
}
}
.sidebar-dropdown-content[data-expanded] {
animation: sidebarNavContentShow 250ms ease-out;
}
@keyframes sidebarNavContentShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes sidebarNavContentHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -0,0 +1,89 @@
.sidebarHeader {
@apply flex items-center justify-center w-full px-1 py-1;
@apply border border-inv-3 rounded-md rounded-bl-none rounded-br-none;
background: linear-gradient(
90deg,
theme(colors.bg.inv.2) 0%,
theme(colors.bg.inv.3) 0%
);
}
.dropDownTrigger {
@apply flex items-center justify-between flex-grow px-1 py-1;
@apply rounded-tl-md rounded-tr-md;
@apply border border-transparent border-b-0;
transition: all 250ms ease-in-out;
.icon[data-icon-name="CaretDown"] {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
&[data-expanded] {
@apply bg-def-1 border-def-2;
.icon[data-icon-name="CaretDown"] {
transform: rotate(180deg);
}
}
}
.clanLabel {
@apply flex items-center gap-2 justify-start;
}
.clanIcon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7;
}
.dropDownContent {
@apply flex flex-col w-full px-2 py-1.5 z-10 gap-3;
@apply bg-def-1 rounded-bl-md rounded-br-md;
@apply border border-def-2;
animation: sidebarNavContentHide 250ms ease-in forwards;
}
.dropDownContent[data-expanded] {
animation: sidebarNavContentShow 250ms ease-out;
}
.dropdownItem {
@apply flex items-center justify-start w-full px-1.5 py-2 gap-2 rounded;
&:hover {
@apply bg-def-acc-2 cursor-pointer;
}
}
.dropdownGroup {
@apply flex flex-col gap-2;
@apply px-1;
}
.dropdownGroupLabel {
@apply flex items-baseline justify-between w-full;
}
.dropdownGroupItems {
@apply rounded px-1 py-1.5 bg-def-2;
}
@keyframes sidebarNavContentShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes sidebarNavContentHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -1,4 +1,4 @@
import "./SidebarHeader.css"; import styles from "./SidebarHeader.module.css";
import Icon from "@/src/components/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu"; import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
@@ -30,7 +30,7 @@ export const SidebarHeader = () => {
.sort((a, b) => a.details.name.localeCompare(b.details.name)); .sort((a, b) => a.details.name.localeCompare(b.details.name));
return ( return (
<div class="sidebar-header"> <div class={styles.sidebarHeader}>
<Show when={ctx.activeClanQuery.isSuccess && showSettings()}> <Show when={ctx.activeClanQuery.isSuccess && showSettings()}>
<ClanSettingsModal <ClanSettingsModal
model={ctx.activeClanQuery.data!} model={ctx.activeClanQuery.data!}
@@ -42,9 +42,9 @@ export const SidebarHeader = () => {
</Show> </Show>
<Suspense fallback={"Loading..."}> <Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}> <DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger"> <DropdownMenu.Trigger class={styles.dropDownTrigger}>
<div class="clan-label"> <div class={styles.clanLabel}>
<div class="clan-icon"> <div class={styles.clanIcon}>
<Typography <Typography
hierarchy="label" hierarchy="label"
size="s" size="s"
@@ -68,9 +68,9 @@ export const SidebarHeader = () => {
</DropdownMenu.Icon> </DropdownMenu.Icon>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content"> <DropdownMenu.Content class={styles.dropDownContent}>
<DropdownMenu.Item <DropdownMenu.Item
class="dropdown-item" class={styles.dropdownItem}
onSelect={() => setShowSettings(true)} onSelect={() => setShowSettings(true)}
> >
<Icon <Icon
@@ -83,8 +83,8 @@ export const SidebarHeader = () => {
Settings Settings
</Typography> </Typography>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group"> <DropdownMenu.Group class={styles.dropdownGroup}>
<DropdownMenu.GroupLabel class="dropdown-group-label"> <DropdownMenu.GroupLabel class={styles.dropdownGroupLabel}>
<Typography <Typography
hierarchy="label" hierarchy="label"
family="mono" family="mono"
@@ -104,12 +104,12 @@ export const SidebarHeader = () => {
Add Add
</Button> </Button>
</DropdownMenu.GroupLabel> </DropdownMenu.GroupLabel>
<div class="dropdown-group-items"> <div class={styles.dropdownGroupItems}>
<For each={clanList()}> <For each={clanList()}>
{(clan) => ( {(clan) => (
<Suspense fallback={"Loading..."}> <Suspense fallback={"Loading..."}>
<DropdownMenu.Item <DropdownMenu.Item
class="dropdown-item" class={styles.dropdownItem}
onSelect={() => { onSelect={() => {
setActiveClanURI(clan.uri); setActiveClanURI(clan.uri);
}} }}

View File

@@ -1,123 +0,0 @@
div.sidebar-pane {
@apply flex flex-col border-none z-20 h-full;
animation: sidebarPaneShow 250ms ease-in forwards;
&.open {
@apply w-72;
}
&.closing {
animation: sidebarPaneHide 250ms ease-out 300ms forwards;
& > div.header > *,
& > div.sub-header > *,
& > div.body > * {
animation: sidebarFadeOut 250ms ease-out forwards;
}
}
& > 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%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 250ms forwards;
}
}
& > div.sub-header {
@apply px-3 py-1;
@apply border-b-[1px] border-b-bg-inv-4;
@apply border-r-[1px] border-r-bg-inv-3 border-l-[1px] border-l-bg-inv-3;
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.08) 0%, rgba(0, 0, 0, 0.08) 100%),
linear-gradient(
90deg,
theme(colors.bg.inv.3) 0%,
theme(colors.bg.inv.4) 100%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 250ms forwards;
}
}
& > 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%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 350ms forwards;
}
}
}
@keyframes sidebarPaneShow {
0% {
@apply w-0;
@apply opacity-0;
}
10% {
@apply w-8;
}
30% {
@apply opacity-100;
}
100% {
@apply w-72;
}
}
@keyframes sidebarPaneHide {
90% {
@apply w-8;
}
100% {
@apply w-0;
@apply opacity-0;
}
}
@keyframes sidebarFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes sidebarFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@@ -0,0 +1,120 @@
.sidebarPane {
@apply flex flex-col border-none z-20 h-full;
@apply w-72;
animation: sidebarPaneShow 250ms ease-in forwards;
&.closing {
animation: sidebarPaneHide 250ms ease-out 300ms forwards;
& > .header > *,
& > .subHeader > *,
& > .body > * {
animation: sidebarFadeOut 250ms ease-out forwards;
}
}
}
.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%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 250ms forwards;
}
}
.subHeader {
@apply px-3 py-1;
@apply border-b-[1px] border-b-bg-inv-4;
@apply border-r-[1px] border-r-bg-inv-3 border-l-[1px] border-l-bg-inv-3;
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.08) 0%, rgba(0, 0, 0, 0.08) 100%),
linear-gradient(
90deg,
theme(colors.bg.inv.3) 0%,
theme(colors.bg.inv.4) 100%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 250ms forwards;
}
}
.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%
);
& > * {
@apply opacity-0;
animation: sidebarFadeIn 250ms ease-in 350ms forwards;
}
}
@keyframes sidebarPaneShow {
0% {
@apply w-0;
@apply opacity-0;
}
10% {
@apply w-8;
}
30% {
@apply opacity-100;
}
100% {
@apply w-72;
}
}
@keyframes sidebarPaneHide {
90% {
@apply w-8;
}
100% {
@apply w-0;
@apply opacity-0;
}
}
@keyframes sidebarFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes sidebarFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@@ -158,7 +158,7 @@ export const Default: Story = {
</Field> </Field>
)} )}
</SidebarSectionForm> </SidebarSectionForm>
<SidebarSection title="Simple" class="flex flex-col"> <SidebarSection title="Simple">
<Typography tag="h2" hierarchy="title" size="m" inverted> <Typography tag="h2" hierarchy="title" size="m" inverted>
Static Content Static Content
</Typography> </Typography>

View File

@@ -1,12 +1,11 @@
import { createSignal, JSX, onMount, Show } from "solid-js"; import { createSignal, JSX, Show } from "solid-js";
import "./SidebarPane.css"; import styles from "./SidebarPane.module.css";
import { Typography } from "@/src/components/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { Button as KButton } from "@kobalte/core/button"; import { Button as KButton } from "@kobalte/core/button";
import cx from "classnames"; import cx from "classnames";
export interface SidebarPaneProps { export interface SidebarPaneProps {
class?: string;
title: string; title: string;
onClose: () => void; onClose: () => void;
subHeader?: JSX.Element; subHeader?: JSX.Element;
@@ -15,26 +14,20 @@ export interface SidebarPaneProps {
export const SidebarPane = (props: SidebarPaneProps) => { export const SidebarPane = (props: SidebarPaneProps) => {
const [closing, setClosing] = createSignal(false); const [closing, setClosing] = createSignal(false);
const [open, setOpened] = createSignal(true);
// FIXME: use animationend event instead of setTimeout
const onClose = () => { const onClose = () => {
setClosing(true); setClosing(true);
setTimeout(() => props.onClose(), 550); setTimeout(() => props.onClose(), 550);
}; };
onMount(() => {
setTimeout(() => {
setOpened(true);
}, 250);
});
return ( return (
<div <div
class={cx("sidebar-pane", props.class, { class={cx(styles.sidebarPane, {
closing: closing(), [styles.closing]: closing(),
open: open(),
})} })}
> >
<div class="header"> <div class={styles.header}>
<Typography hierarchy="body" size="s" weight="bold" inverted={true}> <Typography hierarchy="body" size="s" weight="bold" inverted={true}>
{props.title} {props.title}
</Typography> </Typography>
@@ -43,9 +36,9 @@ export const SidebarPane = (props: SidebarPaneProps) => {
</KButton> </KButton>
</div> </div>
<Show when={props.subHeader}> <Show when={props.subHeader}>
<div class="sub-header">{props.subHeader}</div> <div class={styles.subHeader}>{props.subHeader}</div>
</Show> </Show>
<div class="body">{props.children}</div> <div class={styles.body}>{props.children}</div>
</div> </div>
); );
}; };

View File

@@ -1,15 +0,0 @@
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 items-center justify-end;
}
}
& > div.content {
@apply w-full h-fit px-1.5 py-3 rounded-md bg-inv-4;
}
}

View File

@@ -0,0 +1,12 @@
.sidebarSection {
@apply flex flex-col gap-2 w-full h-full;
}
.header {
@apply flex items-center justify-between px-1.5;
}
.controls {
@apply flex items-center justify-end h-4;
}
.content {
@apply w-full h-fit px-1.5 py-3 rounded-md bg-inv-4;
}

View File

@@ -1,18 +1,17 @@
import { JSX } from "solid-js"; import { JSX, Show } from "solid-js";
import "./SidebarSection.css"; import styles from "./SidebarSection.module.css";
import { Typography } from "@/src/components/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import cx from "classnames";
export interface SidebarSectionProps { export interface SidebarSectionProps {
title: string; title: string;
class?: string; controls?: JSX.Element;
children: JSX.Element; children: JSX.Element;
} }
export const SidebarSection = (props: SidebarSectionProps) => { export const SidebarSection = (props: SidebarSectionProps) => {
return ( return (
<div class={cx("sidebar-section", props.class)}> <div class={styles.sidebarSection}>
<div class="header"> <div class={styles.header}>
<Typography <Typography
hierarchy="label" hierarchy="label"
size="xs" size="xs"
@@ -23,8 +22,11 @@ export const SidebarSection = (props: SidebarSectionProps) => {
> >
{props.title} {props.title}
</Typography> </Typography>
<Show when={props.controls}>
<div class={styles.controls}>{props.controls}</div>
</Show>
</div> </div>
<div class="content">{props.children}</div> <div class={styles.content}>{props.children}</div>
</div> </div>
); );
}; };

View File

@@ -15,8 +15,8 @@ import { GenericSchema, GenericSchemaAsync } from "valibot";
import { Typography } from "@/src/components/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button"; import { Button } from "@/src/components/Button/Button";
import "./SidebarSection.css";
import { Loader } from "../../components/Loader/Loader"; import { Loader } from "../../components/Loader/Loader";
import { SidebarSection } from "./SidebarSection";
export interface SidebarSectionFormProps<FormValues extends FieldValues> { export interface SidebarSectionFormProps<FormValues extends FieldValues> {
title: string; title: string;
@@ -71,31 +71,24 @@ export function SidebarSectionForm<
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<div class="sidebar-section"> <SidebarSection
<div class="header"> title={props.title}
<Typography controls={
hierarchy="label" <>
size="xs" {editing() &&
family="mono" (formStore.submitting ? (
transform="uppercase" <Loader />
color="tertiary" ) : (
inverted <Button
> hierarchy="primary"
{props.title} size="xs"
</Typography> icon="Checkmark"
<div class="controls h-4"> ghost
{editing() && !formStore.submitting && ( type="submit"
<Button >
hierarchy="primary" Save
size="xs" </Button>
icon="Checkmark" ))}
ghost
type="submit"
>
Save
</Button>
)}
{editing() && formStore.submitting && <Loader />}
<Button <Button
hierarchy="primary" hierarchy="primary"
ghost ghost
@@ -103,19 +96,18 @@ export function SidebarSectionForm<
icon={editing() ? "Close" : "Edit"} icon={editing() ? "Close" : "Edit"}
onClick={editOrClose} onClick={editOrClose}
/> />
</>
}
>
<Show when={editing() && formStore.dirty && errorMessage()}>
<div class="mb-2.5" role="alert" aria-live="assertive">
<Typography hierarchy="body" size="xs" inverted color="error">
{errorMessage()}
</Typography>
</div> </div>
</div> </Show>
<div class="content"> {props.children({ editing: editing(), Field, formStore })}
<Show when={editing() && formStore.dirty && errorMessage()}> </SidebarSection>
<div class="mb-2.5" role="alert" aria-live="assertive">
<Typography hierarchy="body" size="xs" inverted color="error">
{errorMessage()}
</Typography>
</div>
</Show>
{props.children({ editing: editing(), Field, formStore })}
</div>
</div>
</Form> </Form>
); );
} }