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:
@@ -8,14 +8,14 @@ import * as ButtonStories from "./Button.stories";
|
||||
|
||||
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
|
||||
<Button hierarchy="primary">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" startIcon="Flash" endIcon="Flash">Label</>
|
||||
<Button hierarchy="primary" icon="Flash" endIcon="Flash">Label</>
|
||||
```
|
||||
|
||||
To create a `Button` which is just an icon:
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
&.s {
|
||||
@apply h-[1.625rem] px-3 py-1.5 rounded-[0.125rem];
|
||||
|
||||
&:has(> .icon-start):has(> .label) {
|
||||
&.hasIcon {
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
&:has(> .icon-end):has(> .label) {
|
||||
&.hasEndIcon {
|
||||
@apply pr-2;
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,11 @@
|
||||
&.xs {
|
||||
@apply h-[1.125rem] gap-0.5 p-2 rounded-[0.125rem];
|
||||
|
||||
&:has(> .icon-start):has(> .label) {
|
||||
&.hasIcon {
|
||||
@apply pl-1.5;
|
||||
}
|
||||
|
||||
&:has(> .icon-end):has(> .label) {
|
||||
&.hasEndIcon {
|
||||
@apply pr-1.5;
|
||||
}
|
||||
}
|
||||
@@ -63,10 +63,6 @@
|
||||
&:disabled {
|
||||
@apply bg-def-acc-3 border-solid border-def-3 fg-def-3 shadow-none;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
@apply fg-inv-1;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@@ -108,25 +104,21 @@
|
||||
&:disabled {
|
||||
@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;
|
||||
}
|
||||
|
||||
&:has(> .icon-start):has(> .label) {
|
||||
&.hasIcon {
|
||||
@apply pl-3.5;
|
||||
}
|
||||
|
||||
&:has(> .icon-end):has(> .label) {
|
||||
&.hasEndIcon {
|
||||
@apply pr-3.5;
|
||||
}
|
||||
|
||||
@@ -134,11 +126,34 @@
|
||||
@apply cursor-wait;
|
||||
}
|
||||
|
||||
& > span.typography {
|
||||
& > .typography {
|
||||
@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:first-child {
|
||||
border-top-right-radius: 0;
|
||||
@@ -52,17 +52,12 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button data-testid="default-start-icon" {...props} startIcon="Flash">
|
||||
<Button data-testid="default-start-icon" {...props} icon="Flash">
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
size="s"
|
||||
>
|
||||
<Button data-testid="small-start-icon" {...props} icon="Flash" size="s">
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
@@ -70,7 +65,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
<Button
|
||||
data-testid="xsmall-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
icon="Flash"
|
||||
size="xs"
|
||||
>
|
||||
Label
|
||||
@@ -80,7 +75,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
<Button
|
||||
data-testid="default-disabled-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
icon="Flash"
|
||||
disabled={true}
|
||||
>
|
||||
Disabled
|
||||
@@ -90,7 +85,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
<Button
|
||||
data-testid="small-disabled-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
icon="Flash"
|
||||
size="s"
|
||||
disabled={true}
|
||||
>
|
||||
@@ -102,7 +97,7 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
<Button
|
||||
data-testid="xsmall-disabled-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
icon="Flash"
|
||||
size="xs"
|
||||
disabled={true}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { mergeProps, splitProps, type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
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 { Loader } from "@/src/components/Loader/Loader";
|
||||
import { getInClasses, joinByDash, keepTruthy } from "@/src/util";
|
||||
|
||||
export type Size = "default" | "s" | "xs";
|
||||
export type Hierarchy = "primary" | "secondary";
|
||||
export type Elasticity = "default" | "fit";
|
||||
|
||||
export type Action = () => Promise<void>;
|
||||
|
||||
@@ -19,79 +21,78 @@ export interface ButtonProps
|
||||
ghost?: boolean;
|
||||
children?: JSX.Element;
|
||||
icon?: IconVariant;
|
||||
startIcon?: IconVariant;
|
||||
endIcon?: IconVariant;
|
||||
class?: string;
|
||||
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) => {
|
||||
const [local, other] = splitProps(props, [
|
||||
const [local, other] = splitProps(
|
||||
mergeProps(
|
||||
{ size: "default", hierarchy: "primary", elasticity: "default" } as const,
|
||||
props,
|
||||
),
|
||||
[
|
||||
"children",
|
||||
"hierarchy",
|
||||
"size",
|
||||
"ghost",
|
||||
"icon",
|
||||
"startIcon",
|
||||
"endIcon",
|
||||
"class",
|
||||
"loading",
|
||||
]);
|
||||
|
||||
const size = local.size || "default";
|
||||
const hierarchy = local.hierarchy || "primary";
|
||||
|
||||
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";
|
||||
"elasticity",
|
||||
"disabled",
|
||||
"in",
|
||||
"onClick",
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<KobalteButton
|
||||
class={cx(
|
||||
local.class,
|
||||
"button", // default button class
|
||||
size,
|
||||
hierarchy,
|
||||
styles.button, // default button class
|
||||
local.size != "default" && styles[local.size],
|
||||
styles[local.hierarchy],
|
||||
local.elasticity != "default" && local.elasticity,
|
||||
getInClasses(styles, local.in),
|
||||
{
|
||||
icon: local.icon,
|
||||
loading: props.loading,
|
||||
ghost: local.ghost,
|
||||
[styles.iconOnly]: local.icon && !local.children,
|
||||
[styles.hasIcon]: local.icon && local.children,
|
||||
[styles.hasEndIcon]: local.endIcon && local.children,
|
||||
[styles.loading]: local.loading,
|
||||
[styles.ghost]: local.ghost,
|
||||
},
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled || props.loading}
|
||||
onClick={local.onClick}
|
||||
disabled={local.disabled || local.loading}
|
||||
{...other}
|
||||
>
|
||||
<Loader
|
||||
hierarchy={hierarchy}
|
||||
class={cx({
|
||||
[idleClass]: !props.loading,
|
||||
[loadingClass]: props.loading,
|
||||
})}
|
||||
<Loader hierarchy={local.hierarchy} loading={local.loading} in="Button" />
|
||||
|
||||
{local.icon && (
|
||||
<Icon
|
||||
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 && (
|
||||
<Icon icon={local.icon} class="icon" size={iconSize} />
|
||||
)}
|
||||
|
||||
{local.children && !local.icon && (
|
||||
{local.children && (
|
||||
<Typography
|
||||
class="label"
|
||||
class={styles.typography}
|
||||
hierarchy="label"
|
||||
size={local.size || "default"}
|
||||
size={local.size}
|
||||
inverted={local.hierarchy === "primary"}
|
||||
weight="bold"
|
||||
tag="span"
|
||||
@@ -100,8 +101,15 @@ export const Button = (props: ButtonProps) => {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{local.endIcon && (
|
||||
<Icon icon={local.endIcon} class="icon-end" size={iconSize} />
|
||||
{local.endIcon && local.children && (
|
||||
<Icon
|
||||
icon={local.endIcon}
|
||||
in={keepTruthy(
|
||||
"Button",
|
||||
joinByDash("Button", local.hierarchy),
|
||||
local.size == "default" ? "" : joinByDash("Button", local.size),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</KobalteButton>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
.vertical_button {
|
||||
@apply w-fit;
|
||||
}
|
||||
|
||||
.horizontal_button {
|
||||
@apply grow max-w-[18rem];
|
||||
}
|
||||
|
||||
/* Vendored from tooltip */
|
||||
.tooltipContent {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
@@ -85,14 +85,17 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
size={styleProps.size}
|
||||
startIcon="Folder"
|
||||
icon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
class={cx(
|
||||
styleProps.orientation === "vertical"
|
||||
? styles.vertical_button
|
||||
: styles.horizontal_button,
|
||||
)}
|
||||
elasticity={
|
||||
styleProps.orientation === "vertical" ? "fit" : undefined
|
||||
}
|
||||
in={
|
||||
styleProps.orientation == "horizontal"
|
||||
? `HostFileInput-${styleProps.orientation}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{props.placeholder || "No Selection"}
|
||||
</Button>
|
||||
@@ -115,14 +118,17 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
</Tooltip.Portal>
|
||||
<Tooltip.Trigger
|
||||
as={Button}
|
||||
class={cx(
|
||||
props.orientation === "vertical"
|
||||
? styles.vertical_button
|
||||
: styles.horizontal_button,
|
||||
)}
|
||||
elasticity={
|
||||
styleProps.orientation === "vertical" ? "fit" : undefined
|
||||
}
|
||||
in={
|
||||
styleProps.orientation == "horizontal"
|
||||
? `HostFileInput-${styleProps.orientation}`
|
||||
: undefined
|
||||
}
|
||||
hierarchy="secondary"
|
||||
size={styleProps.size}
|
||||
startIcon="Folder"
|
||||
icon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
>
|
||||
|
||||
@@ -20,16 +20,6 @@
|
||||
@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 {
|
||||
@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;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Label } from "@/src/components/Form/Label";
|
||||
import { Orienter } from "@/src/components/Form/Orienter";
|
||||
import { CollectionNode } from "@kobalte/core";
|
||||
import styles from "./MachineTags.module.css";
|
||||
import { keepTruthy } from "@/src/util";
|
||||
|
||||
export interface MachineTag {
|
||||
value: string;
|
||||
@@ -247,9 +248,10 @@ export const MachineTags = (props: MachineTagsProps) => {
|
||||
icon="Tag"
|
||||
color="secondary"
|
||||
inverted={props.inverted}
|
||||
class={cx(styles.icon, {
|
||||
[styles.iconSmall]: props.size == "s",
|
||||
})}
|
||||
in={keepTruthy(
|
||||
"MachineTags",
|
||||
props.size == "s" && "MachineTags-s",
|
||||
)}
|
||||
/>
|
||||
<Combobox.Input
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
32
pkgs/clan-app/ui/src/components/Icon/Icon.module.css
Normal file
32
pkgs/clan-app/ui/src/components/Icon/Icon.module.css
Normal 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;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
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 AI from "@/icons/ai.svg";
|
||||
@@ -55,8 +55,10 @@ import User from "@/icons/user.svg";
|
||||
import WarningFilled from "@/icons/warning-filled.svg";
|
||||
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
import { Color, fgClass } from "../colors";
|
||||
import styles from "./Icon.module.css";
|
||||
import { Color } from "../colors";
|
||||
import colorsStyles from "../colors.module.css";
|
||||
import { getInClasses } from "@/src/util";
|
||||
|
||||
const icons = {
|
||||
Address,
|
||||
@@ -119,42 +121,52 @@ const viewBoxes: Partial<Record<IconVariant, string>> = {
|
||||
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> {
|
||||
icon: IconVariant;
|
||||
class?: string;
|
||||
size?: number | string;
|
||||
color?: Color;
|
||||
inverted?: boolean;
|
||||
in?: In | In[];
|
||||
}
|
||||
|
||||
const Icon: Component<IconProps> = (props) => {
|
||||
const [local, iconProps] = splitProps(props, [
|
||||
"icon",
|
||||
"color",
|
||||
"class",
|
||||
"size",
|
||||
"inverted",
|
||||
]);
|
||||
|
||||
const IconComponent = () => icons[local.icon];
|
||||
|
||||
const [local, iconProps] = splitProps(
|
||||
mergeProps({ color: "primary", size: "1em" } as const, props),
|
||||
["icon", "color", "size", "inverted", "in"],
|
||||
);
|
||||
const component = () => icons[local.icon];
|
||||
// we need to adjust the view box for certain icons
|
||||
const viewBox = () => viewBoxes[local.icon] ?? "0 0 48 48";
|
||||
|
||||
return IconComponent() ? (
|
||||
const viewBox = () => viewBoxes[local.icon] || "0 0 48 48";
|
||||
return (
|
||||
<Dynamic
|
||||
component={IconComponent()}
|
||||
class={cx("icon", local.class, fgClass(local.color, local.inverted), {
|
||||
inverted: local.inverted,
|
||||
})}
|
||||
component={component()}
|
||||
class={cx(
|
||||
styles.icon,
|
||||
colorsStyles[local.color],
|
||||
getInClasses(styles, local.in),
|
||||
{
|
||||
[colorsStyles.inverted]: local.inverted,
|
||||
},
|
||||
)}
|
||||
data-icon-name={local.icon}
|
||||
width={local.size || "1em"}
|
||||
height={local.size || "1em"}
|
||||
width={local.size}
|
||||
height={local.size}
|
||||
viewBox={viewBox()}
|
||||
ref={iconProps.ref}
|
||||
{...iconProps}
|
||||
/>
|
||||
) : undefined;
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
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 {
|
||||
0% {
|
||||
transform: translate(0%, 0%) rotate(-45deg);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Loader.tsx
|
||||
import { mergeProps } from "solid-js";
|
||||
import styles from "./Loader.module.css";
|
||||
import cx from "classnames";
|
||||
|
||||
@@ -6,23 +7,28 @@ export type Hierarchy = "primary" | "secondary";
|
||||
|
||||
export interface LoaderProps {
|
||||
hierarchy?: Hierarchy;
|
||||
class?: string;
|
||||
size?: "default" | "l" | "xl";
|
||||
loading?: boolean;
|
||||
in?: "Button";
|
||||
}
|
||||
|
||||
export const Loader = (props: LoaderProps) => {
|
||||
const size = () => props.size || "default";
|
||||
const local = mergeProps(
|
||||
{ hierarchy: "primary", size: "default", loading: false } as const,
|
||||
props,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cx(
|
||||
styles.loader,
|
||||
styles[props.hierarchy || "primary"],
|
||||
props.class,
|
||||
styles[local.hierarchy],
|
||||
local.in ? styles[`in-${local.in}` as `in-${typeof local.in}`] : "",
|
||||
{
|
||||
[styles.sizeDefault]: size() === "default",
|
||||
[styles.sizeLarge]: size() === "l",
|
||||
[styles.sizeExtraLarge]: size() === "xl",
|
||||
[styles.sizeDefault]: local.size === "default",
|
||||
[styles.sizeLarge]: local.size === "l",
|
||||
[styles.sizeExtraLarge]: local.size === "xl",
|
||||
[styles.loading]: local.loading,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -189,6 +189,7 @@ export const Multiple: Story = {
|
||||
// Test with lots of modules
|
||||
options: machinesAndTags,
|
||||
placeholder: "Search for Machine or Tags",
|
||||
values: [],
|
||||
renderItem: (item: MachineOrTag, opts: ItemRenderOptions) => {
|
||||
console.log("Rendering item:", item, "opts", opts);
|
||||
return (
|
||||
@@ -223,13 +224,14 @@ export const Multiple: Story = {
|
||||
)}
|
||||
</Show>
|
||||
</Combobox.ItemLabel>
|
||||
<div class="ml-auto">
|
||||
<Icon
|
||||
class="ml-auto"
|
||||
icon={item.type === "machine" ? "Machine" : "Tag"}
|
||||
color="quaternary"
|
||||
inverted
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,8 +41,8 @@ export function TagSelect<T extends { value: unknown }>(
|
||||
icon="Settings"
|
||||
hierarchy="primary"
|
||||
ghost
|
||||
class="ml-auto"
|
||||
size="xs"
|
||||
in="TagSelect"
|
||||
/>
|
||||
</div>
|
||||
<Combobox<T>
|
||||
|
||||
@@ -102,7 +102,7 @@ const Machines = () => {
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
size="s"
|
||||
startIcon="Machine"
|
||||
icon="Machine"
|
||||
onClick={() => ctx.setShowAddMachine(true)}
|
||||
>
|
||||
Add machine
|
||||
|
||||
@@ -98,7 +98,7 @@ export const SidebarHeader = () => {
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
size="xs"
|
||||
startIcon="Plus"
|
||||
icon="Plus"
|
||||
onClick={() => navigateToOnboarding(navigate, true)}
|
||||
>
|
||||
Add
|
||||
|
||||
@@ -88,7 +88,7 @@ export function SidebarSectionForm<
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
size="xs"
|
||||
startIcon="Checkmark"
|
||||
icon="Checkmark"
|
||||
ghost
|
||||
type="submit"
|
||||
>
|
||||
|
||||
37
pkgs/clan-app/ui/src/components/colors.module.css
Normal file
37
pkgs/clan-app/ui/src/components/colors.module.css
Normal 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;
|
||||
}
|
||||
@@ -195,7 +195,7 @@ export const ClanSettingsModal = (props: ClanSettingsModalProps) => {
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
size="s"
|
||||
startIcon="Trash"
|
||||
icon="Trash"
|
||||
disabled={removeDisabled()}
|
||||
onClick={onRemove}
|
||||
>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const ListClansModal = (props: ListClansModalProps) => {
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
size="s"
|
||||
startIcon="Plus"
|
||||
icon="Plus"
|
||||
onClick={() => {
|
||||
props.onClose?.();
|
||||
navigateToOnboarding(navigate, true);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
pkgs/clan-app/ui/src/routes/Onboarding/Onboarding.module.css
Normal file
67
pkgs/clan-app/ui/src/routes/Onboarding/Onboarding.module.css
Normal 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;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from "@solidjs/router";
|
||||
import "./Onboarding.css";
|
||||
import styles from "./Onboarding.module.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
@@ -66,21 +66,22 @@ const background = (props: { state: State; form: FormStore<SetupForm> }) => {
|
||||
const [showModal, setShowModal] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
<div class={styles.background}>
|
||||
<div class={styles.backgroundLayer1} />
|
||||
<div class={styles.backgroundLayer2} />
|
||||
<div class={styles.backgroundLayer3} />
|
||||
<Logo variant="Clan" inverted />
|
||||
<div class={styles.listClans}>
|
||||
<Button
|
||||
class="list-clans"
|
||||
hierarchy="primary"
|
||||
ghost
|
||||
size="s"
|
||||
startIcon="Grid"
|
||||
icon="Grid"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
All Clans
|
||||
</Button>
|
||||
</div>
|
||||
<Show when={showModal()}>
|
||||
<ListClansModal onClose={() => setShowModal(false)} />
|
||||
</Show>
|
||||
@@ -113,7 +114,7 @@ const welcome = (props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="welcome">
|
||||
<div class={styles.welcome}>
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="xxl"
|
||||
@@ -144,7 +145,7 @@ const welcome = (props: {
|
||||
>
|
||||
Start building
|
||||
</Button>
|
||||
<div class="separator">
|
||||
<div class={styles.welcomeSeparator}>
|
||||
<Divider orientation="horizontal" />
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
@@ -285,9 +286,9 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<main id="welcome">
|
||||
<main class={styles.onboarding}>
|
||||
{background({ form: setupForm, state: state() })}
|
||||
<div class="container">
|
||||
<div class={styles.container}>
|
||||
<Switch>
|
||||
<Match when={state() === "welcome"}>
|
||||
{welcome({
|
||||
@@ -298,8 +299,8 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "setup"}>
|
||||
<div class="setup">
|
||||
<div class="header">
|
||||
<div class={styles.setup}>
|
||||
<div class={styles.setupHeader}>
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost={true}
|
||||
@@ -377,7 +378,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<div class="form-controls">
|
||||
<div class={styles.setupFormControls}>
|
||||
<Button
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
|
||||
@@ -38,3 +38,52 @@ export const removeEmptyStrings = <T>(obj: T): T => {
|
||||
|
||||
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
|
||||
: "";
|
||||
|
||||
@@ -158,8 +158,9 @@ const UpdateProgress = () => {
|
||||
</Typography>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
class="mt-3 w-fit"
|
||||
elasticity="fit"
|
||||
size="s"
|
||||
in="UpdateProgress"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
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 w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
|
||||
<div class="rounded-full bg-semantic-success-4">
|
||||
<Icon icon="Checkmark" class="size-9" />
|
||||
<Icon icon="Checkmark" in="WorkflowPanelTitle" />
|
||||
</div>
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -349,8 +349,9 @@ const FlashProgress = () => {
|
||||
<LoadingBar />
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
class="mt-2 w-fit"
|
||||
elasticity="fit"
|
||||
size="s"
|
||||
in="FlashProgress"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
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 max-w-md flex-col items-center justify-center gap-3 pt-6 fg-inv-1">
|
||||
<div class="rounded-full bg-semantic-success-4">
|
||||
<Icon icon="Checkmark" class="size-9" />
|
||||
<Icon icon="Checkmark" in="WorkflowPanelTitle" />
|
||||
</div>
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -300,10 +300,10 @@ const CheckHardware = () => {
|
||||
<Button
|
||||
disabled={hardwareQuery.isLoading || updatingHardwareReport()}
|
||||
hierarchy="secondary"
|
||||
startIcon="Report"
|
||||
onClick={handleUpdateSummary}
|
||||
class="flex gap-3"
|
||||
icon="Report"
|
||||
loading={hardwareQuery.isFetching || updatingHardwareReport()}
|
||||
in="CheckHardware"
|
||||
onClick={handleUpdateSummary}
|
||||
>
|
||||
Update hardware report
|
||||
</Button>
|
||||
@@ -882,8 +882,9 @@ const InstallProgress = () => {
|
||||
</Typography>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
class="mt-3 w-fit"
|
||||
elasticity="fit"
|
||||
size="s"
|
||||
in="InstallProgress"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
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 w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
|
||||
<div class="rounded-full bg-semantic-success-4">
|
||||
<Icon icon="Checkmark" class="size-9" />
|
||||
<Icon icon="Checkmark" in="WorkflowPanelTitle" />
|
||||
</div>
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -219,7 +219,7 @@ const ConfigureService = () => {
|
||||
color="primary"
|
||||
ghost
|
||||
size="s"
|
||||
class="ml-auto"
|
||||
in="ConfigureService"
|
||||
onClick={() => store.close()}
|
||||
/>
|
||||
</div>
|
||||
@@ -419,10 +419,10 @@ const ConfigureRole = () => {
|
||||
</Show>
|
||||
</Combobox.ItemLabel>
|
||||
<Icon
|
||||
class="ml-auto"
|
||||
icon={item.type === "machine" ? "Machine" : "Tag"}
|
||||
color="quaternary"
|
||||
inverted
|
||||
in="ConfigureRole"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user