ui: use css modules for Typography and SidebarBody

Extra changes:
- Add missing transition to according triggers in SidebarBody
- More sensible tag for each Typography hierarchy
This commit is contained in:
Glen Huang
2025-09-19 19:37:29 +08:00
parent 91985504d0
commit 133f4aee53
20 changed files with 297 additions and 347 deletions

View File

@@ -125,10 +125,6 @@
&.loading {
@apply cursor-wait;
}
& > .typography {
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
}
}
.button.in-HostFileInput-horizontal {

View File

@@ -90,12 +90,11 @@ export const Button = (props: ButtonProps) => {
{local.children && (
<Typography
class={styles.typography}
hierarchy="label"
size={local.size}
inverted={local.hierarchy === "primary"}
weight="bold"
tag="span"
in="Button"
>
{local.children}
</Typography>

View File

@@ -30,3 +30,9 @@
.icon.in-ConfigureRole {
@apply ml-auto;
}
.icon.in-SidebarBody-AccordionTrigger {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
[data-expanded] > .icon.in-SidebarBody-AccordionTrigger {
transform: rotate(180deg);
}

View File

@@ -131,7 +131,8 @@ type In =
| "MachineTags-s"
| "ConfigureRole"
// TODO: better name
| "WorkflowPanelTitle";
| "WorkflowPanelTitle"
| "SidebarBody-AccordionTrigger";
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
icon: IconVariant;
size?: number | string;
@@ -153,8 +154,8 @@ const Icon: Component<IconProps> = (props) => {
component={component()}
class={cx(
styles.icon,
colorsStyles[local.color],
getInClasses(styles, local.in),
colorsStyles[local.color],
{
[colorsStyles.inverted]: local.inverted,
},

View File

@@ -19,10 +19,6 @@
border-bottom: solid 1px theme(colors.border.def.2);
}
.modal_title {
@apply mx-auto;
}
.modal_body {
overflow-y: auto;
@apply rounded-b-md p-4 pt-4 bg-def-1 flex-grow;

View File

@@ -67,10 +67,10 @@ export const Modal = (props: ModalProps) => {
<>
<div class={styles.modal_header}>
<Typography
class={styles.modal_title}
hierarchy="label"
family="mono"
size="xs"
in="Modal-title"
>
{props.title}
</Typography>

View File

@@ -75,6 +75,7 @@ export const Default: Story = {
height: "14.5rem",
// Test with lots of modules
options: generateModules(1000),
// FIXME: replace with a component
renderItem: (item: Module) => {
return (
<div class="flex items-center justify-between gap-2 rounded-md px-2 py-1 pr-4">
@@ -93,7 +94,6 @@ export const Default: Story = {
weight="normal"
color="quaternary"
inverted
class="flex justify-between"
>
<span class="inline-block max-w-72 truncate align-middle">
{item.description}

View File

@@ -29,10 +29,11 @@ export function TagSelect<T extends { value: unknown }>(
<Typography
hierarchy="label"
weight="medium"
class="flex gap-2 uppercase"
size="xs"
inverted
color="secondary"
transform="uppercase"
in="TagSelect-label"
>
{props.label}
</Typography>

View File

@@ -113,7 +113,7 @@ export const Select = (props: SelectProps) => {
size="s"
weight="bold"
family="condensed"
class="flex w-full items-center"
in="Select-item-label"
>
{props.item.rawValue.label}
</Typography>
@@ -129,8 +129,8 @@ export const Select = (props: SelectProps) => {
size="s"
weight="bold"
family="condensed"
class="flex w-full items-center"
color="secondary"
in="Select-item-label"
>
Loading...
</Typography>
@@ -144,8 +144,8 @@ export const Select = (props: SelectProps) => {
size="s"
weight="normal"
family="condensed"
class="flex w-full items-center"
color="secondary"
in="Select-item-label"
>
{props.noOptionsText || "No options available"}
</Typography>
@@ -157,7 +157,7 @@ export const Select = (props: SelectProps) => {
size="s"
weight="bold"
family="condensed"
class="flex w-full items-center"
in="Select-item-label"
>
{props.placeholder}
</Typography>
@@ -186,7 +186,7 @@ export const Select = (props: SelectProps) => {
size="s"
weight="bold"
family="condensed"
class="flex w-full items-center"
in="Select-item-label"
>
{state.selectedOption().label}
</Typography>
@@ -219,7 +219,7 @@ export const Select = (props: SelectProps) => {
hierarchy="body"
size="xs"
weight="bold"
class="flex w-full items-center"
in="Select-item-label"
>
{state.selectedOption().label}
</Typography>

View File

@@ -1,133 +0,0 @@
div.sidebar-body {
@apply py-4 px-2;
/* full - (y padding) */
height: calc(100% - 2rem);
@apply border border-inv-3 rounded-bl-md rounded-br-md;
/* TODO: This is weird, we shouldn't disable native browser features, a11y impacts incomming */
&::-webkit-scrollbar {
display: none;
}
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%),
linear-gradient(
180deg,
theme(colors.bg.inv.1) 0%,
theme(colors.bg.inv.3) 100%
);
@apply backdrop-blur-sm;
.accordion {
@apply w-full mb-4 h-full flex flex-col justify-start gap-4;
&:last-child {
@apply mb-0;
}
& > .item {
max-height: 50%;
&:last-child {
@apply mb-0;
}
& > .header {
@apply flex mb-2 px-2;
& > .trigger {
@apply inline-flex items-center justify-between w-full;
&:focus-visible {
@apply z-10;
outline: 2px solid hsl(200 98% 39%);
outline-offset: 2px;
}
& > .icon {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
&[data-expanded] > .icon {
transform: rotate(180deg);
}
.section-title {
@apply uppercase;
}
}
}
& > .content {
@apply flex flex-col;
@apply py-3 px-1.5 bg-inv-4 rounded-md mb-4;
max-height: calc(100% - 24px);
overflow-y: auto;
scrollbar-width: none;
animation: slideAccordionUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
&[data-expanded] {
animation: slideAccordionDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
nav * {
@apply outline-none;
}
nav > a {
@apply block w-full px-2 py-1.5 min-h-7 my-2 rounded-md;
&:first-child {
@apply mt-0;
}
&:last-child {
@apply mb-0;
}
&:focus-visible {
background: linear-gradient(
90deg,
theme(colors.secondary.900),
60%,
theme(colors.secondary.600) 100%
);
}
&:hover {
@apply bg-inv-acc-2;
}
&:active {
@apply bg-inv-acc-3;
}
&.active {
@apply bg-inv-acc-2;
}
}
}
}
}
}
@keyframes slideAccordionDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideAccordionUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}

View File

@@ -0,0 +1,115 @@
.sidebarBody {
@apply py-4 px-2;
/* full - (y padding) */
height: calc(100% - 2rem);
@apply border border-inv-3 rounded-bl-md rounded-br-md;
/* TODO: This is weird, we shouldn't disable native browser features, a11y impacts incomming */
&::-webkit-scrollbar {
display: none;
}
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%),
linear-gradient(
180deg,
theme(colors.bg.inv.1) 0%,
theme(colors.bg.inv.3) 100%
);
@apply backdrop-blur-sm;
}
.accordion {
@apply w-full mb-4 h-full flex flex-col justify-start gap-4;
&:last-child {
@apply mb-0;
}
}
.accordionItem {
max-height: 50%;
&:last-child {
@apply mb-0;
}
}
.accordionHeader {
@apply flex mb-2 px-2;
}
.accordionTrigger {
@apply inline-flex items-center justify-between w-full;
&:focus-visible {
@apply z-10;
outline: 2px solid hsl(200 98% 39%);
outline-offset: 2px;
}
}
.accordionContent {
@apply flex flex-col;
@apply py-3 px-1.5 bg-inv-4 rounded-md mb-4;
max-height: calc(100% - 24px);
overflow-y: auto;
scrollbar-width: none;
animation: slideAccordionUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
&[data-expanded] {
animation: slideAccordionDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
nav * {
@apply outline-none;
}
nav > a {
@apply block w-full px-2 py-1.5 min-h-7 my-2 rounded-md;
&:first-child {
@apply mt-0;
}
&:last-child {
@apply mb-0;
}
&:focus-visible {
background: linear-gradient(
90deg,
theme(colors.secondary.900),
60%,
theme(colors.secondary.600) 100%
);
}
&:hover {
@apply bg-inv-acc-2;
}
&:active {
@apply bg-inv-acc-3;
}
}
}
@keyframes slideAccordionDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideAccordionUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}

View File

@@ -1,4 +1,4 @@
import "./SidebarBody.css";
import styles from "./SidebarBody.module.css";
import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
@@ -75,23 +75,29 @@ const Machines = () => {
};
return (
<Accordion.Item class="item" value="machines">
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Accordion.Item class={styles.accordionItem} value="machines">
<Accordion.Header class={styles.accordionHeader}>
<Accordion.Trigger class={styles.accordionTrigger}>
<Typography
class="section-title"
hierarchy="label"
family="mono"
size="xs"
inverted
color="tertiary"
transform="uppercase"
>
Your Machines
</Typography>
<Icon icon="CaretDown" color="tertiary" inverted size="0.75rem" />
<Icon
icon="CaretDown"
color="tertiary"
inverted
size="0.75rem"
in="SidebarBody-AccordionTrigger"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<Accordion.Content class={styles.accordionContent}>
<Show
when={machines()}
fallback={
@@ -198,23 +204,29 @@ const Services = () => {
};
return (
<Accordion.Item class="item" value="services">
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Accordion.Item class={styles.accordionItem} value="services">
<Accordion.Header class={styles.accordionHeader}>
<Accordion.Trigger class={styles.accordionTrigger}>
<Typography
class="section-title"
hierarchy="label"
family="mono"
size="xs"
inverted
color="tertiary"
transform="uppercase"
>
Services
</Typography>
<Icon icon="CaretDown" color="tertiary" inverted size="0.75rem" />
<Icon
icon="CaretDown"
color="tertiary"
inverted
size="0.75rem"
in="SidebarBody-AccordionTrigger"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<Accordion.Content class={styles.accordionContent}>
<nav>
<For each={serviceInstances()}>
{(mapped) => (
@@ -242,9 +254,9 @@ export const SidebarBody = (props: SidebarProps) => {
const defaultAccordionValues = ["machines", "services", ...sectionLabels];
return (
<div class="sidebar-body">
<div class={styles.sidebarBody}>
<Accordion
class="accordion"
class={styles.accordion}
multiple
defaultValue={defaultAccordionValues}
>
@@ -253,16 +265,16 @@ export const SidebarBody = (props: SidebarProps) => {
<For each={props.staticSections}>
{(section) => (
<Accordion.Item class="item" value={section.title}>
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Accordion.Item class={styles.accordionItem} value={section.title}>
<Accordion.Header class={styles.accordionHeader}>
<Accordion.Trigger class={styles.accordionTrigger}>
<Typography
class="section-title"
hierarchy="label"
family="mono"
size="xs"
inverted
color="tertiary"
transform="uppercase"
>
{section.title}
</Typography>
@@ -271,10 +283,11 @@ export const SidebarBody = (props: SidebarProps) => {
color="tertiary"
inverted
size="0.75rem"
in="SidebarBody-AccordionTrigger"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<Accordion.Content class={styles.accordionContent}>
<nav>
<For each={section.links || []}>
{(link) => (

View File

@@ -13,6 +13,10 @@
}
&.body {
font-size: 1rem;
line-height: 1.32;
letter-spacing: 0.005rem;
&.family-regular {
font-family: "Archivo", sans-serif;
}
@@ -21,12 +25,6 @@
font-family: "Archivo SemiCondensed", sans-serif;
}
&.size-default {
font-size: 1rem;
line-height: 1.32;
letter-spacing: 0.005rem;
}
&.size-s {
font-size: 0.875rem;
line-height: 1.32;
@@ -50,11 +48,9 @@
&.family-condensed {
font-family: "Archivo SemiCondensed", sans-serif;
&.size-default {
font-size: 1rem;
line-height: normal;
letter-spacing: 0.02rem;
}
font-size: 1rem;
line-height: normal;
letter-spacing: 0.02rem;
&.size-s {
font-size: 0.875rem;
@@ -78,11 +74,9 @@
&.family-mono {
font-family: "Commit Mono", monospace;
&.size-default {
font-size: 1rem;
line-height: normal;
letter-spacing: normal;
}
font-size: 1rem;
line-height: normal;
letter-spacing: normal;
&.size-s {
font-size: 0.875rem;
@@ -104,16 +98,14 @@
}
&.title {
font-size: 1.125rem;
line-height: 124%;
letter-spacing: 0.03375rem;
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.size-default {
font-size: 1.125rem;
line-height: 124%;
letter-spacing: 0.03375rem;
}
&.size-m {
font-size: 1.25rem;
line-height: 124%;
@@ -128,16 +120,14 @@
}
&.headline {
font-size: 1.5rem;
line-height: 116%;
letter-spacing: 0.015rem;
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.size-default {
font-size: 1.5rem;
line-height: 116%;
letter-spacing: 0.015rem;
}
&.size-m {
font-size: 1.75rem;
line-height: 116%;
@@ -164,15 +154,13 @@
}
&.teaser {
font-size: 3rem;
line-height: normal;
letter-spacing: -0.06rem;
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.size-default {
font-size: 3rem;
line-height: normal;
letter-spacing: -0.06rem;
}
}
&.align-left {
@@ -186,4 +174,31 @@
&.align-right {
text-align: right;
}
&.uppercase {
text-transform: uppercase;
}
&.lowercase {
text-transform: lowercase;
}
&.capitalize {
text-transform: capitalize;
}
}
.typography.in-Button {
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
}
.typography.in-Modal-title {
@apply mx-auto;
}
.typography.in-TagSelect-label {
@apply flex gap-2;
}
.typography.in-Select-item-label {
@apply flex w-full items-center;
}
.typography.in-SelectService-item-description {
@apply flex justify-between;
}

View File

@@ -37,7 +37,6 @@ const TypographyExamples: Component<TypographyExamplesProps> = (props) => (
<Show when={!props.colors}>
<Typography
hierarchy={props.hierarchy}
//@ts-expect-error: difficult to generify for the story
size={size}
weight={weight}
family={props.family}
@@ -51,7 +50,6 @@ const TypographyExamples: Component<TypographyExamplesProps> = (props) => (
<>
<Typography
hierarchy={props.hierarchy}
//@ts-expect-error: difficult to generify for the story
size={size}
weight={weight}
color={color}

View File

@@ -1,135 +1,99 @@
import { type JSX } from "solid-js";
import { mergeProps, type ValidComponent, type JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import cx from "classnames";
import "./Typography.css";
import { Color, fgClass } from "@/src/components/colors";
import styles from "./Typography.module.css";
import { Color } from "@/src/components/colors";
import colorsStyles from "../colors.module.css";
import { getInClasses } from "@/src/util";
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
export type Weight = "normal" | "medium" | "bold";
export type Family = "regular" | "condensed" | "mono";
export type Transform = "uppercase" | "lowercase" | "capitalize";
// type Size = "default" | "xs" | "s" | "m" | "l";
interface SizeForHierarchy {
body: {
default: string;
s: string;
xs: string;
xxs: string;
};
label: {
default: string;
s: string;
xs: string;
xxs: string;
};
headline: {
default: string;
m: string;
l: string;
xl: string;
xxl: string;
};
title: {
default: string;
m: string;
l: string;
};
teaser: {
default: string;
};
export interface SizeForHierarchy {
body: "default" | "s" | "xs" | "xxs";
headline: "default" | "m" | "l" | "xl" | "xxl";
title: "default" | "m" | "l";
label: "default" | "s" | "xs" | "xxs";
teaser: "default";
}
export interface TagForHierarchy {
body: "span" | "p" | "div";
headline: "h1" | "h2" | "h3" | "h4";
title: "h1" | "h2" | "h3" | "h4";
label: "span" | "div";
teaser: "h1" | "h2" | "h3" | "h4";
}
export type AllowedSizes<H extends Hierarchy> = keyof SizeForHierarchy[H];
const sizeHierarchyMap: SizeForHierarchy = {
body: {
default: cx("size-default"),
s: cx("size-s"),
xs: cx("size-xs"),
xxs: cx("size-xxs"),
},
headline: {
default: cx("size-default"),
m: cx("size-m"),
l: cx("size-l"),
xl: cx("size-xl"),
xxl: cx("size-xxl"),
},
title: {
default: cx("size-default"),
// xs: cx("size-xs"),
// s: cx("size-s"),
m: cx("size-m"),
l: cx("size-l"),
},
label: {
default: cx("size-default"),
s: cx("size-s"),
xs: cx("size-xs"),
xxs: cx("size-xxs"),
},
teaser: {
default: cx("size-default"),
},
};
const defaultFamilyMap: Record<Hierarchy, Family> = {
const defaultFamilyMap = {
body: "condensed",
label: "condensed",
title: "regular",
headline: "regular",
title: "regular",
label: "condensed",
teaser: "regular",
};
} as const;
const weightMap: Record<Weight, string> = {
normal: "weight-normal",
medium: "weight-medium",
bold: "weight-bold",
};
interface _TypographyProps<H extends Hierarchy> {
const defaultTagMap = {
body: "p",
headline: "h1",
title: "h2",
label: "span",
teaser: "h3",
} as const;
export interface TypographyProps<H extends Hierarchy> {
hierarchy: H;
size: AllowedSizes<H>;
color?: Color;
children: JSX.Element;
size?: SizeForHierarchy[H];
color?: Color;
weight?: Weight;
family?: Family;
inverted?: boolean;
tag?: Tag;
class?: string;
tag?: TagForHierarchy[H];
transform?: Transform;
align?: "left" | "center" | "right";
in?:
| "Button"
| "Modal-title"
| "TagSelect-label"
| "Select-item-label"
| "SelectService-item-description";
}
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
const family = () =>
`family-${props.family || defaultFamilyMap[props.hierarchy]}`;
const hierarchy = () => props.hierarchy || "body";
const size = () => sizeHierarchyMap[props.hierarchy][props.size] as string;
const weight = () => weightMap[props.weight || "normal"];
const color = () => fgClass(props.color, props.inverted);
const align = () => `align-${props.align || "left"}`;
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
const local = mergeProps(
{
size: "default",
color: "primary",
weight: "normal",
family: defaultFamilyMap[props.hierarchy],
align: "left",
tag: defaultTagMap[props.hierarchy],
} as const,
props,
);
return (
<Dynamic
component={local.tag as ValidComponent}
class={cx(
"typography",
hierarchy(),
family(),
weight(),
size(),
color(),
align(),
props.transform,
props.class,
styles.typography,
styles[local.hierarchy],
styles[`family-${local.family}`],
styles[`weight-${local.weight}`],
local.size != "default" &&
styles[
`size-${local.size as Exclude<SizeForHierarchy[H], "default">}`
],
styles[`align-${local.align}`],
local.transform && styles[local.transform],
colorsStyles[local.color],
{
[colorsStyles.inverted]: local.inverted,
},
getInClasses(styles, local.in),
)}
component={props.tag || "span"}
>
{props.children}
{local.children}
</Dynamic>
);
};
export type TypographyProps = _TypographyProps<Hierarchy>;

View File

@@ -138,22 +138,11 @@ const UpdateProgress = () => {
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
<img src={usbLogo} alt="usb logo" class="absolute top-2 z-0" />
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
<Typography
hierarchy="title"
size="default"
weight="bold"
color="inherit"
>
<Typography hierarchy="title" weight="bold" color="inherit">
Machine is being updated
</Typography>
<LoadingBar />
<Typography
hierarchy="label"
size="default"
class=""
color="secondary"
inverted
>
<Typography hierarchy="label" color="secondary" inverted>
Update {updateState()?.topic}...
</Typography>
<Button

View File

@@ -38,17 +38,13 @@ const Prose = () => (
>
Local Setup
</Typography>
<Typography
hierarchy="headline"
size="default"
weight="bold"
color="inherit"
class="text-balance"
>
Here's what you
<br />
need to do
</Typography>
<div class="text-balance">
<Typography hierarchy="headline" weight="bold" color="inherit">
Here's what you
<br />
need to do
</Typography>
</div>
</div>
</div>
<div class="flex flex-col gap-4 px-4">

View File

@@ -841,13 +841,7 @@ const InstallProgress = () => {
Machine is being installed
</Typography>
<LoadingBar />
<Typography
hierarchy="label"
size="default"
class=""
color="secondary"
inverted
>
<Typography hierarchy="label" color="secondary" inverted>
<Switch fallback={"Waiting for preparation to start..."}>
<Match when={store.install.prepareStep === "disk"}>
Configuring disk schema ...

View File

@@ -98,7 +98,7 @@ export const SelectService = (props: FlyoutProps) => {
weight="normal"
color="quaternary"
inverted
class="flex justify-between"
in="SelectService-item-description"
>
<span class="inline-block max-w-80 truncate align-middle">
{item.raw.info.manifest.description}

View File

@@ -380,7 +380,7 @@ const ConfigureRole = () => {
size="s"
weight="medium"
inverted
class="capitalize"
transform="capitalize"
>
Select {store.currentRole}
</Typography>