Merge pull request 'ui/onboarding: use css modules' (#5171) from hgl/clan-core:css into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5171
Reviewed-by: brianmcgee <brian@bmcgee.ie>
This commit is contained in:
brianmcgee
2025-09-19 09:56:37 +00:00
28 changed files with 428 additions and 271 deletions

View File

@@ -8,14 +8,14 @@ import * as ButtonStories from "./Button.stories";
Buttons have a simple hierarchy, `primary` or `secondary`, and two sizes, `default` and `s`. Buttons have a simple hierarchy, `primary` or `secondary`, and two sizes, `default` and `s`.
A `Button` can also have a label with a `startIcon`, an `endIcon` or both: A `Button` can also have a label with a `icon`, an `endIcon` or both:
```tsx ```tsx
<Button hierarchy="primary">Label</> <Button hierarchy="primary">Label</>
<Button hierarchy="secondary" size="s">Label</> <Button hierarchy="secondary" size="s">Label</>
<Button hierarchy="primary" startIcon="Flash">Label</> <Button hierarchy="primary" icon="Flash">Label</>
<Button hierarchy="primary" size="s" endIcon="Flash">Label</> <Button hierarchy="primary" size="s" endIcon="Flash">Label</>
<Button hierarchy="primary" startIcon="Flash" endIcon="Flash">Label</> <Button hierarchy="primary" icon="Flash" endIcon="Flash">Label</>
``` ```
To create a `Button` which is just an icon: To create a `Button` which is just an icon:

View File

@@ -8,11 +8,11 @@
&.s { &.s {
@apply h-[1.625rem] px-3 py-1.5 rounded-[0.125rem]; @apply h-[1.625rem] px-3 py-1.5 rounded-[0.125rem];
&:has(> .icon-start):has(> .label) { &.hasIcon {
@apply pl-2; @apply pl-2;
} }
&:has(> .icon-end):has(> .label) { &.hasEndIcon {
@apply pr-2; @apply pr-2;
} }
} }
@@ -20,11 +20,11 @@
&.xs { &.xs {
@apply h-[1.125rem] gap-0.5 p-2 rounded-[0.125rem]; @apply h-[1.125rem] gap-0.5 p-2 rounded-[0.125rem];
&:has(> .icon-start):has(> .label) { &.hasIcon {
@apply pl-1.5; @apply pl-1.5;
} }
&:has(> .icon-end):has(> .label) { &.hasEndIcon {
@apply pr-1.5; @apply pr-1.5;
} }
} }
@@ -63,10 +63,6 @@
&:disabled { &:disabled {
@apply bg-def-acc-3 border-solid border-def-3 fg-def-3 shadow-none; @apply bg-def-acc-3 border-solid border-def-3 fg-def-3 shadow-none;
} }
& > .icon {
@apply fg-inv-1;
}
} }
&.secondary { &.secondary {
@@ -108,25 +104,21 @@
&:disabled { &:disabled {
@apply bg-def-2 border-solid border-def-2 fg-def-3 shadow-none; @apply bg-def-2 border-solid border-def-2 fg-def-3 shadow-none;
} }
& > .icon {
@apply fg-def-1;
&.icon-loading {
color: #0051ff;
}
}
} }
&.icon { &.fit {
@apply w-fit;
}
&.iconOnly {
@apply p-2; @apply p-2;
} }
&:has(> .icon-start):has(> .label) { &.hasIcon {
@apply pl-3.5; @apply pl-3.5;
} }
&:has(> .icon-end):has(> .label) { &.hasEndIcon {
@apply pr-3.5; @apply pr-3.5;
} }
@@ -134,11 +126,34 @@
@apply cursor-wait; @apply cursor-wait;
} }
& > span.typography { & > .typography {
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis; @apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
} }
} }
.button.in-HostFileInput-horizontal {
@apply max-w-[18rem];
@apply grow;
}
.button.in-TagSelect {
@apply ml-auto;
}
.button.in-UpdateProgress {
@apply mt-3;
}
.button.in-InstallProgress {
@apply mt-3;
}
.button.in-FlashProgress {
@apply mt-2;
}
.button.in-CheckHardware {
@apply gap-3;
}
.button.in-ConfigureService {
@apply ml-auto;
}
/* button group */ /* button group */
.button-group .button:first-child { .button-group .button:first-child {
border-top-right-radius: 0; border-top-right-radius: 0;

View File

@@ -52,17 +52,12 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
</div> </div>
<div> <div>
<Button data-testid="default-start-icon" {...props} startIcon="Flash"> <Button data-testid="default-start-icon" {...props} icon="Flash">
Label Label
</Button> </Button>
</div> </div>
<div> <div>
<Button <Button data-testid="small-start-icon" {...props} icon="Flash" size="s">
data-testid="small-start-icon"
{...props}
startIcon="Flash"
size="s"
>
Label Label
</Button> </Button>
</div> </div>
@@ -70,7 +65,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
<Button <Button
data-testid="xsmall-start-icon" data-testid="xsmall-start-icon"
{...props} {...props}
startIcon="Flash" icon="Flash"
size="xs" size="xs"
> >
Label Label
@@ -80,7 +75,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
<Button <Button
data-testid="default-disabled-start-icon" data-testid="default-disabled-start-icon"
{...props} {...props}
startIcon="Flash" icon="Flash"
disabled={true} disabled={true}
> >
Disabled Disabled
@@ -90,7 +85,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
<Button <Button
data-testid="small-disabled-start-icon" data-testid="small-disabled-start-icon"
{...props} {...props}
startIcon="Flash" icon="Flash"
size="s" size="s"
disabled={true} disabled={true}
> >
@@ -102,7 +97,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
<Button <Button
data-testid="xsmall-disabled-start-icon" data-testid="xsmall-disabled-start-icon"
{...props} {...props}
startIcon="Flash" icon="Flash"
size="xs" size="xs"
disabled={true} disabled={true}
> >

View File

@@ -1,14 +1,16 @@
import { splitProps, type JSX } from "solid-js"; import { mergeProps, splitProps, type JSX } from "solid-js";
import cx from "classnames"; import cx from "classnames";
import { Typography } from "../Typography/Typography"; import { Typography } from "../Typography/Typography";
import { Button as KobalteButton } from "@kobalte/core/button"; import { Button as KobalteButton } from "@kobalte/core/button";
import "./Button.css"; import styles from "./Button.module.css";
import Icon, { IconVariant } from "@/src/components/Icon/Icon"; import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import { Loader } from "@/src/components/Loader/Loader"; import { Loader } from "@/src/components/Loader/Loader";
import { getInClasses, joinByDash, keepTruthy } from "@/src/util";
export type Size = "default" | "s" | "xs"; export type Size = "default" | "s" | "xs";
export type Hierarchy = "primary" | "secondary"; export type Hierarchy = "primary" | "secondary";
export type Elasticity = "default" | "fit";
export type Action = () => Promise<void>; export type Action = () => Promise<void>;
@@ -19,79 +21,78 @@ export interface ButtonProps
ghost?: boolean; ghost?: boolean;
children?: JSX.Element; children?: JSX.Element;
icon?: IconVariant; icon?: IconVariant;
startIcon?: IconVariant;
endIcon?: IconVariant; endIcon?: IconVariant;
class?: string;
loading?: boolean; loading?: boolean;
elasticity?: Elasticity;
in?:
| "HostFileInput-horizontal"
| "TagSelect"
| "UpdateProgress"
| "InstallProgress"
| "FlashProgress"
| "CheckHardware"
| "ConfigureService";
} }
const iconSizes: Record<Size, string> = {
default: "1rem",
s: "0.8125rem",
xs: "0.625rem",
};
export const Button = (props: ButtonProps) => { export const Button = (props: ButtonProps) => {
const [local, other] = splitProps(props, [ const [local, other] = splitProps(
mergeProps(
{ size: "default", hierarchy: "primary", elasticity: "default" } as const,
props,
),
[
"children", "children",
"hierarchy", "hierarchy",
"size", "size",
"ghost", "ghost",
"icon", "icon",
"startIcon",
"endIcon", "endIcon",
"class",
"loading", "loading",
]); "elasticity",
"disabled",
const size = local.size || "default"; "in",
const hierarchy = local.hierarchy || "primary"; "onClick",
],
const iconSize = iconSizes[local.size || "default"]; );
const loadingClass =
"w-4 opacity-100 mr-[revert] transition-all duration-500 ease-linear";
const idleClass =
"hidden w-0 opacity-0 top-0 left-0 -mr-2 transition-all duration-500 ease-linear";
return ( return (
<KobalteButton <KobalteButton
class={cx( class={cx(
local.class, styles.button, // default button class
"button", // default button class local.size != "default" && styles[local.size],
size, styles[local.hierarchy],
hierarchy, local.elasticity != "default" && local.elasticity,
getInClasses(styles, local.in),
{ {
icon: local.icon, [styles.iconOnly]: local.icon && !local.children,
loading: props.loading, [styles.hasIcon]: local.icon && local.children,
ghost: local.ghost, [styles.hasEndIcon]: local.endIcon && local.children,
[styles.loading]: local.loading,
[styles.ghost]: local.ghost,
}, },
)} )}
onClick={props.onClick} onClick={local.onClick}
disabled={props.disabled || props.loading} disabled={local.disabled || local.loading}
{...other} {...other}
> >
<Loader <Loader hierarchy={local.hierarchy} loading={local.loading} in="Button" />
hierarchy={hierarchy}
class={cx({ {local.icon && (
[idleClass]: !props.loading, <Icon
[loadingClass]: props.loading, icon={local.icon}
})} in={keepTruthy(
"Button",
joinByDash("Button", local.hierarchy),
local.size == "default" ? "" : joinByDash("Button", local.size),
)}
/> />
{local.startIcon && (
<Icon icon={local.startIcon} class="icon-start" size={iconSize} />
)} )}
{local.icon && !local.children && ( {local.children && (
<Icon icon={local.icon} class="icon" size={iconSize} />
)}
{local.children && !local.icon && (
<Typography <Typography
class="label" class={styles.typography}
hierarchy="label" hierarchy="label"
size={local.size || "default"} size={local.size}
inverted={local.hierarchy === "primary"} inverted={local.hierarchy === "primary"}
weight="bold" weight="bold"
tag="span" tag="span"
@@ -100,8 +101,15 @@ export const Button = (props: ButtonProps) => {
</Typography> </Typography>
)} )}
{local.endIcon && ( {local.endIcon && local.children && (
<Icon icon={local.endIcon} class="icon-end" size={iconSize} /> <Icon
icon={local.endIcon}
in={keepTruthy(
"Button",
joinByDash("Button", local.hierarchy),
local.size == "default" ? "" : joinByDash("Button", local.size),
)}
/>
)} )}
</KobalteButton> </KobalteButton>
); );

View File

@@ -1,11 +1,3 @@
.vertical_button {
@apply w-fit;
}
.horizontal_button {
@apply grow max-w-[18rem];
}
/* Vendored from tooltip */ /* Vendored from tooltip */
.tooltipContent { .tooltipContent {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none; @apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;

View File

@@ -85,14 +85,17 @@ export const HostFileInput = (props: HostFileInputProps) => {
<Button <Button
hierarchy="secondary" hierarchy="secondary"
size={styleProps.size} size={styleProps.size}
startIcon="Folder" icon="Folder"
onClick={selectFile} onClick={selectFile}
disabled={props.disabled || props.readOnly} disabled={props.disabled || props.readOnly}
class={cx( elasticity={
styleProps.orientation === "vertical" styleProps.orientation === "vertical" ? "fit" : undefined
? styles.vertical_button }
: styles.horizontal_button, in={
)} styleProps.orientation == "horizontal"
? `HostFileInput-${styleProps.orientation}`
: undefined
}
> >
{props.placeholder || "No Selection"} {props.placeholder || "No Selection"}
</Button> </Button>
@@ -115,14 +118,17 @@ export const HostFileInput = (props: HostFileInputProps) => {
</Tooltip.Portal> </Tooltip.Portal>
<Tooltip.Trigger <Tooltip.Trigger
as={Button} as={Button}
class={cx( elasticity={
props.orientation === "vertical" styleProps.orientation === "vertical" ? "fit" : undefined
? styles.vertical_button }
: styles.horizontal_button, in={
)} styleProps.orientation == "horizontal"
? `HostFileInput-${styleProps.orientation}`
: undefined
}
hierarchy="secondary" hierarchy="secondary"
size={styleProps.size} size={styleProps.size}
startIcon="Folder" icon="Folder"
onClick={selectFile} onClick={selectFile}
disabled={props.disabled || props.readOnly} disabled={props.disabled || props.readOnly}
> >

View File

@@ -20,16 +20,6 @@
@apply w-full relative; @apply w-full relative;
} }
.icon {
@apply absolute left-1.5;
top: calc(50% - 0.5rem);
&.iconSmall {
@apply left-[0.3125rem] size-[0.75rem];
top: calc(50% - 0.3125rem);
}
}
.input { .input {
@apply outline outline-1 outline-def-acc-1 bg-def-1 fg-def-1 w-full; @apply outline outline-1 outline-def-acc-1 bg-def-1 fg-def-1 w-full;
@apply px-[1.625rem] py-1.5 rounded-sm; @apply px-[1.625rem] py-1.5 rounded-sm;

View File

@@ -17,6 +17,7 @@ import { Label } from "@/src/components/Form/Label";
import { Orienter } from "@/src/components/Form/Orienter"; import { Orienter } from "@/src/components/Form/Orienter";
import { CollectionNode } from "@kobalte/core"; import { CollectionNode } from "@kobalte/core";
import styles from "./MachineTags.module.css"; import styles from "./MachineTags.module.css";
import { keepTruthy } from "@/src/util";
export interface MachineTag { export interface MachineTag {
value: string; value: string;
@@ -247,9 +248,10 @@ export const MachineTags = (props: MachineTagsProps) => {
icon="Tag" icon="Tag"
color="secondary" color="secondary"
inverted={props.inverted} inverted={props.inverted}
class={cx(styles.icon, { in={keepTruthy(
[styles.iconSmall]: props.size == "s", "MachineTags",
})} props.size == "s" && "MachineTags-s",
)}
/> />
<Combobox.Input <Combobox.Input
onKeyDown={onKeyDown} onKeyDown={onKeyDown}

View File

@@ -0,0 +1,32 @@
.icon.in-Button {
width: 1rem;
height: 1rem;
}
.icon.in-Button-primary {
@apply fg-inv-1;
}
.icon.in-Button-secondary {
@apply fg-def-1;
}
.icon.in-Button-s {
width: 0.8125rem;
height: 0.8125rem;
}
.icon.in-Button-xs {
width: 0.625rem;
height: 0.625rem;
}
.icon.in-WorkflowPanelTitle {
@apply size-9;
}
.icon.in-MachineTags {
@apply absolute left-1.5;
top: calc(50% - 0.5rem);
}
.icon.in-MachineTags-s {
@apply left-[0.3125rem] size-[0.75rem];
top: calc(50% - 0.3125rem);
}
.icon.in-ConfigureRole {
@apply ml-auto;
}

View File

@@ -1,5 +1,5 @@
import cx from "classnames"; import cx from "classnames";
import { Component, JSX, splitProps } from "solid-js"; import { Component, JSX, mergeProps, splitProps } from "solid-js";
import Address from "@/icons/address.svg"; import Address from "@/icons/address.svg";
import AI from "@/icons/ai.svg"; import AI from "@/icons/ai.svg";
@@ -55,8 +55,10 @@ import User from "@/icons/user.svg";
import WarningFilled from "@/icons/warning-filled.svg"; import WarningFilled from "@/icons/warning-filled.svg";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import styles from "./Icon.module.css";
import { Color, fgClass } from "../colors"; import { Color } from "../colors";
import colorsStyles from "../colors.module.css";
import { getInClasses } from "@/src/util";
const icons = { const icons = {
Address, Address,
@@ -119,42 +121,52 @@ const viewBoxes: Partial<Record<IconVariant, string>> = {
ClanIcon: "0 0 72 89", ClanIcon: "0 0 72 89",
}; };
type In =
| "Button"
| "Button-primary"
| "Button-secondary"
| "Button-s"
| "Button-xs"
| "MachineTags"
| "MachineTags-s"
| "ConfigureRole"
// TODO: better name
| "WorkflowPanelTitle";
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> { export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
icon: IconVariant; icon: IconVariant;
class?: string;
size?: number | string; size?: number | string;
color?: Color; color?: Color;
inverted?: boolean; inverted?: boolean;
in?: In | In[];
} }
const Icon: Component<IconProps> = (props) => { const Icon: Component<IconProps> = (props) => {
const [local, iconProps] = splitProps(props, [ const [local, iconProps] = splitProps(
"icon", mergeProps({ color: "primary", size: "1em" } as const, props),
"color", ["icon", "color", "size", "inverted", "in"],
"class", );
"size", const component = () => icons[local.icon];
"inverted",
]);
const IconComponent = () => icons[local.icon];
// we need to adjust the view box for certain icons // we need to adjust the view box for certain icons
const viewBox = () => viewBoxes[local.icon] ?? "0 0 48 48"; const viewBox = () => viewBoxes[local.icon] || "0 0 48 48";
return (
return IconComponent() ? (
<Dynamic <Dynamic
component={IconComponent()} component={component()}
class={cx("icon", local.class, fgClass(local.color, local.inverted), { class={cx(
inverted: local.inverted, styles.icon,
})} colorsStyles[local.color],
getInClasses(styles, local.in),
{
[colorsStyles.inverted]: local.inverted,
},
)}
data-icon-name={local.icon} data-icon-name={local.icon}
width={local.size || "1em"} width={local.size}
height={local.size || "1em"} height={local.size}
viewBox={viewBox()} viewBox={viewBox()}
ref={iconProps.ref} ref={iconProps.ref}
{...iconProps} {...iconProps}
/> />
) : undefined; );
}; };
export default Icon; export default Icon;

View File

@@ -53,6 +53,16 @@
animation: moveLoaderChild 1.8s ease-in-out infinite; animation: moveLoaderChild 1.8s ease-in-out infinite;
} }
.loader.in-Button {
/* FIXME: using hidden prevents transition from working, but without it, flex
creates a gap for it */
@apply hidden w-0 opacity-0 transition-all duration-500 ease-linear;
&.loading {
@apply block w-4 opacity-100 mr-[revert];
}
}
@keyframes moveLoaderWrapper { @keyframes moveLoaderWrapper {
0% { 0% {
transform: translate(0%, 0%) rotate(-45deg); transform: translate(0%, 0%) rotate(-45deg);

View File

@@ -1,4 +1,5 @@
// Loader.tsx // Loader.tsx
import { mergeProps } from "solid-js";
import styles from "./Loader.module.css"; import styles from "./Loader.module.css";
import cx from "classnames"; import cx from "classnames";
@@ -6,23 +7,28 @@ export type Hierarchy = "primary" | "secondary";
export interface LoaderProps { export interface LoaderProps {
hierarchy?: Hierarchy; hierarchy?: Hierarchy;
class?: string;
size?: "default" | "l" | "xl"; size?: "default" | "l" | "xl";
loading?: boolean;
in?: "Button";
} }
export const Loader = (props: LoaderProps) => { export const Loader = (props: LoaderProps) => {
const size = () => props.size || "default"; const local = mergeProps(
{ hierarchy: "primary", size: "default", loading: false } as const,
props,
);
return ( return (
<div <div
class={cx( class={cx(
styles.loader, styles.loader,
styles[props.hierarchy || "primary"], styles[local.hierarchy],
props.class, local.in ? styles[`in-${local.in}` as `in-${typeof local.in}`] : "",
{ {
[styles.sizeDefault]: size() === "default", [styles.sizeDefault]: local.size === "default",
[styles.sizeLarge]: size() === "l", [styles.sizeLarge]: local.size === "l",
[styles.sizeExtraLarge]: size() === "xl", [styles.sizeExtraLarge]: local.size === "xl",
[styles.loading]: local.loading,
}, },
)} )}
> >

View File

@@ -189,6 +189,7 @@ export const Multiple: Story = {
// Test with lots of modules // Test with lots of modules
options: machinesAndTags, options: machinesAndTags,
placeholder: "Search for Machine or Tags", placeholder: "Search for Machine or Tags",
values: [],
renderItem: (item: MachineOrTag, opts: ItemRenderOptions) => { renderItem: (item: MachineOrTag, opts: ItemRenderOptions) => {
console.log("Rendering item:", item, "opts", opts); console.log("Rendering item:", item, "opts", opts);
return ( return (
@@ -223,13 +224,14 @@ export const Multiple: Story = {
)} )}
</Show> </Show>
</Combobox.ItemLabel> </Combobox.ItemLabel>
<div class="ml-auto">
<Icon <Icon
class="ml-auto"
icon={item.type === "machine" ? "Machine" : "Tag"} icon={item.type === "machine" ? "Machine" : "Tag"}
color="quaternary" color="quaternary"
inverted inverted
/> />
</div> </div>
</div>
); );
}, },
}, },

View File

@@ -41,8 +41,8 @@ export function TagSelect<T extends { value: unknown }>(
icon="Settings" icon="Settings"
hierarchy="primary" hierarchy="primary"
ghost ghost
class="ml-auto"
size="xs" size="xs"
in="TagSelect"
/> />
</div> </div>
<Combobox<T> <Combobox<T>

View File

@@ -102,7 +102,7 @@ const Machines = () => {
<Button <Button
hierarchy="primary" hierarchy="primary"
size="s" size="s"
startIcon="Machine" icon="Machine"
onClick={() => ctx.setShowAddMachine(true)} onClick={() => ctx.setShowAddMachine(true)}
> >
Add machine Add machine

View File

@@ -98,7 +98,7 @@ export const SidebarHeader = () => {
hierarchy="secondary" hierarchy="secondary"
ghost ghost
size="xs" size="xs"
startIcon="Plus" icon="Plus"
onClick={() => navigateToOnboarding(navigate, true)} onClick={() => navigateToOnboarding(navigate, true)}
> >
Add Add

View File

@@ -88,7 +88,7 @@ export function SidebarSectionForm<
<Button <Button
hierarchy="primary" hierarchy="primary"
size="xs" size="xs"
startIcon="Checkmark" icon="Checkmark"
ghost ghost
type="submit" type="submit"
> >

View File

@@ -0,0 +1,37 @@
.primary {
@apply fg-def-1;
&.inverted {
@apply fg-inv-1;
}
}
.secondary {
@apply fg-def-2;
&.inverted {
@apply fg-inv-2;
}
}
.tertiary {
@apply fg-def-3;
&.inverted {
@apply fg-inv-3;
}
}
.quaternary {
@apply fg-def-4;
&.inverted {
@apply fg-inv-4;
}
}
.error {
@apply fg-semantic-error-4;
&.inverted {
@apply fg-semantic-error-1;
}
}
.inherit {
@apply text-inherit;
}

View File

@@ -195,7 +195,7 @@ export const ClanSettingsModal = (props: ClanSettingsModalProps) => {
<Button <Button
hierarchy="primary" hierarchy="primary"
size="s" size="s"
startIcon="Trash" icon="Trash"
disabled={removeDisabled()} disabled={removeDisabled()}
onClick={onRemove} onClick={onRemove}
> >

View File

@@ -68,7 +68,7 @@ export const ListClansModal = (props: ListClansModalProps) => {
hierarchy="secondary" hierarchy="secondary"
ghost ghost
size="s" size="s"
startIcon="Plus" icon="Plus"
onClick={() => { onClick={() => {
props.onClose?.(); props.onClose?.();
navigateToOnboarding(navigate, true); navigateToOnboarding(navigate, true);

View File

@@ -1,70 +0,0 @@
main#welcome {
@apply absolute top-0 left-0;
@apply flex items-center justify-center;
@apply min-h-screen w-full;
div.background {
.layer-1 {
@apply -z-30;
background:
url("./background.png") 0 -69.032px / 100% 119.049% no-repeat,
url("./background.png") 50% / cover no-repeat;
}
.layer-2 {
@apply -z-20;
background: #103131;
mix-blend-mode: screen;
}
.layer-3 {
@apply -z-10;
background: #749095;
mix-blend-mode: soft-light;
}
.layer-1,
.layer-2,
.layer-3 {
@apply absolute top-0 left-0 w-full h-full;
}
svg[data-logo-name="Clan"] {
@apply w-16;
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
}
button.list-clans {
@apply absolute bottom-28 right-0 transform -translate-x-1/2;
}
}
& > div.container {
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
& > div.welcome {
@apply flex flex-col w-80 gap-y-6;
& > div.separator {
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
}
}
& > div.setup {
@apply flex flex-col w-[33rem] gap-y-5;
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
& > div.header {
@apply flex items-center justify-start gap-x-2;
}
form {
@apply flex flex-col gap-y-5;
& > div.form-controls {
@apply flex justify-end pt-6;
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
.onboarding {
@apply absolute top-0 left-0;
@apply flex items-center justify-center;
@apply min-h-screen w-full;
}
.background {
svg[data-logo-name="Clan"] {
@apply w-16;
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
}
}
.backgroundLayer1 {
@apply -z-30;
background:
url("./background.png") 0 -69.032px / 100% 119.049% no-repeat,
url("./background.png") 50% / cover no-repeat;
}
.backgroundLayer2 {
@apply -z-20;
background: #103131;
mix-blend-mode: screen;
}
.backgroundLayer3 {
@apply -z-10;
background: #749095;
mix-blend-mode: soft-light;
}
.backgroundLayer1,
.backgroundLayer2,
.backgroundLayer3 {
@apply absolute top-0 left-0 w-full h-full;
}
.listClans {
@apply absolute bottom-28 right-0 transform -translate-x-1/2;
}
.container {
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
}
.welcome {
@apply flex flex-col w-80 gap-y-6;
}
.welcomeSeparator {
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
}
.setup {
@apply flex flex-col w-[33rem] gap-y-5;
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
form {
@apply flex flex-col gap-y-5;
}
}
.setupHeader {
@apply flex items-center justify-start gap-x-2;
}
.setupFormControls {
@apply flex justify-end pt-6;
}

View File

@@ -12,7 +12,7 @@ import {
useNavigate, useNavigate,
useSearchParams, useSearchParams,
} from "@solidjs/router"; } from "@solidjs/router";
import "./Onboarding.css"; import styles from "./Onboarding.module.css";
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 { Alert } from "@/src/components/Alert/Alert"; import { Alert } from "@/src/components/Alert/Alert";
@@ -66,21 +66,22 @@ const background = (props: { state: State; form: FormStore<SetupForm> }) => {
const [showModal, setShowModal] = createSignal(false); const [showModal, setShowModal] = createSignal(false);
return ( return (
<div class="background"> <div class={styles.background}>
<div class="layer-1" /> <div class={styles.backgroundLayer1} />
<div class="layer-2" /> <div class={styles.backgroundLayer2} />
<div class="layer-3" /> <div class={styles.backgroundLayer3} />
<Logo variant="Clan" inverted /> <Logo variant="Clan" inverted />
<div class={styles.listClans}>
<Button <Button
class="list-clans"
hierarchy="primary" hierarchy="primary"
ghost ghost
size="s" size="s"
startIcon="Grid" icon="Grid"
onClick={() => setShowModal(true)} onClick={() => setShowModal(true)}
> >
All Clans All Clans
</Button> </Button>
</div>
<Show when={showModal()}> <Show when={showModal()}>
<ListClansModal onClose={() => setShowModal(false)} /> <ListClansModal onClose={() => setShowModal(false)} />
</Show> </Show>
@@ -113,7 +114,7 @@ const welcome = (props: {
}; };
return ( return (
<div class="welcome"> <div class={styles.welcome}>
<Typography <Typography
hierarchy="headline" hierarchy="headline"
size="xxl" size="xxl"
@@ -144,7 +145,7 @@ const welcome = (props: {
> >
Start building Start building
</Button> </Button>
<div class="separator"> <div class={styles.welcomeSeparator}>
<Divider orientation="horizontal" /> <Divider orientation="horizontal" />
<Typography <Typography
hierarchy="body" hierarchy="body"
@@ -285,9 +286,9 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
}; };
return ( return (
<main id="welcome"> <main class={styles.onboarding}>
{background({ form: setupForm, state: state() })} {background({ form: setupForm, state: state() })}
<div class="container"> <div class={styles.container}>
<Switch> <Switch>
<Match when={state() === "welcome"}> <Match when={state() === "welcome"}>
{welcome({ {welcome({
@@ -298,8 +299,8 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
</Match> </Match>
<Match when={state() === "setup"}> <Match when={state() === "setup"}>
<div class="setup"> <div class={styles.setup}>
<div class="header"> <div class={styles.setupHeader}>
<Button <Button
hierarchy="secondary" hierarchy="secondary"
ghost={true} ghost={true}
@@ -377,7 +378,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
</Field> </Field>
</Fieldset> </Fieldset>
<div class="form-controls"> <div class={styles.setupFormControls}>
<Button <Button
type="submit" type="submit"
hierarchy="primary" hierarchy="primary"

View File

@@ -38,3 +38,52 @@ export const removeEmptyStrings = <T>(obj: T): T => {
return obj; return obj;
}; };
// Join truthy values with dashes
// joinByDash("button", "", false, null, "x")'s return type is "button-x"
export const joinByDash = <
T extends readonly (string | null | undefined | false)[],
>(
...args: T
): DashJoined<FilterFalsy<T>> => {
return keepTruthy(...args).join("-") as DashJoined<FilterFalsy<T>>;
};
// Turn a component's "in" attribute value to a list of css module class names
export const getInClasses = <T extends Record<string, string>, U>(
styles: T,
localIn?: U | U[],
): string[] => {
if (!localIn) {
return [""];
}
let localIns: U[];
if (!Array.isArray(localIn)) {
localIns = [localIn];
} else {
localIns = localIn;
}
return localIns.map((localIn) => styles[`in-${localIn}`]);
};
export const keepTruthy = <T>(...items: T[]): FilterFalsy<T[]> =>
items.filter(Boolean) as FilterFalsy<T[]>;
type FilterFalsy<T extends readonly unknown[]> = T extends readonly [
infer Head,
...infer Tail,
]
? Head extends "" | null | undefined | false
? FilterFalsy<Tail>
: [Head, ...FilterFalsy<Tail>]
: [];
type DashJoined<T extends readonly string[]> = T extends readonly [infer First]
? First
: T extends readonly [infer First, ...infer Rest]
? First extends string
? Rest extends readonly string[]
? `${First}-${DashJoined<Rest>}`
: never
: never
: "";

View File

@@ -158,8 +158,9 @@ const UpdateProgress = () => {
</Typography> </Typography>
<Button <Button
hierarchy="primary" hierarchy="primary"
class="mt-3 w-fit" elasticity="fit"
size="s" size="s"
in="UpdateProgress"
onClick={handleCancel} onClick={handleCancel}
> >
Cancel Cancel
@@ -180,7 +181,7 @@ const UpdateDone = (props: UpdateDoneProps) => {
<div class="flex size-full flex-col items-center justify-center bg-inv-4"> <div class="flex size-full flex-col items-center justify-center bg-inv-4">
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1"> <div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" in="WorkflowPanelTitle" />
</div> </div>
<Typography <Typography
hierarchy="title" hierarchy="title"

View File

@@ -349,8 +349,9 @@ const FlashProgress = () => {
<LoadingBar /> <LoadingBar />
<Button <Button
hierarchy="primary" hierarchy="primary"
class="mt-2 w-fit" elasticity="fit"
size="s" size="s"
in="FlashProgress"
onClick={handleCancel} onClick={handleCancel}
> >
Cancel Cancel
@@ -365,7 +366,7 @@ const FlashDone = () => {
<div class="flex size-full flex-col items-center justify-end bg-inv-4"> <div class="flex size-full flex-col items-center justify-end bg-inv-4">
<div class="flex size-full max-w-md flex-col items-center justify-center gap-3 pt-6 fg-inv-1"> <div class="flex size-full max-w-md flex-col items-center justify-center gap-3 pt-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" in="WorkflowPanelTitle" />
</div> </div>
<Typography <Typography
hierarchy="title" hierarchy="title"

View File

@@ -300,10 +300,10 @@ const CheckHardware = () => {
<Button <Button
disabled={hardwareQuery.isLoading || updatingHardwareReport()} disabled={hardwareQuery.isLoading || updatingHardwareReport()}
hierarchy="secondary" hierarchy="secondary"
startIcon="Report" icon="Report"
onClick={handleUpdateSummary}
class="flex gap-3"
loading={hardwareQuery.isFetching || updatingHardwareReport()} loading={hardwareQuery.isFetching || updatingHardwareReport()}
in="CheckHardware"
onClick={handleUpdateSummary}
> >
Update hardware report Update hardware report
</Button> </Button>
@@ -882,8 +882,9 @@ const InstallProgress = () => {
</Typography> </Typography>
<Button <Button
hierarchy="primary" hierarchy="primary"
class="mt-3 w-fit" elasticity="fit"
size="s" size="s"
in="InstallProgress"
onClick={handleCancel} onClick={handleCancel}
> >
Cancel Cancel
@@ -904,7 +905,7 @@ const InstallDone = (props: InstallDoneProps) => {
<div class="flex size-full flex-col items-center justify-center bg-inv-4"> <div class="flex size-full flex-col items-center justify-center bg-inv-4">
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1"> <div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" in="WorkflowPanelTitle" />
</div> </div>
<Typography <Typography
hierarchy="title" hierarchy="title"

View File

@@ -219,7 +219,7 @@ const ConfigureService = () => {
color="primary" color="primary"
ghost ghost
size="s" size="s"
class="ml-auto" in="ConfigureService"
onClick={() => store.close()} onClick={() => store.close()}
/> />
</div> </div>
@@ -419,10 +419,10 @@ const ConfigureRole = () => {
</Show> </Show>
</Combobox.ItemLabel> </Combobox.ItemLabel>
<Icon <Icon
class="ml-auto"
icon={item.type === "machine" ? "Machine" : "Tag"} icon={item.type === "machine" ? "Machine" : "Tag"}
color="quaternary" color="quaternary"
inverted inverted
in="ConfigureRole"
/> />
</div> </div>
)} )}