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`.
|
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:
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
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 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
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
|
<Button
|
||||||
hierarchy="primary"
|
hierarchy="primary"
|
||||||
size="s"
|
size="s"
|
||||||
startIcon="Trash"
|
icon="Trash"
|
||||||
disabled={removeDisabled()}
|
disabled={removeDisabled()}
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
: "";
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user