{
+ onPress={() => {
if (batchConfig && publicClient) {
setIsSubmittingCase(true);
wrapWithToast(async () => await executeBatch(batchConfig), publicClient)
diff --git a/web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx b/web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx
index c07a5ec3c..13625c33c 100644
--- a/web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx
+++ b/web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx
@@ -97,9 +97,9 @@ const SubmitDisputeButton: React.FC = () => {
{
+ onPress={() => {
if (submitCaseConfig && publicClient) {
setIsSubmittingCase(true);
wrapWithToast(async () => await submitCase(submitCaseConfig.request), publicClient)
diff --git a/web/src/pages/Resolver/Parameters/Category.tsx b/web/src/pages/Resolver/Parameters/Category.tsx
index 59bb2ff21..7848a2483 100644
--- a/web/src/pages/Resolver/Parameters/Category.tsx
+++ b/web/src/pages/Resolver/Parameters/Category.tsx
@@ -3,7 +3,7 @@ import styled, { css } from "styled-components";
import { useTranslation } from "react-i18next";
-import { Field } from "@kleros/ui-components-library";
+import { TextField } from "@kleros/ui-components-library";
import { useNewDisputeContext } from "context/NewDisputeContext";
@@ -26,7 +26,7 @@ const Container = styled.div`
)}
`;
-const StyledField = styled(Field)`
+const StyledField = styled(TextField)`
width: 84vw;
margin-bottom: 74px;
${landscapeStyle(
@@ -35,7 +35,7 @@ const StyledField = styled(Field)`
margin-bottom: 64px;
`
)}
- > small {
+ > span {
margin-top: 16px;
}
`;
@@ -45,8 +45,8 @@ const Category: React.FC = () => {
const { disputeData, setDisputeData } = useNewDisputeContext();
const containerRef = useRef(null);
- const handleWrite = (event: React.ChangeEvent) => {
- setDisputeData({ ...disputeData, category: event.target.value });
+ const handleWrite = (value: string) => {
+ setDisputeData({ ...disputeData, category: value });
};
useEffect(() => {
@@ -62,7 +62,7 @@ const Category: React.FC = () => {
{
const { t } = useTranslation();
const { disputeData, setDisputeData, setSelectedFeatures } = useNewDisputeContext();
const { data: courtTree } = useCourtTree();
- const items = useMemo(() => !isUndefined(courtTree?.court) && [rootCourtToItems(courtTree.court)], [courtTree]);
+ const items = useMemo(
+ () => (!isUndefined(courtTree?.court) ? [rootCourtToItems(courtTree.court)] : false),
+ [courtTree]
+ );
const handleCourtChange = (courtId: string) => {
if (disputeData.courtId !== courtId) {
@@ -72,9 +79,9 @@ const Court: React.FC = () => {
{items ? (
typeof path === "string" && handleCourtChange(path.split("/").pop()!)}
+ callback={(item) => typeof item.itemValue === "string" && handleCourtChange(item.itemValue.split("/").pop()!)}
placeholder={t("forms.placeholders.select_court")}
- value={`/courts/${disputeData.courtId}`}
+ selectedKey={`/courts/${disputeData.courtId}`}
/>
) : (
diff --git a/web/src/pages/Resolver/Parameters/Jurors.tsx b/web/src/pages/Resolver/Parameters/Jurors.tsx
index 22d8cddb6..04252bd32 100644
--- a/web/src/pages/Resolver/Parameters/Jurors.tsx
+++ b/web/src/pages/Resolver/Parameters/Jurors.tsx
@@ -3,7 +3,7 @@ import styled, { css } from "styled-components";
import { useTranslation } from "react-i18next";
-import { DisplaySmall, Field } from "@kleros/ui-components-library";
+import { DisplaySmall, TextField } from "@kleros/ui-components-library";
import ETH from "svgs/icons/eth.svg";
@@ -34,7 +34,7 @@ const Container = styled.div`
)}
`;
-const StyledField = styled(Field)`
+const StyledField = styled(TextField)`
width: 290px;
margin-bottom: ${responsiveSize(20, 48)};
`;
@@ -43,9 +43,12 @@ const StyledDisplay = styled(DisplaySmall)`
width: 290px;
margin-bottom: ${responsiveSize(20, 48)};
- h2::after {
- content: "ETH";
- margin-left: 4px;
+ h2 {
+ margin: 0;
+ ::after {
+ content: "ETH";
+ margin-left: 4px;
+ }
}
path {
@@ -69,8 +72,8 @@ const Jurors: React.FC = () => {
useEffect(() => setDisputeData({ ...disputeData, arbitrationCost: data?.toString() }), [data]);
- const handleJurorsWrite = (event: React.ChangeEvent) => {
- const value = parseInt(event.target.value.replace(/\D/g, ""), 10);
+ const handleJurorsWrite = (inputValue: string) => {
+ const value = parseInt(inputValue.replace(/\D/g, ""), 10);
if (isUndefined(value) || isNaN(value)) {
setDisputeData({ ...disputeData, numberOfJurors: 0 });
} else {
@@ -85,7 +88,7 @@ const Jurors: React.FC = () => {
diff --git a/web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx b/web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx
index bb291ef59..092fc1d5e 100644
--- a/web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx
+++ b/web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx
@@ -78,16 +78,15 @@ const PersonFields: React.FC = () => {
}
}, []);
- const handleAliasesWrite = (event: React.ChangeEvent) => {
- const key = parseInt(event.target.id.replace(/\D/g, ""), 10) - 1;
+ const handleAliasesWrite = (key: number, field: "name" | "address", value: string) => {
const aliases = disputeData.aliasesArray;
if (isUndefined(aliases)) return;
- aliases[key] = { ...aliases[key], [event.target.name]: event.target.value };
+ aliases[key] = { ...aliases[key], [field]: value };
setDisputeData({ ...disputeData, aliasesArray: aliases });
//since resolving ens is async, we update asynchronously too with debounce
- if (event.target.name === "address") debounceValidateAddress(event.target.value, key);
+ if (field === "address") debounceValidateAddress(value, key);
};
const showError = (alias: AliasArray) => {
@@ -103,16 +102,16 @@ const PersonFields: React.FC = () => {
label={t("forms.labels.person_number", { number: index + 1 })}
placeholder={t("forms.placeholders.alice_developer_example")}
value={alias.name}
- onChange={handleAliasesWrite}
+ onChange={(value) => handleAliasesWrite(index, "name", value)}
/>
handleAliasesWrite(index, "address", value)}
/>
))}
diff --git a/web/src/pages/Resolver/Parameters/VotingOptions/OptionsFields.tsx b/web/src/pages/Resolver/Parameters/VotingOptions/OptionsFields.tsx
index b9667e706..ec893a1f4 100644
--- a/web/src/pages/Resolver/Parameters/VotingOptions/OptionsFields.tsx
+++ b/web/src/pages/Resolver/Parameters/VotingOptions/OptionsFields.tsx
@@ -49,13 +49,14 @@ const OptionsFields: React.FC = () => {
const defaultAnswer: Answer = { title: "", id: value.toString(), description: "" };
const answers = disputeData.answers;
- if (value < answers?.length) return setDisputeData({ ...disputeData, answers: answers.splice(0, value) });
+ if (value < answers?.length) return setDisputeData({ ...disputeData, answers: answers.slice(0, value) });
if (value > answers?.length) return setDisputeData({ ...disputeData, answers: [...answers, defaultAnswer] });
};
- const handleOptionWrite = (event: React.ChangeEvent, key: number) => {
- const answers = disputeData.answers;
- answers[key] = { ...answers[key], [event.target.name]: event.target.value };
+ const handleOptionWrite = (field: "title" | "description", key: number, value: string) => {
+ const answers = disputeData.answers.map((answer, index) =>
+ index === key ? { ...answer, [field]: value } : answer
+ );
setDisputeData({ ...disputeData, answers });
};
return (
@@ -68,14 +69,14 @@ const OptionsFields: React.FC = () => {
label={t("forms.labels.voting_option_number", { number: index + 1 })}
placeholder={t("forms.placeholders.pay_dai_example")}
value={answer.title ?? ""}
- onChange={(event) => handleOptionWrite(event, index)}
+ onChange={(value) => handleOptionWrite("title", index, value)}
/>
handleOptionWrite(event, index)}
+ onChange={(value) => handleOptionWrite("description", index, value)}
/>
))}
diff --git a/web/src/pages/Resolver/Parameters/VotingOptions/index.tsx b/web/src/pages/Resolver/Parameters/VotingOptions/index.tsx
index aa7cab613..cfec3d2ec 100644
--- a/web/src/pages/Resolver/Parameters/VotingOptions/index.tsx
+++ b/web/src/pages/Resolver/Parameters/VotingOptions/index.tsx
@@ -24,7 +24,7 @@ const Container = styled.div`
`;
const QuestionField = styled(LabeledInput)`
- margin-bottom: 78px;
+ margin-bottom: 44px;
`;
const AlertMessageContainer = styled.div`
@@ -37,6 +37,10 @@ const AlertMessageContainer = styled.div`
> div {
width: 100%;
}
+
+ h2 {
+ margin: 0;
+ }
`;
const VotingOptions: React.FC = () => {
@@ -44,8 +48,8 @@ const VotingOptions: React.FC = () => {
const containerRef = useRef(null);
const { t } = useTranslation();
- const handleQuestionWrite = (event: React.ChangeEvent) => {
- setDisputeData({ ...disputeData, question: event.target.value });
+ const handleQuestionWrite = (value: string) => {
+ setDisputeData({ ...disputeData, question: value });
};
useEffect(() => {
diff --git a/web/src/pages/Resolver/Policy/index.tsx b/web/src/pages/Resolver/Policy/index.tsx
index 1b8ffd8f7..e38e544ad 100644
--- a/web/src/pages/Resolver/Policy/index.tsx
+++ b/web/src/pages/Resolver/Policy/index.tsx
@@ -58,6 +58,18 @@ const StyledFileUploader = styled(FileUploader)`
small {
white-space: pre-line;
text-align: start;
+ font-size: 14px;
+ }
+ /* The library colors the info icon primary blue; match it to the message text. */
+ svg:has(+ [id="dropzone-label"]) {
+ fill: ${({ theme }) => theme.secondaryText};
+ path {
+ fill: ${({ theme }) => theme.secondaryText};
+ }
+ }
+ /* Align the icon to the first line of the message, not its vertical center. */
+ div:has(> [id="dropzone-label"]) {
+ align-items: flex-start;
}
`;
diff --git a/web/src/pages/Resolver/Preview/BatchCreationCard.tsx b/web/src/pages/Resolver/Preview/BatchCreationCard.tsx
index fc77dd437..9a0278995 100644
--- a/web/src/pages/Resolver/Preview/BatchCreationCard.tsx
+++ b/web/src/pages/Resolver/Preview/BatchCreationCard.tsx
@@ -136,7 +136,7 @@ const BatchCreationCard: React.FC = () => {
return (
- setIsBatchCreation(!isBatchCreation)} />
+ setIsBatchCreation(!isBatchCreation)} />
{t("case_creation.create_multiple_cases")}
diff --git a/web/src/pages/Resolver/Timeline.tsx b/web/src/pages/Resolver/Timeline.tsx
index 3ebe14ca0..cbd876de7 100644
--- a/web/src/pages/Resolver/Timeline.tsx
+++ b/web/src/pages/Resolver/Timeline.tsx
@@ -8,6 +8,10 @@ import { Steps } from "@kleros/ui-components-library";
const StyledSteps = styled(Steps)`
height: 360px;
+
+ h2 {
+ margin: 0;
+ }
`;
const Timeline: React.FC = () => {
diff --git a/web/src/styles/base-elements.css b/web/src/styles/base-elements.css
new file mode 100644
index 000000000..a678420f8
--- /dev/null
+++ b/web/src/styles/base-elements.css
@@ -0,0 +1,75 @@
+/*
+ * Base element typography for kleros-v2's own markup.
+ *
+ * In @layer components so it stays above the ui-components-library's Tailwind preflight
+ * (app headings keep their styles) but below the library's @layer utilities
+ * (library components keep their own text styling instead of being overridden).
+ * Colors use the library's CSS variables so they track the light/dark theme.
+ *
+ * Must be a plain CSS file, not createGlobalStyle โ styled-components v5 mangles
+ * @layer blocks.
+ */
+@layer components {
+ h1 {
+ margin: 0 0 16px 0;
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 32px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+
+ h2 {
+ margin: 0 0 16px 0;
+ font-weight: 400;
+ font-size: 24px;
+ line-height: 32px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+
+ h3 {
+ margin: 0 0 16px 0;
+ font-weight: 600;
+ font-size: 16px;
+ line-height: 24px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+
+ p {
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 24px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+
+ /* :not(...) keeps this off the library's own s (they all carry a
+ klerosUIComponents class); otherwise font-weight: 600 leaks in and bolds their text. */
+ small:not([class*="klerosUIComponents"]) {
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 18px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+
+ label {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 18px;
+ color: var(--klerosUIComponentsSecondaryText);
+ }
+
+ a {
+ font-weight: 400;
+ font-size: 14px;
+ text-decoration: none;
+ color: var(--klerosUIComponentsPrimaryBlue);
+ transition: color 0.1s;
+ }
+
+ ul li,
+ ol li {
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 24px;
+ color: var(--klerosUIComponentsPrimaryText);
+ }
+}
diff --git a/web/src/styles/global-style.ts b/web/src/styles/global-style.ts
index 2b71e4d01..c79b73c2d 100644
--- a/web/src/styles/global-style.ts
+++ b/web/src/styles/global-style.ts
@@ -36,62 +36,15 @@ export const GlobalStyle = createGlobalStyle`
background-color: #1b003fcc !important;
}
- h1 {
- margin: 0 0 16px 0;
- font-weight: 600;
- font-size: 24px;
- line-height: 32px;
- color: ${({ theme }) => theme.primaryText};
- }
-
- h2 {
- margin: 0 0 16px 0;
- font-weight: 400;
- font-size: 24px;
- line-height: 32px;
- color: ${({ theme }) => theme.primaryText};
- }
-
- h3 {
- margin: 0 0 16px 0;
- font-weight: 600;
- font-size: 16px;
- line-height: 24px;
- color: ${({ theme }) => theme.primaryText};
- }
-
- p {
- font-weight: 400;
- font-size: 16px;
- line-height: 24px;
- color: ${({ theme }) => theme.primaryText};
- }
+ /* Base element typography lives in styles/base-elements.css (see there). */
textarea {
font-family: "Open Sans";
font-size: 14px;
}
- small {
- font-weight: 600;
- font-size: 14px;
- line-height: 18px;
- color: ${({ theme }) => theme.primaryText};
- }
-
- label {
- font-weight: 400;
- font-size: 14px;
- line-height: 18px;
- color: ${({ theme }) => theme.secondaryText};
- }
-
- a {
- font-weight: 400;
+ input {
font-size: 14px;
- text-decoration: none;
- color: ${({ theme }) => theme.primaryBlue};
- transition: color 0.1s;
}
hr {
@@ -105,15 +58,6 @@ export const GlobalStyle = createGlobalStyle`
visibility: visible;
}
- ul, ol {
- li {
- font-weight: 400;
- font-size: 16px;
- line-height: 24px;
- color: ${({ theme }) => theme.primaryText};
- }
- }
-
.os-theme-dark {
--os-handle-bg: ${({ theme }) => theme.violetPurple};
--os-handle-bg-hover: ${({ theme }) => theme.secondaryPurple};
diff --git a/web/src/styles/styled.d.ts b/web/src/styles/styled.d.ts
new file mode 100644
index 000000000..30d5f10d7
--- /dev/null
+++ b/web/src/styles/styled.d.ts
@@ -0,0 +1,16 @@
+import "styled-components";
+
+import { lightTheme } from "./themes";
+
+/**
+ * The app owns its styled-components theme type. The v2 ui-components-library
+ * shipped a compatible theme shape via its exported `lightTheme`/`darkTheme`
+ * objects; v3 dropped styled-components theming entirely (it is now CSS-vars +
+ * Tailwind based), so `DefaultTheme` is declared here from the local theme.
+ */
+type KlerosTheme = typeof lightTheme;
+
+declare module "styled-components" {
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+ export interface DefaultTheme extends KlerosTheme {}
+}
diff --git a/web/src/styles/themes.ts b/web/src/styles/themes.ts
index b44dd177e..5a7f707ba 100644
--- a/web/src/styles/themes.ts
+++ b/web/src/styles/themes.ts
@@ -1,7 +1,4 @@
-import { lightTheme as componentsLightTheme, darkTheme as componentsDarkTheme } from "@kleros/ui-components-library";
-
export const lightTheme = {
- ...componentsLightTheme,
name: "light",
white: "#FFFFFF",
black: "#000000",
@@ -19,7 +16,7 @@ export const lightTheme = {
primaryText: "#333333",
secondaryText: "#999999",
stroke: "#e5e5e5",
- lightGrey: "#F0F0F0",
+ lightGrey: "#FAFAFA",
whiteBackground: "#FFFFFF",
lightBackground: "#FAFBFC",
@@ -50,7 +47,6 @@ export const lightTheme = {
};
export const darkTheme = {
- ...componentsDarkTheme,
name: "dark",
white: "#FFFFFF",
black: "#000000",
diff --git a/web/src/utils/uiComponentsTypes.ts b/web/src/utils/uiComponentsTypes.ts
new file mode 100644
index 000000000..38c4af0f9
--- /dev/null
+++ b/web/src/utils/uiComponentsTypes.ts
@@ -0,0 +1,9 @@
+import type { ComponentProps } from "react";
+
+import { CustomTimeline, DropdownCascader, DropdownSelect } from "@kleros/ui-components-library";
+
+export type CustomTimelineItem = ComponentProps["items"][number];
+
+export type SelectItem = ComponentProps["items"][number];
+
+export type CascaderItem = ComponentProps["items"][number];