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 { Separator, SeparatorRootProps } from "@kobalte/core/separator";
export interface DividerProps extends Pick<SeparatorRootProps, "orientation"> {
inverted?: boolean;
class?: string;
}
export const Divider = (props: DividerProps) => {
@@ -12,7 +11,7 @@ export const Divider = (props: DividerProps) => {
return (
<Separator
class={cx({ inverted: inverted }, props?.class)}
class={cx({ [styles.inverted]: inverted })}
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 { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
@@ -30,7 +30,7 @@ export const SidebarHeader = () => {
.sort((a, b) => a.details.name.localeCompare(b.details.name));
return (
<div class="sidebar-header">
<div class={styles.sidebarHeader}>
<Show when={ctx.activeClanQuery.isSuccess && showSettings()}>
<ClanSettingsModal
model={ctx.activeClanQuery.data!}
@@ -42,9 +42,9 @@ export const SidebarHeader = () => {
</Show>
<Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="clan-label">
<div class="clan-icon">
<DropdownMenu.Trigger class={styles.dropDownTrigger}>
<div class={styles.clanLabel}>
<div class={styles.clanIcon}>
<Typography
hierarchy="label"
size="s"
@@ -68,9 +68,9 @@ export const SidebarHeader = () => {
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Content class={styles.dropDownContent}>
<DropdownMenu.Item
class="dropdown-item"
class={styles.dropdownItem}
onSelect={() => setShowSettings(true)}
>
<Icon
@@ -83,8 +83,8 @@ export const SidebarHeader = () => {
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<DropdownMenu.Group class={styles.dropdownGroup}>
<DropdownMenu.GroupLabel class={styles.dropdownGroupLabel}>
<Typography
hierarchy="label"
family="mono"
@@ -104,12 +104,12 @@ export const SidebarHeader = () => {
Add
</Button>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<div class={styles.dropdownGroupItems}>
<For each={clanList()}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
class={styles.dropdownItem}
onSelect={() => {
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>
)}
</SidebarSectionForm>
<SidebarSection title="Simple" class="flex flex-col">
<SidebarSection title="Simple">
<Typography tag="h2" hierarchy="title" size="m" inverted>
Static Content
</Typography>

View File

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

View File

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