feat(ui): prep V2
Preparation for rebuilding the UI with V2 components.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type { Preview } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
import "@/src/components/v2/index.css";
|
||||
import "../src/index.css";
|
||||
import "./preview.css";
|
||||
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./FormSection";
|
||||
export * from "./TextInput";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./Alert.css";
|
||||
import cx from "classnames";
|
||||
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { Alert as KAlert } from "@kobalte/core/alert";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,115 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { splitProps, type JSX, createSignal } from "solid-js";
|
||||
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";
|
||||
type Size = "default" | "s";
|
||||
export type Size = "default" | "s";
|
||||
export type Hierarchy = "primary" | "secondary";
|
||||
|
||||
const variantColors: (
|
||||
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",
|
||||
export type Action = () => Promise<void>;
|
||||
|
||||
!disabled && "button--light-hover", // Hover state
|
||||
!disabled && "button--light-focus", // Focus state
|
||||
!disabled && "button--light-active", // Active state
|
||||
),
|
||||
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;
|
||||
export interface ButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
hierarchy?: Hierarchy;
|
||||
size?: Size;
|
||||
ghost?: boolean;
|
||||
children?: JSX.Element;
|
||||
startIcon?: JSX.Element;
|
||||
endIcon?: 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",
|
||||
"variant",
|
||||
"hierarchy",
|
||||
"size",
|
||||
"ghost",
|
||||
"icon",
|
||||
"startIcon",
|
||||
"endIcon",
|
||||
"class",
|
||||
"onAction",
|
||||
]);
|
||||
|
||||
const buttonInvertion = (variant: Variants) => {
|
||||
return !(!variant || variant === "ghost" || variant === "light");
|
||||
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 (
|
||||
<button
|
||||
<KobalteButton
|
||||
class={cx(
|
||||
local.class,
|
||||
"button", // default button class
|
||||
variantColors(props.disabled)[local.variant || "dark"], // button appereance
|
||||
sizePaddings[local.size || "default"], // button size
|
||||
size,
|
||||
hierarchy,
|
||||
{
|
||||
icon: local.icon,
|
||||
loading: loading(),
|
||||
ghost: local.ghost,
|
||||
},
|
||||
)}
|
||||
onClick={local.onAction ? onClick : undefined}
|
||||
{...other}
|
||||
>
|
||||
<Loader hierarchy={hierarchy} />
|
||||
|
||||
{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
|
||||
class="button__label"
|
||||
class="label"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size={local.size || "default"}
|
||||
color="inherit"
|
||||
inverted={buttonInvertion(local.variant || "dark")}
|
||||
weight="medium"
|
||||
inverted={local.hierarchy === "primary"}
|
||||
weight="bold"
|
||||
tag="span"
|
||||
>
|
||||
{local.children}
|
||||
</Typography>
|
||||
)}
|
||||
{local.endIcon && <span class="button__icon--end">{local.endIcon}</span>}
|
||||
</button>
|
||||
|
||||
{local.endIcon && (
|
||||
<Icon icon={local.endIcon} class="icon-end" size={iconSize} />
|
||||
)}
|
||||
</KobalteButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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> = {
|
||||
title: "Components/Divider",
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
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) => (
|
||||
<div class="flex flex-col gap-8">
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CheckboxInputProps as KCheckboxInputProps,
|
||||
CheckboxRootProps as KCheckboxRootProps,
|
||||
} from "@kobalte/core/checkbox";
|
||||
import Icon from "@/src/components/v2/Icon/Icon";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
|
||||
import cx from "classnames";
|
||||
import { Label } from "./Label";
|
||||
@@ -1,4 +1,4 @@
|
||||
import Icon from "@/src/components/v2/Icon/Icon";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import {
|
||||
Combobox as KCombobox,
|
||||
ComboboxRootOptions as KComboboxRootOptions,
|
||||
@@ -11,9 +11,9 @@ import { Label } from "./Label";
|
||||
import cx from "classnames";
|
||||
import { FieldProps } from "./Field";
|
||||
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 { Tag } from "@/src/components/v2/Tag/Tag";
|
||||
import { Tag } from "@/src/components/Tag/Tag";
|
||||
|
||||
export type ComboboxProps<Option, OptGroup = never> = FieldProps &
|
||||
KComboboxRootOptions<Option, OptGroup> & {
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { TextInput } from "@/src/components/v2/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/v2/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
import { FieldProps } from "./Field";
|
||||
|
||||
const FieldsetExamples = (props: FieldsetProps) => (
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./Fieldset.css";
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { FieldProps } from "./Field";
|
||||
|
||||
export type FieldsetFieldProps = Pick<
|
||||
@@ -1,7 +1,7 @@
|
||||
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 Icon from "@/src/components/v2/Icon/Icon";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { TextField } from "@kobalte/core/text-field";
|
||||
import { Checkbox } from "@kobalte/core/checkbox";
|
||||
import { Combobox } from "@kobalte/core/combobox";
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
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) => (
|
||||
<div class="flex flex-col gap-8">
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
TextFieldInputProps,
|
||||
TextFieldRootProps,
|
||||
} 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 { Label } from "./Label";
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { List } from "./List";
|
||||
@@ -1,5 +1,5 @@
|
||||
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> = {
|
||||
title: "Components/Loader",
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
MachineStatus,
|
||||
MachineStatusProps,
|
||||
} from "@/src/components/v2/MachineStatus/MachineStatus";
|
||||
} from "@/src/components/MachineStatus/MachineStatus";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
|
||||
const meta: Meta<MachineStatusProps> = {
|
||||
@@ -4,7 +4,7 @@ import { Badge } from "@kobalte/core/badge";
|
||||
import cx from "classnames";
|
||||
import { Show } from "solid-js";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
export type MachineStatus =
|
||||
| "Online"
|
||||
@@ -1,15 +1,15 @@
|
||||
import { TagProps } from "@/src/components/v2/Tag/Tag";
|
||||
import { TagProps } from "@/src/components/Tag/Tag";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
Modal,
|
||||
ModalContext,
|
||||
ModalProps,
|
||||
} from "@/src/components/v2/Modal/Modal";
|
||||
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";
|
||||
} from "@/src/components/Modal/Modal";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
const meta: Meta<ModalProps> = {
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import {
|
||||
SidebarNav,
|
||||
SidebarNavProps,
|
||||
} from "@/src/components/v2/Sidebar/SidebarNav";
|
||||
} from "@/src/components/Sidebar/SidebarNav";
|
||||
import { Suspense } from "solid-js";
|
||||
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./SidebarNav.css";
|
||||
import { SidebarNavHeader } from "@/src/components/v2/Sidebar/SidebarNavHeader";
|
||||
import { SidebarNavBody } from "@/src/components/v2/Sidebar/SidebarNavBody";
|
||||
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus";
|
||||
import { SidebarNavHeader } from "@/src/components/Sidebar/SidebarNavHeader";
|
||||
import { SidebarNavBody } from "@/src/components/Sidebar/SidebarNavBody";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
|
||||
export interface LinkProps {
|
||||
path: string;
|
||||
@@ -2,13 +2,13 @@ import "./SidebarNavBody.css";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Accordion } from "@kobalte/core/accordion";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import {
|
||||
MachineProps,
|
||||
SidebarNavProps,
|
||||
} from "@/src/components/v2/Sidebar/SidebarNav";
|
||||
} from "@/src/components/Sidebar/SidebarNav";
|
||||
import { For } from "solid-js";
|
||||
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
|
||||
const MachineRoute = (props: MachineProps) => (
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useNavigate } from "@solidjs/router";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
@@ -7,7 +7,7 @@ import { createSignal, For } from "solid-js";
|
||||
import {
|
||||
ClanLinkProps,
|
||||
ClanProps,
|
||||
} from "@/src/components/v2/Sidebar/SidebarNav";
|
||||
} from "@/src/components/Sidebar/SidebarNav";
|
||||
|
||||
export interface SidebarHeaderProps {
|
||||
clanDetail: ClanProps;
|
||||
@@ -2,12 +2,12 @@ import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import {
|
||||
SidebarPane,
|
||||
SidebarPaneProps,
|
||||
} from "@/src/components/v2/Sidebar/SidebarPane";
|
||||
} from "@/src/components/Sidebar/SidebarPane";
|
||||
import { SidebarSection } from "./SidebarSection";
|
||||
import { Divider } from "@/src/components/v2/Divider/Divider";
|
||||
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 { Divider } from "@/src/components/Divider/Divider";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
import { Combobox } from "../Form/Combobox";
|
||||
|
||||
const meta: Meta<SidebarPaneProps> = {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { JSX } from "solid-js";
|
||||
import "./SidebarPane.css";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSignal, JSX } from "solid-js";
|
||||
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 Icon from "../Icon/Icon";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: "";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 { expect, fn } from "storybook/test";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./Tag.css";
|
||||
|
||||
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 Icon, { IconVariant } from "../Icon/Icon";
|
||||
|
||||
@@ -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";
|
||||
|
||||
const meta: Meta<TagGroupProps> = {
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./TagGroup.css";
|
||||
import cx from "classnames";
|
||||
import { For } from "solid-js";
|
||||
import { Tag } from "@/src/components/v2/Tag/Tag";
|
||||
import { Tag } from "@/src/components/Tag/Tag";
|
||||
|
||||
export interface TagGroupProps {
|
||||
class?: string;
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
|
||||
import { Family, Hierarchy, Typography, Weight } from "./Typography";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { AllColors } from "@/src/components/v2/colors";
|
||||
import { AllColors } from "@/src/components/colors";
|
||||
|
||||
interface TypographyExamplesProps {
|
||||
weights: Weight[];
|
||||
@@ -2,7 +2,7 @@ import { type JSX } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
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 Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
@import "./typography-label.css";
|
||||
@import "./typography-body.css";
|
||||
@import "./typography-title.css";
|
||||
@import "./typography-headline.css";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
>
|
||||
<>*</>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user