Merge pull request 'prep-ui-version2' (#4266) from prep-ui-version2 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4266
This commit is contained in:
brianmcgee
2025-07-08 10:57:54 +00:00
117 changed files with 159 additions and 3798 deletions

View File

@@ -1,6 +1,5 @@
import type { Preview } from "@kachurun/storybook-solid-vite"; import type { Preview } from "@kachurun/storybook-solid-vite";
import "@/src/components/v2/index.css";
import "../src/index.css"; import "../src/index.css";
import "./preview.css"; import "./preview.css";

View File

@@ -1,8 +0,0 @@
import { JSX } from "solid-js";
interface FormSectionProps {
children: JSX.Element;
}
const FormSection = (props: FormSectionProps) => {
return <div class="p-2">{props.children}</div>;
};

View File

@@ -1,57 +0,0 @@
import { splitProps, type JSX } from "solid-js";
import {
InputBase,
InputError,
InputLabel,
InputVariant,
} from "@/src/components/inputBase";
import { FieldLayout } from "./layout";
interface TextInputProps {
// Common
error?: string;
required?: boolean;
disabled?: boolean;
// Passed to input
value: string;
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
placeholder?: string;
variant?: InputVariant;
// Passed to label
label: JSX.Element;
help?: string;
// Passed to layout
class?: string;
}
export function TextInput(props: TextInputProps) {
const [layoutProps, rest] = splitProps(props, ["class"]);
return (
<FieldLayout
label={
<InputLabel
class="col-span-2"
required={props.required}
error={!!props.error}
help={props.help}
>
{props.label}
</InputLabel>
}
field={
<InputBase
variant={props.variant}
error={!!props.error}
required={props.required}
disabled={props.disabled}
placeholder={props.placeholder}
class="col-span-10"
{...props.inputProps}
value={props.value}
/>
}
error={props.error && <InputError error={props.error} />}
{...layoutProps}
/>
);
}

View File

@@ -1,2 +0,0 @@
export * from "./FormSection";
export * from "./TextInput";

View File

@@ -1,26 +0,0 @@
import { JSX, splitProps } from "solid-js";
import cx from "classnames";
interface LayoutProps extends JSX.HTMLAttributes<HTMLDivElement> {
field?: JSX.Element;
label?: JSX.Element;
error?: JSX.Element;
}
export const FieldLayout = (props: LayoutProps) => {
const [intern, divProps] = splitProps(props, [
"field",
"label",
"error",
"class",
]);
return (
<div
class={cx("grid grid-cols-10 items-center", intern.class)}
{...divProps}
>
<div class="col-span-5 flex items-center">{props.label}</div>
<div class="col-span-5">{props.field}</div>
{props.error && <span class="col-span-full">{props.error}</span>}
</div>
);
};

View File

@@ -1,195 +0,0 @@
import { API } from "@/api/API";
import { Schema as Inventory } from "@/api/Inventory";
import { toast } from "solid-toast";
import {
ErrorToastComponent,
CancelToastComponent,
} from "@/src/components/toast";
type OperationNames = keyof API;
type Services = NonNullable<Inventory["services"]>;
type ServiceNames = keyof Services;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
Services[T]
>[string];
export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "success" }
>;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
interface SendHeaderType {
logging?: { group_path: string[] };
}
interface BackendSendType<K extends OperationNames> {
body: OperationArgs<K>;
header?: SendHeaderType;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ReceiveHeaderType {}
interface BackendReturnType<K extends OperationNames> {
body: OperationResponse<K>;
header: ReceiveHeaderType;
}
const _callApi = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
backendOpts?: SendHeaderType,
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
// if window[method] does not exist, throw an error
if (!(method in window)) {
console.error(`Method ${method} not found on window object`);
// return a rejected promise
return {
promise: Promise.resolve({
body: {
status: "error",
errors: [
{
message: `Method ${method} not found on window object`,
code: "method_not_found",
},
],
op_key: "noop",
},
header: {},
}),
op_key: "noop",
};
}
const message: BackendSendType<OperationNames> = {
body: args,
header: backendOpts,
};
const promise = (
window as unknown as Record<
OperationNames,
(
args: BackendSendType<OperationNames>,
) => Promise<BackendReturnType<OperationNames>>
>
)[method](message) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string;
return { promise, op_key };
};
const handleCancel = async <K extends OperationNames>(
ops_key: string,
orig_task: Promise<BackendReturnType<K>>,
) => {
console.log("Canceling operation: ", ops_key);
const { promise, op_key } = _callApi("delete_task", { task_id: ops_key });
promise.catch((error) => {
toast.custom(
(t) => (
<ErrorToastComponent
t={t}
message={"Unexpected error: " + (error?.message || String(error))}
/>
),
{
duration: 5000,
},
);
console.error("Unhandled promise rejection in callApi:", error);
});
const resp = await promise;
if (resp.body.status === "error") {
toast.custom(
(t) => (
<ErrorToastComponent
t={t}
message={"Failed to cancel operation: " + ops_key}
/>
),
{
duration: 5000,
},
);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(orig_task as any).cancelled = true;
}
console.log("Cancel response: ", resp);
};
export const callApi = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
backendOpts?: SendHeaderType,
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args, backendOpts);
const { promise, op_key } = _callApi(method, args, backendOpts);
promise.catch((error) => {
toast.custom(
(t) => (
<ErrorToastComponent
t={t}
message={"Unexpected error: " + (error?.message || String(error))}
/>
),
{
duration: 5000,
},
);
console.error("Unhandled promise rejection in callApi:", error);
});
const toastId = toast.custom(
(
t, // t is the Toast object, t.id is the id of THIS toast instance
) => (
<CancelToastComponent
t={t}
message={"Executing " + method}
onCancel={handleCancel.bind(null, op_key, promise)}
/>
),
{
duration: Infinity,
},
);
const new_promise = promise.then((response) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cancelled = (promise as any).cancelled;
if (cancelled) {
console.log("Not printing toast because operation was cancelled");
}
const body = response.body;
if (body.status === "error" && !cancelled) {
toast.remove(toastId);
toast.custom(
(t) => (
<ErrorToastComponent
t={t}
message={"Error: " + body.errors[0].message}
/>
),
{
duration: Infinity,
},
);
} else {
toast.remove(toastId);
}
return body;
});
return { promise: new_promise, op_key: op_key };
};

View File

@@ -1,188 +0,0 @@
import {
createForm,
FieldValues,
getValues,
setValue,
SubmitHandler,
} from "@modular-forms/solid";
import { TextInput } from "@/src/Form/fields/TextInput";
import { Button } from "./components/Button/Button";
import { callApi } from "./api";
import { API } from "@/api/API";
import { createSignal, Match, Switch, For, Show } from "solid-js";
import { Typography } from "./components/Typography";
import { useQuery } from "@tanstack/solid-query";
import { makePersisted } from "@solid-primitives/storage";
import jsonSchema from "@/api/API.json";
interface APITesterForm extends FieldValues {
endpoint: string;
payload: string;
}
const ACTUAL_API_ENDPOINT_NAMES: (keyof API)[] = jsonSchema.required.map(
(key) => key as keyof API,
);
export const ApiTester = () => {
const [persistedTestData, setPersistedTestData] = makePersisted(
createSignal<APITesterForm>(),
{
name: "_test_data",
storage: localStorage,
},
);
const [formStore, { Form, Field }] = createForm<APITesterForm>({
initialValues: persistedTestData() || { endpoint: "", payload: "" },
});
const [endpointSearchTerm, setEndpointSearchTerm] = createSignal(
getValues(formStore).endpoint || "",
);
const [showSuggestions, setShowSuggestions] = createSignal(false);
const filteredEndpoints = () => {
const term = endpointSearchTerm().toLowerCase();
if (!term) return ACTUAL_API_ENDPOINT_NAMES;
return ACTUAL_API_ENDPOINT_NAMES.filter((ep) =>
ep.toLowerCase().includes(term),
);
};
const query = useQuery(() => {
const currentEndpoint = getValues(formStore).endpoint;
const currentPayload = getValues(formStore).payload;
const values = getValues(formStore);
return {
queryKey: ["api-tester", currentEndpoint, currentPayload],
queryFn: async () => {
return await callApi(
values.endpoint as keyof API,
JSON.parse(values.payload || "{}"),
).promise;
},
staleTime: Infinity,
enabled: false,
};
});
const handleSubmit: SubmitHandler<APITesterForm> = (values) => {
console.log(values);
setPersistedTestData(values);
setEndpointSearchTerm(values.endpoint);
query.refetch();
const v = getValues(formStore);
console.log(v);
};
return (
<div class="p-2">
<h1>API Tester</h1>
<Form onSubmit={handleSubmit}>
<div class="flex flex-col">
<Field name="endpoint">
{(field, fieldProps) => (
<div class="relative">
<TextInput
label={"endpoint"}
value={field.value || ""}
inputProps={{
...fieldProps,
onInput: (e: Event) => {
if (fieldProps.onInput) {
(fieldProps.onInput as (ev: Event) => void)(e);
}
setEndpointSearchTerm(
(e.currentTarget as HTMLInputElement).value,
);
setShowSuggestions(true);
},
onBlur: (e: FocusEvent) => {
if (fieldProps.onBlur) {
(fieldProps.onBlur as (ev: FocusEvent) => void)(e);
}
setTimeout(() => setShowSuggestions(false), 150);
},
onFocus: (e: FocusEvent) => {
setEndpointSearchTerm(field.value || "");
setShowSuggestions(true);
},
onKeyDown: (e: KeyboardEvent) => {
if (e.key === "Escape") {
setShowSuggestions(false);
}
},
}}
/>
<Show
when={showSuggestions() && filteredEndpoints().length > 0}
>
<ul class="absolute z-10 mt-1 max-h-60 w-full overflow-y-auto rounded border border-gray-300 bg-white shadow-lg">
<For each={filteredEndpoints()}>
{(ep) => (
<li
class="cursor-pointer p-2 hover:bg-gray-100"
onMouseDown={(e) => {
e.preventDefault();
setValue(formStore, "endpoint", ep);
setEndpointSearchTerm(ep);
setShowSuggestions(false);
}}
>
{ep}
</li>
)}
</For>
</ul>
</Show>
</div>
)}
</Field>
<Field name="payload">
{(field, fieldProps) => (
<div class="my-2 flex flex-col">
<label class="mb-1 font-medium" for="payload-textarea">
payload
</label>
<textarea
id="payload-textarea"
class="min-h-[120px] resize-y rounded border p-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
placeholder={`{\n "key": "value"\n}`}
value={field.value || ""}
{...fieldProps}
onInput={(e) => {
fieldProps.onInput?.(e);
}}
spellcheck={false}
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</div>
)}
</Field>
<Button class="m-2" disabled={query.isFetching}>
Send
</Button>
</div>
</Form>
<div>
<Typography hierarchy="title" size="default">
Result
</Typography>
<Switch>
<Match when={query.isFetching}>
<span>loading ...</span>
</Match>
<Match when={query.isFetched}>
<pre>
<code>{JSON.stringify(query.data, null, 2)}</code>
</pre>
</Match>
</Switch>
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Alert, AlertProps } from "@/src/components/v2/Alert/Alert"; import { Alert, AlertProps } from "@/src/components/Alert/Alert";
import { expect, fn } from "storybook/test"; import { expect, fn } from "storybook/test";
import { StoryContext } from "@kachurun/storybook-solid-vite"; import { StoryContext } from "@kachurun/storybook-solid-vite";

View File

@@ -1,7 +1,7 @@
import "./Alert.css"; import "./Alert.css";
import cx from "classnames"; import cx from "classnames";
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon"; import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@kobalte/core/button"; import { Button } from "@kobalte/core/button";
import { Alert as KAlert } from "@kobalte/core/alert"; import { Alert as KAlert } from "@kobalte/core/alert";

View File

@@ -1,16 +0,0 @@
import { useNavigate } from "@solidjs/router";
import { Button } from "./Button/Button";
import Icon from "./icon";
export const BackButton = () => {
const navigate = useNavigate();
return (
<Button
variant="ghost"
size="s"
class="mr-2"
onClick={() => navigate(-1)}
startIcon={<Icon icon="CaretLeft" />}
></Button>
);
};

View File

@@ -1,55 +0,0 @@
@import "Button-Light.css";
@import "Button-Dark.css";
@import "Button-Ghost.css";
.button {
@apply inline-flex items-center flex-shrink gap-1 justify-center p-4 font-semibold;
letter-spacing: 0.0275rem;
}
/* button SIZES */
.button--default {
padding: theme(padding.2) theme(padding.4);
height: theme(height.9);
border-radius: theme(borderRadius.DEFAULT);
&:has(> .button__icon--start):has(> .button__label) {
padding-left: theme(padding[2.5]);
}
&:has(> .button__icon--end):has(> .button__label) {
padding-right: theme(padding[2.5]);
}
}
.button--small {
padding: theme(padding[1.5]) theme(padding[3]);
height: theme(height.8);
border-radius: 3px;
&:has(> .button__icon--start):has(> .button__label) {
padding-left: theme(padding.2);
}
&:has(> .button__label):has(> .button__icon--end) {
padding-right: theme(padding.2);
}
}
/* button group */
.button-group .button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.button-group .button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.button-group .button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View File

@@ -1,31 +0,0 @@
/* button DARK and states */
.button--dark {
@apply border border-solid border-secondary-950 bg-primary-800 text-white;
box-shadow: inset 1px 1px theme(backgroundColor.secondary.700);
&:disabled {
@apply disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300;
}
& .button__icon {
color: theme(textColor.secondary.200);
}
}
.button--dark-hover:hover {
@apply hover:bg-secondary-900;
}
.button--dark-focus:focus {
@apply focus:border-secondary-900;
}
.button--dark-active:active {
@apply focus:border-secondary-900;
}
.button--dark-active:active {
@apply active:border-secondary-900;
}

View File

@@ -1,11 +0,0 @@
.button--ghost-hover:hover {
@apply hover:bg-secondary-100 hover:text-secondary-900;
}
.button--ghost-focus:focus {
@apply focus:bg-secondary-200 focus:text-secondary-900;
}
.button--ghost-active:active {
@apply active:bg-secondary-200 active:text-secondary-900;
}

View File

@@ -1,37 +0,0 @@
/* button LIGHT and states */
.button--light {
@apply border border-solid border-secondary-400 bg-secondary-100 text-secondary-950;
box-shadow: inset 1px 1px theme(backgroundColor.white);
&:disabled {
@apply disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700;
}
& .button__icon {
color: theme(textColor.secondary.900);
}
}
.button--light-hover:hover {
@apply hover:bg-secondary-200;
}
.button--light-focus:focus {
@apply focus:bg-secondary-200;
& .button__label {
color: theme(textColor.secondary.900) !important;
}
}
.button--light-active:active {
@apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900;
box-shadow: inset 2px 2px theme(backgroundColor.secondary.300);
& .button__label {
color: theme(textColor.secondary.900) !important;
}
}

View File

@@ -1,96 +1,115 @@
import { splitProps, type JSX } from "solid-js"; import { splitProps, type JSX, createSignal } from "solid-js";
import cx from "classnames"; import cx from "classnames";
import { Typography } from "../Typography"; import { Typography } from "../Typography/Typography";
import { Button as KobalteButton } from "@kobalte/core/button";
import "./Button-Base.css"; import "./Button.css";
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import { Loader } from "@/src/components/Loader/Loader";
type Variants = "dark" | "light" | "ghost"; export type Size = "default" | "s";
type Size = "default" | "s"; export type Hierarchy = "primary" | "secondary";
const variantColors: ( export type Action = () => Promise<void>;
disabled: boolean | undefined,
) => Record<Variants, string> = (disabled) => ({
dark: cx(
"button--dark",
!disabled && "button--dark-hover", // Hover state
!disabled && "button--dark-focus", // Focus state
!disabled && "button--dark-active", // Active state
// Disabled
"disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300",
),
light: cx(
"button--light",
!disabled && "button--light-hover", // Hover state export interface ButtonProps
!disabled && "button--light-focus", // Focus state extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
!disabled && "button--light-active", // Active state hierarchy?: Hierarchy;
),
ghost: cx(
!disabled && "button--ghost-hover", // Hover state
!disabled && "button--ghost-focus", // Focus state
!disabled && "button--ghost-active", // Active state
),
});
const sizePaddings: Record<Size, string> = {
default: cx("button--default"),
s: cx("button button--small"), //cx("rounded-sm py-[0.375rem] px-3"),
};
const sizeFont: Record<Size, string> = {
default: cx("text-[0.8125rem]"),
s: cx("text-[0.75rem]"),
};
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variants;
size?: Size; size?: Size;
ghost?: boolean;
children?: JSX.Element; children?: JSX.Element;
startIcon?: JSX.Element; icon?: IconVariant;
endIcon?: JSX.Element; startIcon?: IconVariant;
endIcon?: IconVariant;
class?: string; class?: string;
onAction?: Action;
} }
const iconSizes: Record<Size, string> = {
default: "1rem",
s: "0.8125rem",
};
export const Button = (props: ButtonProps) => { export const Button = (props: ButtonProps) => {
const [local, other] = splitProps(props, [ const [local, other] = splitProps(props, [
"children", "children",
"variant", "hierarchy",
"size", "size",
"ghost",
"icon",
"startIcon", "startIcon",
"endIcon", "endIcon",
"class", "class",
"onAction",
]); ]);
const buttonInvertion = (variant: Variants) => { const size = local.size || "default";
return !(!variant || variant === "ghost" || variant === "light"); const hierarchy = local.hierarchy || "primary";
const [loading, setLoading] = createSignal(false);
const onClick = async () => {
if (!local.onAction) {
console.error("this should not be possible");
return;
}
setLoading(true);
try {
await local.onAction();
} catch (error) {
console.error("Error while executing action", error);
}
setLoading(false);
}; };
const iconSize = iconSizes[local.size || "default"];
return ( return (
<button <KobalteButton
class={cx( class={cx(
local.class, local.class,
"button", // default button class "button", // default button class
variantColors(props.disabled)[local.variant || "dark"], // button appereance size,
sizePaddings[local.size || "default"], // button size hierarchy,
{
icon: local.icon,
loading: loading(),
ghost: local.ghost,
},
)} )}
onClick={local.onAction ? onClick : undefined}
{...other} {...other}
> >
<Loader hierarchy={hierarchy} />
{local.startIcon && ( {local.startIcon && (
<span class="button__icon--start">{local.startIcon}</span> <Icon icon={local.startIcon} class="icon-start" size={iconSize} />
)} )}
{local.children && (
{local.icon && !local.children && (
<Icon icon={local.icon} class="icon" size={iconSize} />
)}
{local.children && !local.icon && (
<Typography <Typography
class="button__label" class="label"
hierarchy="label" hierarchy="label"
family="mono"
size={local.size || "default"} size={local.size || "default"}
color="inherit" inverted={local.hierarchy === "primary"}
inverted={buttonInvertion(local.variant || "dark")} weight="bold"
weight="medium"
tag="span" tag="span"
> >
{local.children} {local.children}
</Typography> </Typography>
)} )}
{local.endIcon && <span class="button__icon--end">{local.endIcon}</span>}
</button> {local.endIcon && (
<Icon icon={local.endIcon} class="icon-end" size={iconSize} />
)}
</KobalteButton>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Divider, DividerProps } from "@/src/components/v2/Divider/Divider"; import { Divider, DividerProps } from "@/src/components/Divider/Divider";
const meta: Meta<DividerProps> = { const meta: Meta<DividerProps> = {
title: "Components/Divider", title: "Components/Divider",

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import cx from "classnames"; import cx from "classnames";
import { Checkbox, CheckboxProps } from "@/src/components/v2/Form/Checkbox"; import { Checkbox, CheckboxProps } from "@/src/components/Form/Checkbox";
const Examples = (props: CheckboxProps) => ( const Examples = (props: CheckboxProps) => (
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">

View File

@@ -3,7 +3,7 @@ import {
CheckboxInputProps as KCheckboxInputProps, CheckboxInputProps as KCheckboxInputProps,
CheckboxRootProps as KCheckboxRootProps, CheckboxRootProps as KCheckboxRootProps,
} from "@kobalte/core/checkbox"; } from "@kobalte/core/checkbox";
import Icon from "@/src/components/v2/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import cx from "classnames"; import cx from "classnames";
import { Label } from "./Label"; import { Label } from "./Label";

View File

@@ -1,4 +1,4 @@
import Icon from "@/src/components/v2/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { import {
Combobox as KCombobox, Combobox as KCombobox,
ComboboxRootOptions as KComboboxRootOptions, ComboboxRootOptions as KComboboxRootOptions,
@@ -11,9 +11,9 @@ import { Label } from "./Label";
import cx from "classnames"; import cx from "classnames";
import { FieldProps } from "./Field"; import { FieldProps } from "./Field";
import { Orienter } from "./Orienter"; import { Orienter } from "./Orienter";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { Accessor, Component, For, Show, splitProps } from "solid-js"; import { Accessor, Component, For, Show, splitProps } from "solid-js";
import { Tag } from "@/src/components/v2/Tag/Tag"; import { Tag } from "@/src/components/Tag/Tag";
export type ComboboxProps<Option, OptGroup = never> = FieldProps & export type ComboboxProps<Option, OptGroup = never> = FieldProps &
KComboboxRootOptions<Option, OptGroup> & { KComboboxRootOptions<Option, OptGroup> & {

View File

@@ -1,9 +1,9 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { Fieldset, FieldsetProps } from "@/src/components/v2/Form/Fieldset"; import { Fieldset, FieldsetProps } from "@/src/components/Form/Fieldset";
import cx from "classnames"; import cx from "classnames";
import { TextInput } from "@/src/components/v2/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/v2/Form/TextArea"; import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/v2/Form/Checkbox"; import { Checkbox } from "@/src/components/Form/Checkbox";
import { FieldProps } from "./Field"; import { FieldProps } from "./Field";
const FieldsetExamples = (props: FieldsetProps) => ( const FieldsetExamples = (props: FieldsetProps) => (

View File

@@ -1,7 +1,7 @@
import "./Fieldset.css"; import "./Fieldset.css";
import { JSX, splitProps } from "solid-js"; import { JSX, splitProps } from "solid-js";
import cx from "classnames"; import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { FieldProps } from "./Field"; import { FieldProps } from "./Field";
export type FieldsetFieldProps = Pick< export type FieldsetFieldProps = Pick<

View File

@@ -12,7 +12,7 @@ div.form-label {
@apply flex items-center gap-1; @apply flex items-center gap-1;
} }
& > label[data-required] & not(label[data-readonly]) { & > label[data-required]:not(label[data-readonly]) {
span.typography::after { span.typography::after {
@apply fg-def-4 ml-1; @apply fg-def-4 ml-1;

View File

@@ -1,7 +1,7 @@
import { Show } from "solid-js"; import { Show } from "solid-js";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { Tooltip as KTooltip } from "@kobalte/core/tooltip"; import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
import Icon from "@/src/components/v2/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { TextField } from "@kobalte/core/text-field"; import { TextField } from "@kobalte/core/text-field";
import { Checkbox } from "@kobalte/core/checkbox"; import { Checkbox } from "@kobalte/core/checkbox";
import { Combobox } from "@kobalte/core/combobox"; import { Combobox } from "@kobalte/core/combobox";

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import cx from "classnames"; import cx from "classnames";
import { TextInput, TextInputProps } from "@/src/components/v2/Form/TextInput"; import { TextInput, TextInputProps } from "@/src/components/Form/TextInput";
const Examples = (props: TextInputProps) => ( const Examples = (props: TextInputProps) => (
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">

View File

@@ -3,7 +3,7 @@ import {
TextFieldInputProps, TextFieldInputProps,
TextFieldRootProps, TextFieldRootProps,
} from "@kobalte/core/text-field"; } from "@kobalte/core/text-field";
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon"; import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import cx from "classnames"; import cx from "classnames";
import { Label } from "./Label"; import { Label } from "./Label";

View File

@@ -1,20 +0,0 @@
import { type JSX } from "solid-js";
type sizes = "small" | "medium" | "large";
const gapSizes: Record<sizes, string> = {
small: "gap-2",
medium: "gap-4",
large: "gap-6",
};
interface List {
children: JSX.Element;
gapSize: sizes;
}
export const List = (props: List) => {
const { children, gapSize } = props;
return <ul class={`flex flex-col ${gapSizes[gapSize]}`}> {children}</ul>;
};

View File

@@ -1 +0,0 @@
export { List } from "./List";

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Loader, LoaderProps } from "@/src/components/v2/Loader/Loader"; import { Loader, LoaderProps } from "@/src/components/Loader/Loader";
const meta: Meta<LoaderProps> = { const meta: Meta<LoaderProps> = {
title: "Components/Loader", title: "Components/Loader",

View File

@@ -1,7 +1,7 @@
import { import {
MachineStatus, MachineStatus,
MachineStatusProps, MachineStatusProps,
} from "@/src/components/v2/MachineStatus/MachineStatus"; } from "@/src/components/MachineStatus/MachineStatus";
import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<MachineStatusProps> = { const meta: Meta<MachineStatusProps> = {

View File

@@ -4,7 +4,7 @@ import { Badge } from "@kobalte/core/badge";
import cx from "classnames"; import cx from "classnames";
import { Show } from "solid-js"; import { Show } from "solid-js";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
export type MachineStatus = export type MachineStatus =
| "Online" | "Online"

View File

@@ -1,15 +1,11 @@
import { TagProps } from "@/src/components/v2/Tag/Tag"; import { TagProps } from "@/src/components/Tag/Tag";
import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { import { Modal, ModalContext, ModalProps } from "@/src/components/Modal/Modal";
Modal, import { Fieldset } from "@/src/components/Form/Fieldset";
ModalContext, import { TextInput } from "@/src/components/Form/TextInput";
ModalProps, import { TextArea } from "@/src/components/Form/TextArea";
} from "@/src/components/v2/Modal/Modal"; import { Checkbox } from "@/src/components/Form/Checkbox";
import { Fieldset } from "@/src/components/v2/Form/Fieldset";
import { TextInput } from "@/src/components/v2/Form/TextInput";
import { TextArea } from "@/src/components/v2/Form/TextArea";
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
import { Button } from "../Button/Button"; import { Button } from "../Button/Button";
const meta: Meta<ModalProps> = { const meta: Meta<ModalProps> = {

View File

@@ -1,14 +0,0 @@
import { List } from "@/src/components/Helpers";
import { SidebarListItem } from "../SidebarListItem";
export const SidebarFlyout = () => {
return (
<div class="sidebar__flyout">
<div class="sidebar__flyout__inner">
<List gapSize="small">
<SidebarListItem href="/clans" title="Settings" />
</List>
</div>
</div>
);
};

View File

@@ -1,71 +0,0 @@
import { createSignal, Show } from "solid-js";
import { Typography } from "@/src/components/Typography";
import { SidebarFlyout } from "./SidebarFlyout";
import "./css/sidebar.css";
import Icon from "../icon";
interface SidebarProps {
clanName: string;
showFlyout?: () => boolean;
}
const ClanProfile = (props: SidebarProps) => {
return (
<div
class={`sidebar__profile ${props.showFlyout?.() ? "sidebar__profile--flyout" : ""}`}
>
<Typography
class="sidebar__profile__character"
tag="span"
hierarchy="title"
size="m"
weight="bold"
color="primary"
inverted={true}
>
{props.clanName.slice(0, 1).toUpperCase()}
</Typography>
</div>
);
};
const ClanTitle = (props: SidebarProps) => {
return (
<Typography
tag="h3"
hierarchy="body"
size="default"
weight="medium"
color="primary"
inverted={true}
>
{props.clanName}
</Typography>
);
};
export const SidebarHeader = (props: SidebarProps) => {
const [showFlyout, toggleFlyout] = createSignal(false);
function handleClick() {
toggleFlyout(!showFlyout());
}
return (
<header class="sidebar__header">
<div onClick={handleClick} class="sidebar__header__inner">
{/* <ClanProfile clanName={props.clanName} showFlyout={showFlyout} /> */}
<div class="w-full pl-1 text-white">
<ClanTitle clanName={props.clanName} />
</div>
<Show
when={showFlyout}
fallback={<Icon size={12} class="text-white" icon="CaretDown" />}
>
<Icon size={12} class="text-white" icon="CaretDown" />
</Show>
</div>
{showFlyout() && <SidebarFlyout />}
</header>
);
};

View File

@@ -1,30 +0,0 @@
import { A } from "@solidjs/router";
import { Typography } from "@/src/components/Typography";
import "./css/sidebar.css";
interface SidebarListItem {
title: string;
href: string;
}
export const SidebarListItem = (props: SidebarListItem) => {
const { title, href } = props;
return (
<li class="">
<A class="sidebar__list__link" href={href}>
<Typography
class="sidebar__list__content"
tag="span"
hierarchy="body"
size="xs"
weight="normal"
color="primary"
inverted={true}
>
{title}
</Typography>
</A>
</li>
);
};

View File

@@ -8,7 +8,7 @@ import {
import { import {
SidebarNav, SidebarNav,
SidebarNavProps, SidebarNavProps,
} from "@/src/components/v2/Sidebar/SidebarNav"; } from "@/src/components/Sidebar/SidebarNav";
import { Suspense } from "solid-js"; import { Suspense } from "solid-js";
import { StoryContext } from "@kachurun/storybook-solid-vite"; import { StoryContext } from "@kachurun/storybook-solid-vite";

View File

@@ -1,7 +1,7 @@
import "./SidebarNav.css"; import "./SidebarNav.css";
import { SidebarNavHeader } from "@/src/components/v2/Sidebar/SidebarNavHeader"; import { SidebarNavHeader } from "@/src/components/Sidebar/SidebarNavHeader";
import { SidebarNavBody } from "@/src/components/v2/Sidebar/SidebarNavBody"; import { SidebarNavBody } from "@/src/components/Sidebar/SidebarNavBody";
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus"; import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
export interface LinkProps { export interface LinkProps {
path: string; path: string;

View File

@@ -2,13 +2,13 @@ import "./SidebarNavBody.css";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion"; import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { import {
MachineProps, MachineProps,
SidebarNavProps, SidebarNavProps,
} from "@/src/components/v2/Sidebar/SidebarNav"; } from "@/src/components/Sidebar/SidebarNav";
import { For } from "solid-js"; import { For } from "solid-js";
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus"; import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
const MachineRoute = (props: MachineProps) => ( const MachineRoute = (props: MachineProps) => (
<div class="flex w-full flex-col gap-2"> <div class="flex w-full flex-col gap-2">

View File

@@ -1,13 +1,10 @@
import "./SidebarNavHeader.css"; import "./SidebarNavHeader.css";
import Icon from "@/src/components/v2/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu"; import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography"; import { Typography } from "../Typography/Typography";
import { createSignal, For } from "solid-js"; import { createSignal, For } from "solid-js";
import { import { ClanLinkProps, ClanProps } from "@/src/components/Sidebar/SidebarNav";
ClanLinkProps,
ClanProps,
} from "@/src/components/v2/Sidebar/SidebarNav";
export interface SidebarHeaderProps { export interface SidebarHeaderProps {
clanDetail: ClanProps; clanDetail: ClanProps;

View File

@@ -2,12 +2,12 @@ import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { import {
SidebarPane, SidebarPane,
SidebarPaneProps, SidebarPaneProps,
} from "@/src/components/v2/Sidebar/SidebarPane"; } from "@/src/components/Sidebar/SidebarPane";
import { SidebarSection } from "./SidebarSection"; import { SidebarSection } from "./SidebarSection";
import { Divider } from "@/src/components/v2/Divider/Divider"; import { Divider } from "@/src/components/Divider/Divider";
import { TextInput } from "@/src/components/v2/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/v2/Form/TextArea"; import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/v2/Form/Checkbox"; import { Checkbox } from "@/src/components/Form/Checkbox";
import { Combobox } from "../Form/Combobox"; import { Combobox } from "../Form/Combobox";
const meta: Meta<SidebarPaneProps> = { const meta: Meta<SidebarPaneProps> = {

View File

@@ -1,6 +1,6 @@
import { JSX } from "solid-js"; import { JSX } from "solid-js";
import "./SidebarPane.css"; import "./SidebarPane.css";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { Button as KButton } from "@kobalte/core/button"; import { Button as KButton } from "@kobalte/core/button";

View File

@@ -1,6 +1,6 @@
import { createSignal, JSX } from "solid-js"; import { createSignal, JSX } from "solid-js";
import "./SidebarSection.css"; import "./SidebarSection.css";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { Button as KButton } from "@kobalte/core/button"; import { Button as KButton } from "@kobalte/core/button";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";

View File

@@ -1,21 +0,0 @@
.sidebar__flyout {
top: 0;
position: absolute;
z-index: theme(zIndex.30);
padding: theme(padding[1]);
width: 100%;
height: auto;
}
.sidebar__flyout__inner {
position: relative;
width: inherit;
height: inherit;
padding: theme(padding.12) theme(padding.3) theme(padding.3);
background-color: var(--clr-bg-inv-4);
/* / 0.95); */
border: 1px solid var(--clr-border-inv-4);
border-radius: theme(borderRadius.lg);
}

View File

@@ -1,30 +0,0 @@
.sidebar__header {
position: relative;
padding: 1px 1px 0;
cursor: pointer;
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--clr-bg-inv-3);
border-bottom: 1px solid var(--clr-border-inv-3);
border-top-left-radius: theme(borderRadius.xl);
border-top-right-radius: theme(borderRadius.xl);
}
}
.sidebar__header__inner {
position: relative;
z-index: theme(zIndex.40);
display: flex;
align-items: center;
gap: 0 theme(gap.3);
padding: theme(padding.3) theme(padding.3);
}

View File

@@ -1,52 +0,0 @@
.sidebar__list__link {
position: relative;
cursor: theme(cursor.pointer);
&:after {
content: "";
position: absolute;
z-index: theme(zIndex.10);
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: theme(borderRadius.md);
transform: scale(0.98);
transition: transform 0.24s ease-in-out;
}
&:hover:after {
background: var(--clr-bg-inv-acc-2);
transform: scale(theme(scale.100));
transition: transform 0.32s ease-in-out;
}
&:active {
transform: scale(0.99);
transition: transform 0.12s ease-in-out;
}
&:active:after {
background: var(--clr-bg-inv-acc-3);
transform: scale(theme(scale.100));
}
}
.sidebar__list__link {
position: relative;
z-index: 20;
display: block;
padding: theme(padding.2) theme(padding.3);
}
.sidebar__list__link.active {
&:after {
background: var(--clr-bg-inv-acc-3);
}
}
.sidebar__list__content {
position: relative;
z-index: 20;
}

View File

@@ -1,19 +0,0 @@
.sidebar__profile {
display: flex;
justify-content: center;
align-items: center;
width: theme(width.8);
height: theme(height.8);
background: var(--clr-bg-inv-4);
border-radius: 50%;
}
.sidebar__profile--flyout {
background: var(--clr-bg-def-2);
}
.sidebar__profile--flyout > .sidebar__profile__character {
color: var(--clr-fg-def-1) !important;
}

View File

@@ -1,32 +0,0 @@
/* Sidebar Elements */
@import "./sidebar-header";
@import "./sidebar-flyout";
@import "./sidebar-list-item";
@import "./sidebar-profile";
/* Sidebar Structure */
.sidebar {
@apply bg-inv-2 h-full border border-solid border-inv-2 min-w-72 rounded-xl;
display: flex;
flex-direction: column;
}
.sidebar__body {
display: flex;
flex-direction: column;
gap: theme(padding.2);
padding: theme(padding.4) theme(padding.2);
}
.sidebar__section {
@apply bg-primary-800/90;
padding: theme(padding.2);
border-radius: theme(borderRadius.md);
::marker {
content: "";
}
}

View File

@@ -1,85 +0,0 @@
import { For, type JSX, Show } from "solid-js";
import { RouteSectionProps } from "@solidjs/router";
import { AppRoute, routes } from "@/src";
import { SidebarHeader } from "./SidebarHeader";
import { SidebarListItem } from "./SidebarListItem";
import { Typography } from "../Typography";
import "./css/sidebar.css";
import Icon, { IconVariant } from "../icon";
import { clanMetaQuery } from "@/src/queries/clan-meta";
const SidebarSection = (props: {
title: string;
icon: IconVariant;
children: JSX.Element;
}) => {
const { title, children } = props;
return (
<details class="sidebar__section accordeon" open>
<summary style="display: contents;">
<div class="accordeon__header">
<Typography
class="inline-flex w-full gap-2 uppercase !tracking-wider"
tag="p"
hierarchy="body"
size="xxs"
weight="normal"
color="tertiary"
inverted={true}
>
<Icon class="opacity-90" icon={props.icon} size={13} />
{title}
<Icon icon="CaretDown" class="ml-auto" size={10} />
</Typography>
</div>
</summary>
<div class="accordeon__body">{children}</div>
</details>
);
};
export const Sidebar = (props: RouteSectionProps) => {
const query = clanMetaQuery();
return (
<div class="sidebar">
<Show
when={query.data}
fallback={<SidebarHeader clanName={"Untitled"} />}
>
{(meta) => <SidebarHeader clanName={meta().name} />}
</Show>
<div class="sidebar__body max-h-[calc(100vh-4rem)] overflow-scroll">
<For each={routes.filter((r) => !r.hidden)}>
{(route: AppRoute) => (
<Show
when={route.children}
fallback={
<SidebarListItem href={route.path} title={route.label} />
}
>
{(children) => (
<SidebarSection
title={route.label}
icon={route.icon || "Paperclip"}
>
<ul class="flex flex-col gap-y-0.5">
<For each={children().filter((r) => !r.hidden)}>
{(child) => (
<SidebarListItem
href={`${route.path}${child.path}`}
title={child.label}
/>
)}
</For>
</ul>
</SidebarSection>
)}
</Show>
)}
</For>
</div>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { Tag, TagProps } from "@/src/components/v2/Tag/Tag"; import { Tag, TagProps } from "@/src/components/Tag/Tag";
import { Meta, type StoryContext, StoryObj } from "@kachurun/storybook-solid"; import { Meta, type StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { expect, fn } from "storybook/test"; import { expect, fn } from "storybook/test";

View File

@@ -1,7 +1,7 @@
import "./Tag.css"; import "./Tag.css";
import cx from "classnames"; import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography"; import { Typography } from "@/src/components/Typography/Typography";
import { createSignal, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import Icon, { IconVariant } from "../Icon/Icon"; import Icon, { IconVariant } from "../Icon/Icon";

View File

@@ -1,4 +1,4 @@
import { TagGroup, TagGroupProps } from "@/src/components/v2/TagGroup/TagGroup"; import { TagGroup, TagGroupProps } from "@/src/components/TagGroup/TagGroup";
import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagGroupProps> = { const meta: Meta<TagGroupProps> = {

View File

@@ -1,7 +1,7 @@
import "./TagGroup.css"; import "./TagGroup.css";
import cx from "classnames"; import cx from "classnames";
import { For } from "solid-js"; import { For } from "solid-js";
import { Tag } from "@/src/components/v2/Tag/Tag"; import { Tag } from "@/src/components/Tag/Tag";
export interface TagGroupProps { export interface TagGroupProps {
class?: string; class?: string;

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Family, Hierarchy, Typography, Weight } from "./Typography"; import { Family, Hierarchy, Typography, Weight } from "./Typography";
import { Component, For, Show } from "solid-js"; import { Component, For, Show } from "solid-js";
import { AllColors } from "@/src/components/v2/colors"; import { AllColors } from "@/src/components/colors";
interface TypographyExamplesProps { interface TypographyExamplesProps {
weights: Weight[]; weights: Weight[];

View File

@@ -2,7 +2,7 @@ import { type JSX } from "solid-js";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import cx from "classnames"; import cx from "classnames";
import "./Typography.css"; import "./Typography.css";
import { Color, fgClass } from "@/src/components/v2/colors"; import { Color, fgClass } from "@/src/components/colors";
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div"; export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser"; export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";

View File

@@ -1,23 +0,0 @@
.fnt-clr-primary {
color: var(--clr-fg-def-1);
}
.fnt-clr-secondary {
color: var(--clr-fg-def-2);
}
.fnt-clr-tertiary {
color: var(--clr-fg-def-3);
}
.fnt-clr-primary.fnt-clr--inverted {
color: var(--clr-fg-inv-1);
}
.fnt-clr-secondary.fnt-clr--inverted {
color: var(--clr-fg-inv-2);
}
.fnt-clr-tertiary.fnt-clr--inverted {
color: var(--clr-fg-inv-3);
}

View File

@@ -1,4 +0,0 @@
@import "./typography-label.css";
@import "./typography-body.css";
@import "./typography-title.css";
@import "./typography-headline.css";

View File

@@ -1,23 +0,0 @@
.fnt-body-default {
font-size: 1rem;
line-height: 132%;
letter-spacing: 3%;
}
.fnt-body-s {
font-size: 0.925rem;
line-height: 132%;
letter-spacing: 3%;
}
.fnt-body-xs {
font-size: 0.875rem;
line-height: 132%;
letter-spacing: 3%;
}
.fnt-body-xxs {
font-size: 0.75rem;
line-height: 132%;
letter-spacing: 0.00688rem;
}

View File

@@ -1,17 +0,0 @@
.fnt-headline-default {
font-size: 1.5rem;
line-height: 116%;
letter-spacing: 1%;
}
.fnt-headline-m {
font-size: 1.75rem;
line-height: 116%;
letter-spacing: 1%;
}
.fnt-headline-l {
font-size: 2rem;
line-height: 116%;
letter-spacing: 1%;
}

View File

@@ -1,14 +0,0 @@
.fnt-label-default {
font-size: 0.8125rem;
line-height: 100%;
}
.fnt-label-s {
font-size: 0.75rem;
line-height: 100%;
}
.fnt-label-xs {
font-size: 0.6875rem;
line-height: 100%;
}

View File

@@ -1,17 +0,0 @@
.fnt-title-default {
font-size: 1.125rem;
line-height: 124%;
letter-spacing: 3%;
}
.fnt-title-m {
font-size: 1.25rem;
line-height: 124%;
letter-spacing: 3%;
}
.fnt-title-l {
font-size: 1.375rem;
line-height: 124%;
letter-spacing: 3%;
}

View File

@@ -1,26 +0,0 @@
@import "./typography-hierarchy/";
@import "./typography-color.css";
.fnt-weight-normal {
font-weight: 300;
}
.fnt-weight-medium {
font-weight: 500;
}
.fnt-weight-bold {
font-weight: 700;
}
.fnt-weight-normal.fnt-clr--inverted {
font-weight: 300;
}
.fnt-weight-medium.fnt-clr--inverted {
font-weight: 400;
}
.fnt-weight-bold.fnt-clr--inverted {
font-weight: 700;
}

View File

@@ -1,109 +0,0 @@
import { type JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import cx from "classnames";
import "./css/typography.css";
type Hierarchy = "body" | "title" | "headline" | "label";
type Color = "primary" | "secondary" | "tertiary";
type Weight = "normal" | "medium" | "bold";
type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
const colorMap: Record<Color, string> = {
primary: cx("fnt-clr-primary"),
secondary: cx("fnt-clr-secondary"),
tertiary: cx("fnt-clr-tertiary"),
};
// type Size = "default" | "xs" | "s" | "m" | "l";
interface SizeForHierarchy {
label: {
default: string;
xs: string;
s: string;
};
body: {
default: string;
xs: string;
xxs: string;
s: string;
};
headline: {
default: string;
m: string;
l: string;
};
title: {
default: string;
m: string;
l: string;
};
}
type AllowedSizes<H extends Hierarchy> = keyof SizeForHierarchy[H];
const sizeHierarchyMap: SizeForHierarchy = {
body: {
default: cx("fnt-body-default"),
s: cx("fnt-body-s"),
xs: cx("fnt-body-xs"),
xxs: cx("fnt-body-xxs"),
},
headline: {
default: cx("fnt-headline-default"),
// xs: cx("fnt-headline-xs"),
// s: cx("fnt-headline-s"),
m: cx("fnt-headline-m"),
l: cx("fnt-headline-l"),
},
title: {
default: cx("fnt-title-default"),
// xs: cx("fnt-title-xs"),
// s: cx("fnt-title-s"),
m: cx("fnt-title-m"),
l: cx("fnt-title-l"),
},
label: {
default: cx("fnt-label-default"),
s: cx("fnt-label-s"),
xs: cx("fnt-label-xs"),
},
};
const weightMap: Record<Weight, string> = {
normal: cx("fnt-weight-normal"),
medium: cx("fnt-weight-medium"),
bold: cx("fnt-weight-bold"),
};
interface _TypographyProps<H extends Hierarchy> {
hierarchy: H;
size: AllowedSizes<H>;
children: JSX.Element;
weight?: Weight;
color?: Color | "inherit";
inverted?: boolean;
tag?: Tag;
class?: string;
classList?: Record<string, boolean>;
}
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
return (
<Dynamic
component={props.tag || "span"}
class={cx(
props.color === "inherit" && "text-inherit",
props.color !== "inherit" && colorMap[props.color || "primary"],
props.inverted && "fnt-clr--inverted",
sizeHierarchyMap[props.hierarchy][props.size] as string,
weightMap[props.weight || "normal"],
props.class,
)}
classList={props.classList}
>
{props.children}
</Dynamic>
);
};
export type TypographyProps = _TypographyProps<Hierarchy>;

View File

@@ -1,98 +0,0 @@
import { Component, JSX, splitProps } from "solid-js";
import ArrowBottom from "@/icons/arrow-bottom.svg";
import ArrowLeft from "@/icons/arrow-left.svg";
import ArrowRight from "@/icons/arrow-right.svg";
import ArrowTop from "@/icons/arrow-top.svg";
import Attention from "@/icons/attention.svg";
import CaretDown from "@/icons/caret-down.svg";
import CaretLeft from "@/icons/caret-left.svg";
import CaretRight from "@/icons/caret-right.svg";
import CaretUp from "@/icons/caret-up.svg";
import Checkmark from "@/icons/checkmark.svg";
import ClanIcon from "@/icons/clan-icon.svg";
import ClanLogo from "@/icons/clan-logo.svg";
import Close from "@/icons/close.svg";
import Download from "@/icons/download.svg";
import Edit from "@/icons/edit.svg";
import Expand from "@/icons/expand.svg";
import EyeClose from "@/icons/eye-close.svg";
import EyeOpen from "@/icons/eye-open.svg";
import Filter from "@/icons/filter.svg";
import Flash from "@/icons/flash.svg";
import Folder from "@/icons/folder.svg";
import Grid from "@/icons/grid.svg";
import Info from "@/icons/info.svg";
import List from "@/icons/list.svg";
import Load from "@/icons/load.svg";
import More from "@/icons/more.svg";
import Paperclip from "@/icons/paperclip.svg";
import Plus from "@/icons/plus.svg";
import Reload from "@/icons/reload.svg";
import Report from "@/icons/report.svg";
import Search from "@/icons/search.svg";
import Settings from "@/icons/settings.svg";
import Trash from "@/icons/trash.svg";
import Update from "@/icons/update.svg";
import Warning from "@/icons/warning-filled.svg";
const icons = {
ArrowBottom,
ArrowLeft,
ArrowRight,
ArrowTop,
Attention,
CaretDown,
CaretLeft,
CaretRight,
CaretUp,
Checkmark,
ClanIcon,
ClanLogo,
Close,
Download,
Edit,
Expand,
EyeClose,
EyeOpen,
Filter,
Flash,
Folder,
Grid,
Info,
List,
Load,
More,
Paperclip,
Plus,
Reload,
Report,
Search,
Settings,
Trash,
Update,
Warning,
};
export type IconVariant = keyof typeof icons;
interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
icon: IconVariant;
size?: number;
}
const Icon: Component<IconProps> = (props) => {
const [local, iconProps] = splitProps(props, ["icon"]);
const IconComponent = icons[local.icon];
return IconComponent ? (
<IconComponent
width={iconProps.size || 16}
height={iconProps.size || 16}
viewBox="0 0 48 48"
ref={iconProps.ref}
{...iconProps}
/>
) : null;
};
export default Icon;

View File

@@ -1,193 +0,0 @@
import cx from "classnames";
import { JSX, Ref, Show, splitProps } from "solid-js";
import Icon, { IconVariant } from "../icon";
import { Typography, TypographyProps } from "../Typography";
export type InputVariant = "outlined" | "ghost";
interface InputBaseProps {
variant?: InputVariant;
value?: string;
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
required?: boolean;
type?: string;
inlineLabel?: JSX.Element;
class?: string;
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
error?: boolean;
icon?: IconVariant;
/** Overrides the input element */
inputElem?: JSX.Element;
divRef?: Ref<HTMLDivElement>;
}
const variantBorder: Record<InputVariant, string> = {
outlined: "border border-inv-3",
ghost: "",
};
const fgStateClasses = cx("aria-disabled:fg-def-4 aria-readonly:fg-def-3");
export const InputBase = (props: InputBaseProps) => {
const [internal, inputProps] = splitProps(props, ["class", "divRef"]);
return (
<div
// eslint-disable-next-line tailwindcss/no-custom-classname
class={cx(
// Layout
"flex px-2 py-[0.375rem] flex-shrink-0 items-center justify-center gap-2 text-sm leading-6",
// Background
"bg-def-1 hover:bg-def-acc-1",
// Text
"fg-def-1",
fgStateClasses,
// Border
variantBorder[props.variant || "outlined"],
"rounded-sm",
"hover:border-inv-4",
"aria-disabled:border-def-2 aria-disabled:border",
// Outline
"outline-offset-1 outline-1",
"active:outline active:outline-inv-3",
"focus-visible:outline-double focus-visible:outline-int-1",
// Cursor
"aria-readonly:cursor-no-drop",
props.class,
)}
classList={{
// eslint-disable-next-line tailwindcss/no-custom-classname
[cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error,
}}
aria-invalid={props.error}
aria-disabled={props.disabled}
aria-readonly={props.readonly}
tabIndex={0}
role="textbox"
ref={internal.divRef}
>
{props.icon && (
<i
class={cx("inline-flex fg-def-2", fgStateClasses)}
aria-invalid={props.error}
aria-disabled={props.disabled}
aria-readonly={props.readonly}
>
<Icon icon={props.icon} font-size="inherit" color="inherit" />
</i>
)}
<Show when={!props.inputElem} fallback={props.inputElem}>
<input
tabIndex={-1}
class="w-full bg-transparent outline-none"
value={props.value}
type={props.type ? props.type : "text"}
readOnly={props.readonly}
placeholder={`${props.placeholder || ""}`}
required={props.required}
disabled={props.disabled}
aria-invalid={props.error}
aria-disabled={props.disabled}
aria-readonly={props.readonly}
{...inputProps}
/>
</Show>
</div>
);
};
interface InputLabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
description?: string;
required?: boolean;
error?: boolean;
help?: string;
labelAction?: JSX.Element;
}
export const InputLabel = (props: InputLabelProps) => {
const [labelProps, forwardProps] = splitProps(props, [
"class",
"labelAction",
]);
return (
<label
class={cx("flex items-center gap-1", labelProps.class)}
{...forwardProps}
>
<span class="flex flex-col justify-center">
<span>
<Typography
hierarchy="label"
size="default"
weight="bold"
class="inline-flex gap-1 align-middle !fg-def-1"
classList={{
[cx("!fg-semantic-info-1")]: !!props.error,
}}
aria-invalid={props.error}
>
{props.children}
</Typography>
{props.required && (
<Typography
class="inline-flex px-1 align-text-top leading-[0.5] fg-def-4"
color="inherit"
hierarchy="label"
weight="bold"
size="xs"
>
<>&#42;</>
</Typography>
)}
{props.help && (
<span
class=" inline px-2"
data-tip={props.help}
style={{
"--tooltip-color": "#EFFFFF",
"--tooltip-text-color": "#0D1416",
"--tooltip-tail": "0.8125rem",
}}
>
<Icon class="inline fg-def-3" icon={"Info"} width={"0.8125rem"} />
</span>
)}
</span>
<Typography
hierarchy="body"
size="xs"
weight="normal"
color="secondary"
>
{props.description}
</Typography>
</span>
{props.labelAction}
</label>
);
};
interface InputErrorProps {
error: string;
typographyProps?: TypographyProps;
}
export const InputError = (props: InputErrorProps) => {
const [typoClasses, rest] = splitProps(
props.typographyProps || { class: "" },
["class"],
);
return (
<Typography
hierarchy="body"
// @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now
size="xxs"
weight="medium"
class={cx("col-span-full px-1 !fg-semantic-info-4", typoClasses)}
{...rest}
>
{props.error}
</Typography>
);
};

View File

@@ -1,301 +0,0 @@
import { toast, Toast } from "solid-toast"; // Make sure to import Toast type
import { Component, JSX, createSignal, onCleanup } from "solid-js";
// --- Icon Components ---
const ErrorIcon: Component = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ "margin-right": "10px", "flex-shrink": "0" }}
>
<circle cx="12" cy="12" r="10" fill="#FF4D4F" />
<path
d="M12 7V13"
stroke="white"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="12" cy="16.5" r="1.5" fill="white" />
</svg>
);
const InfoIcon: Component = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ "margin-right": "10px", "flex-shrink": "0" }}
>
<circle cx="12" cy="12" r="10" fill="#2196F3" />
<path
d="M12 11V17"
stroke="white"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="12" cy="8.5" r="1.5" fill="white" />
</svg>
);
const WarningIcon: Component = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ "margin-right": "10px", "flex-shrink": "0" }}
>
<path d="M12 2L22 21H2L12 2Z" fill="#FFC107" />
<path
d="M12 9V14"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="12" cy="16.5" r="1" fill="#424242" />
</svg>
);
// --- Base Props and Styles ---
interface BaseToastProps {
t: Toast;
message: string;
onCancel?: () => void; // Optional custom function on X click
}
const baseToastStyle: JSX.CSSProperties = {
display: "flex",
"align-items": "center",
"justify-content": "space-between", // To push X to the right
gap: "10px", // Space between content and close button
background: "#FFFFFF",
color: "#333333",
padding: "12px 16px",
"border-radius": "6px",
"box-shadow": "0 2px 8px rgba(0, 0, 0, 0.12)",
"font-family":
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
"font-size": "14px",
"line-height": "1.4",
"min-width": "280px",
"max-width": "450px",
};
const closeButtonStyle: JSX.CSSProperties = {
background: "none",
border: "none",
color: "red", // As per original example's X button
"font-size": "1.5em",
"font-weight": "bold",
cursor: "pointer",
padding: "0 0 0 10px", // Space to its left
"line-height": "1",
"align-self": "center", // Ensure vertical alignment
};
// --- Toast Component Definitions ---
// Error Toast
export const ErrorToastComponent: Component<BaseToastProps> = (props) => {
// Local state for click feedback and exit animation
let timeoutId: number | undefined;
const [clicked, setClicked] = createSignal(false);
const [exiting, setExiting] = createSignal(false);
const handleToastClick = () => {
setClicked(true);
setTimeout(() => {
setExiting(true);
timeoutId = window.setTimeout(() => {
toast.dismiss(props.t.id);
}, 300); // Match exit animation duration
}, 100); // Brief color feedback before animating out
};
// Cleanup timeout if unmounted early
onCleanup(() => {
if (timeoutId) clearTimeout(timeoutId);
});
return (
<div
style={{
...baseToastStyle,
cursor: "pointer",
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
background: clicked()
? "#ffeaea"
: exiting()
? "#fff"
: baseToastStyle.background,
opacity: exiting() ? 0 : 1,
transform: exiting() ? "translateY(-20px)" : "none",
}}
onClick={handleToastClick}
>
<div
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
>
<ErrorIcon />
<span>{props.message}</span>
</div>
</div>
);
};
// Info Toast
export const CancelToastComponent: Component<BaseToastProps> = (props) => {
let timeoutId: number | undefined;
const [clicked, setClicked] = createSignal(false);
const [exiting, setExiting] = createSignal(false);
// Cleanup timeout if unmounted early
onCleanup(() => {
if (timeoutId) clearTimeout(timeoutId);
});
const handleButtonClick = (e: MouseEvent) => {
e.stopPropagation();
if (props.onCancel) props.onCancel();
toast.dismiss(props.t.id);
};
return (
<div
style={{
...baseToastStyle,
cursor: "pointer",
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
background: clicked()
? "#eaf4ff"
: exiting()
? "#fff"
: baseToastStyle.background,
opacity: exiting() ? 0 : 1,
transform: exiting() ? "translateY(-20px)" : "none",
}}
>
<div
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
>
<InfoIcon />
<span>{props.message}</span>
</div>
<button
onClick={(e) => {
setClicked(true);
handleButtonClick(e);
}}
style={{
...closeButtonStyle,
color: "#2196F3",
"font-size": "1em",
"font-weight": "normal",
padding: "4px 12px",
border: "1px solid #2196F3",
"border-radius": "4px",
background: clicked() ? "#bbdefb" : "#eaf4ff",
cursor: "pointer",
transition: "background 0.15s",
display: "flex",
"align-items": "center",
"justify-content": "center",
width: "70px",
height: "32px",
}}
aria-label="Cancel"
disabled={clicked()}
>
{clicked() ? (
// Simple spinner SVG
<svg
width="18"
height="18"
viewBox="0 0 50 50"
style={{ display: "block" }}
>
<circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#2196F3"
stroke-width="4"
stroke-linecap="round"
stroke-dasharray="31.415, 31.415"
transform="rotate(72 25 25)"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 25 25"
to="360 25 25"
dur="0.8s"
repeatCount="indefinite"
/>
</circle>
</svg>
) : (
"Cancel"
)}
</button>
</div>
);
};
// Warning Toast
const WarningToastComponent: Component<BaseToastProps> = (props) => {
let timeoutId: number | undefined;
const [clicked, setClicked] = createSignal(false);
const [exiting, setExiting] = createSignal(false);
const handleToastClick = () => {
setClicked(true);
setTimeout(() => {
setExiting(true);
timeoutId = window.setTimeout(() => {
toast.dismiss(props.t.id);
}, 300);
}, 100);
};
// Cleanup timeout if unmounted early
onCleanup(() => {
if (timeoutId) clearTimeout(timeoutId);
});
return (
<div
style={{
...baseToastStyle,
cursor: "pointer",
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
background: clicked()
? "#fff8e1"
: exiting()
? "#fff"
: baseToastStyle.background,
opacity: exiting() ? 0 : 1,
transform: exiting() ? "translateY(-20px)" : "none",
}}
onClick={handleToastClick}
>
<div
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
>
<WarningIcon />
<span>{props.message}</span>
</div>
</div>
);
};

View File

@@ -1,115 +0,0 @@
import { splitProps, type JSX, createSignal } from "solid-js";
import cx from "classnames";
import { Typography } from "../Typography/Typography";
import { Button as KobalteButton } from "@kobalte/core/button";
import "./Button.css";
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon";
import { Loader } from "@/src/components/v2/Loader/Loader";
export type Size = "default" | "s";
export type Hierarchy = "primary" | "secondary";
export type Action = () => Promise<void>;
export interface ButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
hierarchy?: Hierarchy;
size?: Size;
ghost?: boolean;
children?: JSX.Element;
icon?: IconVariant;
startIcon?: IconVariant;
endIcon?: IconVariant;
class?: string;
onAction?: Action;
}
const iconSizes: Record<Size, string> = {
default: "1rem",
s: "0.8125rem",
};
export const Button = (props: ButtonProps) => {
const [local, other] = splitProps(props, [
"children",
"hierarchy",
"size",
"ghost",
"icon",
"startIcon",
"endIcon",
"class",
"onAction",
]);
const size = local.size || "default";
const hierarchy = local.hierarchy || "primary";
const [loading, setLoading] = createSignal(false);
const onClick = async () => {
if (!local.onAction) {
console.error("this should not be possible");
return;
}
setLoading(true);
try {
await local.onAction();
} catch (error) {
console.error("Error while executing action", error);
}
setLoading(false);
};
const iconSize = iconSizes[local.size || "default"];
return (
<KobalteButton
class={cx(
local.class,
"button", // default button class
size,
hierarchy,
{
icon: local.icon,
loading: loading(),
ghost: local.ghost,
},
)}
onClick={local.onAction ? onClick : undefined}
{...other}
>
<Loader hierarchy={hierarchy} />
{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 && (
<Typography
class="label"
hierarchy="label"
family="mono"
size={local.size || "default"}
inverted={local.hierarchy === "primary"}
weight="bold"
tag="span"
>
{local.children}
</Typography>
)}
{local.endIcon && (
<Icon icon={local.endIcon} class="icon-end" size={iconSize} />
)}
</KobalteButton>
);
};

View File

@@ -1,7 +0,0 @@
## Overview
We will be updating existing components and developing new components in line with the latest designs inside this
folder. As they become ready, they can be copied into the root `components` folder, replacing any existing components as
necessary.
This is to avoid merge hell and allow us to rapidly match the latest designs without the burden of integration.

View File

@@ -1,75 +0,0 @@
/* Material icons removed - using custom icons */
/* @import url(./components/Typography/css/typography.css); */
@config "../../../tailwind.config.ts";
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "Archivo";
font-weight: 400;
src: url(../../../.fonts/Archivo-Regular.woff2) format("woff2");
}
@font-face {
font-family: "Archivo";
font-weight: 500;
src: url(../../../.fonts/Archivo-Medium.woff2) format("woff2");
}
@font-face {
font-family: "Archivo";
font-weight: 600;
src: url(../../../.fonts/Archivo-SemiBold.woff2) format("woff2");
}
@font-face {
font-family: "Archivo SemiCondensed";
font-weight: 400;
src: url(../../../.fonts/ArchivoSemiCondensed-Regular.woff2) format("woff2");
}
@font-face {
font-family: "Archivo SemiCondensed";
font-weight: 500;
src: url(../../../.fonts/ArchivoSemiCondensed-Medium.woff2) format("woff2");
}
@font-face {
font-family: "Archivo SemiCondensed";
font-weight: 600;
src: url(../../../.fonts/ArchivoSemiCondensed-SemiBold.woff2) format("woff2");
}
@font-face {
font-family: "Commit Mono";
font-weight: 400;
src: url(../../../.fonts/CommitMono-400-Regular.otf) format("otf");
}
html {
@apply font-sans;
overflow-x: hidden;
overflow-y: hidden;
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
/* Standard */
}
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}

Some files were not shown because too many files have changed in this diff Show More